From 32f76f541850677ea6b97d733a92f4207cee04cf Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sun, 25 Jan 2015 01:05:25 +0100 Subject: [PATCH] - git (re)init (git structure was corrupted) - added shuffler and repeat mode - xxxhdpi icons - typos - new styles - not fully working playing queue [alpha] --- .gitignore | 49 + app/build.gradle | 64 + app/manifest-merger-release-report.txt | 192 ++ app/proguard-rules.pro | 21 + .../materialmusic/ApplicationTest.java | 13 + app/src/main/AndroidManifest.xml | 61 + .../iosched/ui/widget/SlidingTabLayout.java | 321 ++ .../iosched/ui/widget/SlidingTabStrip.java | 168 + .../java/com/kabouzeid/materialmusic/App.java | 84 + .../adapter/AlbumViewGridAdapter.java | 105 + .../adapter/ArtistViewListAdapter.java | 60 + .../adapter/NavigationDrawerItemAdapter.java | 51 + .../adapter/PlayListAdapter.java | 47 + .../adapter/songadapter/SongAdapter.java | 100 + .../songadapter/SongViewListAdapter.java | 88 + .../comparator/AlbumAlphabeticComparator.java | 15 + .../ArtistAlphabeticComparator.java | 15 + .../comparator/SongAlphabeticComparator.java | 15 + .../comparator/SongTrackNumberComparator.java | 24 + .../helper/MusicPlayerRemote.java | 271 ++ .../helper/NotificationHelper.java | 178 + .../helper/PlayingQueueDialogHelper.java | 50 + .../materialmusic/helper/Shuffler.java | 52 + .../helper/SongDetailDialogHelper.java | 84 + .../interfaces/KabSearchAbleFragment.java | 10 + .../interfaces/KabViewsDisableAble.java | 12 + .../OnMusicRemoteEventListener.java | 10 + .../materialmusic/lastfm/LastFMUtil.java | 9 + .../lastfm/album/LastFMAlbumImageLoader.java | 110 + .../lastfm/album/LastFMAlbumInfoUtil.java | 87 + .../artist/LastFMArtistBiographyLoader.java | 65 + .../artist/LastFMArtistImageLoader.java | 98 + .../lastfm/artist/LastFMArtistInfoUtil.java | 95 + .../artist/LastFMArtistThumbnailLoader.java | 98 + .../materialmusic/loader/AlbumLoader.java | 106 + .../materialmusic/loader/AlbumSongLoader.java | 65 + .../loader/ArtistAlbumLoader.java | 54 + .../materialmusic/loader/ArtistLoader.java | 97 + .../loader/ArtistSongLoader.java | 64 + .../materialmusic/loader/SongFileLoader.java | 62 + .../materialmusic/loader/SongLoader.java | 117 + .../kabouzeid/materialmusic/misc/AppKeys.java | 23 + .../misc/SmallAnimatorListener.java | 26 + .../SmallObservableScrollViewCallbacks.java | 24 + .../misc/SmallOnGestureListener.java | 39 + .../misc/SmallTransitionListener.java | 35 + .../kabouzeid/materialmusic/model/Album.java | 32 + .../kabouzeid/materialmusic/model/Artist.java | 26 + .../materialmusic/model/MusicRemoteEvent.java | 35 + .../model/NavigationDrawerItem.java | 14 + .../kabouzeid/materialmusic/model/Song.java | 40 + .../provider/AlbumJSONStore.java | 103 + .../provider/ArtistJSONStore.java | 103 + .../service/MediaButtonIntentReceiver.java | 70 + .../materialmusic/service/MusicService.java | 630 ++++ .../ui/activities/AlbumDetailActivity.java | 491 +++ .../ui/activities/ArtistDetailActivity.java | 490 +++ .../ui/activities/MainActivity.java | 320 ++ .../activities/MusicControllerActivity.java | 428 +++ .../ui/activities/base/AbsBaseActivity.java | 128 + .../ui/activities/base/AbsFabActivity.java | 163 + .../tageditor/AbsTagEditorActivity.java | 464 +++ .../tageditor/AlbumTagEditorActivity.java | 199 ++ .../tageditor/SongTagEditorActivity.java | 130 + .../fragments/NavigationDrawerFragment.java | 205 ++ .../AbsViewPagerTabArtistListFragment.java | 151 + .../ViewPagerTabArtistAlbumFragment.java | 86 + .../ViewPagerTabArtistBioFragment.java | 71 + .../ViewPagerTabArtistSongListFragment.java | 38 + .../AlbumViewFragment.java | 173 + .../ArtistViewFragment.java | 155 + .../SongViewFragment.java | 110 + .../materialmusic/util/ImageLoaderUtil.java | 92 + .../util/InternalStorageUtil.java | 37 + .../materialmusic/util/MusicUtil.java | 76 + .../kabouzeid/materialmusic/util/Util.java | 179 + .../materialmusic/util/ViewUtil.java | 98 + .../materialmusic/view/SquareImageView.java | 29 + .../res/drawable-hdpi/default_album_art.png | Bin 0 -> 18335 bytes .../res/drawable-hdpi/drawer_shadow.9.png | Bin 0 -> 161 bytes app/src/main/res/drawable-hdpi/ic_drawer.png | Bin 0 -> 2829 bytes .../res/drawable-mdpi/default_album_art.png | Bin 0 -> 10369 bytes .../res/drawable-mdpi/drawer_shadow.9.png | Bin 0 -> 142 bytes app/src/main/res/drawable-mdpi/ic_drawer.png | Bin 0 -> 2820 bytes .../drawable-v21/notification_selector.xml | 7 + .../res/drawable-xhdpi/default_album_art.png | Bin 0 -> 27408 bytes .../res/drawable-xhdpi/drawer_shadow.9.png | Bin 0 -> 174 bytes app/src/main/res/drawable-xhdpi/ic_drawer.png | Bin 0 -> 2836 bytes app/src/main/res/drawable-xxhdpi/album.png | Bin 0 -> 962 bytes app/src/main/res/drawable-xxhdpi/close.png | Bin 0 -> 527 bytes .../res/drawable-xxhdpi/default_album_art.png | Bin 0 -> 48623 bytes .../drawable-xxhdpi/default_artist_image.png | Bin 0 -> 37488 bytes .../res/drawable-xxhdpi/drawer_shadow.9.png | Bin 0 -> 208 bytes app/src/main/res/drawable-xxhdpi/heart.png | Bin 0 -> 888 bytes .../res/drawable-xxhdpi/heart_outline.png | Bin 0 -> 1373 bytes .../main/res/drawable-xxhdpi/ic_drawer.png | Bin 0 -> 202 bytes .../main/res/drawable-xxhdpi/ic_overflow.png | Bin 0 -> 701 bytes .../main/res/drawable-xxhdpi/interpret.png | Bin 0 -> 845 bytes .../main/res/drawable-xxhdpi/music_box.png | Bin 0 -> 606 bytes .../res/drawable-xxhdpi/music_box_outline.png | Bin 0 -> 675 bytes app/src/main/res/drawable-xxhdpi/play_box.png | Bin 0 -> 567 bytes app/src/main/res/drawable-xxhdpi/playlist.png | Bin 0 -> 464 bytes app/src/main/res/drawable-xxhdpi/settings.png | Bin 0 -> 1182 bytes app/src/main/res/drawable-xxhdpi/songs.png | Bin 0 -> 740 bytes .../drawable-xxxhdpi/default_album_art.png | Bin 0 -> 73484 bytes .../drawable-xxxhdpi/ic_done_white_48dp.png | Bin 0 -> 1031 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 12528 bytes .../drawable-xxxhdpi/ic_pause_white_48dp.png | Bin 0 -> 436 bytes .../ic_play_arrow_white_48dp.png | Bin 0 -> 835 bytes .../ic_repeat_grey600_48dp.png | Bin 0 -> 746 bytes .../ic_repeat_one_white_48dp.png | Bin 0 -> 809 bytes .../drawable-xxxhdpi/ic_repeat_white_48dp.png | Bin 0 -> 728 bytes .../ic_shuffle_grey600_48dp.png | Bin 0 -> 1428 bytes .../ic_shuffle_white_48dp.png | Bin 0 -> 1428 bytes .../ic_skip_next_white_48dp.png | Bin 0 -> 1007 bytes .../ic_skip_previous_white_48dp.png | Bin 0 -> 1017 bytes .../ic_speaker_white_48dp.png | Bin 0 -> 2652 bytes app/src/main/res/drawable-xxxhdpi/logo.png | Bin 0 -> 4957 bytes .../drawable-xxxhdpi/notification_icon.png | Bin 0 -> 1996 bytes .../main/res/drawable/list_item_activated.xml | 5 + .../res/drawable/list_item_activated_dark.xml | 5 + .../main/res/drawable/list_item_selected.xml | 5 + .../res/drawable/list_item_selected_dark.xml | 5 + app/src/main/res/drawable/list_selector.xml | 8 + .../main/res/drawable/list_selector_dark.xml | 8 + .../drawable/navigation_drawer_gradient.xml | 9 + app/src/main/res/drawable/transparent.xml | 5 + .../layout-land/activity_music_controller.xml | 201 ++ .../res/layout-v21/notification_playing.xml | 99 + .../notification_playing_expanded.xml | 112 + .../main/res/layout/activity_album_detail.xml | 94 + .../res/layout/activity_album_tag_editor.xml | 166 + .../res/layout/activity_artist_detail.xml | 106 + app/src/main/res/layout/activity_main.xml | 82 + .../res/layout/activity_music_controller.xml | 183 + .../res/layout/activity_song_tag_editor.xml | 217 ++ app/src/main/res/layout/album_tile.xml | 51 + .../main/res/layout/dialog_file_details.xml | 83 + app/src/main/res/layout/dialog_loading.xml | 25 + app/src/main/res/layout/dialog_playlist.xml | 11 + .../main/res/layout/fragment_albumview.xml | 21 + .../main/res/layout/fragment_artist_view.xml | 18 + app/src/main/res/layout/fragment_drawer.xml | 16 + app/src/main/res/layout/fragment_gridview.xml | 9 + .../res/layout/fragment_navigation_drawer.xml | 85 + .../main/res/layout/fragment_place_holder.xml | 12 + app/src/main/res/layout/fragment_songview.xml | 19 + .../layout/item_artist_details_biography.xml | 9 + app/src/main/res/layout/item_artist_view.xml | 32 + .../res/layout/item_navigation_drawer.xml | 35 + app/src/main/res/layout/item_playlist.xml | 33 + app/src/main/res/layout/item_song.xml | 50 + app/src/main/res/layout/item_song_view.xml | 34 + .../main/res/layout/notification_playing.xml | 99 + .../layout/notification_playing_expanded.xml | 111 + app/src/main/res/layout/tab_indicator.xml | 25 + app/src/main/res/menu/drawer.xml | 25 + app/src/main/res/menu/global.xml | 8 + app/src/main/res/menu/menu_album_detail.xml | 27 + app/src/main/res/menu/menu_artist_detail.xml | 17 + app/src/main/res/menu/menu_song.xml | 36 + app/src/main/res/menu/menu_tag_editor.xml | 10 + app/src/main/res/menu/menu_title_playing.xml | 35 + app/src/main/res/values-land/integers.xml | 4 + .../main/res/values-sw360dp-v19/dimens.xml | 7 + .../main/res/values-sw360dp-v20/dimens.xml | 7 + .../main/res/values-sw360dp-v21/dimens.xml | 7 + app/src/main/res/values-sw360dp/dimens.xml | 6 + app/src/main/res/values-v19/dimens.xml | 6 + app/src/main/res/values-v20/dimens.xml | 6 + app/src/main/res/values-v21/colors.xml | 4 + app/src/main/res/values-v21/dimens.xml | 6 + app/src/main/res/values-v21/styles.xml | 23 + app/src/main/res/values-w820dp/dimens.xml | 6 + app/src/main/res/values/attrs.xml | 9 + app/src/main/res/values/colors.xml | 28 + app/src/main/res/values/dimens.xml | 39 + app/src/main/res/values/integers.xml | 4 + app/src/main/res/values/materialcolors.xml | 290 ++ app/src/main/res/values/strings.xml | 65 + app/src/main/res/values/styles.xml | 32 + app/src/main/res/values/styles_parents.xml | 68 + app/src/main/res/values/values.xml | 9 + build.gradle | 19 + gradle.properties | 18 + gradlew | 164 + gradlew.bat | 90 + libraries/drag-sort-listview/.gitignore | 36 + libraries/drag-sort-listview/CHANGELOG.md | 37 + libraries/drag-sort-listview/README.md | 439 +++ libraries/drag-sort-listview/build.gradle | 9 + libraries/drag-sort-listview/demo/.gitignore | 20 + .../demo/AndroidManifest.xml | 50 + .../demo/app-description.txt | 28 + .../drag-sort-listview/demo/build.gradle | 34 + libraries/drag-sort-listview/demo/pom.xml | 63 + .../demo/proguard-project.txt | 20 + .../demo/project.properties | 15 + .../demo/res/drawable-hdpi/delete_x.png | Bin 0 -> 2776 bytes .../demo/res/drawable-hdpi/drag.9.png | Bin 0 -> 935 bytes .../demo/res/drawable-hdpi/dslv_launcher.png | Bin 0 -> 1983 bytes .../demo/res/drawable-ldpi/dslv_launcher.png | Bin 0 -> 1107 bytes .../demo/res/drawable-mdpi/drag.9.png | Bin 0 -> 640 bytes .../demo/res/drawable-mdpi/dslv_launcher.png | Bin 0 -> 1361 bytes .../demo/res/drawable-xhdpi/dslv_launcher.png | Bin 0 -> 2476 bytes .../demo/res/drawable/bg_handle.xml | 6 + .../demo/res/drawable/bg_handle_section1.xml | 6 + .../drawable/bg_handle_section1_selector.xml | 10 + .../demo/res/drawable/bg_handle_section2.xml | 6 + .../drawable/bg_handle_section2_selector.xml | 10 + .../demo/res/drawable/drag.9.png | Bin 0 -> 640 bytes .../demo/res/drawable/section_div.xml | 11 + .../demo/res/layout/bg_handle_main.xml | 7 + .../demo/res/layout/checkable_main.xml | 21 + .../demo/res/layout/cursor_main.xml | 20 + .../demo/res/layout/dslv_fragment_main.xml | 18 + .../demo/res/layout/header_footer.xml | 8 + .../demo/res/layout/hetero_main.xml | 25 + .../demo/res/layout/jazz_artist_list_item.xml | 30 + .../demo/res/layout/launcher.xml | 6 + .../demo/res/layout/launcher_item.xml | 19 + .../demo/res/layout/list_item_bg_handle.xml | 9 + .../demo/res/layout/list_item_checkable.xml | 28 + .../res/layout/list_item_click_remove.xml | 27 + .../demo/res/layout/list_item_handle_left.xml | 21 + .../res/layout/list_item_handle_right.xml | 21 + .../demo/res/layout/list_item_no_handle.xml | 6 + .../demo/res/layout/list_item_radio.xml | 22 + .../demo/res/layout/section_div.xml | 6 + .../demo/res/layout/sections_main.xml | 11 + .../demo/res/layout/test_bed_main.xml | 6 + .../demo/res/layout/warp_main.xml | 26 + .../demo/res/menu/mode_menu.xml | 13 + .../demo/res/values/colors.xml | 10 + .../demo/res/values/dimens.xml | 5 + .../demo/res/values/ids.xml | 5 + .../demo/res/values/strings.xml | 306 ++ .../android/demodslv/ArbItemSizeDSLV.java | 114 + .../com/mobeta/android/demodslv/BGHandle.java | 14 + .../demodslv/CheckableLinearLayout.java | 38 + .../mobeta/android/demodslv/CursorDSLV.java | 65 + .../mobeta/android/demodslv/DSLVFragment.java | 184 + .../demodslv/DSLVFragmentBGHandle.java | 90 + .../android/demodslv/DSLVFragmentClicks.java | 50 + .../android/demodslv/DragInitModeDialog.java | 74 + .../android/demodslv/EnablesDialog.java | 72 + .../com/mobeta/android/demodslv/Launcher.java | 80 + .../demodslv/MultipleChoiceListView.java | 65 + .../android/demodslv/RemoveModeDialog.java | 74 + .../com/mobeta/android/demodslv/Sections.java | 259 ++ .../demodslv/SingleChoiceListView.java | 58 + .../mobeta/android/demodslv/TestBedDSLV.java | 128 + .../com/mobeta/android/demodslv/WarpDSLV.java | 69 + libraries/drag-sort-listview/gradlew | 164 + libraries/drag-sort-listview/gradlew.bat | 90 + .../drag-sort-listview/library/.gitignore | 22 + .../library/AndroidManifest.xml | 11 + .../drag-sort-listview/library/build.gradle | 34 + libraries/drag-sort-listview/library/pom.xml | 88 + .../library/proguard-project.txt | 20 + .../library/project.properties | 16 + .../library/res/values/dslv_attrs.xml | 39 + .../android/dslv/DragSortController.java | 466 +++ .../android/dslv/DragSortCursorAdapter.java | 232 ++ .../mobeta/android/dslv/DragSortItemView.java | 95 + .../dslv/DragSortItemViewCheckable.java | 53 + .../mobeta/android/dslv/DragSortListView.java | 2983 +++++++++++++++++ .../dslv/ResourceDragSortCursorAdapter.java | 135 + .../dslv/SimpleDragSortCursorAdapter.java | 417 +++ .../android/dslv/SimpleFloatViewManager.java | 85 + libraries/drag-sort-listview/pom.xml | 101 + libraries/drag-sort-listview/settings.gradle | 2 + .../tools/demo-sign-align.sh | 16 + .../drag-sort-listview/tools/doc-dslv.sh | 21 + libraries/drag-sort-listview/tools/dslv.py | 264 ++ .../drag-sort-listview/tools/pkg-dslv.sh | 23 + settings.gradle | 2 + 277 files changed, 20259 insertions(+) create mode 100644 .gitignore create mode 100644 app/build.gradle create mode 100644 app/manifest-merger-release-report.txt create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/kabouzeid/materialmusic/ApplicationTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java create mode 100644 app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/App.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/adapter/AlbumViewGridAdapter.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/adapter/ArtistViewListAdapter.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/adapter/NavigationDrawerItemAdapter.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/adapter/PlayListAdapter.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongAdapter.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongViewListAdapter.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/comparator/AlbumAlphabeticComparator.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/comparator/ArtistAlphabeticComparator.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/comparator/SongAlphabeticComparator.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/comparator/SongTrackNumberComparator.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/helper/MusicPlayerRemote.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/helper/NotificationHelper.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/helper/PlayingQueueDialogHelper.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/helper/Shuffler.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/helper/SongDetailDialogHelper.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabSearchAbleFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabViewsDisableAble.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/interfaces/OnMusicRemoteEventListener.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/LastFMUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumImageLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumInfoUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistBiographyLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistImageLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistInfoUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistThumbnailLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumSongLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistAlbumLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistSongLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/SongFileLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/loader/SongLoader.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/misc/AppKeys.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/misc/SmallAnimatorListener.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/misc/SmallObservableScrollViewCallbacks.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/misc/SmallOnGestureListener.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/misc/SmallTransitionListener.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/model/Album.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/model/Artist.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/model/MusicRemoteEvent.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/model/NavigationDrawerItem.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/model/Song.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/provider/AlbumJSONStore.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/provider/ArtistJSONStore.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/service/MediaButtonIntentReceiver.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/service/MusicService.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/AlbumDetailActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/ArtistDetailActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MainActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MusicControllerActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsBaseActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsFabActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AbsTagEditorActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AlbumTagEditorActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/SongTagEditorActivity.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/NavigationDrawerFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/AbsViewPagerTabArtistListFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistAlbumFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistBioFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistSongListFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/AlbumViewFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/ArtistViewFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/SongViewFragment.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/util/ImageLoaderUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/util/InternalStorageUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/util/MusicUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/util/Util.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/util/ViewUtil.java create mode 100644 app/src/main/java/com/kabouzeid/materialmusic/view/SquareImageView.java create mode 100644 app/src/main/res/drawable-hdpi/default_album_art.png create mode 100644 app/src/main/res/drawable-hdpi/drawer_shadow.9.png create mode 100644 app/src/main/res/drawable-hdpi/ic_drawer.png create mode 100644 app/src/main/res/drawable-mdpi/default_album_art.png create mode 100644 app/src/main/res/drawable-mdpi/drawer_shadow.9.png create mode 100644 app/src/main/res/drawable-mdpi/ic_drawer.png create mode 100644 app/src/main/res/drawable-v21/notification_selector.xml create mode 100644 app/src/main/res/drawable-xhdpi/default_album_art.png create mode 100644 app/src/main/res/drawable-xhdpi/drawer_shadow.9.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_drawer.png create mode 100644 app/src/main/res/drawable-xxhdpi/album.png create mode 100644 app/src/main/res/drawable-xxhdpi/close.png create mode 100644 app/src/main/res/drawable-xxhdpi/default_album_art.png create mode 100644 app/src/main/res/drawable-xxhdpi/default_artist_image.png create mode 100644 app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png create mode 100644 app/src/main/res/drawable-xxhdpi/heart.png create mode 100644 app/src/main/res/drawable-xxhdpi/heart_outline.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_drawer.png create mode 100755 app/src/main/res/drawable-xxhdpi/ic_overflow.png create mode 100644 app/src/main/res/drawable-xxhdpi/interpret.png create mode 100644 app/src/main/res/drawable-xxhdpi/music_box.png create mode 100644 app/src/main/res/drawable-xxhdpi/music_box_outline.png create mode 100644 app/src/main/res/drawable-xxhdpi/play_box.png create mode 100644 app/src/main/res/drawable-xxhdpi/playlist.png create mode 100644 app/src/main/res/drawable-xxhdpi/settings.png create mode 100644 app/src/main/res/drawable-xxhdpi/songs.png create mode 100644 app/src/main/res/drawable-xxxhdpi/default_album_art.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_done_white_48dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_pause_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_repeat_grey600_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_repeat_one_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_repeat_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_shuffle_grey600_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_shuffle_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_skip_next_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_skip_previous_white_48dp.png create mode 100755 app/src/main/res/drawable-xxxhdpi/ic_speaker_white_48dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/logo.png create mode 100644 app/src/main/res/drawable-xxxhdpi/notification_icon.png create mode 100755 app/src/main/res/drawable/list_item_activated.xml create mode 100755 app/src/main/res/drawable/list_item_activated_dark.xml create mode 100755 app/src/main/res/drawable/list_item_selected.xml create mode 100755 app/src/main/res/drawable/list_item_selected_dark.xml create mode 100755 app/src/main/res/drawable/list_selector.xml create mode 100755 app/src/main/res/drawable/list_selector_dark.xml create mode 100644 app/src/main/res/drawable/navigation_drawer_gradient.xml create mode 100755 app/src/main/res/drawable/transparent.xml create mode 100644 app/src/main/res/layout-land/activity_music_controller.xml create mode 100644 app/src/main/res/layout-v21/notification_playing.xml create mode 100644 app/src/main/res/layout-v21/notification_playing_expanded.xml create mode 100644 app/src/main/res/layout/activity_album_detail.xml create mode 100644 app/src/main/res/layout/activity_album_tag_editor.xml create mode 100644 app/src/main/res/layout/activity_artist_detail.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_music_controller.xml create mode 100644 app/src/main/res/layout/activity_song_tag_editor.xml create mode 100644 app/src/main/res/layout/album_tile.xml create mode 100644 app/src/main/res/layout/dialog_file_details.xml create mode 100644 app/src/main/res/layout/dialog_loading.xml create mode 100644 app/src/main/res/layout/dialog_playlist.xml create mode 100644 app/src/main/res/layout/fragment_albumview.xml create mode 100644 app/src/main/res/layout/fragment_artist_view.xml create mode 100644 app/src/main/res/layout/fragment_drawer.xml create mode 100644 app/src/main/res/layout/fragment_gridview.xml create mode 100644 app/src/main/res/layout/fragment_navigation_drawer.xml create mode 100644 app/src/main/res/layout/fragment_place_holder.xml create mode 100644 app/src/main/res/layout/fragment_songview.xml create mode 100644 app/src/main/res/layout/item_artist_details_biography.xml create mode 100644 app/src/main/res/layout/item_artist_view.xml create mode 100644 app/src/main/res/layout/item_navigation_drawer.xml create mode 100644 app/src/main/res/layout/item_playlist.xml create mode 100644 app/src/main/res/layout/item_song.xml create mode 100644 app/src/main/res/layout/item_song_view.xml create mode 100644 app/src/main/res/layout/notification_playing.xml create mode 100644 app/src/main/res/layout/notification_playing_expanded.xml create mode 100644 app/src/main/res/layout/tab_indicator.xml create mode 100644 app/src/main/res/menu/drawer.xml create mode 100644 app/src/main/res/menu/global.xml create mode 100644 app/src/main/res/menu/menu_album_detail.xml create mode 100644 app/src/main/res/menu/menu_artist_detail.xml create mode 100644 app/src/main/res/menu/menu_song.xml create mode 100644 app/src/main/res/menu/menu_tag_editor.xml create mode 100644 app/src/main/res/menu/menu_title_playing.xml create mode 100644 app/src/main/res/values-land/integers.xml create mode 100644 app/src/main/res/values-sw360dp-v19/dimens.xml create mode 100644 app/src/main/res/values-sw360dp-v20/dimens.xml create mode 100644 app/src/main/res/values-sw360dp-v21/dimens.xml create mode 100644 app/src/main/res/values-sw360dp/dimens.xml create mode 100644 app/src/main/res/values-v19/dimens.xml create mode 100644 app/src/main/res/values-v20/dimens.xml create mode 100644 app/src/main/res/values-v21/colors.xml create mode 100644 app/src/main/res/values-v21/dimens.xml create mode 100644 app/src/main/res/values-v21/styles.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/main/res/values/materialcolors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/styles_parents.xml create mode 100644 app/src/main/res/values/values.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 libraries/drag-sort-listview/.gitignore create mode 100644 libraries/drag-sort-listview/CHANGELOG.md create mode 100644 libraries/drag-sort-listview/README.md create mode 100644 libraries/drag-sort-listview/build.gradle create mode 100644 libraries/drag-sort-listview/demo/.gitignore create mode 100644 libraries/drag-sort-listview/demo/AndroidManifest.xml create mode 100644 libraries/drag-sort-listview/demo/app-description.txt create mode 100644 libraries/drag-sort-listview/demo/build.gradle create mode 100644 libraries/drag-sort-listview/demo/pom.xml create mode 100644 libraries/drag-sort-listview/demo/proguard-project.txt create mode 100644 libraries/drag-sort-listview/demo/project.properties create mode 100644 libraries/drag-sort-listview/demo/res/drawable-hdpi/delete_x.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable-hdpi/drag.9.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable-hdpi/dslv_launcher.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable-ldpi/dslv_launcher.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable-mdpi/drag.9.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable-mdpi/dslv_launcher.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable-xhdpi/dslv_launcher.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable/bg_handle.xml create mode 100644 libraries/drag-sort-listview/demo/res/drawable/bg_handle_section1.xml create mode 100644 libraries/drag-sort-listview/demo/res/drawable/bg_handle_section1_selector.xml create mode 100644 libraries/drag-sort-listview/demo/res/drawable/bg_handle_section2.xml create mode 100644 libraries/drag-sort-listview/demo/res/drawable/bg_handle_section2_selector.xml create mode 100644 libraries/drag-sort-listview/demo/res/drawable/drag.9.png create mode 100644 libraries/drag-sort-listview/demo/res/drawable/section_div.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/bg_handle_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/checkable_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/cursor_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/dslv_fragment_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/header_footer.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/hetero_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/jazz_artist_list_item.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/launcher.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/launcher_item.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_bg_handle.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_checkable.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_click_remove.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_handle_left.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_handle_right.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_no_handle.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/list_item_radio.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/section_div.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/sections_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/test_bed_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/layout/warp_main.xml create mode 100644 libraries/drag-sort-listview/demo/res/menu/mode_menu.xml create mode 100644 libraries/drag-sort-listview/demo/res/values/colors.xml create mode 100644 libraries/drag-sort-listview/demo/res/values/dimens.xml create mode 100644 libraries/drag-sort-listview/demo/res/values/ids.xml create mode 100644 libraries/drag-sort-listview/demo/res/values/strings.xml create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/ArbItemSizeDSLV.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/BGHandle.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/CheckableLinearLayout.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/CursorDSLV.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/DSLVFragment.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/DSLVFragmentBGHandle.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/DSLVFragmentClicks.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/DragInitModeDialog.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/EnablesDialog.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/Launcher.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/MultipleChoiceListView.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/RemoveModeDialog.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/Sections.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/SingleChoiceListView.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/TestBedDSLV.java create mode 100644 libraries/drag-sort-listview/demo/src/com/mobeta/android/demodslv/WarpDSLV.java create mode 100755 libraries/drag-sort-listview/gradlew create mode 100644 libraries/drag-sort-listview/gradlew.bat create mode 100644 libraries/drag-sort-listview/library/.gitignore create mode 100644 libraries/drag-sort-listview/library/AndroidManifest.xml create mode 100644 libraries/drag-sort-listview/library/build.gradle create mode 100644 libraries/drag-sort-listview/library/pom.xml create mode 100644 libraries/drag-sort-listview/library/proguard-project.txt create mode 100644 libraries/drag-sort-listview/library/project.properties create mode 100644 libraries/drag-sort-listview/library/res/values/dslv_attrs.xml create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/DragSortController.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/DragSortCursorAdapter.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/DragSortItemView.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/DragSortItemViewCheckable.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/DragSortListView.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java create mode 100644 libraries/drag-sort-listview/library/src/com/mobeta/android/dslv/SimpleFloatViewManager.java create mode 100644 libraries/drag-sort-listview/pom.xml create mode 100644 libraries/drag-sort-listview/settings.gradle create mode 100755 libraries/drag-sort-listview/tools/demo-sign-align.sh create mode 100755 libraries/drag-sort-listview/tools/doc-dslv.sh create mode 100644 libraries/drag-sort-listview/tools/dslv.py create mode 100755 libraries/drag-sort-listview/tools/pkg-dslv.sh create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bfb19ef6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +app/src/main/res/values/com_crashlytics_export_strings.xml +app/src/main/assets/crashlytics-build.properties +app/app-release.apk +app/build +app/crashlytics.properties + +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ +target/ + +# external libraries +libs/ + +# gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties +.gitattributes + +# Eclipse project files +.classpath +.project + +# Android Studio +*.iml +.idea + +# Mac +.DS_Store +gradle \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..32815916 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,64 @@ +buildscript { + repositories { + maven { url 'https://maven.fabric.io/public' } + } + + dependencies { + classpath 'io.fabric.tools:gradle:1.+' + } +} +apply plugin: 'com.android.application' +apply plugin: 'io.fabric' + +repositories { + maven { url 'https://maven.fabric.io/public' } +} + + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.1" + + defaultConfig { + applicationId "com.kabouzeid.materialmusic" + minSdkVersion 16 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.+' + compile 'com.android.support:gridlayout-v7:21.0.+' + compile 'com.android.support:recyclerview-v7:21.0.+' + compile 'com.android.support:palette-v7:21.0.+' + compile 'com.android.support:support-v13:21.0.+' + compile 'com.nhaarman.listviewanimations:lib-core:3.1.0@aar' + compile 'com.nhaarman.listviewanimations:lib-manipulation:3.1.0@aar' + compile 'com.nhaarman.listviewanimations:lib-core-slh:3.1.0@aar' + compile 'com.nineoldandroids:library:2.4.0+' + compile 'com.melnykov:floatingactionbutton:1.1.+' + compile 'com.github.ksoichiro:android-observablescrollview:1.3.0' + compile 'com.mcxiaoke.volley:library:1.0.+' + compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' + compile 'com.afollestad:material-dialogs:0.6.1.5' + compile('com.crashlytics.sdk.android:crashlytics:2.1.0@aar') { + transitive = true; + } + compile files('/Users/karim/Google Drive/Dokumente/AndroidStudioProjects/MaterialMusic/libs/jaudiotagger-2.0.4-20111207.115108-15.jar') + compile project(':libraries:drag-sort-listview:library') +} diff --git a/app/manifest-merger-release-report.txt b/app/manifest-merger-release-report.txt new file mode 100644 index 00000000..0ab9f9e3 --- /dev/null +++ b/app/manifest-merger-release-report.txt @@ -0,0 +1,192 @@ +-- Merging decision tree log --- +manifest +ADDED from AndroidManifest.xml:2:1 + package + ADDED from AndroidManifest.xml:3:11 + INJECTED from AndroidManifest.xml:0:0 + INJECTED from AndroidManifest.xml:0:0 + android:versionName + INJECTED from AndroidManifest.xml:0:0 + INJECTED from AndroidManifest.xml:0:0 + xmlns:android + ADDED from AndroidManifest.xml:2:11 + android:versionCode + INJECTED from AndroidManifest.xml:0:0 + INJECTED from AndroidManifest.xml:0:0 +uses-permission#android.permission.WAKE_LOCK +ADDED from AndroidManifest.xml:5:5 + android:name + ADDED from AndroidManifest.xml:5:22 +uses-permission#android.permission.READ_EXTERNAL_STORAGE +ADDED from AndroidManifest.xml:6:5 + android:name + ADDED from AndroidManifest.xml:6:22 +uses-permission#android.permission.WRITE_EXTERNAL_STORAGE +ADDED from AndroidManifest.xml:7:5 + android:name + ADDED from AndroidManifest.xml:7:22 +uses-permission#android.permission.VIBRATE +ADDED from AndroidManifest.xml:8:5 + android:name + ADDED from AndroidManifest.xml:8:22 +uses-permission#android.permission.INTERNET +ADDED from AndroidManifest.xml:9:5 +MERGED from com.crashlytics.sdk.android:crashlytics:2.1.0:11:5 + android:name + ADDED from AndroidManifest.xml:9:22 +application +ADDED from AndroidManifest.xml:11:5 +MERGED from com.android.support:appcompat-v7:21.0.3:16:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.android.support:gridlayout-v7:21.0.3:16:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.android.support:recyclerview-v7:21.0.3:17:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.android.support:palette-v7:21.0.3:16:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.android.support:support-v13:21.0.3:16:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.nhaarman.listviewanimations:lib-core:3.1.0:26:5 +MERGED from com.nhaarman.listviewanimations:lib-manipulation:3.1.0:26:5 +MERGED from com.nhaarman.listviewanimations:lib-core-slh:3.1.0:26:5 +MERGED from com.melnykov:floatingactionbutton:1.1.0:12:5 +MERGED from com.android.support:recyclerview-v7:21.0.3:17:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.android.support:recyclerview-v7:21.0.3:17:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.android.support:appcompat-v7:21.0.3:16:5 +MERGED from com.android.support:support-v4:21.0.3:16:5 +MERGED from com.crashlytics.sdk.android:crashlytics:2.1.0:13:5 +MERGED from com.crashlytics.sdk.android:beta:1.0.2:11:5 +MERGED from io.fabric.sdk.android:fabric:1.0.2:11:5 +MERGED from com.crashlytics.sdk.android:answers:1.0.2:11:5 +MERGED from io.fabric.sdk.android:fabric:1.0.2:11:5 +MERGED from io.fabric.sdk.android:fabric:1.0.2:11:5 + android:label + ADDED from AndroidManifest.xml:15:9 + android:allowBackup + ADDED from AndroidManifest.xml:13:9 + android:icon + ADDED from AndroidManifest.xml:14:9 + android:theme + ADDED from AndroidManifest.xml:16:9 + android:name + ADDED from AndroidManifest.xml:12:9 +activity#com.kabouzeid.materialmusic.ui.activities.MainActivity +ADDED from AndroidManifest.xml:17:9 + android:label + ADDED from AndroidManifest.xml:19:13 + android:name + ADDED from AndroidManifest.xml:18:13 +intent-filter#android.intent.action.MAIN+android.intent.category.LAUNCHER +ADDED from AndroidManifest.xml:20:13 +action#android.intent.action.MAIN +ADDED from AndroidManifest.xml:21:17 + android:name + ADDED from AndroidManifest.xml:21:25 +category#android.intent.category.LAUNCHER +ADDED from AndroidManifest.xml:23:17 + android:name + ADDED from AndroidManifest.xml:23:27 +activity#com.kabouzeid.materialmusic.ui.activities.AlbumDetailActivity +ADDED from AndroidManifest.xml:26:9 + android:name + ADDED from AndroidManifest.xml:26:19 +activity#com.kabouzeid.materialmusic.ui.activities.ArtistDetailActivity +ADDED from AndroidManifest.xml:28:9 + android:name + ADDED from AndroidManifest.xml:28:19 +activity#com.kabouzeid.materialmusic.ui.activities.MusicControllerActivity +ADDED from AndroidManifest.xml:30:9 + android:parentActivityName + ADDED from AndroidManifest.xml:32:13 + android:name + ADDED from AndroidManifest.xml:31:13 +service#com.kabouzeid.materialmusic.service.MusicService +ADDED from AndroidManifest.xml:35:9 + android:enabled + ADDED from AndroidManifest.xml:37:13 + android:name + ADDED from AndroidManifest.xml:36:13 +receiver#com.kabouzeid.materialmusic.service.MediaButtonIntentReceiver +ADDED from AndroidManifest.xml:40:9 + android:name + ADDED from AndroidManifest.xml:40:19 +intent-filter#android.intent.action.MEDIA_BUTTON +ADDED from AndroidManifest.xml:41:13 +action#android.intent.action.MEDIA_BUTTON +ADDED from AndroidManifest.xml:42:17 + android:name + ADDED from AndroidManifest.xml:42:25 +meta-data#com.crashlytics.ApiKey +ADDED from AndroidManifest.xml:46:9 + android:value + ADDED from AndroidManifest.xml:48:13 + android:name + ADDED from AndroidManifest.xml:47:13 +activity#com.kabouzeid.materialmusic.ui.activities.tageditor.SongTagEditorActivity +ADDED from AndroidManifest.xml:50:9 + android:label + ADDED from AndroidManifest.xml:52:13 + android:windowSoftInputMode + ADDED from AndroidManifest.xml:53:13 + android:name + ADDED from AndroidManifest.xml:51:13 +activity#com.kabouzeid.materialmusic.ui.activities.tageditor.AlbumTagEditorActivity +ADDED from AndroidManifest.xml:55:9 + android:label + ADDED from AndroidManifest.xml:57:13 + android:name + ADDED from AndroidManifest.xml:56:13 +uses-sdk +INJECTED from AndroidManifest.xml:0:0 reason: use-sdk injection requested +MERGED from com.android.support:appcompat-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.android.support:gridlayout-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.android.support:recyclerview-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.android.support:palette-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.android.support:support-v13:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.nhaarman.listviewanimations:lib-core:3.1.0:22:5 +MERGED from com.nhaarman.listviewanimations:lib-manipulation:3.1.0:22:5 +MERGED from com.nhaarman.listviewanimations:lib-core-slh:3.1.0:22:5 +MERGED from com.melnykov:floatingactionbutton:1.1.0:8:5 +MERGED from com.android.support:recyclerview-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.github.ksoichiro:android-observablescrollview:1.3.0:21:5 +MERGED from com.android.support:recyclerview-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.afollestad:material-dialogs:0.4.8:8:5 +MERGED from com.android.support:appcompat-v7:21.0.3:15:5 +MERGED from com.android.support:support-v4:21.0.3:15:5 +MERGED from com.crashlytics.sdk.android:crashlytics:2.1.0:7:5 +MERGED from com.crashlytics.sdk.android:beta:1.0.2:7:5 +MERGED from io.fabric.sdk.android:fabric:1.0.2:7:5 +MERGED from com.crashlytics.sdk.android:answers:1.0.2:7:5 +MERGED from io.fabric.sdk.android:fabric:1.0.2:7:5 +MERGED from io.fabric.sdk.android:fabric:1.0.2:7:5 + android:targetSdkVersion + INJECTED from AndroidManifest.xml:0:0 + INJECTED from AndroidManifest.xml:0:0 + android:minSdkVersion + INJECTED from AndroidManifest.xml:0:0 + INJECTED from AndroidManifest.xml:0:0 +activity#android.support.v7.widget.TestActivity +ADDED from com.android.support:recyclerview-v7:21.0.3:18:9 +MERGED from com.android.support:recyclerview-v7:21.0.3:18:9 +MERGED from com.android.support:recyclerview-v7:21.0.3:18:9 + android:label + ADDED from com.android.support:recyclerview-v7:21.0.3:18:19 + android:name + ADDED from com.android.support:recyclerview-v7:21.0.3:18:60 +activity#com.nhaarman.listviewanimations.itemmanipulation.swipedismiss.SwipeTouchListenerTestActivity +ADDED from com.nhaarman.listviewanimations:lib-manipulation:3.1.0:27:9 + android:name + ADDED from com.nhaarman.listviewanimations:lib-manipulation:3.1.0:27:19 +activity#com.nhaarman.listviewanimations.itemmanipulation.dragdrop.DynamicListViewTestActivity +ADDED from com.nhaarman.listviewanimations:lib-manipulation:3.1.0:28:9 + android:name + ADDED from com.nhaarman.listviewanimations:lib-manipulation:3.1.0:28:19 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..56446cc5 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Applications/android_sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} +-keep class !android.support.v7.internal.view.menu.**,** {*;} +-dontwarn +-ignorewarnings +-dontshrink \ No newline at end of file diff --git a/app/src/androidTest/java/com/kabouzeid/materialmusic/ApplicationTest.java b/app/src/androidTest/java/com/kabouzeid/materialmusic/ApplicationTest.java new file mode 100644 index 00000000..59b8d703 --- /dev/null +++ b/app/src/androidTest/java/com/kabouzeid/materialmusic/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.kabouzeid.materialmusic; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..27e46cd5 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java b/app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java new file mode 100644 index 00000000..92672500 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java @@ -0,0 +1,321 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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.google.samples.apps.iosched.ui.widget; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as to + * the user's scroll progress. + *

+ * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(android.support.v4.view.ViewPager)} providing it the ViewPager this layout is being used for. + *

+ * The colors can be customized in two ways. The first and simplest is to provide an array of colors + * via {@link #setSelectedIndicatorColors(int...)}. The + * alternative is via the {@link com.google.samples.apps.iosched.ui.widget.SlidingTabLayout.TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + *

+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + */ +public class SlidingTabLayout extends HorizontalScrollView { + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(com.google.samples.apps.iosched.ui.widget.SlidingTabLayout.TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + private boolean mDistributeEvenly; + + private ViewPager mViewPager; + private SparseArray mContentDescriptions = new SparseArray(); + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link com.google.samples.apps.iosched.ui.widget.SlidingTabLayout.TabColorizer} to be used. + *

+ * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + public void setDistributeEvenly(boolean distributeEvenly) { + mDistributeEvenly = distributeEvenly; + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Set the {@link android.support.v4.view.ViewPager.OnPageChangeListener}. When using {@link com.google.samples.apps.iosched.ui.widget.SlidingTabLayout} you are + * required to set any {@link android.support.v4.view.ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see android.support.v4.view.ViewPager#setOnPageChangeListener(android.support.v4.view.ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link android.widget.TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + textView.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + textView.setAllCaps(true); + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + if (mDistributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + String desc = mContentDescriptions.get(i, null); + if (desc != null) { + tabView.setContentDescription(desc); + } + + mTabStrip.addView(tabView); + if (i == mViewPager.getCurrentItem()) { + tabView.setSelected(true); + } + } + } + + public void setContentDescription(int i, String desc) { + mContentDescriptions.put(i, desc); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + mTabStrip.getChildAt(i).setSelected(position == i); + } + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} diff --git a/app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java b/app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java new file mode 100644 index 00000000..ae2c1fb9 --- /dev/null +++ b/app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java @@ -0,0 +1,168 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * 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.google.samples.apps.iosched.ui.widget; + +import android.R; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/App.java b/app/src/main/java/com/kabouzeid/materialmusic/App.java new file mode 100644 index 00000000..20f8cc4c --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/App.java @@ -0,0 +1,84 @@ +package com.kabouzeid.materialmusic; + +import android.app.Application; +import android.app.Fragment; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.preference.PreferenceManager; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; +import com.crashlytics.android.Crashlytics; +import com.kabouzeid.materialmusic.helper.MusicPlayerRemote; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.util.ImageLoaderUtil; + +import io.fabric.sdk.android.Fabric; + +/** + * Created by karim on 25.11.14. + */ +public class App extends Application { + private static final String TAG = App.class.getSimpleName(); + + public Fragment[] MainActivityFragments = new Fragment[5]; + private MusicPlayerRemote playerRemote; + private int appTheme; + private SharedPreferences defaultSharedPreferences; + private RequestQueue requestQueue; + + @Override + public void onCreate() { + super.onCreate(); + Fabric.with(this, new Crashlytics()); + ImageLoaderUtil.initImageLoader(this); + } + + public MusicPlayerRemote getMusicPlayerRemote() { + if (playerRemote == null) { + playerRemote = new MusicPlayerRemote(this); + playerRemote.restorePreviousState(); + } + return playerRemote; + } + + public SharedPreferences getDefaultSharedPreferences() { + if (defaultSharedPreferences == null) { + defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + } + return defaultSharedPreferences; + } + + public int getAppTheme() { + if (appTheme == 0) { + appTheme = getDefaultSharedPreferences().getInt(AppKeys.SP_THEME, R.style.Theme_MaterialMusic); + } + return appTheme; + } + + public void setAppTheme(int appTheme) { + this.appTheme = appTheme; + defaultSharedPreferences.edit().putInt(AppKeys.SP_THEME, appTheme); + } + + public boolean isTablet() { + return getResources().getConfiguration().smallestScreenWidthDp >= 600; + } + + public boolean isInPortraitMode() { + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + public RequestQueue getRequestQueue() { + if (requestQueue == null) { + requestQueue = Volley.newRequestQueue(this); + } + return requestQueue; + } + + public void addToRequestQueue(Request request) { + request.setTag(TAG); + getRequestQueue().add(request); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/adapter/AlbumViewGridAdapter.java b/app/src/main/java/com/kabouzeid/materialmusic/adapter/AlbumViewGridAdapter.java new file mode 100644 index 00000000..c5828f90 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/adapter/AlbumViewGridAdapter.java @@ -0,0 +1,105 @@ +package com.kabouzeid.materialmusic.adapter; + +import android.content.Context; +import android.graphics.Bitmap; +import android.support.v7.graphics.Palette; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.model.Album; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; +import com.kabouzeid.materialmusic.util.ViewUtil; +import com.kabouzeid.materialmusic.view.SquareImageView; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import java.util.List; + +/** + * Created by karim on 24.11.14. + */ +public class AlbumViewGridAdapter extends ArrayAdapter { + public static final String TAG = AlbumViewGridAdapter.class.getSimpleName(); + private Context context; + private boolean usePalette; + + public AlbumViewGridAdapter(Context context, List objects) { + super(context, R.layout.album_tile, objects); + this.context = context; + usePalette = true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Album album = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.album_tile, parent, false); + } + final SquareImageView albumArt = (SquareImageView) convertView.findViewById(R.id.album_art); + final TextView title = (TextView) convertView.findViewById(R.id.album_title); + final TextView artist = (TextView) convertView.findViewById(R.id.album_interpret); + final View footer = convertView.findViewById(R.id.footer); + + title.setText(album.title); + artist.setText(album.artistName); + + ImageLoader.getInstance().displayImage(MusicUtil.getAlbumArtUri(album.id).toString(), albumArt, new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + albumArt.setImageDrawable(null); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + if (usePalette) { + paletteBugFixBlackAndWhite(title, artist, footer); + } + albumArt.setImageResource(R.drawable.default_album_art); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + if (usePalette) { + applyPalette(loadedImage, title, artist, footer); + } + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + + } + }); + + return convertView; + } + + private void applyPalette(Bitmap bitmap, final TextView title, final TextView artist, final View footer) { + Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + final Palette.Swatch vibrantSwatch = palette.getVibrantSwatch(); + if (vibrantSwatch != null) { + title.setTextColor(vibrantSwatch.getTitleTextColor()); + artist.setTextColor(vibrantSwatch.getTitleTextColor()); + ViewUtil.animateViewColor(footer, Util.resolveColor(context, R.attr.colorPrimary), + vibrantSwatch.getRgb()); + } else { + paletteBugFixBlackAndWhite(title, artist, footer); + } + } + }); + } + + private void paletteBugFixBlackAndWhite(TextView title, TextView artist, View footer) { + title.setTextColor(Util.resolveColor(context, R.attr.title_text_color)); + artist.setTextColor(Util.resolveColor(context, R.attr.caption_text_color)); + ViewUtil.animateViewColor(footer, Util.resolveColor(context, R.attr.colorPrimary), + Util.resolveColor(context, R.attr.colorPrimary)); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/adapter/ArtistViewListAdapter.java b/app/src/main/java/com/kabouzeid/materialmusic/adapter/ArtistViewListAdapter.java new file mode 100644 index 00000000..37d1488a --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/adapter/ArtistViewListAdapter.java @@ -0,0 +1,60 @@ +package com.kabouzeid.materialmusic.adapter; + +import android.content.Context; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.lastfm.artist.LastFMArtistThumbnailLoader; +import com.kabouzeid.materialmusic.model.Artist; + +import java.util.List; + +/** + * Created by karim on 29.12.14. + */ +public class ArtistViewListAdapter extends ArrayAdapter { + private Context context; + + + public ArtistViewListAdapter(Context context, List objects) { + super(context, R.layout.item_artist_view, objects); + this.context = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Artist artist = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.item_artist_view, parent, false); + } + final TextView artistName = (TextView) convertView.findViewById(R.id.artist_name); + final ImageView artistArt = (ImageView) convertView.findViewById(R.id.artist_image); + + artistName.setText(artist.name); + artistArt.setImageResource(R.drawable.default_artist_image); + + final Object tag = artist.name; + artistArt.setTag(tag); + + LastFMArtistThumbnailLoader.loadArtistThumbnail(context, artist.name, new LastFMArtistThumbnailLoader.ArtistThumbnailLoaderCallback() { + @Override + public void onArtistThumbnailLoaded(Bitmap thumbnail) { + if (artistArt.getTag().equals(tag)) { + if (thumbnail != null) { + artistArt.setImageBitmap(thumbnail); + } else { + artistArt.setImageResource(R.drawable.default_artist_image); + } + } + } + }); + + return convertView; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/adapter/NavigationDrawerItemAdapter.java b/app/src/main/java/com/kabouzeid/materialmusic/adapter/NavigationDrawerItemAdapter.java new file mode 100644 index 00000000..e2e82a76 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/adapter/NavigationDrawerItemAdapter.java @@ -0,0 +1,51 @@ +package com.kabouzeid.materialmusic.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.model.NavigationDrawerItem; +import com.kabouzeid.materialmusic.util.Util; + +import java.util.List; + +/** + * Created by karim on 23.11.14. + */ +public class NavigationDrawerItemAdapter extends ArrayAdapter { + private int currentChecked = -1; + + public NavigationDrawerItemAdapter(Context context, int resource, List objects) { + super(context, resource, objects); + } + + public void setChecked(int position) { + currentChecked = position; + notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + NavigationDrawerItem item = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_navigation_drawer, parent, false); + } + TextView title = (TextView) convertView.findViewById(R.id.title); + ImageView icon = (ImageView) convertView.findViewById(R.id.album_art); + title.setText(item.title); + icon.setImageResource(item.imageRes); + if (position == currentChecked) { + title.setTextColor(Util.resolveColor(getContext(), R.attr.colorAccent)); + } else { + title.setTextColor(Util.resolveColor(getContext(), R.attr.title_text_color)); + } + View container = convertView.findViewById(R.id.container); + container.setActivated(position == currentChecked); + return convertView; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/adapter/PlayListAdapter.java b/app/src/main/java/com/kabouzeid/materialmusic/adapter/PlayListAdapter.java new file mode 100644 index 00000000..9d0f9837 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/adapter/PlayListAdapter.java @@ -0,0 +1,47 @@ +package com.kabouzeid.materialmusic.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.model.Song; + +import java.util.List; + +/** + * Created by karim on 24.01.15. + */ +public class PlayListAdapter extends ArrayAdapter { + private Context context; + private App app; + + public PlayListAdapter(Context context, List playList) { + super(context, R.layout.item_playlist, playList); + this.context = context; + app = (App) context.getApplicationContext(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Song song = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.item_playlist, parent, false); + } + final TextView title = (TextView) convertView.findViewById(R.id.song_title); + final ImageView albumArt = (ImageView) convertView.findViewById(R.id.album_art); + + title.setText(song.title); + if (app.getMusicPlayerRemote().getCurrentSongId() == song.id) { + albumArt.setImageResource(R.drawable.ic_speaker_white_48dp); + } else { + albumArt.setImageBitmap(null); + } + return convertView; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongAdapter.java b/app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongAdapter.java new file mode 100644 index 00000000..3ce09172 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongAdapter.java @@ -0,0 +1,100 @@ +package com.kabouzeid.materialmusic.adapter.songadapter; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.helper.SongDetailDialogHelper; +import com.kabouzeid.materialmusic.loader.SongFileLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.ui.activities.tageditor.SongTagEditorActivity; +import com.kabouzeid.materialmusic.util.MusicUtil; + +import java.io.File; +import java.util.List; + +/** + * Created by karim on 27.11.14. + */ +public class SongAdapter extends ArrayAdapter { + public static final String TAG = SongAdapter.class.getSimpleName(); + protected Context context; + protected GoToAble goToAble; + + public SongAdapter(Context context, GoToAble goToAble, List objects) { + super(context, R.layout.item_song, objects); + this.context = context; + this.goToAble = goToAble; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Song song = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_song, parent, false); + } + TextView songTitle = (TextView) convertView.findViewById(R.id.song_title); + TextView trackNumber = (TextView) convertView.findViewById(R.id.track_number); + TextView songDuration = (TextView) convertView.findViewById(R.id.song_duration); + ImageView overflowButton = (ImageView) convertView.findViewById(R.id.menu); + + overflowButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + PopupMenu popupMenu = new PopupMenu(context, v); + popupMenu.inflate(R.menu.menu_song); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_tag_editor: + Intent intent = new Intent(context, SongTagEditorActivity.class); + intent.putExtra(AppKeys.E_ID, song.id); + context.startActivity(intent); + return true; + case R.id.action_details: + String songFilePath = SongFileLoader.getSongFile(context, song.id); + File songFile = new File(songFilePath); + SongDetailDialogHelper.getDialog(context, songFile).show(); + return true; + case R.id.action_go_to_album: + if (goToAble != null) { + goToAble.goToAlbum(song.albumId); + } + return true; + case R.id.action_go_to_artist: + if (goToAble != null) { + goToAble.goToArtist(song.artistId); + } + return true; + } + return false; + } + }); + popupMenu.show(); + } + }); + + songTitle.setText(song.title); + trackNumber.setText(String.valueOf(MusicUtil.getFixedTrackNumber(song.trackNumber))); + songDuration.setText(MusicUtil.getReadableDurationString(song.duration)); + + return convertView; + } + + public static interface GoToAble { + public void goToAlbum(int albumId); + + public void goToArtist(int artistId); + } + +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongViewListAdapter.java b/app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongViewListAdapter.java new file mode 100644 index 00000000..90036742 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/adapter/songadapter/SongViewListAdapter.java @@ -0,0 +1,88 @@ +package com.kabouzeid.materialmusic.adapter.songadapter; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.helper.SongDetailDialogHelper; +import com.kabouzeid.materialmusic.loader.SongFileLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.ui.activities.tageditor.SongTagEditorActivity; +import com.kabouzeid.materialmusic.util.ImageLoaderUtil; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.nostra13.universalimageloader.core.ImageLoader; + +import java.io.File; +import java.util.List; + +/** + * Created by karim on 27.11.14. + */ +public class SongViewListAdapter extends SongAdapter { + public static final String TAG = SongViewListAdapter.class.getSimpleName(); + + public SongViewListAdapter(Context context, SongAdapter.GoToAble goToAble, List objects) { + super(context, goToAble, objects); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Song song = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_song_view, parent, false); + } + TextView songTitle = (TextView) convertView.findViewById(R.id.song_title); + final ImageView albumArt = (ImageView) convertView.findViewById(R.id.album_art); + ImageView overflowButton = (ImageView) convertView.findViewById(R.id.menu); + + overflowButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + PopupMenu popupMenu = new PopupMenu(context, v); + popupMenu.inflate(R.menu.menu_song); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_tag_editor: + Intent intent = new Intent(context, SongTagEditorActivity.class); + intent.putExtra(AppKeys.E_ID, song.id); + context.startActivity(intent); + return true; + case R.id.action_details: + String songFilePath = SongFileLoader.getSongFile(context, song.id); + File songFile = new File(songFilePath); + SongDetailDialogHelper.getDialog(context, songFile).show(); + return true; + case R.id.action_go_to_album: + if (goToAble != null) { + goToAble.goToAlbum(song.albumId); + } + return true; + case R.id.action_go_to_artist: + if (goToAble != null) { + goToAble.goToArtist(song.artistId); + } + return true; + } + return false; + } + }); + popupMenu.show(); + } + }); + + songTitle.setText(song.title); + ImageLoader.getInstance().displayImage(MusicUtil.getAlbumArtUri(song.albumId).toString(), albumArt, new ImageLoaderUtil.defaultAlbumArtOnFailed()); + + return convertView; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/comparator/AlbumAlphabeticComparator.java b/app/src/main/java/com/kabouzeid/materialmusic/comparator/AlbumAlphabeticComparator.java new file mode 100644 index 00000000..7a5a91eb --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/comparator/AlbumAlphabeticComparator.java @@ -0,0 +1,15 @@ +package com.kabouzeid.materialmusic.comparator; + +import com.kabouzeid.materialmusic.model.Album; + +import java.util.Comparator; + +/** + * Created by karim on 25.11.14. + */ +public class AlbumAlphabeticComparator implements Comparator { + @Override + public int compare(Album lhs, Album rhs) { + return lhs.title.trim().compareToIgnoreCase(rhs.title.trim()); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/comparator/ArtistAlphabeticComparator.java b/app/src/main/java/com/kabouzeid/materialmusic/comparator/ArtistAlphabeticComparator.java new file mode 100644 index 00000000..8ce27f05 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/comparator/ArtistAlphabeticComparator.java @@ -0,0 +1,15 @@ +package com.kabouzeid.materialmusic.comparator; + +import com.kabouzeid.materialmusic.model.Artist; + +import java.util.Comparator; + +/** + * Created by karim on 29.12.14. + */ +public class ArtistAlphabeticComparator implements Comparator { + @Override + public int compare(Artist lhs, Artist rhs) { + return lhs.name.trim().compareToIgnoreCase(rhs.name.trim()); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/comparator/SongAlphabeticComparator.java b/app/src/main/java/com/kabouzeid/materialmusic/comparator/SongAlphabeticComparator.java new file mode 100644 index 00000000..09040db3 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/comparator/SongAlphabeticComparator.java @@ -0,0 +1,15 @@ +package com.kabouzeid.materialmusic.comparator; + +import com.kabouzeid.materialmusic.model.Song; + +import java.util.Comparator; + +/** + * Created by karim on 28.12.14. + */ +public class SongAlphabeticComparator implements Comparator { + @Override + public int compare(Song lhs, Song rhs) { + return lhs.title.trim().compareToIgnoreCase(rhs.title.trim()); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/comparator/SongTrackNumberComparator.java b/app/src/main/java/com/kabouzeid/materialmusic/comparator/SongTrackNumberComparator.java new file mode 100644 index 00000000..0765d522 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/comparator/SongTrackNumberComparator.java @@ -0,0 +1,24 @@ +package com.kabouzeid.materialmusic.comparator; + +import com.kabouzeid.materialmusic.model.Song; + +import java.util.Comparator; + +/** + * Created by karim on 25.11.14. + */ +public class SongTrackNumberComparator implements Comparator { + @Override + public int compare(Song lhs, Song rhs) { + // 0 gleich + // -1 steht über dem anderen + // 1 steht unter dem anderen + if (lhs.trackNumber == rhs.trackNumber) { + return 0; + } + if (lhs.trackNumber > rhs.trackNumber) { + return 1; + } + return -1; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/helper/MusicPlayerRemote.java b/app/src/main/java/com/kabouzeid/materialmusic/helper/MusicPlayerRemote.java new file mode 100644 index 00000000..5ca44c36 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/helper/MusicPlayerRemote.java @@ -0,0 +1,271 @@ +package com.kabouzeid.materialmusic.helper; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.MusicRemoteEvent; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.service.MusicService; +import com.kabouzeid.materialmusic.util.InternalStorageUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 29.11.14. + */ +public class MusicPlayerRemote implements OnMusicRemoteEventListener { + private static final String TAG = MusicPlayerRemote.class.getSimpleName(); + + private App app; + + private int position = -1; + + private List playingQueue; + private List onMusicRemoteEventListeners; + + private MusicService musicService; + private Intent musicServiceIntent; + private boolean musicBound = false; + + public MusicPlayerRemote(Context context) { + app = (App) context.getApplicationContext(); + playingQueue = new ArrayList<>(); + onMusicRemoteEventListeners = new ArrayList<>(); + startAndBindService(); + } + + private void startAndBindService() { + if (musicServiceIntent == null) { + musicServiceIntent = new Intent(app, MusicService.class); + app.bindService(musicServiceIntent, musicConnection, Context.BIND_AUTO_CREATE); + app.startService(musicServiceIntent); + } + } + + private ServiceConnection musicConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + MusicService.MusicBinder binder = (MusicService.MusicBinder) service; + musicService = binder.getService(); + musicService.setPosition(position); + musicBound = true; + musicService.addOnMusicRemoteEventListener(MusicPlayerRemote.this); + setPlayingQueue(playingQueue); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.SERVICE_CONNECTED); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + musicBound = false; + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.SERVICE_DISCONNECTED); + } + }; + + public boolean playSongAt(int position) { + if (musicBound) { + if (position < getPlayingQueue().size() && position >= 0) { + this.position = position; + musicService.setPosition(position); + musicService.playSong(); + return true; + } else { + Log.e(TAG, "No song in queue at given index!"); + } + } + return false; + } + + public void pauseSong() { + if (musicBound) { + musicService.pausePlaying(); + } + } + + public void playNextSong() { + if (musicBound) { + musicService.playNextSong(); + } + } + + public void playPreviousSong() { + if (musicBound) { + musicService.back(); + } + } + + public void back() { + if (musicBound) { + musicService.back(); + } + } + + public boolean isPlaying() { + if (musicBound) { + return musicService.isPlaying(); + } + return false; + } + + public void resumePlaying() { + if (musicBound) { + musicService.resumePlaying(); + } + } + + public long getCurrentSongId() { + if (musicBound) { + return musicService.getCurrentSongId(); + } + try { + return playingQueue.get(position).id; + } catch (Exception e) { + return -1; + } + } + + public int getPosition() { + if (musicBound) { + position = musicService.getPosition(); + } + return position; + } + + public void setPlayingQueue(List songs) { + playingQueue = songs; + if (musicBound) { + musicService.setPlayingQueue(playingQueue); + } + } + + public List getPlayingQueue() { + if (musicBound) { + playingQueue = musicService.getPlayingQueue(); + } + return playingQueue; + } + + public Song getCurrentSong() { + final int position = getPosition(); + if (position != -1) { + return getPlayingQueue().get(position); + } + return new Song(); + } + + public int getSongProgressMillis() { + if (isPlayerPrepared()) { + return musicService.getSongProgressMillis(); + } + return -1; + } + + public int getSongDurationMillis() { + if (isPlayerPrepared()) { + return musicService.getSongDurationMillis(); + } + return -1; + } + + public boolean isMusicBound() { + return musicBound; + } + + public void seekTo(int millis) { + if (musicBound) { + musicService.seekTo(millis); + } + } + + public boolean isPlayerPrepared() { + if (musicBound) { + return musicService.isPlayerPrepared(); + } + return false; + } + + public void notifyPlayingQueueChanged() { + final long currentSongId = getCurrentSongId(); + } + + public int getRepeatMode() { + if (musicBound) { + return musicService.getRepeatMode(); + } + return app.getDefaultSharedPreferences().getInt(AppKeys.SP_REPEAT_MODE, 0); + } + + public int getShuffleMode() { + if (musicBound) { + return musicService.getShuffleMode(); + } + return app.getDefaultSharedPreferences().getInt(AppKeys.SP_SHUFFLE_MODE, 0); + } + + public boolean cycleRepeatMode() { + if (musicBound) { + musicService.cycleRepeatMode(); + return true; + } + return false; + } + + public boolean toggleShuffleMode() { + if (musicBound) { + musicService.toggleShuffle(); + return true; + } + return false; + } + + @Override + public void onMusicRemoteEvent(MusicRemoteEvent event) { + notifyOnMusicRemoteEventListeners(event.getAction()); + } + + public void addOnMusicRemoteEventListener(OnMusicRemoteEventListener onMusicRemoteEventListener) { + onMusicRemoteEventListeners.add(onMusicRemoteEventListener); + } + + public void removeOnMusicRemoteEventListener(OnMusicRemoteEventListener onMusicRemoteEventListener) { + onMusicRemoteEventListeners.remove(onMusicRemoteEventListener); + } + + public void removeAllOnMusicRemoteEventListeners() { + onMusicRemoteEventListeners.clear(); + } + + private void notifyOnMusicRemoteEventListeners(int event) { + MusicRemoteEvent musicRemoteEvent = new MusicRemoteEvent(event); + for (OnMusicRemoteEventListener listener : onMusicRemoteEventListeners) { + listener.onMusicRemoteEvent(musicRemoteEvent); + } + } + + @SuppressWarnings("unchecked") + public void restorePreviousState() { + try { + List restoredQueue = (ArrayList) InternalStorageUtil.readObject(app, AppKeys.IS_PLAYING_QUEUE); + int restoredPosition = (int) InternalStorageUtil.readObject(app, AppKeys.IS_POSITION_IN_QUEUE); + setPlayingQueue(restoredQueue); + position = restoredPosition; + if (musicBound) { + musicService.setPosition(restoredPosition); + } + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.STATE_RESTORED); + Log.i(TAG, "restored last state"); + } catch (IOException | ClassNotFoundException | ClassCastException e) { + Log.e(TAG, "error while restoring music service state", e); + playingQueue = new ArrayList<>(); + position = -1; + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/helper/NotificationHelper.java b/app/src/main/java/com/kabouzeid/materialmusic/helper/NotificationHelper.java new file mode 100644 index 00000000..910609b8 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/helper/NotificationHelper.java @@ -0,0 +1,178 @@ +package com.kabouzeid.materialmusic.helper; + +/** + * Created by karim on 27.12.14. + */ + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.TaskStackBuilder; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.support.v4.app.NotificationCompat; +import android.widget.RemoteViews; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.service.MusicService; +import com.kabouzeid.materialmusic.ui.activities.MusicControllerActivity; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.nostra13.universalimageloader.core.ImageLoader; + +public class NotificationHelper { + public static final String TAG = NotificationHelper.class.getSimpleName(); + public static final int NOTIFICATION_ID = 1337; + + private final MusicService service; + + private final NotificationManager notificationManager; + private Notification notification = null; + + private RemoteViews notificationLayout; + private RemoteViews notificationLayoutExpanded; + + public NotificationHelper(final MusicService service) { + this.service = service; + notificationManager = (NotificationManager) service + .getSystemService(Context.NOTIFICATION_SERVICE); + } + + public void buildNotification(Song song, final boolean isPlaying) { + notificationLayout = new RemoteViews(service.getPackageName(), + R.layout.notification_playing); + notificationLayoutExpanded = new RemoteViews(service.getPackageName(), + R.layout.notification_playing_expanded); + + setUpCollapsedLayout(song); + setUpExpandedLayout(song); + setUpPlaybackActions(isPlaying); + setUpExpandedPlaybackActions(isPlaying); + + notification = new NotificationCompat.Builder(service) + .setSmallIcon(R.drawable.notification_icon) + .setContentIntent(getOpenMusicControllerPendingIntent()) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContent(notificationLayout) + .build(); + notification.bigContentView = notificationLayoutExpanded; + + service.startForeground(NOTIFICATION_ID, notification); + } + + public void killNotification() { + service.stopForeground(true); + notification = null; + } + + public void updatePlayState(final boolean isPlaying) { + if (notification == null || notificationManager == null) { + return; + } + if (notificationLayout != null) { + notificationLayout.setImageViewResource(R.id.button_toggle_playpause, + isPlaying ? R.drawable.ic_pause_white_48dp : R.drawable.ic_play_arrow_white_48dp); + } + if (notificationLayoutExpanded != null) { + notificationLayoutExpanded.setImageViewResource(R.id.button_toggle_playpause, + isPlaying ? R.drawable.ic_pause_white_48dp : R.drawable.ic_play_arrow_white_48dp); + } + notificationManager.notify(NOTIFICATION_ID, notification); + } + + private PendingIntent getOpenMusicControllerPendingIntent() { + Intent result = new Intent(service, MusicControllerActivity.class); + TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(service); + taskStackBuilder.addParentStack(MusicControllerActivity.class); + taskStackBuilder.addNextIntent(result); + return taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private void setUpExpandedPlaybackActions(boolean isPlaying) { + notificationLayoutExpanded.setOnClickPendingIntent(R.id.button_toggle_playpause, + retrievePlaybackActions(1)); + + notificationLayoutExpanded.setOnClickPendingIntent(R.id.button_next, + retrievePlaybackActions(2)); + + notificationLayoutExpanded.setOnClickPendingIntent(R.id.button_prev, + retrievePlaybackActions(3)); + + notificationLayoutExpanded.setOnClickPendingIntent(R.id.button_quit, + retrievePlaybackActions(4)); + + notificationLayoutExpanded.setImageViewResource(R.id.button_toggle_playpause, + isPlaying ? R.drawable.ic_pause_white_48dp : R.drawable.ic_play_arrow_white_48dp); + } + + private void setUpPlaybackActions(boolean isPlaying) { + notificationLayout.setOnClickPendingIntent(R.id.button_toggle_playpause, + retrievePlaybackActions(1)); + + notificationLayout.setOnClickPendingIntent(R.id.button_next, + retrievePlaybackActions(2)); + + notificationLayout.setOnClickPendingIntent(R.id.button_quit, + retrievePlaybackActions(4)); + + notificationLayout.setImageViewResource(R.id.button_toggle_playpause, + isPlaying ? R.drawable.ic_pause_white_48dp : R.drawable.ic_play_arrow_white_48dp); + } + + private PendingIntent retrievePlaybackActions(final int which) { + Intent action; + PendingIntent pendingIntent; + final ComponentName serviceName = new ComponentName(service, MusicService.class); + switch (which) { + case 1: + action = new Intent(MusicService.ACTION_TOGGLE_PLAYBACK); + action.setComponent(serviceName); + pendingIntent = PendingIntent.getService(service, 1, action, 0); + return pendingIntent; + case 2: + action = new Intent(MusicService.ACTION_SKIP); + action.setComponent(serviceName); + pendingIntent = PendingIntent.getService(service, 2, action, 0); + return pendingIntent; + case 3: + action = new Intent(MusicService.ACTION_REWIND); + action.setComponent(serviceName); + pendingIntent = PendingIntent.getService(service, 3, action, 0); + return pendingIntent; + case 4: + action = new Intent(MusicService.ACTION_QUIT); + action.setComponent(serviceName); + pendingIntent = PendingIntent.getService(service, 4, action, 0); + return pendingIntent; + default: + break; + } + return null; + } + + private void setUpCollapsedLayout(Song song) { + loadAlbumArt(notificationLayout, MusicUtil.getAlbumArtUri(song.albumId).toString()); + notificationLayout.setTextViewText(R.id.song_title, song.title); + notificationLayout.setTextViewText(R.id.song_artist, song.title); + } + + private void setUpExpandedLayout(Song song) { + loadAlbumArt(notificationLayoutExpanded, MusicUtil.getAlbumArtUri(song.albumId).toString()); + notificationLayoutExpanded.setTextViewText(R.id.song_title, song.title); + notificationLayoutExpanded.setTextViewText(R.id.song_artist, song.artistName); + notificationLayoutExpanded.setTextViewText(R.id.album_title, song.albumName); + } + + private static void loadAlbumArt(RemoteViews notificationView, String albumArtUri) { + Bitmap albumArtBitmap = ImageLoader.getInstance().loadImageSync(albumArtUri); + if (albumArtBitmap == null) { + notificationView.setImageViewResource(R.id.album_art, R.drawable.default_album_art); + } else { + notificationView.setImageViewBitmap(R.id.album_art, albumArtBitmap); + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/helper/PlayingQueueDialogHelper.java b/app/src/main/java/com/kabouzeid/materialmusic/helper/PlayingQueueDialogHelper.java new file mode 100644 index 00000000..93523ed4 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/helper/PlayingQueueDialogHelper.java @@ -0,0 +1,50 @@ +package com.kabouzeid.materialmusic.helper; + +import android.content.Context; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.PlayListAdapter; +import com.kabouzeid.materialmusic.model.Song; +import com.mobeta.android.dslv.DragSortListView; + +/** + * Created by karim on 24.01.15. + */ +public class PlayingQueueDialogHelper { + public static MaterialDialog getDialog(Context context) { + final App app = (App) context.getApplicationContext(); + MaterialDialog dialog = new MaterialDialog.Builder(context) + .title(context.getResources().getString(R.string.label_current_playing_queue)) + .customView(R.layout.dialog_playlist, false) + .positiveText(context.getResources().getString(R.string.close)) + .negativeText(context.getResources().getString(R.string.save_as_playlist)) + .callback(new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + super.onPositive(dialog); + dialog.dismiss(); + } + + @Override + public void onNegative(MaterialDialog dialog) { + super.onNegative(dialog); + } + }) + .build(); + final DragSortListView dragSortListView = (DragSortListView) dialog.getCustomView().findViewById(R.id.dragSortListView); + final PlayListAdapter playListAdapter = new PlayListAdapter(context, app.getMusicPlayerRemote().getPlayingQueue()); + dragSortListView.setAdapter(playListAdapter); + dragSortListView.setDropListener(new DragSortListView.DropListener() { + @Override + public void drop(int from, int to) { + Song songToMove = app.getMusicPlayerRemote().getPlayingQueue().get(from); + app.getMusicPlayerRemote().getPlayingQueue().remove(from); + app.getMusicPlayerRemote().getPlayingQueue().add(to, songToMove); + playListAdapter.notifyDataSetChanged(); + } + }); + return dialog; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/helper/Shuffler.java b/app/src/main/java/com/kabouzeid/materialmusic/helper/Shuffler.java new file mode 100644 index 00000000..56a5de70 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/helper/Shuffler.java @@ -0,0 +1,52 @@ +package com.kabouzeid.materialmusic.helper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by karim on 24.01.15. + */ +public class Shuffler { + private static int MAX_HISTORY_SIZE = 250; + private List order = new ArrayList<>(); + private int position; + private int interval; + + public Shuffler(final int interval) { + order = getShuffledOrderList(interval); + this.interval = interval; + } + + public int nextInt(boolean infinite) { + position = position + 1; + if (position > order.size() - 1) { + if (infinite) { + order.addAll(getShuffledOrderList(interval)); + if (order.size() > Math.max(interval, MAX_HISTORY_SIZE)) { + order = order.subList(order.size() / 2 - 1, order.size() - 1); + } + } else { + return order.get(order.size() - 1); + } + } + return order.get(position); + } + + public int previousInt() { + position = position - 1; + if (position < 0) { + position = 0; + } + return order.get(position); + } + + private List getShuffledOrderList(int interval) { + final List newList = new ArrayList<>(); + for (int i = 0; i < interval; i++) { + newList.add(i); + } + Collections.shuffle(newList); + return newList; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/helper/SongDetailDialogHelper.java b/app/src/main/java/com/kabouzeid/materialmusic/helper/SongDetailDialogHelper.java new file mode 100644 index 00000000..a8485e02 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/helper/SongDetailDialogHelper.java @@ -0,0 +1,84 @@ +package com.kabouzeid.materialmusic.helper; + +import android.content.Context; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; + +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.AudioFileIO; +import org.jaudiotagger.audio.AudioHeader; +import org.jaudiotagger.audio.exceptions.CannotReadException; +import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; +import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; +import org.jaudiotagger.tag.TagException; + +import java.io.File; +import java.io.IOException; + +/** + * Created by karim on 19.01.15. + */ +public class SongDetailDialogHelper { + public static final String TAG = SongDetailDialogHelper.class.getSimpleName(); + + public static MaterialDialog getDialog(final Context context, final File songFile) { + MaterialDialog dialog = new MaterialDialog.Builder(context) + .customView(R.layout.dialog_file_details, true) + .title(context.getResources().getString(R.string.label_details)) + .positiveText(context.getResources().getString(R.string.ok)) + .callback(new MaterialDialog.ButtonCallback() { + @Override + public void onPositive(MaterialDialog dialog) { + dialog.dismiss(); + } + }) + .build(); + + View dialogView = dialog.getCustomView(); + final TextView fileName = (TextView) dialogView.findViewById(R.id.file_name); + final TextView filePath = (TextView) dialogView.findViewById(R.id.file_path); + final TextView fileSize = (TextView) dialogView.findViewById(R.id.file_size); + final TextView fileFormat = (TextView) dialogView.findViewById(R.id.file_format); + final TextView trackLength = (TextView) dialogView.findViewById(R.id.track_length); + final TextView bitRate = (TextView) dialogView.findViewById(R.id.bitrate); + final TextView samplingRate = (TextView) dialogView.findViewById(R.id.sampling_rate); + + fileName.setText(makeTextWithTitle(context, R.string.label_file_name, "-")); + filePath.setText(makeTextWithTitle(context, R.string.label_file_path, "-")); + fileSize.setText(makeTextWithTitle(context, R.string.label_file_size, "-")); + fileFormat.setText(makeTextWithTitle(context, R.string.label_file_format, "-")); + trackLength.setText(makeTextWithTitle(context, R.string.label_track_length, "-")); + bitRate.setText(makeTextWithTitle(context, R.string.label_bit_rate, "-")); + samplingRate.setText(makeTextWithTitle(context, R.string.label_sampling_rate, "-")); + + try { + if (songFile != null && songFile.exists()) { + AudioFile audioFile = AudioFileIO.read(songFile); + AudioHeader audioHeader = audioFile.getAudioHeader(); + + fileName.setText(makeTextWithTitle(context, R.string.label_file_name, songFile.getName())); + filePath.setText(makeTextWithTitle(context, R.string.label_file_path, songFile.getAbsolutePath())); + fileSize.setText(makeTextWithTitle(context, R.string.label_file_size, Util.getFileSizeString(songFile.length()))); + fileFormat.setText(makeTextWithTitle(context, R.string.label_file_format, audioHeader.getFormat())); + trackLength.setText(makeTextWithTitle(context, R.string.label_track_length, MusicUtil.getReadableDurationString(audioHeader.getTrackLength() * 1000))); + bitRate.setText(makeTextWithTitle(context, R.string.label_bit_rate, audioHeader.getBitRate() + " kb/s")); + samplingRate.setText(makeTextWithTitle(context, R.string.label_sampling_rate, audioHeader.getSampleRate() + " Hz")); + } + } catch (CannotReadException | IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { + Log.e(TAG, "error while reading the song file", e); + } + return dialog; + } + + private static Spanned makeTextWithTitle(Context context, int titleResId, String text) { + return Html.fromHtml("" + context.getResources().getString(titleResId) + ": " + "" + text); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabSearchAbleFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabSearchAbleFragment.java new file mode 100644 index 00000000..b74cfe53 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabSearchAbleFragment.java @@ -0,0 +1,10 @@ +package com.kabouzeid.materialmusic.interfaces; + +/** + * Created by karim on 29.12.14. + */ +public interface KabSearchAbleFragment { + public void search(String query); + + public void returnToNonSearch(); +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabViewsDisableAble.java b/app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabViewsDisableAble.java new file mode 100644 index 00000000..9703383d --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/interfaces/KabViewsDisableAble.java @@ -0,0 +1,12 @@ +package com.kabouzeid.materialmusic.interfaces; + +/** + * Created by karim on 23.12.14. + */ +public interface KabViewsDisableAble { + public void enableViews(); + + public void disableViews(); + + public boolean areViewsEnabled(); +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/interfaces/OnMusicRemoteEventListener.java b/app/src/main/java/com/kabouzeid/materialmusic/interfaces/OnMusicRemoteEventListener.java new file mode 100644 index 00000000..8d931d2a --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/interfaces/OnMusicRemoteEventListener.java @@ -0,0 +1,10 @@ +package com.kabouzeid.materialmusic.interfaces; + +import com.kabouzeid.materialmusic.model.MusicRemoteEvent; + +/** + * Created by karim on 19.12.14. + */ +public interface OnMusicRemoteEventListener { + public void onMusicRemoteEvent(MusicRemoteEvent event); +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/LastFMUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/LastFMUtil.java new file mode 100644 index 00000000..c58900a1 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/LastFMUtil.java @@ -0,0 +1,9 @@ +package com.kabouzeid.materialmusic.lastfm; + +/** + * Created by karim on 15.01.15. + */ +public class LastFMUtil { + public static String BASE_URL = "ws.audioscrobbler.com"; + public static String API_KEY = "bd9c6ea4d55ec9ed3af7d276e5ece304"; +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumImageLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumImageLoader.java new file mode 100644 index 00000000..bcd6e5ee --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumImageLoader.java @@ -0,0 +1,110 @@ +package com.kabouzeid.materialmusic.lastfm.album; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.View; + +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.provider.AlbumJSONStore; +import com.kabouzeid.materialmusic.util.Util; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; +import com.nostra13.universalimageloader.core.process.BitmapProcessor; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by karim on 01.01.15. + */ +public class LastFMAlbumImageLoader { + public static final String TAG = LastFMAlbumImageLoader.class.getSimpleName(); + + public static void loadAlbumImage(Context context, String queryAlbum, String queryArtist, AlbumImageLoaderCallback callback) { + if (queryAlbum != null) { + String albumJSON = AlbumJSONStore.getInstance(context).getAlbumJSON(queryAlbum + queryArtist); + if (albumJSON != null) { + Log.i(TAG, queryAlbum + " by " + queryArtist + " is in cache."); + try { + loadAlbumImageFromJSON(new JSONObject(albumJSON), callback); + } catch (JSONException e) { + Log.e(TAG, "Error while parsing string from cache to JSONObject", e); + } + } else { + Log.i(TAG, queryAlbum + " is not in cache."); + downloadAlbumImage(context, queryAlbum, queryArtist, callback); + } + } + } + + private static void loadAlbumImageFromJSON(JSONObject jsonObject, final AlbumImageLoaderCallback callback) { + Log.i(TAG, "Applying album art..."); + String url = LastFMAlbumInfoUtil.getAlbumImageUrlFromJSON(jsonObject); + if (!url.trim().equals("")) { + DisplayImageOptions options = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(false) + .postProcessor(new BitmapProcessor() { + @Override + public Bitmap process(Bitmap bmp) { + return Util.getAlbumArtScaledBitmap(bmp, true); + } + }) + .build(); + ImageLoader.getInstance().loadImage(url, options, new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + callback.onAlbumImageLoaded(null, null); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + callback.onAlbumImageLoaded(loadedImage, imageUri); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + callback.onAlbumImageLoaded(null, null); + } + }); + } else { + callback.onAlbumImageLoaded(null, null); + } + } + + private static void downloadAlbumImage(final Context context, final String album, final String artist, final AlbumImageLoaderCallback callback) { + Log.i(TAG, "Downloading details for " + album); + App app = (App) context.getApplicationContext(); + String albumUrl = LastFMAlbumInfoUtil.getAlbumUrl(album, artist); + JsonObjectRequest albumInfoJSONRequest = new JsonObjectRequest(0, albumUrl, null, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + Log.i(TAG, "Download was successful!"); + LastFMAlbumInfoUtil.saveAlbumJSONDataToCacheAndDisk(context, album, artist, response); + loadAlbumImageFromJSON(response, callback); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.e(TAG, "Download failed!", error); + callback.onAlbumImageLoaded(null, null); + } + }); + app.addToRequestQueue(albumInfoJSONRequest); + } + + public static interface AlbumImageLoaderCallback { + public void onAlbumImageLoaded(Bitmap albumImage, String uri); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumInfoUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumInfoUtil.java new file mode 100644 index 00000000..57e19842 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/album/LastFMAlbumInfoUtil.java @@ -0,0 +1,87 @@ +package com.kabouzeid.materialmusic.lastfm.album; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.kabouzeid.materialmusic.lastfm.LastFMUtil; +import com.kabouzeid.materialmusic.provider.AlbumJSONStore; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by karim on 24.12.14. + */ +public class LastFMAlbumInfoUtil { + public static final String TAG = LastFMAlbumInfoUtil.class.getSimpleName(); + + private static String AUTO_CORRECT = "1"; + + public static String getAlbumUrl(String album, String artist) { + if (album != null) { + Uri.Builder builder = new Uri.Builder(); + builder.scheme("http") + .authority(LastFMUtil.BASE_URL) + .appendPath("2.0") + .appendQueryParameter("method", "album.getinfo") + .appendQueryParameter("album", album) + .appendQueryParameter("artist", artist) + //.appendQueryParameter("lang", "de") + .appendQueryParameter("autocorrect", AUTO_CORRECT) + .appendQueryParameter("api_key", LastFMUtil.API_KEY) + .appendQueryParameter("format", "json"); + return builder.build().toString(); + } + return ""; + } + + public static String getAlbumNameFromJSON(JSONObject rootJSON) { + try { + return rootJSON.getJSONObject("album").getString("name"); + } catch (JSONException e) { + //Log.e(TAG, "Error while getting album name from JSON parameter!", e); + return ""; + } + } + + public static String getAlbumThumbnailUrlFromJSON(JSONObject rootJSON) { + try { + JSONArray images = getAlbumImageArrayFromJSON(rootJSON); + if (images.length() > 2) { + return images.getJSONObject(2).getString("#text"); + } else if (images.length() > 1) { + return images.getJSONObject(1).getString("#text"); + } + return images.getJSONObject(0).getString("#text"); + } catch (JSONException | NullPointerException e) { + //Log.e(TAG, "Error while getting album thumbnail image from JSON parameter!", e); + return ""; + } + } + + public static String getAlbumImageUrlFromJSON(JSONObject rootJSON) { + try { + JSONArray images = getAlbumImageArrayFromJSON(rootJSON); + return images.getJSONObject(images.length() - 1).getString("#text"); + } catch (JSONException | NullPointerException e) { + //Log.e(TAG, "Error while getting album image from JSON parameter!", e); + return ""; + } + } + + public static JSONArray getAlbumImageArrayFromJSON(JSONObject rootJSON) { + try { + return rootJSON.getJSONObject("album").getJSONArray("image"); + } catch (JSONException e) { + //Log.e(TAG, "Error while getting album image array from JSON parameter!", e); + return null; + } + } + + public static void saveAlbumJSONDataToCacheAndDisk(Context context, String album, String artist, JSONObject jsonObject) { + Log.i(TAG, "Saving new JSON album data for " + album + "..."); + AlbumJSONStore.getInstance(context).addAlbumJSON(album + artist, jsonObject.toString()); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistBiographyLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistBiographyLoader.java new file mode 100644 index 00000000..7b43751b --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistBiographyLoader.java @@ -0,0 +1,65 @@ +package com.kabouzeid.materialmusic.lastfm.artist; + +import android.content.Context; +import android.util.Log; + +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.provider.ArtistJSONStore; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by karim on 01.01.15. + */ +public class LastFMArtistBiographyLoader { + public static final String TAG = LastFMArtistBiographyLoader.class.getSimpleName(); + + public static void loadArtistBio(Context context, String queryArtist, ArtistBioLoaderCallback callback) { + if (queryArtist != null) { + String artistJSON = ArtistJSONStore.getInstance(context).getArtistJSON(queryArtist); + if (artistJSON != null) { + Log.i(TAG, queryArtist + " is in cache."); + try { + JSONObject json = new JSONObject(artistJSON); + String bio = LastFMArtistInfoUtil.getArtistBiographyFromJSON(json); + callback.onArtistBioLoaded(bio); + } catch (JSONException e) { + Log.e(TAG, "Error while parsing bio from cache to JSONObject", e); + } + } else { + Log.i(TAG, queryArtist + " is not in cache."); + downloadArtistBio(context, queryArtist, callback); + } + } + } + + private static void downloadArtistBio(final Context context, final String artist, final ArtistBioLoaderCallback callback) { + Log.i(TAG, "Downloading details for " + artist); + App app = (App) context.getApplicationContext(); + String artistUrl = LastFMArtistInfoUtil.getArtistUrl(artist); + JsonObjectRequest artistInfoJSONRequest = new JsonObjectRequest(0, artistUrl, null, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + Log.i(TAG, "Download was successful!"); + LastFMArtistInfoUtil.saveArtistJSONDataToCacheAndDisk(context, artist, response); + String bio = LastFMArtistInfoUtil.getArtistBiographyFromJSON(response); + callback.onArtistBioLoaded(bio); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.e(TAG, "Download failed!", error); + callback.onArtistBioLoaded(""); + } + }); + app.addToRequestQueue(artistInfoJSONRequest); + } + + public static interface ArtistBioLoaderCallback { + public void onArtistBioLoaded(String bio); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistImageLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistImageLoader.java new file mode 100644 index 00000000..c327d5d1 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistImageLoader.java @@ -0,0 +1,98 @@ +package com.kabouzeid.materialmusic.lastfm.artist; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.View; + +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.provider.ArtistJSONStore; +import com.kabouzeid.materialmusic.util.ImageLoaderUtil; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by karim on 01.01.15. + */ +public class LastFMArtistImageLoader { + public static final String TAG = LastFMArtistImageLoader.class.getSimpleName(); + + public static void loadArtistImage(Context context, String queryArtist, ArtistImageLoaderCallback callback) { + if (queryArtist != null) { + String artistJSON = ArtistJSONStore.getInstance(context).getArtistJSON(queryArtist); + if (artistJSON != null) { + Log.i(TAG, queryArtist + " is in cache."); + try { + loadArtistImageFromJSON(new JSONObject(artistJSON), callback); + } catch (JSONException e) { + Log.e(TAG, "Error while parsing string from cache to JSONObject", e); + } + } else { + Log.i(TAG, queryArtist + " is not in cache."); + downloadArtistImage(context, queryArtist, callback); + } + } + } + + private static void loadArtistImageFromJSON(JSONObject jsonObject, final ArtistImageLoaderCallback callback) { + Log.i(TAG, "Applying artist art..."); + String url = LastFMArtistInfoUtil.getArtistImageUrlFromJSON(jsonObject); + if (!url.trim().equals("")) { + ImageLoader.getInstance().loadImage(url, ImageLoaderUtil.getCacheOnDiskOptions(), new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + callback.onArtistImageLoaded(null); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + callback.onArtistImageLoaded(loadedImage); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + callback.onArtistImageLoaded(null); + } + }); + } else { + callback.onArtistImageLoaded(null); + } + } + + private static void downloadArtistImage(final Context context, final String artist, final ArtistImageLoaderCallback callback) { + Log.i(TAG, "Downloading details for " + artist); + App app = (App) context.getApplicationContext(); + String artistUrl = LastFMArtistInfoUtil.getArtistUrl(artist); + JsonObjectRequest artistInfoJSONRequest = new JsonObjectRequest(0, artistUrl, null, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + Log.i(TAG, "Download was successful!"); + LastFMArtistInfoUtil.saveArtistJSONDataToCacheAndDisk(context, artist, response); + loadArtistImageFromJSON(response, callback); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.e(TAG, "Download failed!", error); + callback.onArtistImageLoaded(null); + } + }); + app.addToRequestQueue(artistInfoJSONRequest); + } + + public static interface ArtistImageLoaderCallback { + public void onArtistImageLoaded(Bitmap artistImage); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistInfoUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistInfoUtil.java new file mode 100644 index 00000000..e2f3c10a --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistInfoUtil.java @@ -0,0 +1,95 @@ +package com.kabouzeid.materialmusic.lastfm.artist; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.kabouzeid.materialmusic.lastfm.LastFMUtil; +import com.kabouzeid.materialmusic.provider.ArtistJSONStore; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by karim on 24.12.14. + */ +public class LastFMArtistInfoUtil { + public static final String TAG = LastFMArtistInfoUtil.class.getSimpleName(); + + private static String AUTO_CORRECT = "1"; + + public static String getArtistUrl(String artist) { + if (artist != null) { + Uri.Builder builder = new Uri.Builder(); + builder.scheme("http") + .authority(LastFMUtil.BASE_URL) + .appendPath("2.0") + .appendQueryParameter("method", "artist.getinfo") + .appendQueryParameter("artist", artist) + //.appendQueryParameter("lang", "de") + .appendQueryParameter("autocorrect", AUTO_CORRECT) + .appendQueryParameter("api_key", LastFMUtil.API_KEY) + .appendQueryParameter("format", "json"); + return builder.build().toString(); + } + return ""; + } + + public static String getArtistNameFromJSON(JSONObject rootJSON) { + try { + return rootJSON.getJSONObject("artist").getString("name"); + } catch (JSONException e) { + //Log.e(TAG, "Error while getting artist name from JSON parameter!", e); + return ""; + } + } + + public static String getArtistThumbnailUrlFromJSON(JSONObject rootJSON) { + try { + JSONArray images = getArtistImageArrayFromJSON(rootJSON); + if (images.length() > 2) { + return images.getJSONObject(2).getString("#text"); + } else if (images.length() > 1) { + return images.getJSONObject(1).getString("#text"); + } + return images.getJSONObject(0).getString("#text"); + } catch (JSONException | NullPointerException e) { + //Log.e(TAG, "Error while getting artist thumbnail image from JSON parameter!", e); + return ""; + } + } + + public static String getArtistImageUrlFromJSON(JSONObject rootJSON) { + try { + JSONArray images = getArtistImageArrayFromJSON(rootJSON); + return images.getJSONObject(images.length() - 1).getString("#text"); + } catch (JSONException | NullPointerException e) { + //Log.e(TAG, "Error while getting artist image from JSON parameter!", e); + return ""; + } + } + + public static JSONArray getArtistImageArrayFromJSON(JSONObject rootJSON) { + try { + return rootJSON.getJSONObject("artist").getJSONArray("image"); + } catch (JSONException e) { + //Log.e(TAG, "Error while getting artist image array from JSON parameter!", e); + return null; + } + } + + public static String getArtistBiographyFromJSON(JSONObject rootJSON) { + try { + return rootJSON.getJSONObject("artist").getJSONObject("bio").getString("content"); + } catch (JSONException e) { + //Log.e(TAG, "Error while getting artist biografie from JSON parameter!", e); + return ""; + } + } + + public static void saveArtistJSONDataToCacheAndDisk(Context context, String artist, JSONObject jsonObject) { + Log.i(TAG, "Saving new JSON artist data for " + artist + "..."); + ArtistJSONStore.getInstance(context).addArtistJSON(artist, jsonObject.toString()); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistThumbnailLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistThumbnailLoader.java new file mode 100644 index 00000000..d9b54c41 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/lastfm/artist/LastFMArtistThumbnailLoader.java @@ -0,0 +1,98 @@ +package com.kabouzeid.materialmusic.lastfm.artist; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.View; + +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.provider.ArtistJSONStore; +import com.kabouzeid.materialmusic.util.ImageLoaderUtil; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by karim on 01.01.15. + */ +public class LastFMArtistThumbnailLoader { + public static final String TAG = LastFMArtistThumbnailLoader.class.getSimpleName(); + + public static void loadArtistThumbnail(Context context, String queryArtist, ArtistThumbnailLoaderCallback callback) { + if (queryArtist != null) { + String artistJSON = ArtistJSONStore.getInstance(context).getArtistJSON(queryArtist); + if (artistJSON != null) { + Log.i(TAG, queryArtist + " is in cache."); + try { + loadArtistThumbnailFromJSON(new JSONObject(artistJSON), callback); + } catch (JSONException e) { + Log.e(TAG, "Error while parsing string from cache to JSONObject", e); + } + } else { + Log.i(TAG, queryArtist + " is not in cache."); + downloadArtistThumbnail(context, queryArtist, callback); + } + } + } + + private static void loadArtistThumbnailFromJSON(JSONObject jsonObject, final ArtistThumbnailLoaderCallback callback) { + Log.i(TAG, "Applying artist thumbnail..."); + String url = LastFMArtistInfoUtil.getArtistThumbnailUrlFromJSON(jsonObject); + if (!url.trim().equals("")) { + ImageLoader.getInstance().loadImage(url, ImageLoaderUtil.getCacheOnDiskOptions(), new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + callback.onArtistThumbnailLoaded(null); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + callback.onArtistThumbnailLoaded(loadedImage); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + callback.onArtistThumbnailLoaded(null); + } + }); + } else { + callback.onArtistThumbnailLoaded(null); + } + } + + private static void downloadArtistThumbnail(final Context context, final String artist, final ArtistThumbnailLoaderCallback callback) { + Log.i(TAG, "Downloading details for " + artist); + App app = (App) context.getApplicationContext(); + String artistUrl = LastFMArtistInfoUtil.getArtistUrl(artist); + JsonObjectRequest artistInfoJSONRequest = new JsonObjectRequest(0, artistUrl, null, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + Log.i(TAG, "Download was successful!"); + LastFMArtistInfoUtil.saveArtistJSONDataToCacheAndDisk(context, artist, response); + loadArtistThumbnailFromJSON(response, callback); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.e(TAG, "Download failed!", error); + callback.onArtistThumbnailLoaded(null); + } + }); + app.addToRequestQueue(artistInfoJSONRequest); + } + + public static interface ArtistThumbnailLoaderCallback { + public void onArtistThumbnailLoaded(Bitmap thumbnail); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumLoader.java new file mode 100644 index 00000000..6bd806ad --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumLoader.java @@ -0,0 +1,106 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import com.kabouzeid.materialmusic.model.Album; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 29.12.14. + */ +public class AlbumLoader { + + public static List getAllAlbums(Context context) { + Cursor cursor = makeAlbumCursor(context); + List albums = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + final String albumName = cursor.getString(1); + final String artist = cursor.getString(2); + final int artistId = cursor.getInt(3); + final int songCount = cursor.getInt(4); + final int year = cursor.getInt(5); + + final Album album = new Album(id, albumName, artist, artistId, songCount, year); + albums.add(album); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return albums; + } + + public static Album getAlbum(Context context, int albumId) { + Cursor cursor = makeAlbumCursor(context); + Album album = new Album(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + if (id == albumId) { + final String albumName = cursor.getString(1); + final String artist = cursor.getString(2); + final int artistId = cursor.getInt(3); + final int songCount = cursor.getInt(4); + final int year = cursor.getInt(5); + + album = new Album(id, albumName, artist, artistId, songCount, year); + } + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return album; + } + + public static List getAlbums(Context context, String query) { + Cursor cursor = makeAlbumCursor(context); + List albums = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final String albumName = cursor.getString(1); + if (albumName.trim().toLowerCase().contains(query.trim().toLowerCase())) { + final int id = cursor.getInt(0); + final String artist = cursor.getString(2); + final int artistId = cursor.getInt(3); + final int songCount = cursor.getInt(4); + final int year = cursor.getInt(5); + + final Album album = new Album(id, albumName, artist, artistId, songCount, year); + albums.add(album); + } + } while (cursor.moveToNext()); + } + if (cursor != null) { + cursor.close(); + } + return albums; + } + + private static Cursor makeAlbumCursor(final Context context) { + return context.getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, + new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.AlbumColumns.ALBUM, + /* 2 */ + MediaStore.Audio.AlbumColumns.ARTIST, + /* 3 */ + MediaStore.Audio.Media.ARTIST_ID, + /* 4 */ + MediaStore.Audio.AlbumColumns.NUMBER_OF_SONGS, + /* 5 */ + MediaStore.Audio.AlbumColumns.FIRST_YEAR + }, null, null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumSongLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumSongLoader.java new file mode 100644 index 00000000..35d2bb57 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/AlbumSongLoader.java @@ -0,0 +1,65 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import com.kabouzeid.materialmusic.model.Song; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 29.12.14. + */ +public class AlbumSongLoader { + + public static List getAlbumSongList(final Context context, final int albumId) { + Cursor cursor = makeAlbumSongCursor(context, albumId); + List songs = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + final String songName = cursor.getString(1); + final String artist = cursor.getString(2); + final String album = cursor.getString(3); + final long duration = cursor.getLong(4); + final int trackNumber = cursor.getInt(5); + final int artistId = cursor.getInt(6); + + final Song song = new Song(id, albumId, artistId, songName, artist, album, duration, trackNumber); + songs.add(song); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return songs; + } + + public static final Cursor makeAlbumSongCursor(final Context context, final int albumId) { + final StringBuilder selection = new StringBuilder(); + selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"); + selection.append(" AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"); + selection.append(" AND " + MediaStore.Audio.AudioColumns.ALBUM_ID + "=" + albumId); + return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.AudioColumns.TITLE, + /* 2 */ + MediaStore.Audio.AudioColumns.ARTIST, + /* 3 */ + MediaStore.Audio.AudioColumns.ALBUM, + /* 4 */ + MediaStore.Audio.AudioColumns.DURATION, + /* 5 */ + MediaStore.Audio.AudioColumns.TRACK, + /* 6 */ + MediaStore.Audio.AudioColumns.ARTIST_ID + }, selection.toString(), null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistAlbumLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistAlbumLoader.java new file mode 100644 index 00000000..ce980ce0 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistAlbumLoader.java @@ -0,0 +1,54 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import com.kabouzeid.materialmusic.model.Album; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 04.01.15. + */ +public class ArtistAlbumLoader { + public static List getArtistAlbumList(final Context context, final int artistId) { + Cursor cursor = makeArtistAlbumCursor(context, artistId); + List albums = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + final String albumName = cursor.getString(1); + final String artist = cursor.getString(2); + final int songCount = cursor.getInt(3); + final int year = cursor.getInt(4); + + final Album album = new Album(id, albumName, artist, artistId, songCount, year); + albums.add(album); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return albums; + } + + public static Cursor makeArtistAlbumCursor(final Context context, final int artistId) { + return context.getContentResolver().query( + MediaStore.Audio.Artists.Albums.getContentUri("external", artistId), new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.AlbumColumns.ALBUM, + /* 2 */ + MediaStore.Audio.AlbumColumns.ARTIST, + /* 3 */ + MediaStore.Audio.AlbumColumns.NUMBER_OF_SONGS, + /* 4 */ + MediaStore.Audio.AlbumColumns.FIRST_YEAR + }, null, null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistLoader.java new file mode 100644 index 00000000..be950a12 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistLoader.java @@ -0,0 +1,97 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import com.kabouzeid.materialmusic.model.Artist; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 29.12.14. + */ +public class ArtistLoader { + + public static List getAllArtists(Context context) { + Cursor cursor = makeArtistCursor(context); + List artists = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + final String artistName = cursor.getString(1); + final int albumCount = cursor.getInt(2); + final int songCount = cursor.getInt(3); + + final Artist artist = new Artist(id, artistName, albumCount, songCount); + artists.add(artist); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return artists; + } + + public static Artist getArtist(Context context, int artistId) { + Cursor cursor = makeArtistCursor(context); + Artist artist = new Artist(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + if (id == artistId) { + final String artistName = cursor.getString(1); + final int albumCount = cursor.getInt(2); + final int songCount = cursor.getInt(3); + + artist = new Artist(id, artistName, albumCount, songCount); + } + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return artist; + } + + public static List getArtists(Context context, String query) { + Cursor cursor = makeArtistCursor(context); + List artists = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final String artistName = cursor.getString(1); + if (artistName.trim().toLowerCase().contains(query.trim().toLowerCase())) { + final int id = cursor.getInt(0); + final int albumCount = cursor.getInt(2); + final int songCount = cursor.getInt(3); + + final Artist artist = new Artist(id, artistName, albumCount, songCount); + artists.add(artist); + } + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return artists; + } + + public static final Cursor makeArtistCursor(final Context context) { + return context.getContentResolver().query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI, + new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.ArtistColumns.ARTIST, + /* 2 */ + MediaStore.Audio.ArtistColumns.NUMBER_OF_ALBUMS, + /* 3 */ + MediaStore.Audio.ArtistColumns.NUMBER_OF_TRACKS + }, null, null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistSongLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistSongLoader.java new file mode 100644 index 00000000..b89cb4ac --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/ArtistSongLoader.java @@ -0,0 +1,64 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import com.kabouzeid.materialmusic.model.Song; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 01.01.15. + */ +public class ArtistSongLoader { + public static List getArtistSongList(final Context context, final int artistId) { + Cursor cursor = makeArtistSongCursor(context, artistId); + List songs = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + final String songName = cursor.getString(1); + final String artist = cursor.getString(2); + final String album = cursor.getString(3); + final long duration = cursor.getLong(4); + final int trackNumber = cursor.getInt(5); + final int albumId = cursor.getInt(6); + + final Song song = new Song(id, albumId, artistId, songName, artist, album, duration, trackNumber); + songs.add(song); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return songs; + } + + public static Cursor makeArtistSongCursor(final Context context, final int artistId) { + final StringBuilder selection = new StringBuilder(); + selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"); + selection.append(" AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"); + selection.append(" AND " + MediaStore.Audio.AudioColumns.ARTIST_ID + "=" + artistId); + return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.AudioColumns.TITLE, + /* 2 */ + MediaStore.Audio.AudioColumns.ARTIST, + /* 3 */ + MediaStore.Audio.AudioColumns.ALBUM, + /* 4 */ + MediaStore.Audio.AudioColumns.DURATION, + /* 5 */ + MediaStore.Audio.AudioColumns.TRACK, + /* 6 */ + MediaStore.Audio.AudioColumns.ALBUM_ID + }, selection.toString(), null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/SongFileLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/SongFileLoader.java new file mode 100644 index 00000000..93a61298 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/SongFileLoader.java @@ -0,0 +1,62 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 11.01.15. + */ +public class SongFileLoader { + public static final String TAG = SongFileLoader.class.getSimpleName(); + + public static List getSongFiles(Context context, List queryIds) { + Cursor cursor = makeSongFileCursor(context); + List songFiles = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + if (queryIds.contains(id)) { + songFiles.add(cursor.getString(1)); + } + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return songFiles; + } + + public static String getSongFile(Context context, int queryId) { + Cursor cursor = makeSongFileCursor(context); + String filePath = ""; + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + if (id == queryId) { + filePath = cursor.getString(1); + break; + } + } while (cursor.moveToNext()); + } + if (cursor != null) { + cursor.close(); + } + return filePath; + } + + public static final Cursor makeSongFileCursor(final Context context) { + return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.AudioColumns.DATA, + }, null, null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/loader/SongLoader.java b/app/src/main/java/com/kabouzeid/materialmusic/loader/SongLoader.java new file mode 100644 index 00000000..de31dfab --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/loader/SongLoader.java @@ -0,0 +1,117 @@ +package com.kabouzeid.materialmusic.loader; + +import android.content.Context; +import android.database.Cursor; +import android.provider.BaseColumns; +import android.provider.MediaStore; + +import com.kabouzeid.materialmusic.model.Song; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by karim on 29.12.14. + */ +public class SongLoader { + public static List getAllSongs(Context context) { + Cursor cursor = makeAlbumSongCursor(context); + List songs = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + final String songName = cursor.getString(1); + final String artist = cursor.getString(2); + final String album = cursor.getString(3); + final long duration = cursor.getLong(4); + final int trackNumber = cursor.getInt(5); + final int artistId = cursor.getInt(6); + final int albumId = cursor.getInt(7); + + final Song song = new Song(id, albumId, artistId, songName, artist, album, duration, trackNumber); + songs.add(song); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return songs; + } + + public static List getSongs(Context context, String query) { + Cursor cursor = makeAlbumSongCursor(context); + List songs = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + final String songName = cursor.getString(1); + if (songName.trim().toLowerCase().contains(query.trim().toLowerCase())) { + final int id = cursor.getInt(0); + final String artist = cursor.getString(2); + final String album = cursor.getString(3); + final long duration = cursor.getLong(4); + final int trackNumber = cursor.getInt(5); + final int artistId = cursor.getInt(6); + final int albumId = cursor.getInt(7); + + final Song song = new Song(id, albumId, artistId, songName, artist, album, duration, trackNumber); + songs.add(song); + } + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return songs; + } + + public static Song getSong(Context context, int queryId) { + Cursor cursor = makeAlbumSongCursor(context); + Song song = null; + if (cursor != null && cursor.moveToFirst()) { + do { + final int id = cursor.getInt(0); + if (id == queryId) { + final String songName = cursor.getString(1); + final String artist = cursor.getString(2); + final String album = cursor.getString(3); + final long duration = cursor.getLong(4); + final int trackNumber = cursor.getInt(5); + final int artistId = cursor.getInt(6); + final int albumId = cursor.getInt(7); + song = new Song(id, albumId, artistId, songName, artist, album, duration, trackNumber); + break; + } + } while (cursor.moveToNext()); + } + if (cursor != null) { + cursor.close(); + } + return song; + } + + public static final Cursor makeAlbumSongCursor(final Context context) { + final StringBuilder selection = new StringBuilder(); + selection.append(MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"); + return context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + new String[]{ + /* 0 */ + BaseColumns._ID, + /* 1 */ + MediaStore.Audio.AudioColumns.TITLE, + /* 2 */ + MediaStore.Audio.AudioColumns.ARTIST, + /* 3 */ + MediaStore.Audio.AudioColumns.ALBUM, + /* 4 */ + MediaStore.Audio.AudioColumns.DURATION, + /* 5 */ + MediaStore.Audio.AudioColumns.TRACK, + /* 6 */ + MediaStore.Audio.AudioColumns.ARTIST_ID, + /* 7 */ + MediaStore.Audio.AudioColumns.ALBUM_ID + }, selection.toString(), null, null); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/misc/AppKeys.java b/app/src/main/java/com/kabouzeid/materialmusic/misc/AppKeys.java new file mode 100644 index 00000000..b517cd6e --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/misc/AppKeys.java @@ -0,0 +1,23 @@ +package com.kabouzeid.materialmusic.misc; + +/** + * Created by karim on 22.12.14. + */ +public final class AppKeys { + public static final String SP_THEME = "com.kabouzeid.materialmusic.THEME"; + public static final String SP_NAVIGATION_DRAWER_ITEM_POSITION = "com.kabouzeid.materialmusic.NAVIGATION_DRAWER_ITEM_POSITION"; + public static final String SP_USER_LEARNED_DRAWER = "com.kabouzeid.materialmusic.NAVIGATION_DRAWER_LEARNED"; + public static final String SP_ONLY_ON_WIFI = "com.kabouzeid.materialmusic.ONLY_ON_WIFI"; + public static final String SP_SHUFFLE_MODE = "com.kabouzeid.materialmusic.SHUFFLE_MODE"; + public static final String SP_REPEAT_MODE = "com.kabouzeid.materialmusic.REPEAT_MODE"; + + public static final String IS_PLAYING_QUEUE = "com.kabouzeid.materialmusic.PLAYING_QUEUE"; + public static final String IS_POSITION_IN_QUEUE = "com.kabouzeid.materialmusic.POSITION_IN_QUEUE"; + public static final String IS_ARTIST_JSON_INFO_CACHE = "com.kabouzeid.materialmusic.ARTIST_JSON_INFO_CACHE"; + + public static final String E_ALBUM = "com.kabouzeid.materialmusic.ALBUM"; + public static final String E_ARTIST = "com.kabouzeid.materialmusic.ARTIST"; + public static final String E_SONG = "com.kabouzeid.materialmusic.SONG"; + public static final String E_TAG_EDIT_MODE = "com.kabouzeid.materialmusic.TAG_EDIT_MODE"; + public static final String E_ID = "com.kabouzeid.materialmusic.ID"; +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallAnimatorListener.java b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallAnimatorListener.java new file mode 100644 index 00000000..d9dd6242 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallAnimatorListener.java @@ -0,0 +1,26 @@ +package com.kabouzeid.materialmusic.misc; + +/** + * Created by karim on 20.12.14. + */ +public class SmallAnimatorListener implements com.nineoldandroids.animation.Animator.AnimatorListener { + @Override + public void onAnimationStart(com.nineoldandroids.animation.Animator animation) { + + } + + @Override + public void onAnimationEnd(com.nineoldandroids.animation.Animator animation) { + + } + + @Override + public void onAnimationCancel(com.nineoldandroids.animation.Animator animation) { + + } + + @Override + public void onAnimationRepeat(com.nineoldandroids.animation.Animator animation) { + + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallObservableScrollViewCallbacks.java b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallObservableScrollViewCallbacks.java new file mode 100644 index 00000000..a60aca83 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallObservableScrollViewCallbacks.java @@ -0,0 +1,24 @@ +package com.kabouzeid.materialmusic.misc; + +import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +import com.github.ksoichiro.android.observablescrollview.ScrollState; + +/** + * Created by karim on 20.12.14. + */ +public class SmallObservableScrollViewCallbacks implements ObservableScrollViewCallbacks { + @Override + public void onScrollChanged(int i, boolean b, boolean b2) { + + } + + @Override + public void onDownMotionEvent() { + + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallOnGestureListener.java b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallOnGestureListener.java new file mode 100644 index 00000000..1153a0da --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallOnGestureListener.java @@ -0,0 +1,39 @@ +package com.kabouzeid.materialmusic.misc; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +/** + * Created by karim on 20.12.14. + */ +public class SmallOnGestureListener implements GestureDetector.OnGestureListener { + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return false; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallTransitionListener.java b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallTransitionListener.java new file mode 100644 index 00000000..a9c6af2e --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/misc/SmallTransitionListener.java @@ -0,0 +1,35 @@ +package com.kabouzeid.materialmusic.misc; + +import android.annotation.TargetApi; +import android.transition.Transition; + +/** + * Created by karim on 20.12.14. + */ +@TargetApi(21) +public class SmallTransitionListener implements Transition.TransitionListener { + @Override + public void onTransitionStart(Transition transition) { + + } + + @Override + public void onTransitionEnd(Transition transition) { + + } + + @Override + public void onTransitionCancel(Transition transition) { + + } + + @Override + public void onTransitionPause(Transition transition) { + + } + + @Override + public void onTransitionResume(Transition transition) { + + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/model/Album.java b/app/src/main/java/com/kabouzeid/materialmusic/model/Album.java new file mode 100644 index 00000000..7a703dab --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/model/Album.java @@ -0,0 +1,32 @@ +package com.kabouzeid.materialmusic.model; + +/** + * Created by karim on 22.11.14. + */ +public class Album { + + public int id; + public int artistId; + public String title; + public String artistName; + public int songCount; + public int year; + + public Album(final int id, final String title, final String artistName, final int artistId, + final int songNumber, final int albumYear) { + this.id = id; + this.title = title; + this.artistName = artistName; + this.artistId = artistId; + songCount = songNumber; + year = albumYear; + } + + public Album() { + this.id = -1; + this.title = ""; + this.artistName = ""; + songCount = -1; + year = -1; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/model/Artist.java b/app/src/main/java/com/kabouzeid/materialmusic/model/Artist.java new file mode 100644 index 00000000..1310f656 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/model/Artist.java @@ -0,0 +1,26 @@ +package com.kabouzeid.materialmusic.model; + +/** + * Created by karim on 29.12.14. + */ +public class Artist { + public int id; + public String name; + public int albumCount; + public int songCount; + + public Artist(final int id, final String name, final int songCount, + final int albumCount) { + this.id = id; + this.name = name; + this.songCount = songCount; + this.albumCount = albumCount; + } + + public Artist() { + id = -1; + name = ""; + songCount = -1; + albumCount = -1; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/model/MusicRemoteEvent.java b/app/src/main/java/com/kabouzeid/materialmusic/model/MusicRemoteEvent.java new file mode 100644 index 00000000..a0f31636 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/model/MusicRemoteEvent.java @@ -0,0 +1,35 @@ +package com.kabouzeid.materialmusic.model; + +/** + * Created by karim on 19.12.14. + */ +public class MusicRemoteEvent { + public static final int PLAY = 0; + public static final int PAUSE = 1; + public static final int RESUME = 2; + public static final int STOP = 3; + public static final int NEXT = 4; + public static final int PREV = 5; + + public static final int SONG_COMPLETED = 6; + public static final int QUEUE_COMPLETED = 7; + + public static final int SERVICE_CONNECTED = 8; + public static final int SERVICE_DISCONNECTED = 9; + + public static final int STATE_SAVED = 10; + public static final int STATE_RESTORED = 11; + + public static final int SHUFFLE_MODE_CHANGED = 12; + public static final int REPEAT_MODE_CHANGED = 13; + + private int action; + + public MusicRemoteEvent(int action) { + this.action = action; + } + + public int getAction() { + return action; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/model/NavigationDrawerItem.java b/app/src/main/java/com/kabouzeid/materialmusic/model/NavigationDrawerItem.java new file mode 100644 index 00000000..9c8974ed --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/model/NavigationDrawerItem.java @@ -0,0 +1,14 @@ +package com.kabouzeid.materialmusic.model; + +/** + * Created by karim on 23.11.14. + */ +public class NavigationDrawerItem { + public String title; + public int imageRes; + + public NavigationDrawerItem(String title, int imageRes) { + this.title = title; + this.imageRes = imageRes; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/model/Song.java b/app/src/main/java/com/kabouzeid/materialmusic/model/Song.java new file mode 100644 index 00000000..9ce41d92 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/model/Song.java @@ -0,0 +1,40 @@ +package com.kabouzeid.materialmusic.model; + +import java.io.Serializable; + +/** + * Created by karim on 23.11.14. + */ +public class Song implements Serializable { + public int id; + public int albumId; + public int artistId; + public String title; + public String artistName; + public String albumName; + public long duration; + public int trackNumber; + + public Song(final int id, final int albumId, final int artistId, final String title, final String artistName, + final String albumName, final long duration, final int trackNumber) { + this.id = id; + this.albumId = albumId; + this.artistId = artistId; + this.title = title; + this.artistName = artistName; + this.albumName = albumName; + this.duration = duration; + this.trackNumber = trackNumber; + } + + public Song() { + this.id = -1; + this.albumId = -1; + this.artistId = -1; + this.title = ""; + this.artistName = ""; + this.albumName = ""; + this.duration = -1; + this.trackNumber = -1; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/provider/AlbumJSONStore.java b/app/src/main/java/com/kabouzeid/materialmusic/provider/AlbumJSONStore.java new file mode 100644 index 00000000..9139e105 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/provider/AlbumJSONStore.java @@ -0,0 +1,103 @@ +package com.kabouzeid.materialmusic.provider; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class AlbumJSONStore extends SQLiteOpenHelper { + + private static final int VERSION = 1; + public static final String DATABASE_NAME = "albumJSONLastFM.db"; + private static AlbumJSONStore sInstance = null; + + public AlbumJSONStore(final Context context) { + super(context, DATABASE_NAME, null, VERSION); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + AlbumJSONColumns.NAME + + " (" + AlbumJSONColumns.ALBUMANDARTIST_NAME + " TEXT NOT NULL," + + AlbumJSONColumns.JSON + " TEXT NOT NULL);" + ); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + AlbumJSONColumns.NAME); + onCreate(db); + } + + public static synchronized AlbumJSONStore getInstance(final Context context) { + if (sInstance == null) { + sInstance = new AlbumJSONStore(context.getApplicationContext()); + } + return sInstance; + } + + public void addAlbumJSON(final String albumAndArtistName, final String JSON) { + if (albumAndArtistName == null || JSON == null) { + return; + } + + final SQLiteDatabase database = getWritableDatabase(); + final ContentValues values = new ContentValues(2); + + database.beginTransaction(); + + values.put(AlbumJSONColumns.ALBUMANDARTIST_NAME, albumAndArtistName.trim().toLowerCase()); + values.put(AlbumJSONColumns.JSON, JSON); + + database.insert(AlbumJSONColumns.NAME, null, values); + database.setTransactionSuccessful(); + database.endTransaction(); + } + + public String getAlbumJSON(final String albumAndArtistName) { + if (albumAndArtistName == null) { + return null; + } + + final SQLiteDatabase database = getReadableDatabase(); + final String[] projection = new String[]{ + AlbumJSONColumns.JSON, + AlbumJSONColumns.ALBUMANDARTIST_NAME + }; + final String selection = AlbumJSONColumns.ALBUMANDARTIST_NAME + "=?"; + final String[] having = new String[]{ + albumAndArtistName.trim().toLowerCase() + }; + Cursor cursor = database.query(AlbumJSONColumns.NAME, projection, selection, having, null, + null, null, null); + if (cursor != null && cursor.moveToFirst()) { + final String JSON = cursor.getString(cursor.getColumnIndexOrThrow(AlbumJSONColumns.JSON)); + cursor.close(); + return JSON; + } + if (cursor != null) { + cursor.close(); + } + return null; + } + + public static void deleteDatabase(final Context context) { + context.deleteDatabase(DATABASE_NAME); + } + + public void removeItem(final String albumAndArtistName) { + final SQLiteDatabase database = getReadableDatabase(); + database.delete(AlbumJSONColumns.NAME, AlbumJSONColumns.ALBUMANDARTIST_NAME + " = ?", new String[]{ + albumAndArtistName.trim().toLowerCase() + }); + + } + + public interface AlbumJSONColumns { + public static final String NAME = "AlbumJSON"; + public static final String ALBUMANDARTIST_NAME = "AlbumAndArtistName"; + public static final String JSON = "JSON"; + } + +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/provider/ArtistJSONStore.java b/app/src/main/java/com/kabouzeid/materialmusic/provider/ArtistJSONStore.java new file mode 100644 index 00000000..ae9575e0 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/provider/ArtistJSONStore.java @@ -0,0 +1,103 @@ +package com.kabouzeid.materialmusic.provider; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class ArtistJSONStore extends SQLiteOpenHelper { + + private static final int VERSION = 1; + public static final String DATABASE_NAME = "artistJSONLastFM.db"; + private static ArtistJSONStore sInstance = null; + + public ArtistJSONStore(final Context context) { + super(context, DATABASE_NAME, null, VERSION); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + ArtistJSONColumns.NAME + + " (" + ArtistJSONColumns.ARTIST_NAME + " TEXT NOT NULL," + + ArtistJSONColumns.JSON + " TEXT NOT NULL);" + ); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + ArtistJSONColumns.NAME); + onCreate(db); + } + + public static synchronized ArtistJSONStore getInstance(final Context context) { + if (sInstance == null) { + sInstance = new ArtistJSONStore(context.getApplicationContext()); + } + return sInstance; + } + + public void addArtistJSON(final String artistName, final String JSON) { + if (artistName == null || JSON == null) { + return; + } + + final SQLiteDatabase database = getWritableDatabase(); + final ContentValues values = new ContentValues(2); + + database.beginTransaction(); + + values.put(ArtistJSONColumns.ARTIST_NAME, artistName.trim().toLowerCase()); + values.put(ArtistJSONColumns.JSON, JSON); + + database.insert(ArtistJSONColumns.NAME, null, values); + database.setTransactionSuccessful(); + database.endTransaction(); + } + + public String getArtistJSON(final String artistName) { + if (artistName == null) { + return null; + } + + final SQLiteDatabase database = getReadableDatabase(); + final String[] projection = new String[]{ + ArtistJSONColumns.JSON, + ArtistJSONColumns.ARTIST_NAME + }; + final String selection = ArtistJSONColumns.ARTIST_NAME + "=?"; + final String[] having = new String[]{ + artistName.trim().toLowerCase() + }; + Cursor cursor = database.query(ArtistJSONColumns.NAME, projection, selection, having, null, + null, null, null); + if (cursor != null && cursor.moveToFirst()) { + final String JSON = cursor.getString(cursor.getColumnIndexOrThrow(ArtistJSONColumns.JSON)); + cursor.close(); + return JSON; + } + if (cursor != null) { + cursor.close(); + } + return null; + } + + public static void deleteDatabase(final Context context) { + context.deleteDatabase(DATABASE_NAME); + } + + public void removeItem(final String artistName) { + final SQLiteDatabase database = getReadableDatabase(); + database.delete(ArtistJSONColumns.NAME, ArtistJSONColumns.ARTIST_NAME + " = ?", new String[]{ + artistName.trim().toLowerCase() + }); + + } + + public interface ArtistJSONColumns { + public static final String NAME = "ArtistJSON"; + public static final String ARTIST_NAME = "ArtistName"; + public static final String JSON = "JSON"; + } + +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/service/MediaButtonIntentReceiver.java b/app/src/main/java/com/kabouzeid/materialmusic/service/MediaButtonIntentReceiver.java new file mode 100644 index 00000000..84f02d87 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/service/MediaButtonIntentReceiver.java @@ -0,0 +1,70 @@ +package com.kabouzeid.materialmusic.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; + +public class MediaButtonIntentReceiver extends BroadcastReceiver { + public static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); + + private static final int DOUBLE_CLICK = 500; + private static long mLastClickTime = 0; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) { + Log.i(TAG, intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT).toString()); + final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (event == null) + return; + 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 = MusicService.ACTION_STOP; + break; + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY: + command = MusicService.ACTION_TOGGLE_PLAYBACK; + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + command = MusicService.ACTION_SKIP; + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + command = MusicService.ACTION_REWIND; + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + command = MusicService.ACTION_PAUSE; + break; + } + if (command != null) { + if (action == KeyEvent.ACTION_DOWN) { + if (event.getRepeatCount() == 0) { + /** + * If another app received the broadcast first, this if statement will skip. + */ + //TODO triple click to rewind + final Intent i = new Intent(context, MusicService.class); + if (keycode == KeyEvent.KEYCODE_HEADSETHOOK + && eventTime - mLastClickTime < DOUBLE_CLICK) { + i.setAction(MusicService.ACTION_SKIP); + mLastClickTime = 0; + } else { + i.setAction(command); + mLastClickTime = eventTime; + } + context.startService(i); + } + } + if (isOrderedBroadcast()) + abortBroadcast(); + } + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/service/MusicService.java b/app/src/main/java/com/kabouzeid/materialmusic/service/MusicService.java new file mode 100644 index 00000000..f1d28b60 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/service/MusicService.java @@ -0,0 +1,630 @@ +package com.kabouzeid.materialmusic.service; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.helper.NotificationHelper; +import com.kabouzeid.materialmusic.helper.Shuffler; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.MusicRemoteEvent; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.util.InternalStorageUtil; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.nostra13.universalimageloader.core.ImageLoader; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class MusicService extends Service implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, AudioManager.OnAudioFocusChangeListener { + private static final String TAG = MusicService.class.getSimpleName(); + + public static final String ACTION_TOGGLE_PLAYBACK = "com.kabouzeid.materialmusic.action.TOGGLE_PLAYBACK"; + public static final String ACTION_PLAY = "com.kabouzeid.materialmusic.action.PLAY"; + public static final String ACTION_PAUSE = "com.kabouzeid.materialmusic.action.PAUSE"; + public static final String ACTION_STOP = "com.kabouzeid.materialmusic.action.STOP"; + public static final String ACTION_SKIP = "com.kabouzeid.materialmusic.action.SKIP"; + public static final String ACTION_REWIND = "com.kabouzeid.materialmusic.action.REWIND"; + public static final String ACTION_QUIT = "com.kabouzeid.materialmusic.action.QUIT"; + + public static final int SHUFFLE_MODE_NONE = 0; + public static final int SHUFFLE_MODE_SHUFFLE = 1; + + public static final int REPEAT_MODE_NONE = 0; + public static final int REPEAT_MODE_ALL = 1; + public static final int REPEAT_MODE_THIS = 2; + + private MediaPlayer player; + private List playingQueue; + private LinkedList playingHistory; + private List onMusicRemoteEventListeners; + private int currentSongId = -1; + private int position = -1; + private int shuffleMode; + private int repeatMode; + private final IBinder musicBind = new MusicBinder(); + private boolean isPlayerPrepared; + private boolean wasPlayingBeforeFocusLoss; + private boolean thingsRegistered; + private NotificationHelper notificationHelper; + private AudioManager audioManager; + private RemoteControlClient remoteControlClient; + private Shuffler shuffler; + + private final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { + pausePlaying(); + } + } + }; + + private AudioManager getAudioManager() { + if (audioManager == null) { + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + } + return audioManager; + } + + public MusicService() { + } + + @Override + public void onCreate() { + super.onCreate(); + isPlayerPrepared = false; + playingQueue = new ArrayList<>(); + playingHistory = new LinkedList<>(); + onMusicRemoteEventListeners = new ArrayList<>(); + notificationHelper = new NotificationHelper(this); + + shuffleMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(AppKeys.SP_SHUFFLE_MODE, 0); + repeatMode = PreferenceManager.getDefaultSharedPreferences(this).getInt(AppKeys.SP_REPEAT_MODE, 0); + + registerEverything(); + } + + private Shuffler getShuffler() { + if (shuffler == null) { + shuffler = new Shuffler(playingQueue.size()); + } + return shuffler; + } + + private boolean requestFocus() { + int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + + return (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + } + + private void initRemoteControlClient() { + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class)); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); + remoteControlClient = new RemoteControlClient(mediaPendingIntent); + remoteControlClient.setTransportControlFlags( + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); + getAudioManager().registerRemoteControlClient(remoteControlClient); + } + + private void registerEverything() { + if (!thingsRegistered) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + registerReceiver(receiver, intentFilter); + getAudioManager().registerMediaButtonEventReceiver(new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class)); + initRemoteControlClient(); + thingsRegistered = true; + } + } + + private void unregisterEverything() { + if (thingsRegistered) { + unregisterReceiver(receiver); + getAudioManager().unregisterRemoteControlClient(remoteControlClient); + getAudioManager().unregisterMediaButtonEventReceiver(new ComponentName(getApplicationContext(), MediaButtonIntentReceiver.class)); + thingsRegistered = false; + } + } + + private void updateRemoteControlClient(Song song) { + Bitmap loadedImage = ImageLoader.getInstance().loadImageSync(MusicUtil.getAlbumArtUri(song.albumId).toString()); + remoteControlClient + .editMetadata(false) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, song.artistName) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, song.title) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, song.duration) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, loadedImage) + .apply(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + setUpMediaPlayerIfNeeded(); + if (intent != null) { + if (intent.getAction() != null) { + String action = intent.getAction(); + switch (action) { + case ACTION_TOGGLE_PLAYBACK: + if (isPlaying()) { + pausePlaying(); + } else { + resumePlaying(); + } + break; + case ACTION_PAUSE: + pausePlaying(); + break; + case ACTION_PLAY: + playSong(); + break; + case ACTION_REWIND: + back(); + break; + case ACTION_SKIP: + playNextSong(); + break; + case ACTION_STOP: + stopPlaying(); + break; + case ACTION_QUIT: + killEverythingAndReleaseResources(); + } + } + } + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.i(TAG, "onBind"); + return musicBind; + } + + @Override + public boolean onUnbind(Intent intent) { + unregisterEverything(); + killEverythingAndReleaseResources(); + return false; + } + + @Override + public void onCompletion(MediaPlayer mp) { + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.SONG_COMPLETED); + if (isLastTrack()) { + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.QUEUE_COMPLETED); + notificationHelper.updatePlayState(isPlaying()); + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.STOP); + } else { + playNextSong(); + } + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + isPlayerPrepared = false; + player.reset(); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.STOP); + return false; + } + + @Override + public void onPrepared(MediaPlayer mp) { + player.start(); + isPlayerPrepared = true; + notificationHelper.updatePlayState(isPlaying()); + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.PLAY); + savePosition(); + } + + @Override + public void onDestroy() { + unregisterEverything(); + killEverythingAndReleaseResources(); + } + + private void killEverythingAndReleaseResources() { + savePosition(); + saveQueue(); + stopPlaying(); + notificationHelper.killNotification(); + stopSelf(); + } + + private void setUpMediaPlayerIfNeeded() { + if (player == null) { + player = new MediaPlayer(); + + player.setOnPreparedListener(this); + player.setOnCompletionListener(this); + player.setOnErrorListener(this); + + player.setAudioStreamType(AudioManager.STREAM_MUSIC); + player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); + } + } + + private void updateNotification() { + notificationHelper.buildNotification(playingQueue.get(position), isPlaying()); + } + + public void setPlayingQueue(List songs) { + if (!playingQueue.equals(songs)) { + this.playingQueue = songs; + shuffler = new Shuffler(playingQueue.size()); + saveQueue(); + } + } + + public List getPlayingQueue() { + return playingQueue; + } + + public void setPosition(int position) { + this.position = position; + } + + public int getPosition() { + return position; + } + + public long getCurrentSongId() { + return currentSongId; + } + + @Override + public void onAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + // resume playback + registerEverything(); + player.setVolume(1.0f, 1.0f); + if (wasPlayingBeforeFocusLoss) { + resumePlaying(); + updateRemoteControlClient(getPlayingQueue().get(position)); + } + updateRemoteControlClient(getPlayingQueue().get(position)); + break; + + case AudioManager.AUDIOFOCUS_LOSS: + // Lost focus for an unbounded amount of time: stop playback and release media player + //TODO maybe also release player + wasPlayingBeforeFocusLoss = isPlaying(); + pausePlaying(); + unregisterEverything(); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + // Lost focus for a short time, but we have to stop + // playback. We don't release the media player because playback + // is likely to resume + wasPlayingBeforeFocusLoss = isPlaying(); + pausePlaying(); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + // Lost focus for a short time, but it's ok to keep playing + // at an attenuated level + player.setVolume(0.2f, 0.2f); + break; + } + } + + public class MusicBinder extends Binder { + public MusicService getService() { + return MusicService.this; + } + } + + public void playSong() { + if (requestFocus()) { + setUpMediaPlayerIfNeeded(); + registerEverything(); + isPlayerPrepared = false; + player.reset(); + Uri trackUri = getCurrentPositionTrackUri(); + try { + player.setDataSource(getApplicationContext(), trackUri); + currentSongId = playingQueue.get(position).id; + updateNotification(); + updateRemoteControlClient(getPlayingQueue().get(position)); + player.prepareAsync(); + } catch (Exception e) { + Log.e("MUSIC SERVICE", "Error setting data source", e); + player.reset(); + Toast.makeText(getApplicationContext(), getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.STOP); + notificationHelper.updatePlayState(false); + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + } + } else { + Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT).show(); + } + } + + public void pausePlaying() { + if (isPlaying()) { + player.pause(); + notificationHelper.updatePlayState(isPlaying()); + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.PAUSE); + } + } + + public void resumePlaying() { + if (requestFocus()) { + if (isPlayerPrepared) { + player.start(); + notificationHelper.updatePlayState(isPlaying()); + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.RESUME); + } else { + playSong(); + } + } else { + Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT).show(); + } + } + + public void stopPlaying() { + isPlayerPrepared = false; + player.stop(); + notificationHelper.updatePlayState(isPlaying()); + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + player.release(); + player = null; + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.STOP); + } + + public void playNextSong() { + if (position != -1) { + if (isPlayerPrepared) { + + setPosition(getNextPosition()); + playSong(); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.NEXT); + } + } + } + + public void playPreviousSong() { + if (position != -1) { + setPosition(getPreviousPosition()); + playSong(); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.PREV); + } + } + + public void back() { + if (position != -1) { + if (getSongProgressMillis() > 2000) { + seekTo(0); + } else { + playPreviousSong(); + } + } + } + + public int getNextPosition() { + int position = 0; + switch (repeatMode) { + case REPEAT_MODE_NONE: + switch (shuffleMode) { + case SHUFFLE_MODE_NONE: + position = getPosition() + 1; + if (isLastTrack()) { + position -= 1; + } + break; + case SHUFFLE_MODE_SHUFFLE: + position = getShuffler().nextInt(false); + break; + } + break; + case REPEAT_MODE_ALL: + switch (shuffleMode) { + case SHUFFLE_MODE_NONE: + position = getPosition() + 1; + if (isLastTrack()) { + position = 0; + } + break; + case SHUFFLE_MODE_SHUFFLE: + position = getShuffler().nextInt(true); + break; + } + break; + case REPEAT_MODE_THIS: + position = getPosition(); + break; + } + return position; + } + + public int getPreviousPosition() { + int position = 0; + switch (repeatMode) { + case REPEAT_MODE_NONE: + switch (shuffleMode) { + case SHUFFLE_MODE_NONE: + position = getPosition() - 1; + if (position < 0) { + position = 0; + } + break; + case SHUFFLE_MODE_SHUFFLE: + position = getShuffler().previousInt(); + break; + } + break; + case REPEAT_MODE_ALL: + switch (shuffleMode) { + case SHUFFLE_MODE_NONE: + position = getPosition() - 1; + if (position < 0) { + position = getPlayingQueue().size() - 1; + } + break; + case SHUFFLE_MODE_SHUFFLE: + position = getShuffler().previousInt(); + break; + } + break; + case REPEAT_MODE_THIS: + position = getPosition(); + break; + } + return position; + } + + public boolean isPlaying() { + return player != null && player.isPlaying(); + } + + private Uri getCurrentPositionTrackUri() { + return ContentUris.withAppendedId(android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, playingQueue.get(position).id); + } + + public int getSongProgressMillis() { + return player.getCurrentPosition(); + } + + public int getSongDurationMillis() { + return player.getDuration(); + } + + public void seekTo(int millis) { + player.seekTo(millis); + } + + public boolean isPlayerPrepared() { + if (player == null) { + return false; + } + return isPlayerPrepared; + } + + private boolean isLastTrack() { + return getPosition() == getPlayingQueue().size() - 1; + } + + private void notifyOnMusicRemoteEventListeners(int event) { + MusicRemoteEvent musicRemoteEvent = new MusicRemoteEvent(event); + for (OnMusicRemoteEventListener listener : onMusicRemoteEventListeners) { + listener.onMusicRemoteEvent(musicRemoteEvent); + } + } + + public void addOnMusicRemoteEventListener(OnMusicRemoteEventListener onMusicRemoteEventListener) { + onMusicRemoteEventListeners.add(onMusicRemoteEventListener); + } + + public void saveQueue() { + try { + InternalStorageUtil.writeObject(MusicService.this, AppKeys.IS_PLAYING_QUEUE, getPlayingQueue()); + Log.i(TAG, "saved current queue state"); + } catch (IOException e) { + Log.e(TAG, "error while saving music service queue state", e); + } + } + + public void savePosition() { + new Thread(new Runnable() { + @Override + public void run() { + try { + InternalStorageUtil.writeObject(MusicService.this, AppKeys.IS_POSITION_IN_QUEUE, getPosition()); + Log.i(TAG, "saved current position state"); + } catch (IOException e) { + Log.e(TAG, "error while saving music service position state", e); + } + } + }).start(); + } + + public void setShuffleMode(final int shuffleMode) { + switch (shuffleMode) { + case SHUFFLE_MODE_SHUFFLE: + shuffler = new Shuffler(getPlayingQueue().size()); + case SHUFFLE_MODE_NONE: + this.shuffleMode = shuffleMode; + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putInt(AppKeys.SP_SHUFFLE_MODE, shuffleMode) + .apply(); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.SHUFFLE_MODE_CHANGED); + break; + } + } + + public void setRepeatMode(final int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_NONE: + case REPEAT_MODE_ALL: + case REPEAT_MODE_THIS: + this.repeatMode = repeatMode; + PreferenceManager.getDefaultSharedPreferences(this).edit() + .putInt(AppKeys.SP_REPEAT_MODE, repeatMode) + .apply(); + notifyOnMusicRemoteEventListeners(MusicRemoteEvent.REPEAT_MODE_CHANGED); + break; + } + } + + public void cycleRepeatMode() { + switch (repeatMode) { + case REPEAT_MODE_NONE: + setRepeatMode(REPEAT_MODE_ALL); + break; + case REPEAT_MODE_ALL: + setRepeatMode(REPEAT_MODE_THIS); + break; + default: + setRepeatMode(REPEAT_MODE_NONE); + break; + } + } + + public void toggleShuffle() { + if (shuffleMode == SHUFFLE_MODE_NONE) { + setShuffleMode(SHUFFLE_MODE_SHUFFLE); + } else { + setShuffleMode(SHUFFLE_MODE_NONE); + } + } + + public int getRepeatMode() { + return repeatMode; + } + + public int getShuffleMode() { + return shuffleMode; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/AlbumDetailActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/AlbumDetailActivity.java new file mode 100644 index 00000000..6fd41021 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/AlbumDetailActivity.java @@ -0,0 +1,491 @@ +package com.kabouzeid.materialmusic.ui.activities; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.graphics.Palette; +import android.support.v7.widget.Toolbar; +import android.transition.Transition; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.TextView; + +import com.github.ksoichiro.android.observablescrollview.ObservableListView; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.songadapter.SongAdapter; +import com.kabouzeid.materialmusic.comparator.SongTrackNumberComparator; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.loader.AlbumLoader; +import com.kabouzeid.materialmusic.loader.AlbumSongLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.misc.SmallObservableScrollViewCallbacks; +import com.kabouzeid.materialmusic.model.Album; +import com.kabouzeid.materialmusic.model.MusicRemoteEvent; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.ui.activities.base.AbsFabActivity; +import com.kabouzeid.materialmusic.ui.activities.tageditor.AlbumTagEditorActivity; +import com.kabouzeid.materialmusic.util.ImageLoaderUtil; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; +import com.kabouzeid.materialmusic.util.ViewUtil; +import com.melnykov.fab.FloatingActionButton; +import com.nhaarman.listviewanimations.appearance.AnimationAdapter; +import com.nhaarman.listviewanimations.appearance.simple.ScaleInAnimationAdapter; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import java.util.Collections; +import java.util.List; + +/* +* +* A lot of hackery is done in this activity. Changing things may will brake the whole activity. +* +* Should be kinda stable ONLY AS IT IS!!! +* +* */ + +public class AlbumDetailActivity extends AbsFabActivity implements OnMusicRemoteEventListener, KabViewsDisableAble { + public static final String TAG = AlbumDetailActivity.class.getSimpleName(); + + private static final boolean TOOLBAR_IS_STICKY = true; + private static final int DEFAULT_DELAY_NO_TRANSITION = 200; + private static final int DEFAULT_DELAY = 450; + private static final int DEFAULT_ANIMATION_TIME = 1000; + + private App app; + + private Album album; + + private AnimationAdapter animatedSongsAdapter; + private ObservableListView absSongListView; + private View statusBar; + private ImageView albumArtImageView; + private View albumArtOverlayView; + private View songsBackgroundView; + private TextView albumTitleView; + private FloatingActionButton fab; + private Toolbar toolbar; + private int toolbarHeight; + private int headerOffset; + private int titleViewHeight; + private int albumArtViewHeight; + private int toolbarColor; + + private Bitmap albumCover; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + app = (App) getApplicationContext(); + setTheme(app.getAppTheme()); + setUpTranslucence(); + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_album_detail); + + Bundle intentExtras = getIntent().getExtras(); + int albumId = -1; + if (intentExtras != null) { + albumId = intentExtras.getInt(AppKeys.E_ALBUM); + } + album = AlbumLoader.getAlbum(this, albumId); + if (album.id == -1) { + finish(); + } + + initViews(); + setUpObservableListViewParams(); + setUpToolBar(); + setUpViews(); + lollipopTransitionImageWrongSizeFix(); + animateEnterActivity(); + } + + @Override + public void onResume() { + super.onResume(); + enableViews(); + updateFabIcon(); + app.getMusicPlayerRemote().addOnMusicRemoteEventListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + app.getMusicPlayerRemote().removeOnMusicRemoteEventListener(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_album_detail, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + super.onBackPressed(); + return true; + case R.id.action_settings: + return true; + case R.id.action_current_playing: + return openCurrentPlayingIfPossible(null); + case R.id.action_tag_editor: + Intent intent = new Intent(this, AlbumTagEditorActivity.class); + intent.putExtra(AppKeys.E_ID, album.id); + startActivity(intent); + return true; + case R.id.action_go_to_artist: + goToArtistDetailsActivity(album.artistId, null); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void initViews() { + albumArtImageView = (ImageView) findViewById(R.id.album_art); + toolbar = (Toolbar) findViewById(R.id.toolbar); + albumArtOverlayView = findViewById(R.id.overlay); + absSongListView = (ObservableListView) findViewById(R.id.list); + albumTitleView = (TextView) findViewById(R.id.album_title); + fab = (FloatingActionButton) findViewById(R.id.fab); + songsBackgroundView = findViewById(R.id.list_background); + statusBar = findViewById(R.id.statusBar); + } + + private void setUpObservableListViewParams() { + albumArtViewHeight = getResources().getDimensionPixelSize(R.dimen.header_image_height); + toolbarColor = Util.resolveColor(this, R.attr.colorPrimary); + toolbarHeight = Util.getActionBarSize(this); + titleViewHeight = getResources().getDimensionPixelSize(R.dimen.title_view_height); + headerOffset = toolbarHeight; + headerOffset += getResources().getDimensionPixelSize(R.dimen.statusMargin); + } + + private void setUpViews() { + albumTitleView.setText(album.title); + ViewHelper.setAlpha(albumArtOverlayView, 0); + + prepareViewsForOpenAnimation(); + setUpAlbumArtAndApplyPalette(); + setUpListView(); + } + + private void prepareViewsForOpenAnimation() { + albumTitleView.setPivotY(0); + albumTitleView.setScaleY(0); + } + + private void setUpAlbumArtAndApplyPalette() { + ImageLoader.getInstance().displayImage(MusicUtil.getAlbumArtUri(album.id).toString(), albumArtImageView, new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + albumArtImageView.setImageResource(R.drawable.default_album_art); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + albumArtImageView.setImageResource(R.drawable.default_album_art); + } + + @Override + public void onLoadingComplete(String imageUri, final View view, Bitmap loadedImage) { + albumCover = loadedImage; + applyPalette(loadedImage); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + albumArtImageView.setImageResource(R.drawable.default_album_art); + } + }); + } + + private void setUpSongsAdapter() { + final List songs = AlbumSongLoader.getAlbumSongList(this, album.id); + Collections.sort(songs, new SongTrackNumberComparator()); + final SongAdapter songAdapter = new SongAdapter(this, this, songs); + +// SwingBottomInAnimationAdapter songsAdapter = new SwingBottomInAnimationAdapter(songAdapter); +// SwingRightInAnimationAdapter songsAdapter = new SwingRightInAnimationAdapter(songAdapter); +// SwingLeftInAnimationAdapter songsAdapter = new SwingLeftInAnimationAdapter(songAdapter); + ScaleInAnimationAdapter songsAdapter = new ScaleInAnimationAdapter(songAdapter); +// AlphaInAnimationAdapter songsAdapter = new AlphaInAnimationAdapter(songAdapter); + + animatedSongsAdapter = songsAdapter; + animatedSongsAdapter.setAbsListView(absSongListView); + + absSongListView.setAdapter(animatedSongsAdapter); + absSongListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (position > 0) { + app.getMusicPlayerRemote().setPlayingQueue(songs); + app.getMusicPlayerRemote().playSongAt(position - 1); + } + } + }); + } + + private void setUpListView() { + absSongListView.setScrollViewCallbacks(observableScrollViewCallbacks); + setListViewPadding(); + final View contentView = getWindow().getDecorView().findViewById(android.R.id.content); + contentView.post(new Runnable() { + @Override + public void run() { + songsBackgroundView.getLayoutParams().height = contentView.getHeight(); + observableScrollViewCallbacks.onScrollChanged(0, false, false); + } + }); + } + + private void setListViewPadding() { + setListViewPaddingTop(); + if (app.isInPortraitMode() || app.isTablet()) { + setListViewPaddingBottom(); + } + } + + private void setListViewPaddingTop() { + final View paddingView = new View(AlbumDetailActivity.this); + AbsListView.LayoutParams lp = new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, + albumArtViewHeight + titleViewHeight); + paddingView.setLayoutParams(lp); + paddingView.setClickable(true); + absSongListView.addHeaderView(paddingView); + } + + private void setListViewPaddingBottom() { + final View paddingView = new View(AlbumDetailActivity.this); + AbsListView.LayoutParams lp = new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, + Util.getNavigationBarHeight(this)); + paddingView.setLayoutParams(lp); + paddingView.setClickable(true); + absSongListView.addFooterView(paddingView); + } + + private void setUpToolBar() { + setSupportActionBar(toolbar); + getSupportActionBar().setTitle(null); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + if (!TOOLBAR_IS_STICKY) { + toolbar.setBackgroundColor(Color.TRANSPARENT); + } + } + + private void applyPalette(Bitmap bitmap) { + Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + Palette.Swatch swatch = palette.getVibrantSwatch(); + if (swatch != null) { + toolbarColor = swatch.getRgb(); + albumArtOverlayView.setBackgroundColor(swatch.getRgb()); + albumTitleView.setBackgroundColor(swatch.getRgb()); + albumTitleView.setTextColor(swatch.getTitleTextColor()); + } + } + }); + } + + private SmallObservableScrollViewCallbacks observableScrollViewCallbacks = new SmallObservableScrollViewCallbacks() { + @Override + public void onScrollChanged(int scrollY, boolean b, boolean b2) { + super.onScrollChanged(scrollY, b, b2); + // Translate overlay and image + float flexibleRange = albumArtViewHeight - headerOffset; + int minOverlayTransitionY = headerOffset - albumArtOverlayView.getHeight(); + ViewHelper.setTranslationY(albumArtOverlayView, Math.max(minOverlayTransitionY, Math.min(0, -scrollY))); + ViewHelper.setTranslationY(albumArtImageView, Math.max(minOverlayTransitionY, Math.min(0, -scrollY / 2))); + + // Translate list background + ViewHelper.setTranslationY(songsBackgroundView, Math.max(0, -scrollY + albumArtViewHeight)); + + // Change alpha of overlay + ViewHelper.setAlpha(albumArtOverlayView, Math.max(0, Math.min(1, (float) scrollY / flexibleRange))); + + // Translate name text + int maxTitleTranslationY = albumArtViewHeight; + int titleTranslationY = maxTitleTranslationY - scrollY; + if (TOOLBAR_IS_STICKY) { + titleTranslationY = Math.max(headerOffset, titleTranslationY); + } + ViewHelper.setTranslationY(albumTitleView, titleTranslationY); + + // Translate FAB + int fabTranslationY = titleTranslationY + titleViewHeight - (fab.getHeight() / 2); + ViewHelper.setTranslationY(fab, fabTranslationY); + + if (TOOLBAR_IS_STICKY) { + // Change alpha of toolbar background + if (-scrollY + albumArtViewHeight <= headerOffset) { + ViewUtil.setBackgroundAlpha(toolbar, 1, toolbarColor); + ViewUtil.setBackgroundAlpha(statusBar, 1, toolbarColor); + + } else { + ViewUtil.setBackgroundAlpha(toolbar, 0, toolbarColor); + ViewUtil.setBackgroundAlpha(statusBar, 0, toolbarColor); + } + } else { + // Translate Toolbar + if (scrollY < albumArtViewHeight) { + ViewHelper.setTranslationY(toolbar, 0); + } else { + ViewHelper.setTranslationY(toolbar, -scrollY); + } + } + } + }; + + @Override + public void onMusicRemoteEvent(MusicRemoteEvent event) { + switch (event.getAction()) { + case MusicRemoteEvent.PLAY: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_pause_white_48dp)); + break; + case MusicRemoteEvent.PAUSE: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_play_arrow_white_48dp)); + break; + case MusicRemoteEvent.RESUME: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_pause_white_48dp)); + break; + case MusicRemoteEvent.STOP: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_play_arrow_white_48dp)); + break; + case MusicRemoteEvent.QUEUE_COMPLETED: + fab.setImageResource(R.drawable.ic_play_arrow_white_48dp); + break; + } + } + + @Override + public void enableViews() { + super.enableViews(); + absSongListView.setEnabled(true); + fab.setEnabled(true); + toolbar.setEnabled(true); + } + + @Override + public void disableViews() { + super.disableViews(); + absSongListView.setEnabled(false); + fab.setEnabled(false); + toolbar.setEnabled(false); + } + + private void setUpTranslucence() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Util.setStatusBarTranslucent(getWindow(), true); + if (app.isInPortraitMode() || app.isTablet()) { + Util.setNavBarTranslucent(getWindow(), true); + } + } + } + + private void updateFabIcon() { + if (app.getMusicPlayerRemote().isPlaying()) { + fab.setImageResource(R.drawable.ic_pause_white_48dp); + } else { + fab.setImageResource(R.drawable.ic_play_arrow_white_48dp); + } + } + + private void animateHeader(int startDelay) { + ViewPropertyAnimator.animate(albumTitleView) + .scaleX(1) + .scaleY(1) + .setInterpolator(new DecelerateInterpolator(4)) + .setDuration(DEFAULT_ANIMATION_TIME) + .setStartDelay(startDelay) + .start(); + } + + private void animateFab(int startDelay) { + ViewPropertyAnimator.animate(fab) + .scaleX(1) + .scaleY(1) + .setInterpolator(new DecelerateInterpolator(4)) + .setDuration(DEFAULT_ANIMATION_TIME) + .setStartDelay(startDelay) + .start(); + } + + private void animateEnterActivity() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + animateHeader(0); + setUpSongsAdapter(); + } + }, DEFAULT_DELAY); + + } else { + setUpSongsAdapter(); + fab.setScaleX(0); + fab.setScaleY(0); + animateHeader(DEFAULT_DELAY_NO_TRANSITION); + animateFab(DEFAULT_DELAY_NO_TRANSITION); + } + } + + @Override + public void goToAlbum(int albumId) { + if (album.id != albumId) { + goToAlbum(albumId); + } + } + + private void lollipopTransitionImageWrongSizeFix() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().getSharedElementEnterTransition().addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(Transition transition) { + + } + + @Override + public void onTransitionEnd(Transition transition) { + if (albumCover == null) { + ImageLoader.getInstance().displayImage(MusicUtil.getAlbumArtUri(album.id).toString(), albumArtImageView, new ImageLoaderUtil.defaultAlbumArtOnFailed()); + } else { + albumArtImageView.setImageBitmap(albumCover); + } + } + + @Override + public void onTransitionCancel(Transition transition) { + + } + + @Override + public void onTransitionPause(Transition transition) { + + } + + @Override + public void onTransitionResume(Transition transition) { + + } + }); + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/ArtistDetailActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/ArtistDetailActivity.java new file mode 100644 index 00000000..3838e198 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/ArtistDetailActivity.java @@ -0,0 +1,490 @@ +package com.kabouzeid.materialmusic.ui.activities; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.graphics.Palette; +import android.support.v7.widget.Toolbar; +import android.transition.Transition; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.TextView; + +import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +import com.github.ksoichiro.android.observablescrollview.ScrollState; +import com.google.samples.apps.iosched.ui.widget.SlidingTabLayout; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.lastfm.artist.LastFMArtistImageLoader; +import com.kabouzeid.materialmusic.loader.ArtistLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.Artist; +import com.kabouzeid.materialmusic.ui.activities.base.AbsFabActivity; +import com.kabouzeid.materialmusic.ui.fragments.artistviewpager.AbsViewPagerTabArtistListFragment; +import com.kabouzeid.materialmusic.ui.fragments.artistviewpager.ViewPagerTabArtistAlbumFragment; +import com.kabouzeid.materialmusic.ui.fragments.artistviewpager.ViewPagerTabArtistBioFragment; +import com.kabouzeid.materialmusic.ui.fragments.artistviewpager.ViewPagerTabArtistSongListFragment; +import com.kabouzeid.materialmusic.util.Util; +import com.kabouzeid.materialmusic.util.ViewUtil; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; + +/* +* +* A lot of hackery is done in this activity. Changing things may will brake the whole activity. +* +* Should be kinda stable ONLY AS IT IS!!! +* +* */ + +public class ArtistDetailActivity extends AbsFabActivity implements OnMusicRemoteEventListener, KabViewsDisableAble, ObservableScrollViewCallbacks { + public static final String TAG = ArtistDetailActivity.class.getSimpleName(); + + public static final String ARG_ARTIST_ID = "com.kabouzeid.materialmusic.artist.id"; + public static final String ARG_ARTIST_NAME = "com.kabouzeid.materialmusic.artist.name"; + + private static final boolean TOOLBAR_IS_STICKY = true; + + private boolean isAnimating; + + private Artist artist; + + private SlidingTabLayout slidingTabs; + private View statusBar; + private ImageView artistImageView; + private View artistArtOverlayView; + private View absAlbumListBackgroundView; + private TextView artistTitleText; + private Toolbar toolbar; + private ViewPager viewPager; + private NavigationAdapter navigationAdapter; + private int toolbarHeight; + private int headerOffset; + private int titleViewHeight; + private int artistImageViewHeight; + private int toolbarColor; + private int tabHeight; + + private Bitmap artistImage; + + private Fragment currentFragment; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + setUpTranslucence(true, true); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_artist_detail); + + getIntentExtras(); + initViews(); + setUpObservableListViewParams(); + setUpToolBar(); + setUpViews(); + lollipopTransitionImageWrongSizeFix(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_artist_detail, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + super.onBackPressed(); + return true; + case R.id.action_settings: + return true; + case R.id.action_current_playing: + openCurrentPlayingIfPossible(null); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void initViews() { + artistImageView = (ImageView) findViewById(R.id.artist_image); + toolbar = (Toolbar) findViewById(R.id.toolbar); + artistArtOverlayView = findViewById(R.id.overlay); + artistTitleText = (TextView) findViewById(R.id.artist_name); + absAlbumListBackgroundView = findViewById(R.id.list_background); + statusBar = findViewById(R.id.statusBar); + slidingTabs = (SlidingTabLayout) findViewById(R.id.sliding_tabs); + } + + private void setUpObservableListViewParams() { + artistImageViewHeight = getResources().getDimensionPixelSize(R.dimen.header_image_height); + toolbarColor = Util.resolveColor(this, R.attr.colorPrimary); + toolbarHeight = Util.getActionBarSize(this); + titleViewHeight = getResources().getDimensionPixelSize(R.dimen.title_view_height); + headerOffset = toolbarHeight; + headerOffset += getResources().getDimensionPixelSize(R.dimen.statusMargin); + tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height); + } + + private void setUpViews() { + artistTitleText.setText(artist.name); + ViewHelper.setAlpha(artistArtOverlayView, 0); + + setUpArtistImageAndApplyPalette(); + setUpViewPatch(); + setUpSlidingTabs(); + } + + private void setUpSlidingTabs() { + navigationAdapter = new NavigationAdapter(this, artist); + viewPager = (ViewPager) findViewById(R.id.pager); + viewPager.setOffscreenPageLimit(2); + viewPager.setAdapter(navigationAdapter); + viewPager.setCurrentItem(1); + + slidingTabs.setViewPager(viewPager); + slidingTabs.setDistributeEvenly(true); + slidingTabs.setCustomTabView(R.layout.tab_indicator, android.R.id.text1); + slidingTabs.setSelectedIndicatorColors(Util.resolveColor(this, R.attr.colorAccent)); + slidingTabs.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int position) { + currentFragment = navigationAdapter.getItemAt(position); + if (currentFragment instanceof AbsViewPagerTabArtistListFragment) { + restoreY(((AbsViewPagerTabArtistListFragment) currentFragment).getY()); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + } + + private void setUpArtistImageAndApplyPalette() { + if (artistImage == null) { + LastFMArtistImageLoader.loadArtistImage(this, artist.name, new LastFMArtistImageLoader.ArtistImageLoaderCallback() { + @Override + public void onArtistImageLoaded(Bitmap artistImage) { + if (artistImage != null) { + ArtistDetailActivity.this.artistImage = artistImage; + artistImageView.setImageBitmap(artistImage); + applyPalette(artistImage); + } + } + }); + } else { + artistImageView.setImageBitmap(artistImage); + applyPalette(artistImage); + } + } + + private void setUpViewPatch() { + final View contentView = getWindow().getDecorView().findViewById(android.R.id.content); + contentView.post(new Runnable() { + @Override + public void run() { + absAlbumListBackgroundView.getLayoutParams().height = contentView.getHeight(); + } + }); + } + + private void setUpToolBar() { + setSupportActionBar(toolbar); + getSupportActionBar().setTitle(null); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + if (!TOOLBAR_IS_STICKY) { + toolbar.setBackgroundColor(Color.TRANSPARENT); + } + } + + private void applyPalette(Bitmap bitmap) { + Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + Palette.Swatch swatch = palette.getVibrantSwatch(); + if (swatch != null) { + toolbarColor = swatch.getRgb(); + artistArtOverlayView.setBackgroundColor(swatch.getRgb()); + artistTitleText.setBackgroundColor(swatch.getRgb()); + slidingTabs.setBackgroundColor(swatch.getRgb()); + artistTitleText.setTextColor(swatch.getTitleTextColor()); + } + } + }); + } + + @Override + public void enableViews() { + super.enableViews(); + viewPager.setEnabled(true); + toolbar.setEnabled(true); + } + + @Override + public void disableViews() { + super.disableViews(); + viewPager.setEnabled(false); + toolbar.setEnabled(false); + } + + private void getIntentExtras() { + Bundle intentExtras = getIntent().getExtras(); + final int artistId = intentExtras.getInt(AppKeys.E_ARTIST); + artist = ArtistLoader.getArtist(this, artistId); + if (artist == null) { + finish(); + } + } + + @Override + public void onScrollChanged(int scrollY, boolean b, boolean b2) { + if (!isAnimating) { + int titleTranslationY = getTitleTranslation(scrollY); + ViewHelper.setTranslationY(artistArtOverlayView, getOverlayTranslation(scrollY)); + ViewHelper.setTranslationY(artistImageView, getImageViewTranslation(scrollY)); + ViewHelper.setTranslationY(absAlbumListBackgroundView, getListBackgroundTranslation(scrollY)); + ViewHelper.setAlpha(artistArtOverlayView, getOverlayAlpha(scrollY)); + ViewHelper.setTranslationY(artistTitleText, titleTranslationY); + ViewHelper.setTranslationY(slidingTabs, titleTranslationY); + ViewHelper.setTranslationY(getFab(), getFabTranslation(scrollY)); + translateToolBar(scrollY); + } + } + + private int getImageViewTranslation(int scrollY) { + int minOverlayTransitionY = headerOffset - artistArtOverlayView.getHeight(); + return Math.max(minOverlayTransitionY, Math.min(0, -scrollY / 2)); + } + + private int getOverlayTranslation(int scrollY) { + int minOverlayTransitionY = headerOffset - artistArtOverlayView.getHeight(); + return Math.max(minOverlayTransitionY, Math.min(0, -scrollY)); + } + + private int getListBackgroundTranslation(int scrollY) { + return Math.max(0, -scrollY + artistImageViewHeight); + } + + private int getTitleTranslation(int scrollY) { + int maxTitleTranslationY = artistImageViewHeight; + int titleTranslationY = maxTitleTranslationY - scrollY; + if (TOOLBAR_IS_STICKY) { + titleTranslationY = Math.max(headerOffset, titleTranslationY); + } + return titleTranslationY; + } + + private int getFabTranslation(int scrollY) { + return getTitleTranslation(scrollY) + titleViewHeight + tabHeight - (getFab().getHeight() / 2); + } + + private float getOverlayAlpha(int scrollY) { + float flexibleRange = artistImageViewHeight - headerOffset; + return Math.max(0, Math.min(1, (float) scrollY / flexibleRange)); + } + + private void translateToolBar(int scrollY) { + if (TOOLBAR_IS_STICKY) { + // Change alpha of toolbar background + if (-scrollY + artistImageViewHeight <= headerOffset) { + ViewUtil.setBackgroundAlpha(toolbar, 1, toolbarColor); + ViewUtil.setBackgroundAlpha(statusBar, 1, toolbarColor); + + } else { + ViewUtil.setBackgroundAlpha(toolbar, 0, toolbarColor); + ViewUtil.setBackgroundAlpha(statusBar, 0, toolbarColor); + } + } else { + // Translate Toolbar + if (scrollY < artistImageViewHeight) { + ViewHelper.setTranslationY(toolbar, 0); + } else { + ViewHelper.setTranslationY(toolbar, -scrollY); + } + } + } + + public void restoreY(final int scrollY) { + translateToolBar(scrollY); + int animationTime = 1000; + DecelerateInterpolator interpolator = new DecelerateInterpolator(4); + int titleTranslationY = getTitleTranslation(scrollY); + ViewPropertyAnimator.animate(artistArtOverlayView).y(getOverlayTranslation(scrollY)).setDuration(animationTime).setInterpolator(interpolator).start(); + ViewPropertyAnimator.animate(artistImageView).y(getImageViewTranslation(scrollY)).setDuration(animationTime).setInterpolator(interpolator).start(); + ViewPropertyAnimator.animate(absAlbumListBackgroundView).y(getListBackgroundTranslation(scrollY)).setDuration(animationTime).setInterpolator(interpolator).start(); + ViewPropertyAnimator.animate(artistArtOverlayView).alpha(getOverlayAlpha(scrollY)).setDuration(animationTime).setInterpolator(interpolator).start(); + ViewPropertyAnimator.animate(slidingTabs).y(titleTranslationY + titleViewHeight).setDuration(animationTime).setInterpolator(interpolator).start(); + ViewPropertyAnimator.animate(artistTitleText).y(titleTranslationY).setDuration(animationTime).setInterpolator(interpolator).start(); + ViewPropertyAnimator.animate(getFab()).y(getFabTranslation(scrollY)).setDuration(animationTime).setInterpolator(interpolator).setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + isAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + translateToolBar(scrollY); + isAnimating = false; + if (currentFragment instanceof AbsViewPagerTabArtistListFragment) { + onScrollChanged((((AbsViewPagerTabArtistListFragment) currentFragment).getY()), false, false); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + isAnimating = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + isAnimating = true; + } + }).start(); + } + + @Override + public void onDownMotionEvent() { + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + } + + @Override + public void goToArtist(int artistId) { + if (artist.id != artistId) { + super.goToArtist(artistId); + } + } + + private void lollipopTransitionImageWrongSizeFix() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().getSharedElementEnterTransition().addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(Transition transition) { + + } + + @Override + public void onTransitionEnd(Transition transition) { + if (artistImage == null) { + LastFMArtistImageLoader.loadArtistImage(ArtistDetailActivity.this, artist.name, new LastFMArtistImageLoader.ArtistImageLoaderCallback() { + @Override + public void onArtistImageLoaded(Bitmap artistImage) { + if (artistImage != null) { + artistImageView.setImageBitmap(artistImage); + } + } + }); + } else { + artistImageView.setImageBitmap(artistImage); + } + } + + @Override + public void onTransitionCancel(Transition transition) { + + } + + @Override + public void onTransitionPause(Transition transition) { + + } + + @Override + public void onTransitionResume(Transition transition) { + + } + }); + } + } + + private static class NavigationAdapter extends FragmentPagerAdapter { + + private String[] titles; + + private SparseArray mPages; + private Artist artist; + private Context context; + + public NavigationAdapter(Activity activity, Artist artist) { + super(activity.getFragmentManager()); + this.artist = artist; + mPages = new SparseArray<>(); + context = activity; + titles = new String[]{ + context.getResources().getString(R.string.tab_songs), + context.getResources().getString(R.string.tab_albums), + context.getResources().getString(R.string.tab_biography) + }; + } + + @Override + public Fragment getItem(int position) { + Bundle args = new Bundle(); + args.putInt(ARG_ARTIST_ID, artist.id); + args.putString(ARG_ARTIST_NAME, artist.name); + Fragment f; + switch (position) { + case 1: + f = mPages.get(position, new ViewPagerTabArtistAlbumFragment()); + break; + case 0: + f = mPages.get(position, new ViewPagerTabArtistSongListFragment()); + break; + case 2: + f = mPages.get(position, new ViewPagerTabArtistBioFragment()); + break; + default: + f = mPages.get(position, new MainActivity.PlaceholderFragment()); + break; + } + f.setArguments(args); + mPages.put(position, f); + return f; + } + + public Fragment getItemAt(int position) { + return mPages.get(position, null); + } + + @Override + public int getCount() { + return titles.length; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (0 <= mPages.indexOfKey(position)) { + mPages.remove(position); + } + super.destroyItem(container, position, object); + } + + @Override + public CharSequence getPageTitle(int position) { + return titles[position]; + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MainActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MainActivity.java new file mode 100644 index 00000000..93237258 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MainActivity.java @@ -0,0 +1,320 @@ +package com.kabouzeid.materialmusic.ui.activities; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.SearchView; +import android.support.v7.widget.Toolbar; +import android.transition.Explode; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.interfaces.KabSearchAbleFragment; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.ui.activities.base.AbsFabActivity; +import com.kabouzeid.materialmusic.ui.fragments.NavigationDrawerFragment; +import com.kabouzeid.materialmusic.ui.fragments.mainactivityfragments.AlbumViewFragment; +import com.kabouzeid.materialmusic.ui.fragments.mainactivityfragments.ArtistViewFragment; +import com.kabouzeid.materialmusic.ui.fragments.mainactivityfragments.SongViewFragment; +import com.kabouzeid.materialmusic.util.ImageLoaderUtil; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; +import com.kabouzeid.materialmusic.util.ViewUtil; +import com.nostra13.universalimageloader.core.ImageLoader; + + +public class MainActivity extends AbsFabActivity + implements NavigationDrawerFragment.NavigationDrawerCallbacks, OnMusicRemoteEventListener, KabViewsDisableAble { + public static final String TAG = MainActivity.class.getSimpleName(); + + private int currentFragmentPosition = -1; + + private DrawerLayout drawerLayout; + private ActionBarDrawerToggle drawerToggle; + private NavigationDrawerFragment navigationDrawerFragment; + private CharSequence toolbarTitle; + private Toolbar toolbar; + private View statusBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setUpTranslucence(true, true); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + initViews(); + setUpToolBar(); + + navigationDrawerFragment.setUp( + R.id.navigation_drawer, + drawerLayout + ); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setExitTransition(new Explode()); + } + } + + @Override + protected void onResume() { + super.onResume(); + updateNavigationDrawerHeader(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getApp().getMusicPlayerRemote().removeAllOnMusicRemoteEventListeners(); + } + + @Override + public void onNavigationDrawerItemSelected(int position) { + if (position == NavigationDrawerFragment.NAVIGATION_DRAWER_HEADER) { + openCurrentPlayingIfPossible(null); + } else { + setFragment(position); + } + } + + private void setFragment(int position) { + if (currentFragmentPosition != position) { + switch (position) { + case 0: + if (getApp().MainActivityFragments[position] == null) { + getApp().MainActivityFragments[position] = new SongViewFragment(); + } + toolbarTitle = getString(R.string.all_songs); + break; + case 1: + if (getApp().MainActivityFragments[position] == null) { + getApp().MainActivityFragments[position] = new AlbumViewFragment(); + } + toolbarTitle = getString(R.string.albums); + break; + case 2: + if (getApp().MainActivityFragments[position] == null) { + getApp().MainActivityFragments[position] = new ArtistViewFragment(); + } + toolbarTitle = getString(R.string.artists); + break; + case 3: + if (getApp().MainActivityFragments[position] == null) { + getApp().MainActivityFragments[position] = new PlaceholderFragment(); + } + toolbarTitle = getString(R.string.genres); + break; + case 4: + if (getApp().MainActivityFragments[position] == null) { + getApp().MainActivityFragments[position] = new PlaceholderFragment(); + } + toolbarTitle = getString(R.string.playlists); + break; + default: + toolbarTitle = getString(R.string.app_name); + return; + } + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.container, getApp().MainActivityFragments[position]) + .commit(); + currentFragmentPosition = position; + supportInvalidateOptionsMenu(); + } + } + + private void initViews() { + drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + navigationDrawerFragment = (NavigationDrawerFragment) + getFragmentManager().findFragmentById(R.id.navigation_drawer); + updateNavigationDrawerHeader(); + } + + public void restoreActionBar() { + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setTitle(toolbarTitle); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.drawer, menu); + restoreActionBar(); + + final MenuItem search = menu.findItem(R.id.action_search); + search.setVisible(currentFragmentPosition != -1 && getApp().MainActivityFragments[currentFragmentPosition] instanceof KabSearchAbleFragment); + + + SearchView searchView = (SearchView) MenuItemCompat.getActionView(search); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (currentFragmentPosition != -1 && getApp().MainActivityFragments[currentFragmentPosition] instanceof KabSearchAbleFragment) { + ((KabSearchAbleFragment) getApp().MainActivityFragments[currentFragmentPosition]).search(newText); + } + return false; + } + }); + MenuItemCompat.setOnActionExpandListener(search, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + if (currentFragmentPosition != -1 && getApp().MainActivityFragments[currentFragmentPosition] instanceof KabSearchAbleFragment) { + ((KabSearchAbleFragment) getApp().MainActivityFragments[currentFragmentPosition]).returnToNonSearch(); + } + return true; + } + }); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + int id = item.getItemId(); + switch (id) { + case R.id.action_settings: + return true; + case R.id.action_current_playing: + openCurrentPlayingIfPossible(null); + return true; + + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + drawerToggle.onConfigurationChanged(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + public void onBackPressed() { + if (navigationDrawerFragment.isDrawerOpen()) { + drawerLayout.closeDrawers(); + return; + } + super.onBackPressed(); + } + + private void setUpToolBar() { + toolbarTitle = getTitle(); + toolbar = (Toolbar) findViewById(R.id.toolbar); + statusBar = findViewById(R.id.statusBar); + setSupportActionBar(toolbar); + ViewUtil.setBackgroundAlpha(toolbar, 0.97f, Util.resolveColor(this, R.attr.colorPrimary)); + ViewUtil.setBackgroundAlpha(statusBar, 0.97f, Util.resolveColor(this, R.attr.colorPrimary)); + setUpDrawerToggle(); + } + + private void setUpDrawerToggle() { + drawerToggle = new ActionBarDrawerToggle( + this, + drawerLayout, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close + ); + drawerLayout.post(new Runnable() { + @Override + public void run() { + drawerToggle.syncState(); + } + }); + drawerLayout.setDrawerListener(drawerToggle); + } + + private void updateNavigationDrawerHeader() { + Song song = getApp().getMusicPlayerRemote().getCurrentSong(); + if (navigationDrawerFragment != null && song.id != -1) { + ImageLoader.getInstance().displayImage(MusicUtil.getAlbumArtUri(song.albumId).toString(), navigationDrawerFragment.getAlbumArtImageView(), new ImageLoaderUtil.defaultAlbumArtOnFailed()); + navigationDrawerFragment.getSongTitle().setText(song.title); + navigationDrawerFragment.getSongArtist().setText(song.artistName); + } + } + + private void disableFragmentViews() { + if (currentFragmentPosition >= 0 && currentFragmentPosition < getApp().MainActivityFragments.length) { + if (getApp().MainActivityFragments[currentFragmentPosition] instanceof KabViewsDisableAble) { + ((KabViewsDisableAble) getApp().MainActivityFragments[currentFragmentPosition]).disableViews(); + } + } + } + + private void enableFragmentViews() { + if (currentFragmentPosition >= 0 && currentFragmentPosition < getApp().MainActivityFragments.length) { + if (getApp().MainActivityFragments[currentFragmentPosition] instanceof KabViewsDisableAble) { + ((KabViewsDisableAble) getApp().MainActivityFragments[currentFragmentPosition]).enableViews(); + } + } + } + + private boolean areFragmentViewsEnabled() { + if (currentFragmentPosition >= 0 && currentFragmentPosition < getApp().MainActivityFragments.length) { + if (getApp().MainActivityFragments[currentFragmentPosition] instanceof KabViewsDisableAble) { + return ((KabViewsDisableAble) getApp().MainActivityFragments[currentFragmentPosition]).areViewsEnabled(); + } + } + return true; + } + + @Override + public void enableViews() { + try { + super.enableViews(); + toolbar.setEnabled(true); + } catch (NullPointerException e) { + Log.e(TAG, "wasn't able to enable the views", e.fillInStackTrace()); + } + } + + @Override + public void disableViews() { + try { + super.disableViews(); + toolbar.setEnabled(false); + } catch (NullPointerException e) { + Log.e(TAG, "wasn't able to disable the views", e.fillInStackTrace()); + } + } + + public static class PlaceholderFragment extends Fragment { + + public PlaceholderFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_place_holder, container, false); + return rootView; + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MusicControllerActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MusicControllerActivity.java new file mode 100644 index 00000000..ee82bcf3 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/MusicControllerActivity.java @@ -0,0 +1,428 @@ +package com.kabouzeid.materialmusic.ui.activities; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.util.Pair; +import android.support.v7.graphics.Palette; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.helper.PlayingQueueDialogHelper; +import com.kabouzeid.materialmusic.helper.SongDetailDialogHelper; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.lastfm.artist.LastFMArtistImageLoader; +import com.kabouzeid.materialmusic.loader.SongFileLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.MusicRemoteEvent; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.service.MusicService; +import com.kabouzeid.materialmusic.ui.activities.base.AbsFabActivity; +import com.kabouzeid.materialmusic.ui.activities.tageditor.SongTagEditorActivity; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; +import com.kabouzeid.materialmusic.util.ViewUtil; +import com.nineoldandroids.view.ViewPropertyAnimator; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import java.io.File; + +public class MusicControllerActivity extends AbsFabActivity implements OnMusicRemoteEventListener { + public static final String TAG = MusicControllerActivity.class.getSimpleName(); + + private static final int DEFAULT_DELAY = 350; + private static final int DEFAULT_ANIMATION_TIME = 1000; + + private Song song; + private ImageView albumArt; + private ImageView artistArt; + private TextView songTitle; + private TextView songArtist; + private TextView currentSongProgress; + private TextView totalSongDuration; + private View footer; + private SeekBar progressSlider; + private ImageButton nextButton; + private ImageButton prevButton; + private ImageButton repeatButton; + private ImageButton shuffleButton; + + private int lastFooterColor = -1; + + private boolean killThreads = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setUpTranslucence(true, false); + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_music_controller); + + initViews(); + + moveSeekBarIntoPlace(); + + updateCurrentSong(); + + setUpMusicControllers(); + + prepareViewsForOpenAnimation(); + + setUpToolBar(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_title_playing, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + super.onBackPressed(); + return true; + case R.id.action_playing_queue: + final MaterialDialog materialDialog = PlayingQueueDialogHelper.getDialog(this); + materialDialog.show(); + return true; + case R.id.action_tag_editor: + Intent intent = new Intent(this, SongTagEditorActivity.class); + intent.putExtra(AppKeys.E_ID, song.id); + startActivity(intent); + return true; + case R.id.action_details: + String songFilePath = SongFileLoader.getSongFile(this, song.id); + File songFile = new File(songFilePath); + SongDetailDialogHelper.getDialog(this, songFile).show(); + return true; + case R.id.action_go_to_album: + goToAlbumDetailsActivity(song.albumId, null); + return true; + case R.id.action_go_to_artist: + goToArtistDetailsActivity(song.artistId, null); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + protected void onResume() { + super.onResume(); + startMusicControllerStateUpdateThread(); + } + + @Override + protected void onPause() { + super.onPause(); + killThreads = true; + } + + private void updateCurrentSong() { + getCurrentSongAndQueue(); + setHeadersText(); + setUpArtistArt(); + setUpAlbumArtAndApplyPalette(); + totalSongDuration.setText(MusicUtil.getReadableDurationString(song.duration)); + currentSongProgress.setText(MusicUtil.getReadableDurationString(-1)); + } + + private void moveSeekBarIntoPlace() { + RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) progressSlider.getLayoutParams(); + progressSlider.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + lp.setMargins(0, 0, 0, -(progressSlider.getMeasuredHeight() / 2)); + progressSlider.setLayoutParams(lp); + } + + private void setHeadersText() { + songTitle.setText(song.title); + songArtist.setText(song.artistName); + } + + private void setUpAlbumArtAndApplyPalette() { + ImageLoader.getInstance().displayImage(MusicUtil.getAlbumArtUri(song.albumId).toString(), albumArt, new ImageLoadingListener() { + @Override + public void onLoadingStarted(String imageUri, View view) { + albumArt.setImageResource(R.drawable.default_album_art); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + albumArt.setImageResource(R.drawable.default_album_art); + setStandardColors(); + } + + @Override + public void onLoadingComplete(String imageUri, final View view, Bitmap loadedImage) { + applyPalette(loadedImage); + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + albumArt.setImageResource(R.drawable.default_album_art); + setStandardColors(); + } + }); + } + + private void setUpArtistArt() { + if (artistArt != null) { + artistArt.setImageResource(R.drawable.default_artist_image); + LastFMArtistImageLoader.loadArtistImage(this, song.artistName, new LastFMArtistImageLoader.ArtistImageLoaderCallback() { + @Override + public void onArtistImageLoaded(Bitmap artistImage) { + artistArt.setImageBitmap(artistImage); + } + }); + } + } + + private void getCurrentSongAndQueue() { + if (getApp().getMusicPlayerRemote().getPosition() >= 0) { + song = getApp().getMusicPlayerRemote().getPlayingQueue().get(getApp().getMusicPlayerRemote().getPosition()); + } else { + finish(); + } + } + + private void initViews() { + nextButton = (ImageButton) findViewById(R.id.next_button); + prevButton = (ImageButton) findViewById(R.id.prev_button); + repeatButton = (ImageButton) findViewById(R.id.repeat_button); + shuffleButton = (ImageButton) findViewById(R.id.shuffle_button); + albumArt = (ImageView) findViewById(R.id.album_art); + artistArt = (ImageView) findViewById(R.id.artist_image); + songTitle = (TextView) findViewById(R.id.song_title); + songArtist = (TextView) findViewById(R.id.song_artist); + currentSongProgress = (TextView) findViewById(R.id.song_current_progress); + totalSongDuration = (TextView) findViewById(R.id.song_total_time); + footer = findViewById(R.id.footer); + progressSlider = (SeekBar) findViewById(R.id.progress_slider); + } + + private void applyPalette(Bitmap bitmap) { + Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + Palette.Swatch swatch = palette.getVibrantSwatch(); + if (swatch != null) { + animateColorChange(swatch.getRgb()); + songTitle.setTextColor(swatch.getTitleTextColor()); + songArtist.setTextColor(swatch.getBodyTextColor()); + } else { + setStandardColors(); + } + } + }); + } + + private void setStandardColors() { + int songTitleTextColor = Util.resolveColor(this, R.attr.title_text_color); + int artistNameTextColor = Util.resolveColor(this, R.attr.caption_text_color); + int colorPrimary = Util.resolveColor(MusicControllerActivity.this, R.attr.colorPrimary); + + animateColorChange(colorPrimary); + + songTitle.setTextColor(songTitleTextColor); + songArtist.setTextColor(artistNameTextColor); + } + + private void animateColorChange(final int newColor) { + if (lastFooterColor != -1 && lastFooterColor != newColor) { + ViewUtil.animateViewColor(footer, lastFooterColor, newColor, 300); + } else { + footer.setBackgroundColor(newColor); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setNavigationBarColor(newColor); + } + lastFooterColor = newColor; + } + + private void setUpMusicControllers() { + setUpPrevNext(); + setUpRepeatButton(); + setUpShuffleButton(); + setUpProgressSlider(); + } + + private void setUpProgressSlider() { + progressSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + getApp().getMusicPlayerRemote().seekTo(progress); + } + currentSongProgress.setText(MusicUtil.getReadableDurationString(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + + private void setUpPrevNext() { + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getApp().getMusicPlayerRemote().playNextSong(); + } + }); + prevButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getApp().getMusicPlayerRemote().back(); + } + }); + } + + private void setUpShuffleButton() { + updateShuffleState(); + shuffleButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getApp().getMusicPlayerRemote().toggleShuffleMode(); + } + }); + } + + private void setUpRepeatButton() { + updateRepeatState(); + repeatButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getApp().getMusicPlayerRemote().cycleRepeatMode(); + } + }); + } + + private void updateRepeatState() { + switch (getApp().getMusicPlayerRemote().getRepeatMode()) { + case MusicService.REPEAT_MODE_NONE: + repeatButton.setImageResource(R.drawable.ic_repeat_grey600_48dp); + break; + case MusicService.REPEAT_MODE_ALL: + repeatButton.setImageResource(R.drawable.ic_repeat_white_48dp); + break; + default: + repeatButton.setImageResource(R.drawable.ic_repeat_one_white_48dp); + break; + } + } + + private void updateShuffleState() { + switch (getApp().getMusicPlayerRemote().getShuffleMode()) { + case MusicService.SHUFFLE_MODE_SHUFFLE: + shuffleButton.setImageResource(R.drawable.ic_shuffle_white_48dp); + break; + default: + shuffleButton.setImageResource(R.drawable.ic_shuffle_grey600_48dp); + break; + } + } + + @Override + protected void updateControllerState() { + super.updateControllerState(); + updateRepeatState(); + updateShuffleState(); + } + + private void startMusicControllerStateUpdateThread() { + killThreads = false; + new Thread(new Runnable() { + @Override + public void run() { + int currentPosition = 0; + int total = 0; + while (getApp().getMusicPlayerRemote().isMusicBound() && !killThreads) { + try { + total = getApp().getMusicPlayerRemote().getSongDurationMillis(); + currentPosition = getApp().getMusicPlayerRemote().getSongProgressMillis(); + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } catch (Exception e) { + } + progressSlider.setMax(total); + progressSlider.setProgress(currentPosition); + } + } + }).start(); + } + + @Override + public void onMusicRemoteEvent(MusicRemoteEvent event) { + super.onMusicRemoteEvent(event); + switch (event.getAction()) { + case MusicRemoteEvent.NEXT: + updateCurrentSong(); + break; + case MusicRemoteEvent.PREV: + updateCurrentSong(); + break; + case MusicRemoteEvent.REPEAT_MODE_CHANGED: + updateRepeatState(); + break; + case MusicRemoteEvent.SHUFFLE_MODE_CHANGED: + updateShuffleState(); + break; + } + } + + private void prepareViewsForOpenAnimation() { + footer.setPivotY(0); + footer.setScaleY(0); + } + + private void animateActivityOpened(int startDelay) { + ViewPropertyAnimator.animate(footer) + .scaleX(1) + .scaleY(1) + .setInterpolator(new DecelerateInterpolator(4)) + .setDuration(DEFAULT_ANIMATION_TIME) + .setStartDelay(startDelay) + .start(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + animateActivityOpened(DEFAULT_DELAY); + } + } + + private void setUpToolBar() { + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + getSupportActionBar().setTitle(null); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + protected boolean openCurrentPlayingIfPossible(Pair[] sharedViews) { + onBackPressed(); + return true; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsBaseActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsBaseActivity.java new file mode 100644 index 00000000..7db540d0 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsBaseActivity.java @@ -0,0 +1,128 @@ +package com.kabouzeid.materialmusic.ui.activities.base; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.util.Pair; +import android.support.v7.app.ActionBarActivity; +import android.widget.Toast; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.songadapter.SongAdapter; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.ui.activities.AlbumDetailActivity; +import com.kabouzeid.materialmusic.ui.activities.ArtistDetailActivity; +import com.kabouzeid.materialmusic.ui.activities.MusicControllerActivity; +import com.kabouzeid.materialmusic.util.Util; + +/** + * Created by karim on 20.01.15. + */ +public abstract class AbsBaseActivity extends ActionBarActivity implements KabViewsDisableAble, SongAdapter.GoToAble { + private App app; + private boolean areViewsEnabled; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(getApp().getAppTheme()); + super.onCreate(savedInstanceState); + } + + @Override + protected void onResume() { + super.onResume(); + enableViews(); + } + + protected void setUpTranslucence(boolean statusBarTranslucent, boolean navigationBarTranslucent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Util.setStatusBarTranslucent(getWindow(), statusBarTranslucent); + if (getApp().isInPortraitMode() || getApp().isTablet()) { + Util.setNavBarTranslucent(getWindow(), navigationBarTranslucent); + } + } + } + + protected boolean openCurrentPlayingIfPossible(Pair[] sharedViews) { + if (getApp().getMusicPlayerRemote().getPosition() != -1) { + if (areViewsEnabled()) { + disableViews(); + Intent intent = new Intent(this, MusicControllerActivity.class); + if (sharedViews != null) { + ActivityOptionsCompat optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this, + sharedViews + ); + ActivityCompat.startActivity(this, intent, optionsCompat.toBundle()); + } else { + startActivity(intent); + } + return true; + } + } else { + Toast.makeText(this, getResources().getString(R.string.nothing_playing), Toast.LENGTH_SHORT).show(); + } + return false; + } + + @Override + public void goToAlbum(int albumId) { + goToAlbumDetailsActivity(albumId, null); + } + + @Override + public void goToArtist(int artistId) { + goToArtistDetailsActivity(artistId, null); + } + + public void goToAlbumDetailsActivity(int albumId, Pair[] sharedViews) { + final Intent intent = new Intent(this, AlbumDetailActivity.class); + intent.putExtra(AppKeys.E_ALBUM, albumId); + if (sharedViews != null) { + ActivityOptionsCompat optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this, + sharedViews + ); + ActivityCompat.startActivity(this, intent, optionsCompat.toBundle()); + } else { + startActivity(intent); + } + } + + public void goToArtistDetailsActivity(int artistId, Pair[] sharedViews) { + final Intent intent = new Intent(this, ArtistDetailActivity.class); + intent.putExtra(AppKeys.E_ARTIST, artistId); + if (sharedViews != null) { + ActivityOptionsCompat optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this, + sharedViews + ); + ActivityCompat.startActivity(this, intent, optionsCompat.toBundle()); + } else { + startActivity(intent); + } + } + + @Override + public boolean areViewsEnabled() { + return areViewsEnabled; + } + + @Override + public void enableViews() { + areViewsEnabled = true; + } + + @Override + public void disableViews() { + areViewsEnabled = false; + } + + protected App getApp() { + if (app == null) { + app = (App) getApplicationContext(); + } + return app; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsFabActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsFabActivity.java new file mode 100644 index 00000000..50ce89af --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/base/AbsFabActivity.java @@ -0,0 +1,163 @@ +package com.kabouzeid.materialmusic.ui.activities.base; + +import android.os.Bundle; +import android.support.v4.util.Pair; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Toast; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.interfaces.OnMusicRemoteEventListener; +import com.kabouzeid.materialmusic.misc.SmallOnGestureListener; +import com.kabouzeid.materialmusic.model.MusicRemoteEvent; +import com.melnykov.fab.FloatingActionButton; + +/** + * Created by karim on 22.01.15. + */ +public abstract class AbsFabActivity extends AbsBaseActivity implements OnMusicRemoteEventListener { + private FloatingActionButton fab; + + protected FloatingActionButton getFab() { + if (fab == null) { + fab = (FloatingActionButton) findViewById(R.id.fab); + } + return fab; + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + setUpFab(); + } + + @Override + protected void onResume() { + super.onResume(); + updateControllerState(); + getApp().getMusicPlayerRemote().addOnMusicRemoteEventListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + getApp().getMusicPlayerRemote().removeOnMusicRemoteEventListener(this); + } + + @Override + public void enableViews() { + super.enableViews(); + fab.setEnabled(true); + } + + @Override + public void disableViews() { + super.disableViews(); + fab.setEnabled(false); + } + + @Override + protected boolean openCurrentPlayingIfPossible(Pair[] sharedViews) { + return super.openCurrentPlayingIfPossible(getSharedViewsWithFab(sharedViews)); + } + + @Override + public void goToArtistDetailsActivity(int artistId, Pair[] sharedViews) { + super.goToArtistDetailsActivity(artistId, getSharedViewsWithFab(sharedViews)); + } + + @Override + public void goToAlbumDetailsActivity(int albumId, Pair[] sharedViews) { + super.goToAlbumDetailsActivity(albumId, getSharedViewsWithFab(sharedViews)); + } + + private Pair[] getSharedViewsWithFab(Pair[] sharedViews) { + Pair[] sharedViewsWithFab; + if (sharedViews != null) { + sharedViewsWithFab = new Pair[sharedViews.length + 1]; + for (int i = 0; i < sharedViews.length; i++) { + sharedViewsWithFab[i] = sharedViews[i]; + } + } else { + sharedViewsWithFab = new Pair[1]; + } + sharedViewsWithFab[sharedViewsWithFab.length - 1] = Pair.create((View) getFab(), getString(R.string.transition_fab)); + return sharedViewsWithFab; + } + + private void setUpFab() { + updateFabState(); + final GestureDetector gestureDetector = new GestureDetector(this, new SmallOnGestureListener() { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + openCurrentPlayingIfPossible(null); + return true; + } + }); + + getFab().setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getApp().getMusicPlayerRemote().getPosition() != -1) { + if (getApp().getMusicPlayerRemote().isPlaying()) { + getApp().getMusicPlayerRemote().pauseSong(); + } else { + getApp().getMusicPlayerRemote().resumePlaying(); + } + } else { + Toast.makeText(AbsFabActivity.this, getResources().getString(R.string.nothing_playing), Toast.LENGTH_SHORT).show(); + } + } + }); + + getFab().setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + Toast.makeText(AbsFabActivity.this, getResources().getString(R.string.hint_fling_to_open), Toast.LENGTH_SHORT).show(); + return true; + } + }); + + getFab().setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + gestureDetector.onTouchEvent(event); + return false; + } + }); + } + + protected void updateControllerState() { + updateFabState(); + } + + private void updateFabState() { + if (getApp().getMusicPlayerRemote().isPlaying()) { + getFab().setImageResource(R.drawable.ic_pause_white_48dp); + } else { + getFab().setImageResource(R.drawable.ic_play_arrow_white_48dp); + } + } + + @Override + public void onMusicRemoteEvent(MusicRemoteEvent event) { + switch (event.getAction()) { + case MusicRemoteEvent.PLAY: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_pause_white_48dp)); + break; + case MusicRemoteEvent.PAUSE: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_play_arrow_white_48dp)); + break; + case MusicRemoteEvent.RESUME: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_pause_white_48dp)); + break; + case MusicRemoteEvent.STOP: + fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_play_arrow_white_48dp)); + break; + case MusicRemoteEvent.QUEUE_COMPLETED: + fab.setImageResource(R.drawable.ic_play_arrow_white_48dp); + break; + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AbsTagEditorActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AbsTagEditorActivity.java new file mode 100644 index 00000000..ffff28cb --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AbsTagEditorActivity.java @@ -0,0 +1,464 @@ +package com.kabouzeid.materialmusic.ui.activities.tageditor; + +import android.app.SearchManager; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.graphics.Palette; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.OvershootInterpolator; +import android.widget.ImageView; +import android.widget.TextView; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.github.ksoichiro.android.observablescrollview.ObservableScrollView; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.misc.SmallObservableScrollViewCallbacks; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; +import com.kabouzeid.materialmusic.util.ViewUtil; +import com.melnykov.fab.FloatingActionButton; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.utils.MemoryCacheUtils; + +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 org.jaudiotagger.tag.images.Artwork; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Created by karim on 18.01.15. + */ +public abstract class AbsTagEditorActivity extends ActionBarActivity { + public static final String TAG = AbsTagEditorActivity.class.getSimpleName(); + private static final int REQUEST_CODE_SELECT_IMAGE = 1337; + + private App app; + private int id; + private int headerVariableSpace; + private int paletteColorPrimary; + private boolean isInNoImageMode; + + private FloatingActionButton fab; + private ObservableScrollView scrollView; + private Toolbar toolBar; + private ImageView image; + private View header; + + private List songPaths; + + @Override + protected void onCreate(Bundle savedInstanceState) { + app = (App) getApplicationContext(); + setTheme(app.getAppTheme()); + setUpTranslucence(); + + super.onCreate(savedInstanceState); + setContentView(getContentViewResId()); + + getIntentExtras(); + headerVariableSpace = getResources().getDimensionPixelSize(R.dimen.tagEditorHeaderVariableSpace); + songPaths = getSongPaths(); + + initViews(); + setUpViews(); + setUpToolBar(); + } + + private void initViews() { + fab = (FloatingActionButton) findViewById(R.id.fab); + scrollView = (ObservableScrollView) findViewById(R.id.observableScrollView); + toolBar = (Toolbar) findViewById(R.id.toolbar); + image = (ImageView) findViewById(R.id.image); + header = findViewById(R.id.header); + } + + private void setUpViews() { + restoreStandardColors(); + setUpScrollView(); + setUpFab(); + setUpImageView(); + } + + private void setUpScrollView() { + scrollView.setScrollViewCallbacks(observableScrollViewCallbacks); + scrollView.post(new Runnable() { + @Override + public void run() { + scrollView.scrollVerticallyTo(headerVariableSpace / 2); + } + }); + } + + private void setUpImageView() { + loadCurrentImage(); + image.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new MaterialDialog.Builder(AbsTagEditorActivity.this) + .title("Update image") + .items(new CharSequence[]{"Download from LastFM", "Pick from internal storage", "Web search", "Delete"}) + .itemsCallback(new MaterialDialog.ListCallback() { + @Override + public void onSelection(MaterialDialog dialog, View view, int which, CharSequence text) { + switch (which) { + case 0: + getImageFromLastFM(); + break; + case 1: + startImagePicker(); + break; + case 2: + searchImageOnWeb(); + break; + case 3: + deleteImage(); + break; + } + } + }) + .build() + .show(); + } + }); + } + + protected void searchWebFor(List strings) { + StringBuilder stringBuilder = new StringBuilder(); + for (String string : strings) { + stringBuilder.append(string); + stringBuilder.append(" "); + } + Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.putExtra(SearchManager.QUERY, stringBuilder.toString()); + startActivity(intent); + } + + private void startImagePicker() { + Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); + photoPickerIntent.setType("image/*"); + startActivityForResult(photoPickerIntent, REQUEST_CODE_SELECT_IMAGE); + } + + private void setUpTranslucence() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Util.setStatusBarTranslucent(getWindow(), false); + Util.setNavBarTranslucent(getWindow(), false); + } + } + + private void getIntentExtras() { + Bundle intentExtras = getIntent().getExtras(); + if (intentExtras != null) { + id = intentExtras.getInt(AppKeys.E_ID); + } + } + + protected void setUpToolBar() { + setSupportActionBar(toolBar); + getSupportActionBar().setTitle(getResources().getString(R.string.tag_editor)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_tag_editor, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + switch (id) { + case android.R.id.home: + super.onBackPressed(); + return true; + case R.id.action_settings: + return true; + } + return super.onOptionsItemSelected(item); + } + + protected void setUpFab() { + ViewHelper.setScaleX(fab, 0); + ViewHelper.setScaleY(fab, 0); + fab.setEnabled(false); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + save(); + } + }); + } + + private void showFab() { + ViewPropertyAnimator.animate(fab) + .setDuration(500) + .setInterpolator(new OvershootInterpolator()) + .scaleX(1) + .scaleY(1) + .start(); + fab.setEnabled(true); + } + + private SmallObservableScrollViewCallbacks observableScrollViewCallbacks = new SmallObservableScrollViewCallbacks() { + @Override + public void onScrollChanged(int scrollY, boolean b, boolean b2) { + float alpha; + if (!isInNoImageMode) { + alpha = 1 - (float) Math.max(0, headerVariableSpace - scrollY) / headerVariableSpace; + } else { + ViewHelper.setTranslationY(header, scrollY); + alpha = 1; + } + ViewUtil.setBackgroundAlpha(toolBar, alpha, paletteColorPrimary); + ViewUtil.setBackgroundAlpha(header, alpha, paletteColorPrimary); + ViewHelper.setTranslationY(image, scrollY / 2); + } + }; + + private void applyPalette(final Bitmap bitmap) { + if (bitmap != null) { + Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + final int vibrantColor = palette.getVibrantColor(Util.resolveColor(AbsTagEditorActivity.this, R.attr.colorPrimary)); + paletteColorPrimary = vibrantColor; + observableScrollViewCallbacks.onScrollChanged(scrollView.getCurrentScrollY(), false, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(vibrantColor); + getWindow().setNavigationBarColor(vibrantColor); + } + } + }); + } else { + restoreStandardColors(); + } + } + + private void restoreStandardColors() { + final int vibrantColor = Util.resolveColor(this, R.attr.colorPrimary); + paletteColorPrimary = vibrantColor; + observableScrollViewCallbacks.onScrollChanged(scrollView.getCurrentScrollY(), false, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(vibrantColor); + getWindow().setNavigationBarColor(vibrantColor); + } + } + + protected void setNoImageMode() { + isInNoImageMode = true; + image.setVisibility(View.GONE); + image.setEnabled(false); + scrollView.setPadding(0, Util.getActionBarSize(this), 0, 0); + observableScrollViewCallbacks.onScrollChanged(scrollView.getCurrentScrollY(), false, false); + } + + protected void dataChanged() { + showFab(); + } + + protected void setImageBitmap(final Bitmap bitmap) { + if (bitmap != null) { + image.setImageBitmap(bitmap); + applyPalette(bitmap); + } + } + + protected void setImageRes(int resId) { + Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId); + setImageBitmap(bitmap); + } + + private void rescanMedia() { + String[] toBeScanned = new String[songPaths.size()]; + toBeScanned = songPaths.toArray(toBeScanned); + MediaScannerConnection.scanFile(this, toBeScanned, null, null); + } + + private AudioFile getAudioFile(String path) { + try { + return AudioFileIO.read(new File(path)); + } catch (CannotReadException | ReadOnlyFileException | InvalidAudioFrameException | TagException | IOException e) { + Log.e(TAG, "error while trying to create the AudioFile from File", e); + } + return null; + } + + protected void writeValuesToFiles(final Map fieldKeyValueMap) { + writeValuesToFiles(fieldKeyValueMap, null, false); + } + + protected void writeValuesToFiles(final Map fieldKeyValueMap, final Artwork artwork) { + if (artwork == null) { + writeValuesToFiles(fieldKeyValueMap, null, true); + } else { + writeValuesToFiles(fieldKeyValueMap, artwork, false); + } + } + + protected void writeValuesToFiles(final Map fieldKeyValueMap, boolean deleteArtwork) { + writeValuesToFiles(fieldKeyValueMap, null, deleteArtwork); + } + + protected void writeValuesToFiles(final Map fieldKeyValueMap, final Artwork artwork, final boolean deleteArtwork) { + final String writingFileStr = getResources().getString(R.string.writing_file_number); + final MaterialDialog progressDialog = new MaterialDialog.Builder(AbsTagEditorActivity.this) + .customView(R.layout.dialog_loading, true) + .title(writingFileStr) + .cancelable(false) + .build(); + final TextView progressText = (TextView) progressDialog.getCustomView().findViewById(R.id.text); + progressDialog.show(); + new Thread(new Runnable() { + @Override + public void run() { + for (int i = 0; i < songPaths.size(); i++) { + String songPath = songPaths.get(i); + final int finalI = i; + runOnUiThread(new Runnable() { + @Override + public void run() { + progressText.setText((finalI + 1) + "/" + songPaths.size()); + } + }); + try { + AudioFile audioFile = AudioFileIO.read(new File(songPath)); + Tag tag = audioFile.getTagOrCreateAndSetDefault(); + for (Map.Entry entry : fieldKeyValueMap.entrySet()) { + tag.setField(entry.getKey(), entry.getValue()); + } + if (deleteArtwork) { + tag.deleteArtworkField(); + } else if (artwork != null) { + tag.deleteArtworkField(); + tag.setField(artwork); + } + audioFile.commit(); + } catch (CannotReadException | IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { + Log.e(TAG, "Error while reading audio file.", e); + } catch (CannotWriteException e) { + Log.e(TAG, "Error while writing audio file.", e); + } + } + if (deleteArtwork) { + String imagePath = MusicUtil.getAlbumArtUri(getId()).toString(); + ImageLoader.getInstance().getDiskCache().remove(imagePath); + MemoryCacheUtils.removeFromCache(imagePath, ImageLoader.getInstance().getMemoryCache()); + MusicUtil.deleteAlbumArt(AbsTagEditorActivity.this, getId()); + } else if (artwork != null) { + String imagePath = MusicUtil.getAlbumArtUri(getId()).toString(); + MemoryCacheUtils.removeFromCache(imagePath, ImageLoader.getInstance().getMemoryCache()); + ImageLoader.getInstance().getDiskCache().remove(imagePath); + } + progressDialog.dismiss(); + rescanMedia(); + restartApp(); + } + }).start(); + } + + private void restartApp() { + Intent i = getBaseContext().getPackageManager() + .getLaunchIntentForPackage(getBaseContext().getPackageName()); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent imageReturnedIntent) { + super.onActivityResult(requestCode, resultCode, imageReturnedIntent); + switch (requestCode) { + case REQUEST_CODE_SELECT_IMAGE: + if (resultCode == RESULT_OK) { + Uri selectedImage = imageReturnedIntent.getData(); + loadImageFromFile(selectedImage); + } + } + } + + protected abstract void save(); + + protected abstract int getContentViewResId(); + + protected abstract void loadCurrentImage(); + + protected abstract void getImageFromLastFM(); + + protected abstract void searchImageOnWeb(); + + protected abstract void loadImageFromFile(Uri selectedFile); + + protected abstract void deleteImage(); + + protected abstract List getSongPaths(); + + protected App getApp() { + return app; + } + + protected int getId() { + return id; + } + + protected String getSongTitle() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.TITLE); + } + + protected String getAlbumTitle() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.ALBUM); + } + + protected String getArtistName() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.ARTIST); + } + + protected String getAlbumArtistName() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.ALBUM_ARTIST); + } + + protected String getGenreName() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.GENRE); + } + + protected String getSongYear() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.YEAR); + } + + protected String getTrackNumber() throws NullPointerException { + return getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirst(FieldKey.TRACK); + } + + protected Bitmap getAlbumArt() throws NullPointerException { + Artwork artworkTag = getAudioFile(songPaths.get(0)).getTagOrCreateAndSetDefault().getFirstArtwork(); + if (artworkTag != null) { + byte[] artworkBinaryData = artworkTag.getBinaryData(); + return BitmapFactory.decodeByteArray(artworkBinaryData, 0, artworkBinaryData.length); + } + return null; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AlbumTagEditorActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AlbumTagEditorActivity.java new file mode 100644 index 00000000..64db4390 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/AlbumTagEditorActivity.java @@ -0,0 +1,199 @@ +package com.kabouzeid.materialmusic.ui.activities.tageditor; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.widget.EditText; +import android.widget.Toast; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.lastfm.album.LastFMAlbumImageLoader; +import com.kabouzeid.materialmusic.loader.AlbumSongLoader; +import com.kabouzeid.materialmusic.loader.SongFileLoader; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.util.MusicUtil; +import com.kabouzeid.materialmusic.util.Util; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.process.BitmapProcessor; + +import org.jaudiotagger.tag.FieldKey; +import org.jaudiotagger.tag.images.Artwork; +import org.jaudiotagger.tag.images.ArtworkFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class AlbumTagEditorActivity extends AbsTagEditorActivity implements TextWatcher { + public static final String TAG = AlbumTagEditorActivity.class.getSimpleName(); + + private File albumArtFile; + private Bitmap albumArtBitmap; + private boolean deleteAlbumArt; + + private EditText albumTitle; + private EditText albumArtistName; + private EditText genreName; + private EditText year; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initViews(); + setUpViews(); + } + + private void initViews() { + albumTitle = (EditText) findViewById(R.id.album_title); + albumArtistName = (EditText) findViewById(R.id.album_artist); + genreName = (EditText) findViewById(R.id.genre); + year = (EditText) findViewById(R.id.year); + } + + private void setUpViews() { + fillViewsWithFileTags(); + albumTitle.addTextChangedListener(this); + albumArtistName.addTextChangedListener(this); + genreName.addTextChangedListener(this); + year.addTextChangedListener(this); + } + + + private void fillViewsWithFileTags() { + albumTitle.setText(getAlbumTitle()); + albumArtistName.setText(getAlbumArtistName()); + genreName.setText(getGenreName()); + year.setText(getSongYear()); + } + + @Override + protected void save() { + Artwork artwork = null; + Map fieldKeyValueMap = new EnumMap<>(FieldKey.class); + fieldKeyValueMap.put(FieldKey.ALBUM, albumTitle.getText().toString()); + fieldKeyValueMap.put(FieldKey.ALBUM_ARTIST, albumArtistName.getText().toString()); + fieldKeyValueMap.put(FieldKey.GENRE, genreName.getText().toString()); + fieldKeyValueMap.put(FieldKey.YEAR, year.getText().toString()); + + try { + albumArtFile = MusicUtil.getAlbumArtFile(this, String.valueOf(getId())); + } catch (IOException e) { + Log.e(TAG, "error while creating albumArtFile", e); + } + + if (albumArtBitmap != null && albumArtFile != null) { + try { + albumArtBitmap.compress(Bitmap.CompressFormat.PNG, 0, new FileOutputStream(albumArtFile)); + artwork = ArtworkFactory.createArtworkFromFile(albumArtFile); + MusicUtil.insertAlbumArt(this, getId(), albumArtFile.getAbsolutePath()); + } catch (IOException e) { + Log.e(TAG, "error while trying to create the artwork from file", e); + } + } + writeValuesToFiles(fieldKeyValueMap, artwork, deleteAlbumArt); + } + + @Override + protected int getContentViewResId() { + return R.layout.activity_album_tag_editor; + } + + @Override + protected void loadCurrentImage() { + setImageBitmap(getAlbumArt()); + deleteAlbumArt = false; + } + + @Override + protected void getImageFromLastFM() { + String albumTitleStr = albumTitle.getText().toString(); + String albumArtistNameStr = albumArtistName.getText().toString(); + if (albumArtistNameStr.trim().equals("") || albumTitleStr.trim().equals("")) { + Toast.makeText(this, getResources().getString(R.string.album_or_artist_empty), Toast.LENGTH_SHORT).show(); + return; + } + LastFMAlbumImageLoader.loadAlbumImage(this, albumTitleStr, albumArtistNameStr, new LastFMAlbumImageLoader.AlbumImageLoaderCallback() { + @Override + public void onAlbumImageLoaded(Bitmap albumImage, String uri) { + if (albumImage != null) { + setImageBitmap(albumImage); + albumArtBitmap = albumImage; + deleteAlbumArt = false; + dataChanged(); + Toast.makeText(AlbumTagEditorActivity.this, "Success.", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(AlbumTagEditorActivity.this, "Failed.", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + @Override + protected void searchImageOnWeb() { + List query = new ArrayList<>(); + query.add(albumTitle.getText().toString()); + query.add(albumArtistName.getText().toString()); + searchWebFor(query); + } + + @Override + protected void loadImageFromFile(final Uri selectedFileUri) { + DisplayImageOptions options = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(false) + .postProcessor(new BitmapProcessor() { + @Override + public Bitmap process(Bitmap bmp) { + return Util.getAlbumArtScaledBitmap(bmp, true); + } + }) + .build(); + albumArtBitmap = ImageLoader.getInstance().loadImageSync(selectedFileUri.toString(), options); + if (albumArtBitmap != null) { + setImageBitmap(albumArtBitmap); + deleteAlbumArt = false; + dataChanged(); + } + } + + @Override + protected void deleteImage() { + setImageRes(R.drawable.default_album_art); + deleteAlbumArt = true; + dataChanged(); + } + + @Override + protected List getSongPaths() { + List songs = AlbumSongLoader.getAlbumSongList(this, getId()); + List songIds = new ArrayList<>(); + for (Song song : songs) { + songIds.add(song.id); + } + return SongFileLoader.getSongFiles(this, songIds); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + dataChanged(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/SongTagEditorActivity.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/SongTagEditorActivity.java new file mode 100644 index 00000000..8f3688ea --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/activities/tageditor/SongTagEditorActivity.java @@ -0,0 +1,130 @@ +package com.kabouzeid.materialmusic.ui.activities.tageditor; + +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.EditText; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.loader.SongFileLoader; + +import org.jaudiotagger.tag.FieldKey; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class SongTagEditorActivity extends AbsTagEditorActivity implements TextWatcher { + public static final String TAG = SongTagEditorActivity.class.getSimpleName(); + + private EditText songTitle; + private EditText albumTitle; + private EditText artistName; + private EditText genreName; + private EditText year; + private EditText trackNumber; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setNoImageMode(); + initViews(); + setUpViews(); + } + + private void initViews() { + songTitle = (EditText) findViewById(R.id.title1); + albumTitle = (EditText) findViewById(R.id.title2); + artistName = (EditText) findViewById(R.id.artist); + genreName = (EditText) findViewById(R.id.genre); + year = (EditText) findViewById(R.id.year); + trackNumber = (EditText) findViewById(R.id.track_number); + } + + private void setUpViews() { + fillViewsWithFileTags(); + songTitle.addTextChangedListener(this); + albumTitle.addTextChangedListener(this); + artistName.addTextChangedListener(this); + genreName.addTextChangedListener(this); + year.addTextChangedListener(this); + trackNumber.addTextChangedListener(this); + } + + + private void fillViewsWithFileTags() { + songTitle.setText(getSongTitle()); + albumTitle.setText(getAlbumTitle()); + artistName.setText(getArtistName()); + genreName.setText(getGenreName()); + year.setText(getSongYear()); + trackNumber.setText(getTrackNumber()); + } + + @Override + protected void save() { + Map fieldKeyValueMap = new EnumMap<>(FieldKey.class); + fieldKeyValueMap.put(FieldKey.TITLE, songTitle.getText().toString()); + fieldKeyValueMap.put(FieldKey.ALBUM, albumTitle.getText().toString()); + fieldKeyValueMap.put(FieldKey.ARTIST, artistName.getText().toString()); + fieldKeyValueMap.put(FieldKey.GENRE, genreName.getText().toString()); + fieldKeyValueMap.put(FieldKey.YEAR, year.getText().toString()); + fieldKeyValueMap.put(FieldKey.TRACK, trackNumber.getText().toString()); + writeValuesToFiles(fieldKeyValueMap); + } + + @Override + protected int getContentViewResId() { + return R.layout.activity_song_tag_editor; + } + + @Override + protected void loadCurrentImage() { + + } + + @Override + protected void getImageFromLastFM() { + + } + + @Override + protected void searchImageOnWeb() { + + } + + @Override + protected void loadImageFromFile(Uri imageFilePath) { + + } + + @Override + protected void deleteImage() { + + } + + @Override + protected List getSongPaths() { + List tempIdList = new ArrayList<>(); + tempIdList.add(getId()); + return SongFileLoader.getSongFiles(this, tempIdList); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + dataChanged(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/NavigationDrawerFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/NavigationDrawerFragment.java new file mode 100644 index 00000000..a5e82e2c --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/NavigationDrawerFragment.java @@ -0,0 +1,205 @@ +package com.kabouzeid.materialmusic.ui.fragments; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.NavigationDrawerItemAdapter; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.NavigationDrawerItem; +import com.nhaarman.listviewanimations.appearance.simple.AlphaInAnimationAdapter; + +import java.util.ArrayList; + +public class NavigationDrawerFragment extends Fragment { + private static final String TAG = NavigationDrawerFragment.class.getSimpleName(); + + public static final int NAVIGATION_DRAWER_HEADER = -1; + private static final String STATE_SELECTED_POSITION = "selected_navigation_drawer_position"; + + private App app; + + private NavigationDrawerCallbacks mCallbacks; + + private NavigationDrawerItemAdapter drawerAdapter; + + private DrawerLayout drawerLayout; + public View fragmentRootView; + private ListView drawerListView; + private View fragmentContainerView; + + private Button headerButton; + private ImageView albumArt; + private TextView songTitle; + private TextView songArtist; + + private int currentSelectedPosition; + private boolean fromSavedInstanceState; + private boolean userLearnedDrawer; + + public NavigationDrawerFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + app = (App) getActivity().getApplicationContext(); + userLearnedDrawer = app.getDefaultSharedPreferences().getBoolean(AppKeys.SP_USER_LEARNED_DRAWER, false); + + if (savedInstanceState != null) { + currentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION); + fromSavedInstanceState = true; + } else { + currentSelectedPosition = app.getDefaultSharedPreferences().getInt(AppKeys.SP_NAVIGATION_DRAWER_ITEM_POSITION, 0); + } + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_navigation_drawer, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + fragmentRootView = view; + + super.onViewCreated(view, savedInstanceState); + + initViews(); + setUpViews(); + + selectItem(currentSelectedPosition); + } + + private void initViews() { + drawerListView = (ListView) fragmentRootView.findViewById(R.id.navigation_drawer_list); + final View drawerHeader = fragmentRootView.findViewById(R.id.header); + headerButton = (Button) drawerHeader.findViewById(R.id.header_clickable); + albumArt = (ImageView) drawerHeader.findViewById(R.id.album_art); + songTitle = (TextView) drawerHeader.findViewById(R.id.song_title); + songArtist = (TextView) drawerHeader.findViewById(R.id.song_artist); + } + + private void setUpViews() { + headerButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + selectItem(NAVIGATION_DRAWER_HEADER); + } + }); + setUpListView(); + } + + private void setUpListView() { + final ArrayList navigationDrawerItems = new ArrayList<>(); + navigationDrawerItems.add(new NavigationDrawerItem(getString(R.string.all_songs), R.drawable.songs)); + navigationDrawerItems.add(new NavigationDrawerItem(getString(R.string.albums), R.drawable.album)); + navigationDrawerItems.add(new NavigationDrawerItem(getString(R.string.artists), R.drawable.interpret)); + navigationDrawerItems.add(new NavigationDrawerItem(getString(R.string.genres), R.drawable.songs)); + navigationDrawerItems.add(new NavigationDrawerItem(getString(R.string.playlists), R.drawable.playlist)); + + drawerAdapter = new NavigationDrawerItemAdapter(getActivity(), R.id.navigation_drawer, navigationDrawerItems); + + final AlphaInAnimationAdapter animationAdapter = new AlphaInAnimationAdapter(drawerAdapter); + animationAdapter.setAbsListView(drawerListView); + + drawerListView.setAdapter(animationAdapter); + drawerListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + selectItem(position); + } + }); + } + + public boolean isDrawerOpen() { + return drawerLayout != null && drawerLayout.isDrawerOpen(fragmentContainerView); + } + + public void setUp(int fragmentId, final DrawerLayout drawerLayout) { + fragmentContainerView = getActivity().findViewById(fragmentId); + this.drawerLayout = drawerLayout; + this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + + + if (!userLearnedDrawer && !fromSavedInstanceState) { + this.drawerLayout.openDrawer(fragmentContainerView); + userLearnedDrawer = true; + app.getDefaultSharedPreferences().edit().putBoolean(AppKeys.SP_USER_LEARNED_DRAWER, true).apply(); + } + } + + private void selectItem(int position) { + if (position != NAVIGATION_DRAWER_HEADER) { + currentSelectedPosition = position; + if (drawerAdapter != null) { + drawerAdapter.setChecked(position); + } + if (drawerLayout != null) { + //close drawer lag workaround + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + drawerLayout.closeDrawer(fragmentContainerView); + } + }, 100); + } + app.getDefaultSharedPreferences().edit().putInt(AppKeys.SP_NAVIGATION_DRAWER_ITEM_POSITION, position).apply(); + } + if (mCallbacks != null) { + mCallbacks.onNavigationDrawerItemSelected(position); + } + } + + public TextView getSongArtist() { + return songArtist; + } + + public ImageView getAlbumArtImageView() { + return albumArt; + } + + public TextView getSongTitle() { + return songTitle; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mCallbacks = (NavigationDrawerCallbacks) activity; + } catch (ClassCastException e) { + throw new ClassCastException("Activity must implement NavigationDrawerCallbacks."); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_SELECTED_POSITION, currentSelectedPosition); + } + + public static interface NavigationDrawerCallbacks { + void onNavigationDrawerItemSelected(int position); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/AbsViewPagerTabArtistListFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/AbsViewPagerTabArtistListFragment.java new file mode 100644 index 00000000..f3d409f0 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/AbsViewPagerTabArtistListFragment.java @@ -0,0 +1,151 @@ +package com.kabouzeid.materialmusic.ui.fragments.artistviewpager; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListAdapter; + +import com.github.ksoichiro.android.observablescrollview.ObservableGridView; +import com.github.ksoichiro.android.observablescrollview.ObservableScrollViewCallbacks; +import com.github.ksoichiro.android.observablescrollview.ScrollState; +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.ui.activities.ArtistDetailActivity; +import com.kabouzeid.materialmusic.util.Util; + +public abstract class AbsViewPagerTabArtistListFragment extends Fragment implements ObservableScrollViewCallbacks, KabViewsDisableAble { + public static final String TAG = AbsViewPagerTabArtistListFragment.class.getSimpleName(); + protected App app; + private ObservableGridView observableGridView; + private Activity parentActivity; + private int artistId = -1; + private String artistName = ""; + private int paddingViewHeight; + private boolean areViewsEnabled; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + app = (App) getActivity().getApplicationContext(); + parentActivity = getActivity(); + getArgs(); + + View view = inflater.inflate(R.layout.fragment_gridview, container, false); + observableGridView = (ObservableGridView) view.findViewById(R.id.scroll); + setGridViewPadding(); + observableGridView.setScrollViewCallbacks(this); + ListAdapter adapter = getAdapter(); + if (adapter != null) { + observableGridView.setAdapter(adapter); + } + + return view; + } + + private void setGridViewPadding() { + final int artistImageViewHeight = getResources().getDimensionPixelSize(R.dimen.header_image_height); + final int titleViewHeight = getResources().getDimensionPixelSize(R.dimen.title_view_height); + final int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height); + + paddingViewHeight = artistImageViewHeight + titleViewHeight + tabHeight; + + if (app.isInPortraitMode() || app.isTablet()) { + observableGridView.setPadding(0, paddingViewHeight, 0, Util.getNavigationBarHeight(getActivity())); + } else { + observableGridView.setPadding(0, paddingViewHeight, 0, 0); + } + } + + private void getArgs() { + Bundle args = getArguments(); + if (args != null) { + artistId = args.getInt(ArtistDetailActivity.ARG_ARTIST_ID, -1); + artistName = args.getString(ArtistDetailActivity.ARG_ARTIST_NAME, ""); + } + } + + public int getY() { + return observableGridView.getCurrentScrollY() + paddingViewHeight; + } + + protected int getArtistId() { + return artistId; + } + + protected String getArtistName() { + return artistName; + } + + public Activity getParentActivity() { + return parentActivity; + } + + protected void setAdapter(ListAdapter adapter) { + observableGridView.setAdapter(adapter); + } + + protected void setOnItemClickListener(AdapterView.OnItemClickListener onItemClickListener) { + observableGridView.setOnItemClickListener(onItemClickListener); + } + + protected void setColumns(int columns) { + observableGridView.setNumColumns(columns); + } + + /* + * + * IMPORTANT: + * + * You CAN return null here and use setAdapter(ListAdapter adapter) inside getAdapter() to manually set the adapter. + * + * (i.e. if you must set the adapter async). + * + * */ + protected abstract ListAdapter getAdapter(); + + @Override + public void onScrollChanged(int scrollY, boolean b, boolean b2) { + if (parentActivity instanceof ObservableScrollViewCallbacks) { + if (getUserVisibleHint()) { + ((ObservableScrollViewCallbacks) parentActivity).onScrollChanged(scrollY + paddingViewHeight, b, b2); + } + } + } + + @Override + public void onDownMotionEvent() { + + } + + @Override + public void onUpOrCancelMotionEvent(ScrollState scrollState) { + + } + + @Override + public void disableViews() { + areViewsEnabled = false; + observableGridView.setEnabled(false); + } + + @Override + public boolean areViewsEnabled() { + return areViewsEnabled; + } + + @Override + public void enableViews() { + areViewsEnabled = true; + observableGridView.setEnabled(true); + } + + @Override + public void onResume() { + super.onResume(); + enableViews(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistAlbumFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistAlbumFragment.java new file mode 100644 index 00000000..f6e7a810 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistAlbumFragment.java @@ -0,0 +1,86 @@ +package com.kabouzeid.materialmusic.ui.fragments.artistviewpager; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.util.Pair; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListAdapter; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.AlbumViewGridAdapter; +import com.kabouzeid.materialmusic.comparator.AlbumAlphabeticComparator; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.loader.ArtistAlbumLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.Album; +import com.kabouzeid.materialmusic.ui.activities.AlbumDetailActivity; +import com.melnykov.fab.FloatingActionButton; + +import java.util.Collections; +import java.util.List; + +/** + * Created by karim on 04.01.15. + */ +public class ViewPagerTabArtistAlbumFragment extends AbsViewPagerTabArtistListFragment { + private FloatingActionButton fab; + + @Override + protected ListAdapter getAdapter() { + List albums = ArtistAlbumLoader.getArtistAlbumList(getParentActivity(), getArtistId()); + Collections.sort(albums, new AlbumAlphabeticComparator()); + ListAdapter adapter = new AlbumViewGridAdapter(getParentActivity(), albums); + setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Album album = (Album) parent.getItemAtPosition(position); + View albumArtView = view.findViewById(R.id.album_art); + + openAlbumDetailsActivityIfPossible(album, albumArtView); + } + }); + setColumns(2); + return adapter; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + fab = (FloatingActionButton) getParentActivity().findViewById(R.id.fab); + } + + @SuppressWarnings("unchecked") + private void openAlbumDetailsActivityIfPossible(Album album, View albumArtForTransition) { + if (areParentActivitiesViewsEnabled()) { + disableViews(); + disableParentActivitiesViews(); + + final Intent intent = new Intent(getActivity(), AlbumDetailActivity.class); + intent.putExtra(AppKeys.E_ALBUM, album.id); + + final ActivityOptionsCompat activityOptions; + if (fab != null && albumArtForTransition != null) { + activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), + Pair.create(albumArtForTransition, getString(R.string.transition_album_cover)), + Pair.create((View) fab, getString(R.string.transition_fab)) + ); + } else { + activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity()); + } + ActivityCompat.startActivity(getActivity(), intent, activityOptions.toBundle()); + } + } + + private void disableParentActivitiesViews() { + if (getParentActivity() instanceof KabViewsDisableAble) { + ((KabViewsDisableAble) getParentActivity()).disableViews(); + } + } + + private boolean areParentActivitiesViewsEnabled() { + return !(getParentActivity() instanceof KabViewsDisableAble) || ((KabViewsDisableAble) getParentActivity()).areViewsEnabled(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistBioFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistBioFragment.java new file mode 100644 index 00000000..95f9cb25 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistBioFragment.java @@ -0,0 +1,71 @@ +package com.kabouzeid.materialmusic.ui.fragments.artistviewpager; + + +import android.content.Context; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.lastfm.artist.LastFMArtistBiographyLoader; + +import java.util.ArrayList; +import java.util.List; + +public class ViewPagerTabArtistBioFragment extends AbsViewPagerTabArtistListFragment { + + + @Override + protected ListAdapter getAdapter() { + final List strings = new ArrayList<>(); + strings.add("loading"); + ListAdapter adapter = new SimpleTextAdapter(getParentActivity(), strings); + setAdapter(adapter); + + LastFMArtistBiographyLoader.loadArtistBio(getParentActivity(), getArtistName(), new LastFMArtistBiographyLoader.ArtistBioLoaderCallback() { + @Override + public void onArtistBioLoaded(String biography) { + if (biography == null || biography.trim().equals("")) { + try { + biography = getResources().getString(R.string.biography_unavailable); + } catch (IllegalStateException e) { + Log.e(TAG, "error while trying to get ressources", e); + biography = "Errorm"; + } + } + strings.clear(); + strings.add(biography); + ListAdapter adapter = new SimpleTextAdapter(getParentActivity(), strings); + setAdapter(adapter); + } + }); + return null; + } + + private static class SimpleTextAdapter extends ArrayAdapter { + private Context context; + + public SimpleTextAdapter(Context context, List objects) { + super(context, R.layout.item_artist_details_biography, objects); + this.context = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + String string = getItem(position); + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.item_artist_details_biography, parent, false); + } + TextView text = (TextView) convertView.findViewById(R.id.text); + text.setText(Html.fromHtml(string)); + text.setMovementMethod(LinkMovementMethod.getInstance()); + return convertView; + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistSongListFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistSongListFragment.java new file mode 100644 index 00000000..ab732b32 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/artistviewpager/ViewPagerTabArtistSongListFragment.java @@ -0,0 +1,38 @@ +package com.kabouzeid.materialmusic.ui.fragments.artistviewpager; + +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListAdapter; + +import com.kabouzeid.materialmusic.adapter.songadapter.SongAdapter; +import com.kabouzeid.materialmusic.comparator.SongAlphabeticComparator; +import com.kabouzeid.materialmusic.loader.ArtistSongLoader; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.ui.activities.base.AbsBaseActivity; + +import java.util.Collections; +import java.util.List; + +/** + * Created by karim on 04.01.15. + */ +public class ViewPagerTabArtistSongListFragment extends AbsViewPagerTabArtistListFragment { + @Override + protected ListAdapter getAdapter() { + final List songs = ArtistSongLoader.getArtistSongList(getParentActivity(), getArtistId()); + Collections.sort(songs, new SongAlphabeticComparator()); + AbsBaseActivity absBaseActivity = null; + if (getParentActivity() instanceof AbsBaseActivity) { + absBaseActivity = (AbsBaseActivity) getParentActivity(); + } + ListAdapter adapter = new SongAdapter(getParentActivity(), absBaseActivity, songs); + setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + app.getMusicPlayerRemote().setPlayingQueue(songs); + app.getMusicPlayerRemote().playSongAt(position); + } + }); + return adapter; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/AlbumViewFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/AlbumViewFragment.java new file mode 100644 index 00000000..566c5a66 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/AlbumViewFragment.java @@ -0,0 +1,173 @@ +package com.kabouzeid.materialmusic.ui.fragments.mainactivityfragments; + +import android.app.Fragment; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.AlbumViewGridAdapter; +import com.kabouzeid.materialmusic.comparator.AlbumAlphabeticComparator; +import com.kabouzeid.materialmusic.interfaces.KabSearchAbleFragment; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.loader.AlbumLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.Album; +import com.kabouzeid.materialmusic.ui.activities.AlbumDetailActivity; +import com.kabouzeid.materialmusic.util.Util; +import com.melnykov.fab.FloatingActionButton; + +import java.util.Collections; +import java.util.List; + +/** + * Created by karim on 22.11.14. + */ +public class AlbumViewFragment extends Fragment implements KabViewsDisableAble, KabSearchAbleFragment { + public static final String TAG = AlbumViewFragment.class.getSimpleName(); + + private App app; + private AbsListView absListView; + private View fragmentRootView; + private FloatingActionButton fab; + private boolean areViewsEnabled; + + @Override + public void onCreate(Bundle savedInstanceState) { + app = (App) getActivity().getApplicationContext(); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_albumview, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + fragmentRootView = view; + + super.onViewCreated(view, savedInstanceState); + + initViews(); + setUpViews(); + } + + @Override + public void onResume() { + super.onResume(); + enableViews(); + } + + private void initViews() { + absListView = (AbsListView) fragmentRootView.findViewById(R.id.absList); + fab = (FloatingActionButton) getActivity().findViewById(R.id.fab); + } + + private void setUpViews() { + setUpAbsListView(); + } + + private void setUpAbsListView() { + List albums = AlbumLoader.getAllAlbums(getActivity()); + fillAbsListView(albums); + } + + private void setUpAbsListView(String query) { + List albums = AlbumLoader.getAlbums(getActivity(), query); + fillAbsListView(albums); + } + + private void fillAbsListView(List albums) { + Collections.sort(albums, new AlbumAlphabeticComparator()); + AlbumViewGridAdapter albumTileAdapter = new AlbumViewGridAdapter(getActivity(), albums); + absListView.setAdapter(albumTileAdapter); + absListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Album album = (Album) parent.getItemAtPosition(position); + View albumArtView = view.findViewById(R.id.album_art); + + openAlbumDetailsActivityIfPossible(album, albumArtView); + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (app.isInPortraitMode() || app.isTablet()) { + absListView.setPadding(0, Util.getActionBarSize(getActivity()) + Util.getStatusBarHeight(getActivity()), 0, Util.getNavigationBarHeight(getActivity())); + } else { + absListView.setPadding(0, Util.getActionBarSize(getActivity()) + Util.getStatusBarHeight(getActivity()), 0, 0); + } + } else { + absListView.setPadding(0, Util.getActionBarSize(getActivity()), 0, 0); + } + } + + @SuppressWarnings("unchecked") + private void openAlbumDetailsActivityIfPossible(Album album, View albumArtForTransition) { + if (areParentActivitiesViewsEnabled()) { + disableViews(); + disableParentActivitiesViews(); + + final Intent intent = new Intent(getActivity(), AlbumDetailActivity.class); + intent.putExtra(AppKeys.E_ALBUM, album.id); + + final ActivityOptionsCompat activityOptions; + if (fab != null && albumArtForTransition != null) { + activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), + Pair.create(albumArtForTransition, getString(R.string.transition_album_cover)), + Pair.create((View) fab, getString(R.string.transition_fab)) + ); + } else { + activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity()); + } + ActivityCompat.startActivity(getActivity(), intent, activityOptions.toBundle()); + } + } + + private void disableParentActivitiesViews() { + if (getActivity() instanceof KabViewsDisableAble) { + ((KabViewsDisableAble) getActivity()).disableViews(); + } + } + + private boolean areParentActivitiesViewsEnabled() { + return !(getActivity() instanceof KabViewsDisableAble) || ((KabViewsDisableAble) getActivity()).areViewsEnabled(); + } + + @Override + public void disableViews() { + areViewsEnabled = false; + absListView.setEnabled(false); + } + + @Override + public boolean areViewsEnabled() { + return areViewsEnabled; + } + + @Override + public void enableViews() { + areViewsEnabled = true; + absListView.setEnabled(true); + } + + @Override + public void search(String query) { + setUpAbsListView(query); + } + + @Override + public void returnToNonSearch() { + setUpAbsListView(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/ArtistViewFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/ArtistViewFragment.java new file mode 100644 index 00000000..a0224008 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/ArtistViewFragment.java @@ -0,0 +1,155 @@ +package com.kabouzeid.materialmusic.ui.fragments.mainactivityfragments; + + +import android.app.Fragment; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.ArtistViewListAdapter; +import com.kabouzeid.materialmusic.comparator.ArtistAlphabeticComparator; +import com.kabouzeid.materialmusic.interfaces.KabSearchAbleFragment; +import com.kabouzeid.materialmusic.interfaces.KabViewsDisableAble; +import com.kabouzeid.materialmusic.loader.ArtistLoader; +import com.kabouzeid.materialmusic.misc.AppKeys; +import com.kabouzeid.materialmusic.model.Artist; +import com.kabouzeid.materialmusic.ui.activities.ArtistDetailActivity; +import com.kabouzeid.materialmusic.ui.activities.base.AbsFabActivity; +import com.kabouzeid.materialmusic.util.Util; + +import java.util.Collections; +import java.util.List; + +/** + * A simple {@link Fragment} subclass. + */ +public class ArtistViewFragment extends Fragment implements KabSearchAbleFragment, KabViewsDisableAble { + public static final String TAG = ArtistViewFragment.class.getSimpleName(); + + private App app; + private AbsListView absListView; + private View fragmentRootView; + private boolean areViewsEnabled; + + @Override + public void onCreate(Bundle savedInstanceState) { + app = (App) getActivity().getApplicationContext(); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_artist_view, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + fragmentRootView = view; + super.onViewCreated(view, savedInstanceState); + initViews(); + setUpViews(); + } + + @Override + public void onResume() { + super.onResume(); + enableViews(); + } + + private void initViews() { + absListView = (AbsListView) fragmentRootView.findViewById(R.id.absList); + } + + private void setUpViews() { + setUpAbsListView(); + } + + private void setUpAbsListView() { + List artists = ArtistLoader.getAllArtists(getActivity()); + fillAbsListView(artists); + } + + private void setUpAbsListView(String query) { + List artists = ArtistLoader.getArtists(getActivity(), query); + fillAbsListView(artists); + } + + private void fillAbsListView(List artists) { + Collections.sort(artists, new ArtistAlphabeticComparator()); + ArtistViewListAdapter artistAdapter = new ArtistViewListAdapter(getActivity(), artists); + absListView.setAdapter(artistAdapter); + absListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Artist artist = (Artist) parent.getItemAtPosition(position); + final View artistImageView = view.findViewById(R.id.artist_image); + + if (getActivity() instanceof AbsFabActivity) { + AbsFabActivity absFabActivity = (AbsFabActivity) getActivity(); + Pair[] sharedElements = {Pair.create(artistImageView, getString(R.string.transition_artist_image))}; + absFabActivity.goToArtistDetailsActivity(artist.id, sharedElements); + } else { + Intent intent = new Intent(getActivity(), ArtistDetailActivity.class); + intent.putExtra(AppKeys.E_ARTIST, artist.id); + startActivity(intent); + } + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (app.isInPortraitMode() || app.isTablet()) { + absListView.setPadding(0, Util.getActionBarSize(getActivity()) + Util.getStatusBarHeight(getActivity()), 0, Util.getNavigationBarHeight(getActivity())); + } else { + absListView.setPadding(0, Util.getActionBarSize(getActivity()) + Util.getStatusBarHeight(getActivity()), 0, 0); + } + } else { + absListView.setPadding(0, Util.getActionBarSize(getActivity()), 0, 0); + } + } + + @Override + public void search(String query) { + setUpAbsListView(query); + } + + @Override + public void returnToNonSearch() { + setUpAbsListView(); + } + + private void disableParentActivityViews() { + if (getActivity() instanceof KabViewsDisableAble) { + ((KabViewsDisableAble) getActivity()).disableViews(); + } + } + + private boolean areParentActivityViewsEnabled() { + return !(getActivity() instanceof KabViewsDisableAble) || ((KabViewsDisableAble) getActivity()).areViewsEnabled(); + } + + @Override + public void enableViews() { + areViewsEnabled = true; + absListView.setEnabled(true); + } + + @Override + public void disableViews() { + areViewsEnabled = false; + absListView.setEnabled(false); + } + + @Override + public boolean areViewsEnabled() { + return areViewsEnabled; + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/SongViewFragment.java b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/SongViewFragment.java new file mode 100644 index 00000000..3d48ccd5 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/ui/fragments/mainactivityfragments/SongViewFragment.java @@ -0,0 +1,110 @@ +package com.kabouzeid.materialmusic.ui.fragments.mainactivityfragments; + + +import android.app.Fragment; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.adapter.songadapter.SongViewListAdapter; +import com.kabouzeid.materialmusic.comparator.SongAlphabeticComparator; +import com.kabouzeid.materialmusic.interfaces.KabSearchAbleFragment; +import com.kabouzeid.materialmusic.loader.SongLoader; +import com.kabouzeid.materialmusic.model.Song; +import com.kabouzeid.materialmusic.ui.activities.base.AbsBaseActivity; +import com.kabouzeid.materialmusic.util.Util; + +import java.util.Collections; +import java.util.List; + +/** + * Created by karim on 29.12.14. + */ +public class SongViewFragment extends Fragment implements KabSearchAbleFragment { + public static final String TAG = SongViewFragment.class.getSimpleName(); + + private App app; + private AbsListView absListView; + private View fragmentRootView; + + @Override + public void onCreate(Bundle savedInstanceState) { + app = (App) getActivity().getApplicationContext(); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_songview, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + fragmentRootView = view; + super.onViewCreated(view, savedInstanceState); + initViews(); + setUpViews(); + } + + private void initViews() { + absListView = (AbsListView) fragmentRootView.findViewById(R.id.absList); + } + + private void setUpViews() { + setUpAbsListView(); + } + + private void setUpAbsListView() { + List songs = SongLoader.getAllSongs(getActivity()); + fillAbsListView(songs); + } + + private void setUpAbsListView(String query) { + List songs = SongLoader.getSongs(getActivity(), query); + fillAbsListView(songs); + } + + private void fillAbsListView(final List songs) { + Collections.sort(songs, new SongAlphabeticComparator()); + AbsBaseActivity absBaseActivity = null; + if (getActivity() instanceof AbsBaseActivity) { + absBaseActivity = (AbsBaseActivity) getActivity(); + } + SongViewListAdapter songAdapter = new SongViewListAdapter(getActivity(), absBaseActivity, songs); + absListView.setAdapter(songAdapter); + absListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + app.getMusicPlayerRemote().setPlayingQueue(songs); + app.getMusicPlayerRemote().playSongAt(position); + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (app.isInPortraitMode() || app.isTablet()) { + absListView.setPadding(0, Util.getActionBarSize(getActivity()) + Util.getStatusBarHeight(getActivity()), 0, Util.getNavigationBarHeight(getActivity())); + } else { + absListView.setPadding(0, Util.getActionBarSize(getActivity()) + Util.getStatusBarHeight(getActivity()), 0, 0); + } + } else { + absListView.setPadding(0, Util.getActionBarSize(getActivity()), 0, 0); + } + } + + @Override + public void search(String query) { + setUpAbsListView(query); + } + + @Override + public void returnToNonSearch() { + setUpAbsListView(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/util/ImageLoaderUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/util/ImageLoaderUtil.java new file mode 100644 index 00000000..1d45c437 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/util/ImageLoaderUtil.java @@ -0,0 +1,92 @@ +package com.kabouzeid.materialmusic.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.view.View; +import android.widget.ImageView; + +import com.kabouzeid.materialmusic.R; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; +import com.nostra13.universalimageloader.utils.L; + +/** + * Created by karim on 28.12.14. + */ +public class ImageLoaderUtil { + public static void initImageLoader(Context context) { + DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(false) + .build(); + ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context) + .defaultDisplayImageOptions(defaultOptions) + //.memoryCache(new LRULimitedMemoryCache(1024*1024*CACHE_SIZE_MB)) + .build(); + ImageLoader.getInstance().init(config); + + L.writeLogs(false); + } + + public static DisplayImageOptions getCacheOnDiskOptions() { + return new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .build(); + } + + public static DisplayImageOptions getCacheInMemoryOptions() { + return new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(false) + .build(); + } + + public static class defaultAlbumArtOnFailed implements ImageLoadingListener { + @Override + public void onLoadingStarted(String imageUri, View view) { + ((ImageView) view).setImageResource(R.drawable.default_album_art); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + ((ImageView) view).setImageResource(R.drawable.default_album_art); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + ((ImageView) view).setImageResource(R.drawable.default_album_art); + } + } + + public static class defaultArtistArtOnFailed implements ImageLoadingListener { + + @Override + public void onLoadingStarted(String imageUri, View view) { + ((ImageView) view).setImageResource(R.drawable.default_artist_image); + } + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + ((ImageView) view).setImageResource(R.drawable.default_artist_image); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + ((ImageView) view).setImageResource(R.drawable.default_artist_image); + } + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/util/InternalStorageUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/util/InternalStorageUtil.java new file mode 100644 index 00000000..2435c99b --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/util/InternalStorageUtil.java @@ -0,0 +1,37 @@ +package com.kabouzeid.materialmusic.util; + +import android.content.Context; +import android.util.Log; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * Created by karim on 22.12.14. + */ +public final class InternalStorageUtil { + private static final String TAG = InternalStorageUtil.class.getSimpleName(); + + public static synchronized void writeObject(final Context context, final String key, final Object object) throws IOException { + try { + FileOutputStream fos; + fos = context.openFileOutput(key, Context.MODE_PRIVATE); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(object); + oos.close(); + fos.close(); + } catch (IOException e) { + Log.e(TAG, "Writing Object to internal storage failed! Maybe the Object is not serializable?", e); + } + } + + public static synchronized Object readObject(Context context, String key) throws IOException, + ClassNotFoundException { + FileInputStream fis = context.openFileInput(key); + ObjectInputStream ois = new ObjectInputStream(fis); + return ois.readObject(); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/util/MusicUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/util/MusicUtil.java new file mode 100644 index 00000000..3649035e --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/util/MusicUtil.java @@ -0,0 +1,76 @@ +package com.kabouzeid.materialmusic.util; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * Created by karim on 29.12.14. + */ +public class MusicUtil { + public static final String TAG = MusicUtil.class.getSimpleName(); + + public static Uri getAlbumArtUri(int album_id) { + final Uri sArtworkUri = Uri + .parse("content://media/external/audio/albumart"); + + return ContentUris.withAppendedId(sArtworkUri, album_id); + } + + public static String getReadableDurationString(long songDurationMillis) { + long minutes = (songDurationMillis / 1000) / 60; + long seconds = (songDurationMillis / 1000) % 60; + return String.format("%02d:%02d", minutes, seconds); + } + + + //iTunes uses for example 1002 for track 2 CD1 or 3011 for track 11 CD3. + //this method converts those values to normal tracknumbers + public static int getFixedTrackNumber(int trackNumberToFix) { + return trackNumberToFix % 1000; + } + + public static void insertAlbumArt(Context context, int albumId, String path) { + ContentResolver contentResolver = context.getContentResolver(); + + Uri artworkUri = Uri.parse("content://media/external/audio/albumart"); + contentResolver.delete(ContentUris.withAppendedId(artworkUri, albumId), null, null); + + ContentValues values = new ContentValues(); + values.put("album_id", albumId); + values.put("_data", path); + + contentResolver.insert(artworkUri, values); + } + + public static void deleteAlbumArt(Context context, int albumId) { + ContentResolver contentResolver = context.getContentResolver(); + Uri localUri = Uri.parse("content://media/external/audio/albumart"); + contentResolver.delete(ContentUris.withAppendedId(localUri, albumId), null, null); + } + + public static File createAlbumArtDir(Context paramContext) { + File albumArtDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "/.albumart/"); + if (!albumArtDir.exists()) { + albumArtDir.mkdirs(); + try { + new File(albumArtDir, ".nomedia").createNewFile(); + } catch (IOException e) { + Log.e(TAG, "error while creating .nomedia file", e); + } + } + return albumArtDir; + } + + public static File getAlbumArtFile(Context context, String name) + throws IOException { + return new File(createAlbumArtDir(context), name + System.currentTimeMillis()); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/util/Util.java b/app/src/main/java/com/kabouzeid/materialmusic/util/Util.java new file mode 100644 index 00000000..fc9e0f57 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/util/Util.java @@ -0,0 +1,179 @@ +package com.kabouzeid.materialmusic.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.provider.MediaStore; +import android.util.TypedValue; +import android.view.Window; +import android.view.WindowManager; + +import com.kabouzeid.materialmusic.App; +import com.kabouzeid.materialmusic.R; +import com.kabouzeid.materialmusic.misc.AppKeys; + +/** + * Created by karim on 12.12.14. + */ +public class Util { + public static int resolveDrawable(Context context, int drawable) { + TypedArray a = context.obtainStyledAttributes(new int[]{drawable}); + int resId = a.getResourceId(0, 0); + a.recycle(); + return resId; + } + + public static int resolveColor(Context context, int color) { + TypedArray a = context.obtainStyledAttributes(new int[]{color}); + int resId = a.getColor(0, context.getResources().getColor(R.color.materialmusic_color)); + a.recycle(); + return resId; + } + + public static boolean isWindowTranslucent(Context context) { + TypedArray a = context.obtainStyledAttributes(new int[]{android.R.attr.windowTranslucentStatus}); + boolean result = a.getBoolean(0, false); + a.recycle(); + return result; + } + + public static int getActionBarSize(Context context) { + TypedValue typedValue = new TypedValue(); + int[] textSizeAttr = new int[]{R.attr.actionBarSize}; + int indexOfAttrTextSize = 0; + TypedArray a = context.obtainStyledAttributes(typedValue.data, textSizeAttr); + int actionBarSize = a.getDimensionPixelSize(indexOfAttrTextSize, -1); + a.recycle(); + return actionBarSize; + } + + public static int getStatusBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + public static int getNavigationBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + @TargetApi(19) + public static void setNavBarTranslucent(Window window, boolean translucent) { + if (translucent) { + window.setFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + return; + } + + final WindowManager.LayoutParams attrs = window + .getAttributes(); + attrs.flags &= (~WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.setAttributes(attrs); + window.clearFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + @TargetApi(19) + public static void setStatusBarTranslucent(Window window, boolean translucent) { + if (translucent) { + window.setFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + return; + } + + final WindowManager.LayoutParams attrs = window + .getAttributes(); + attrs.flags &= (~WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.setAttributes(attrs); + window.clearFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + public static final boolean isOnline(final Context context) { + if (context == null) { + return false; + } + + boolean state = false; + final boolean onlyOnWifi = ((App) context.getApplicationContext()).getDefaultSharedPreferences().getBoolean(AppKeys.SP_ONLY_ON_WIFI, true); + + /* 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(); + } + } + + return state; + } + + public static String getFileSizeString(long sizeInBytes) { + long fileSizeInKB = sizeInBytes / 1024; + long fileSizeInMB = fileSizeInKB / 1024; + return fileSizeInMB + " MB"; + } + + public static String getFilePathFromContentProviderUri(Context context, Uri uri) { + String[] projection = {MediaStore.MediaColumns.DATA}; + Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); + if (cursor == null) return null; + int column_index = cursor.getColumnIndexOrThrow(projection[0]); + cursor.moveToFirst(); + String path = cursor.getString(column_index); + cursor.close(); + return path; + } + + private static int albumArtSize = 600; + + public static Bitmap getAlbumArtScaledBitmap(final Bitmap bitmap, boolean keepAspectRatio) { + if (keepAspectRatio) { + double aspectRatio = (double) bitmap.getHeight() / (double) bitmap.getWidth(); + int targetWidth = albumArtSize; + int targetHeight = (int) (targetWidth * aspectRatio); + return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false); + } else { + return getScaledBitmap(bitmap); + } + } + + private static Bitmap getScaledBitmap(final Bitmap bitmap) { + return Bitmap.createScaledBitmap(bitmap, albumArtSize, albumArtSize, false); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/util/ViewUtil.java b/app/src/main/java/com/kabouzeid/materialmusic/util/ViewUtil.java new file mode 100644 index 00000000..d04ad965 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/util/ViewUtil.java @@ -0,0 +1,98 @@ +package com.kabouzeid.materialmusic.util; + +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.PathInterpolator; +import android.widget.ListAdapter; +import android.widget.ListView; + +/** + * Created by karim on 06.12.14. + */ +public class ViewUtil { + public final static int DEFAULT_COLOR_ANIMATION_DURATION = 1000; + + public static void disableViews(ViewGroup layout) { + for (int i = 0; i < layout.getChildCount(); i++) { + View child = layout.getChildAt(i); + if (child instanceof ViewGroup) { + disableViews((ViewGroup) child); + } else { + child.setEnabled(false); + } + } + } + + public static void enableViews(ViewGroup layout) { + for (int i = 0; i < layout.getChildCount(); i++) { + View child = layout.getChildAt(i); + if (child instanceof ViewGroup) { + enableViews((ViewGroup) child); + } else { + child.setEnabled(true); + } + } + } + + public static void setListViewHeightBasedOnChildren(ListView listView) { + ListAdapter listAdapter = listView.getAdapter(); + if (listAdapter == null) + return; + + int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.UNSPECIFIED); + int totalHeight = 0; + View view = null; + for (int i = 0; i < listAdapter.getCount(); i++) { + view = listAdapter.getView(i, view, listView); + if (i == 0) + view.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, ViewGroup.LayoutParams.WRAP_CONTENT)); + + view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED); + totalHeight += view.getMeasuredHeight(); + } + ViewGroup.LayoutParams params = listView.getLayoutParams(); + params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); + listView.setLayoutParams(params); + listView.requestLayout(); + } + + public static void animateViewColor(final View v, final int startColor, final int endColor) { + animateViewColor(v, startColor, endColor, DEFAULT_COLOR_ANIMATION_DURATION); + } + + public static void animateViewColor(final View v, final int startColor, final int endColor, final int duration) { + ObjectAnimator animator = ObjectAnimator.ofObject(v, "backgroundColor", + new ArgbEvaluator(), startColor, endColor); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + animator.setInterpolator(new PathInterpolator(0.4f, 0f, 1f, 1f)); + } + animator.setDuration(duration); + animator.start(); + } + + public static void setBackgroundAlpha(View view, float alpha, int baseColor) { + int a = Math.min(255, Math.max(0, (int) (alpha * 255))) << 24; + int rgb = 0x00ffffff & baseColor; + view.setBackgroundColor(a + rgb); + } + + public static void addOnGlobalLayoutListener(final View view, final Runnable runnable) { + ViewTreeObserver vto = view.getViewTreeObserver(); + vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + view.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + runnable.run(); + } + }); + } +} diff --git a/app/src/main/java/com/kabouzeid/materialmusic/view/SquareImageView.java b/app/src/main/java/com/kabouzeid/materialmusic/view/SquareImageView.java new file mode 100644 index 00000000..75cb9375 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/materialmusic/view/SquareImageView.java @@ -0,0 +1,29 @@ +package com.kabouzeid.materialmusic.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Created by karim on 22.11.14. + */ +public class SquareImageView extends ImageView { + + public SquareImageView(Context context) { + super(context); + } + + public SquareImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } + +} diff --git a/app/src/main/res/drawable-hdpi/default_album_art.png b/app/src/main/res/drawable-hdpi/default_album_art.png new file mode 100644 index 0000000000000000000000000000000000000000..5b82f3d74c512f340f50fa10c232a759fb9f09ea GIT binary patch literal 18335 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%0Aow@Adu}J10%KuA#&7 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/drawer_shadow.9.png b/app/src/main/res/drawable-hdpi/drawer_shadow.9.png new file mode 100644 index 0000000000000000000000000000000000000000..236bff558af07faa3921ba35e2515edf62d04bb9 GIT binary patch literal 161 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/default_album_art.png b/app/src/main/res/drawable-mdpi/default_album_art.png new file mode 100644 index 0000000000000000000000000000000000000000..577a955c367c33b9945b891e32e88ff3984c3bab GIT binary patch literal 10369 zcmeAS@N?(olHy`uVBq!ia0y~yV9Wqv4mJh`hN`EDR~Z->7>k44ofy`glX=O&pkeLl z;uuoF_;xO5vB+`Bw%cwV3Ven4+Z|;TXJ`u~pDKJ*WO-8m)O_!la}Rgq#N0?oZraJ> zbZ5DWK)Zli-R-ywhQ`*rZyVn}emOKa>g>hKe{25ky?pud@#y;Bx9`VEGO;KyFmhD5 zKgfL_Y|H0|S%5hbat9Z&xOIh=DY+ z%ratNjJt(pJ*?f;sjUv|yk|M%OR+s*VF5> z?HQT&2{|mN-Rh(Ca?%GsAD=Tf=Gjyx{jdM^Wu@pNtw4_j;(9R|V!BZ&zkdII8~-;i zN2rL2WtS^M<2$jRN8YWz_3p=y9g@=0)q8vU`fklCG!I;%_V35X#!+Ta|E+5G(UTIxV>z5!NaDr%VxQ#T+6<;#_;vEway!oP3zA7bXt(jCU^R+O3*4< z?W6`1xv|j^Vztx3hLueqL7j z@saBxldjF{*r#oium5xL%zXR%YvT9c%el4XB-gg8nF0y{Z46AYZ9#{gdrY~SV`yZQ zly_%GqsI0?u`de2XQO5xsXE-ooA&wH*=?3A8}pbmoTe*W;FR1wbO7#jOd&XRqWnw52m>(kTI)6X`u^IP@w zo}Mjuq51u#rQT)H83wR+KqvX3{euG@c zt$n}WS#M4~Ew(xNIN#!b*IKr4DqP^a9iMyp`T6mvt~ z?BY=4Pb!Jmj<4LDeqQQqsUt^wgWQt%`oCL$K5UmiGtajA%(b=Ag0BkBTo3dTe9`ye zXZ=RDDJ#0~%rs7a!{YL9`3#X~f)|><&d%Srv3B+McXzYxHH;Y4`om;?3%;;;W1%iC z^z8NZ_0frSUtV0C^Y{(N^aeNGRc>pyD{_84)+;T!QevGYcW|GB_#@8G3)&LqTa}(V zCn}i6=TR!jRP~|m>(%hIpP!ySdvLJ1%%gJVYYTN5CXNz|ZCSH|&d&ey_xt_ZYQ4+U zi=9kZObRdC|NrTK`^m=o|8>^;OHF2a+bDCCu&_E5OU<_~KbQB**JH9nvc`f8mr=OTcsrn2^z~suTVp2kC5xZDX8gPV|KIa5JByCW*wx%v6TACb;2D?8yi6P= z<}0jsEN|n?zA|&yIY9-3*$r|x=GXt*`F2wIg9D7a|2=wB{NsK-6UUbSX0s+c?LGBM zc|q831&0M44RQ~pJ}m0*m^GtQK#Y;aN})RQs*q`VhF%&I%PFA?&3}G;d|b9p)UK6%8$RP`X+?C@gu z{#&A_jQtX)v$JG8`fz|F((}TSpYv%*F(Ojj;9tMTvW>2;`1;r|-@_tR7mboT9Hm><2(#o_XeRcdP$AL&Hg7P3-T zSH9q9Fs;Kp8dU!R5YnDY8%Q~lj^Z);0FI+d# za(j`pgDBV3Si6+n4-&4IajQ5exG-t$);g?px}}Ok;eu&_ zoV@&Y=d7>O85$>W226XsRgNjvFkdtyOIFlb?N=11$vw7}1tI;ijq4_Ckq z603Yc;(}AL`&KoF1=^3i8P{>>T&!|#;PUvo%wd6c!kTp@vqa{GAK-c*v^1P0HlB6SYcWDDdk*XnKJ`Z zB+C=yPqI$gECMgYDpu;g{qgZJPiAhTqm789PBea_V-*-sf8^BSlBDz~fpvZ7Vn zLX=4$Ltw&tgG(D67vG6z*(IaCyYInL@9AFi&SnZUIEXR+6yR1%pLp_KyF+^M%#Gov z&lyXkvI+fbbhyp(#QT$`Q!x`qNwHhmv~|zV&!28FIaqPoi6rm^exp<$GHeO|E5B;B#ES%=Agu?U5L>%L48nD;O?r z31sMS7I@LnP}9v7VCcYN)qZ|$^!77XSBDq(ZIWgZC~$PJEBpN|S3J;FlAUR8Vp!?2 z|KD!s8~1JCW)djyb&%(gJTS>uwDH`7qIY*JmwBFfa7d3M#L*#m(y8Yh#heNkHqWrB z{Wax~Q8(v*rv+I|LK#2h64e-4s?_+cMW$X~8yWCJoJmx8!g)iU8od;jDyhl8zrVk1 zUM$65=d{3&>6N&yyviQM28aKroV>qWc>3J;-~(}%U5*ZRCxnXfPbgp5ziCI^Un}h; zdo;!WG&zKGWUz|;RC*}R#PQ|4%$Fl}#m{(BnV1AVG&IyqjQXPA=CGjt*@A8Q&nmwi zF!p3(sZ&#kPnp*j-{s{0O4Ui*HlX_3evv(Yzuivz_~>Zb$w{i;^6TsOzkK;}=Ek%0 z?eAaPvuxS2GjDurbLH*pa^}X@rof{Sy|uT-8C+Mf6tU(&gQ|^TdXneg(uXX z&c3}Zw^$^3#zhs~3#(k`+E$zSoOYe|suBXGkou#j@Ey=!kwbId3x#aVf?Ca|cm+bCli`bCRcym)~cc~L+ ziFvaehfsFz?{9B6Gp&`HqxL!RYyZ0lw|vV_fm0`jIA3C_{rBf*$lH5cYgT{TRrxth z`&9O|HHFu59$Gn9CA_)6|9;okxz^=xUT?FDxw!j)>B~nyo-F1lF1yZhoZ zv-@XWIxe?W{oTF2w^!I&PFcWy>eklmx8L6Fet)YnN-uuj8TVtecr@gvu78y{FUeyY zoA2A5^B%2R)_m}w_k zqPy)FUOEdbjAB}4xY~C6_U+qmNne%OEoX9S`aL$k`F6P>i`7Eh7ks?9?e=}mRi_i{ z&Uls??Wy>fWGC2H;*$WXxgYo2o3-=H_xa6Pe)wXudu2%6)O4p(mY}aUpRh!&U(yq_ z>exh+02f~dA)T+ueySSlPi_9B$HwxV<;iAEGj&z<3oE5=?LB>_Cy?3e->2#OZ+uIu zjqujhIG>d*&b(1!hw6l^qgs}Y%l2Ma`&eV!{XfslpZVASnjE>KVBzl*%-w-JN1S{( z@;H~g*zL)=#f~q_L;oOeeHWkE2zO8Ze??7qTgky}{^d1Rtvh!v7JmUM zKK5K}&8_r*t=6Xd>eciMwyr3;_ z_cAMUJ43N-r`Ie&A97iCeJv9FD>}>FY4HP&uL1(u&)#LsHD$VMWxRgfg{1j9*;i+I znh4*%x3@ZacgJRHHHBwqnyyQ^{VaRK{mU)3+3z9e?Wl=*cG03(lP#hw8E-Z`-Fzy!@9y!%q8VF#eSLhUY%h6~Zq#$TpXq`G-#hoLKmM&X zHqXjlWU&6ax3~J=bUpu|6<4$^*ezWuB1l<&2Crc*7-&Rbq>v&wM!~FgGZ*I?z z-|~~&G1G zN=yD1e?GMKr$X7gJ2(HhoO{8=c+<^HNkn+U`r}rAGp1aVmzVcXQkwcG>Gq!$w=BhV zA}&BxwQYOx?1gFOq#CP%>}Yg8+j#udXO5EM z+wPUeo}HcTTVG#4rLtQyc+WEP`r2B(#meirI1^SsD@gwO zQzQFAje?He1*xDjS#n2vYOJ)P4O#ZzP}=ybriAJ5pP$8xdnfoitUt3Q``PD@Z#!~a zjomkfCVl<=f@RjjyGL0g-78b`&Yjq9ydko<`^bym93{oO-Aj)hIM8s>jiZ~PEIUu1 zWtZjRb@K%mPmFPj-JUo1^ox$wZcI1UNtE^M^XJ`lo*u7PyC6!*P&NtTGxWrLn zD6ijVSNZ8kcgc?#9JM(~k*e~#!Fs-%WaZ00M;?9g`^tjPZ)NWa`X-;`$$U5~bX#)N zjhf#_Bbxj2><%yYpZ_e{;G^=!ueEhQKP}x^`+M8PT_vk*P1W+Q%ZN!eE;ox_JHs~r z-kuB{ed%L1)zRv0T^GME7Rs<(W9DSmI8{45O`aoX{jWXqY^$%u?5o*%;hwXzbJ!fy zhZpAAR{O-q$0u0MoqZ=F$HF{)?)%Ii^*;PhRTckc9%IkN!7dQ{N1VA;b&F` zFF$kTO32DRCo=Wp_GFaYJ=n~C*82UP;8#}h{#^&#LN`riJ7V(6Xq8!po4_;i+Oyg+ zpPqQk3JN+@^XN$DrAIb%+gM_+Yw;Zw&6>bFQ`zc%)y7PR5GG$R6Ez1hJ9{nxf+^bQn{mR{ksaM zZK+q+2)tOuSuRkYbL=I<(K+7ErCED7t%^KU8Zc+!KEC!W@9Sn~y7bL>J65sz^7pM# z`h9v|fc=~m+ud&RH~ri>zuKix>z#n}ifwC}OLL?IraBzoRj{PHN%j$gsO9WkmqJBn zm0n)`YTw*r(u;Jh&lcZ&oqU;rW!IXg%=Y>&%das^(aLt|ie`>=;#R!1N$(g(N$ir_ zJN$EwWimUyQ&qGom3_3b-62EKCHmS?3p3sYY_Z`dUw*1}DtTq4wtwYgxpltsi*#d+ zPjvG)o>P8&@Sj$EyX}cMch4F2^>yyo%O5{Fc1J4o-tNBY9e(v4+e*$V=qOeNJ zB-rqoXteN`7d!vNEUi>|GI2s^w2m2Xht~p0rrGg-R=-#^t=`<+-9$U~>cYP29p{|k zni(3;Z7xaq?OgK8>KPx%1>HdqIXf1s{vP(<$;N%&OF$0K{&2OBe|lL+jN+|Jrpl4FZOH7?^)&%-#K(!GEVQ3P`*$)G4HY6M#<&8EIX&SytpD1;JYn8 zG~kY+-#fWQx~n%md{dEpxnV`t1x2gdZR-KCYY7&h^X?7G#>se60QMM#Dx2=@)_+{_0spM*zC1X{+{Rh%v$0w*qoFIuP z;`i?A7Zm;KCdb~n^FEZHA#UQ`;md zXR?dmD_=Nj*8eV}Bm0AEdW)HH%iFGliWiR7?6HtqJTaz8R$AKl7DuAsfxNSCMHjyh zZqCd5{^Fd3oLrxt|HgEV*{0hKnC{LAyna4F{-y+*N(g6BN=i$=Zzxll%yv7TxV=?Z z3)d97im$%!9^M?bF?w50WX#g@g?aPrYO~(m+4er|5D zq-tTPgM(R8fmiW{jlXK9n7&x*J)O;VY0BqEFP`z7Wpls1OnO@A5vAKF+**>DX4!?= z$tQONE!bL+vPPaaM$GA~cFn^>tkVO9raLoj;w`o}XV;4F&)s;#ZQWv#jMTiRr>6GW zeA?b{V5#@?Gf$qR9F&xK#m0EkWt*aj`i88ltDdQ|R=I6qlHGhUW}4~Q31z>&WP;3M zW`b0~3eQeSFx|cL@8bN0Wq!*xemQ<-6Kn4Ir*8#|Q+1*mudR|Y)Ocqn{^v6UJU&Fbb7pzybJ47$KsrhwGzTn>I(Cx zU#@ib={mC@t-`iyTbgA``B+X@OvM$?Q4ym!?Yz&JQJuCCp`ta85 z>!6D7R)2$bx`2SUq3RZQhkbLLes6xEqIh9eSHa;n-nUcP)@@=j*(`PVptUwnZ{nRZ zLDwbSe-^E|+^X8WS%b(cL;@Ye^WAP5x0{i)TX-VxC+t1~F6V?1# zWW=;qapB&*dpGBv{jV!@VfD}Y|G(p_%C{Ww-D`TJC#_d(!P7Zf@_n@x-@mL;SH4iC z>hR&&;w*UX%{9m5a8CKVwOFGJaP-uhK93^?brIpSWlLRwT%YJ`*`^&k& z!Qt*9fP0oLj>F#coV;?RIUt{Fx)8;pp@E^?B(D zi>2>LdnGN5e0ikJQ6qmx$uIAH(yw3Lt<0Gul;L<|TW+)*_qXky4xc?PJo(7I$89oO zarTzT8C-ha7t1FLXKYRU`sIs=2s7VSX9werfB(L(-@aEQXpOJ-`Bw`S+0J-f_v6&s z=Vmwik@=D*Q@q;lZA#@{-7fOmIokbdv}5hk^)Wk>_++i7_!LjNCJ?eOl5^?Wq~KRd zKXbZ7S3k+RpvBwV+`N>FReh_wgVWcW>GNmi@B4Yo$Iow@xF4tV5UpIB7yPMk^=ECRa=3ew!_wUV3jMnAv=7^fqKDoKKdi%@w@7I6myXyM) z$w}ea;&Eog*VdM~%y=K{ z_UiF`wwXNq{I|C^ir6K<_>k;3$ud5n7O{Z@>(&!q2>LbpP$RJYs4e= zR&9;knicvs{>@d9#qSEs-{0Hn8!wdMRL;3VQ)g=B{Ldchx)-D_?@vE3_gmGDU*Pv^ zMFqzz%f4lqR%cw95tqAl&BaNo-Z{Isu6`zP_0iXWx-Ty#?yUd6FZWPDxaBPUR~t)j z?ml52JgZ!}+xrreu0j^~)fMYIJ3Dpk-n?dF;BB>ui@P=?_cbejEP@;{U?V}rj6ajQ4)5B0LivJy zaNzA_^KP(kI4t#f?5C%vtrwrXD$wBI&hn)Blj|XU z7J(P>-c9pvu=C3;iE&_Hid9jNU-IqGF$Rt=(qh}Z#dG5?H+&FgG8LNeer2@kLtaLf zU%cGEv;Y75n|5zc<>qgn^jQ=FR2hHHsp&VE%)n$jx&3YYYR*~?fd+?KmM6upZu`yf zWN4gs^6)?LPd(X^HxtEK_NXmTyc^QB$+yKJz2M^OYYz{%!-voIxou7|3uUpI-5t5R zOxM}B_{Iilrga<{t5|JKKT>4KH86*0yMN7HDwT&GIDrlcAFxBa7Ai z-j{FRF3m4tl;?9?Ak6gYvyQy-6Jdu1pKsn!el>e%-QQno>4g$Z0vW9hHC=}6XFC-y zoRz%E-)2%eh2f~CfQF;Pz9a5Ggcj7;k1X2Rj1(A`kep>|lu=;m*M7dd(}t4&3wRxUQaxIvi7m-9rq z#JRg7mwSW-UPxMW{SQq4wb;FX(Y&%fpNI&43GE@Re_@AvEH zt5#VhoLE{YbYW8{XdDYj7o2l8?)+7pexAW|@v18i*|L^%L|A z&h+^2$F9(+#KrWfRX2K@%f#t<=}e7fDUGqa%QkAYC{J?{)?-}TqZF^OL$SeO_rr|j zo7?l{C;zd2;u(@CcwzREnuZ5`3`}qPmiBsXE-}2`ki5d>xh><=%Cr^5`D!e;41;+y zuU>yWRXg0OZ{`by2kMMdOQ%*&t!;{C5-2@=`Pj0$`SLX%8Y6d=WU_7)R%Q94xWH)L zCcpUSSvgG(;fJsCCH(wwnE&jdR&MQG%om*&>}Cns7J4jl+bSz-g$q|VZhhp#xK6+z zu)MdyWhR%$*B^o}x+GtVepuXZw@5A4Fn!Vb{)Vh`T9-{8hMt#cTsLo{?g{z&KZdNk z*sk4K6vZTzda7t^&+_Aa4*tsnAHH{6+k8(})Jg4?7w3vMpUfgOA6ZURzEC_NWV`d* zvid)duC68x%$l~#|=scEH5fuuuNEW>+XzYA&b{>2yiquoYK17R(3qd z_U_KyTjincb!Wd_y?#CY+%muHVn6ze3 zIq&2$RelLutmUIe(kt2)&HBzA=SbR^z%LtYs-%Pz@< zQhlp?{rB4J`}IoO*wQlcu5lovd^ny~3_j$P^drJalO9$9{$>|FEdq`HpF z?8EJMuL@saTCdgIQT%ny>3fWgdfuzrK5wa;{cC2{%qk|9S1JpPmTx*P)l?dkU#CCI zVwUl$uRNuPFYno<9k#|m-G82m{ogOaYrj8F%M=XgU*z1*7pVE+${Wy{h`?UA^>dE* z%bWMh*?uyas-m630HpjQwwu)t#KvGDO^JTI%PUUip{Ycz^M7 zudhG3@ZX>3_KzdtqEEg*64>@u{Orog;I>;^mbZP< ziCkqWZ&Pt0=iZ*1o2N{ilg1+OBCW;7M$bKZciCFem#f3qSIuym^n|-nH+ow{VVam# zyw0U4D}fi+Dk_V&$-LY9{ho2rf&Be{uh~?7Ix>69#0l?&1L~jM|NE}|+ilnH!OmQ_ zKr`ar=O(}Y^6|s^`oF6!%iqnp8@I3KrROORH*<~^@_*mm-Tn5M*X_F-8g5rBUC65} zQ#G@Elwbcl+WN+W-|zREXDwD+;_l!ZKdaX7^Dco$3Jng&6WwQXTP|ahdvjys;cfo* zH9rg%-I+MmhDqwelKuvr<E_*|F`k?@pOe?&`Sxb$91j7Pn38*tz78-GYe? z)!*NlN}K1I%u<>nAkWBRWjFhl%*jOa+*>Jg!+qt1UUhbMdKAwN+Xh;=&>HD0#Zi1` zoBM*t+t!yReEl*Zf}fv%_U82SmmVtz%wC;-Y@TiP8<$_Ro^%MWamI-Xn$L2r&N9tRqk+1Gf)@2k0KQ}IDz4v))(Al(ZK z)HG^;O!g2~P+*$Ea_V3fOXcrxZ?8<7JE>RQ zh{Z}S+Ujg)*40_AH^zmrwYyO*!-AaQ#z`fM&hpotx7HCp=ga zwe?iq`+dLHRepOjQ#9MO-D5|v^wt#@YlV_bqdjJ9-ND4jG*`%Bfwk(1uhSY>Ud}DQ zw{dov$A~RUH|VGk)*OnLe$1lEmx9naR<+N;Kb|2%Hl1-?-7?cm86HpcUV;Ip=PB z>BzM9*@Ty~!`d(Y_n&WP8mjNGVCNQ7-_^4;Os=>7^SiHdfrX=_n}O-B?x#D~g(fg$ zU3TbQqtF;;pToKHpPAX)@U=C+t_c)(u9M@qZ2mTQ|KnrJs$XsYCamy4n~~*K_~{9H z-U|W`r%n~%a_M53vDv_N_2hI82L%o$juL@1rHKL!4qYq)FGSY*%~ND(RNzp!;5ehx z#fgDQQ^;XK@D{7eHU^d^g$4)qL^UB!Mvf&O42^ylUp7l}+FhPlt i#cI^xVHOns_&@m@#m0ZQnasezz~JfX=d#Wzp$PzI^5H`O literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/drawer_shadow.9.png b/app/src/main/res/drawable-mdpi/drawer_shadow.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe3a28d77c72094021013c6442560803b3d344c GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0y~yVBlb2U{K;@i%G5OQ!u)5PTrQ67Jew>cIuq~&|~N>6@e$H2hA;OXk;vd$@?2>=ElC$<0p literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_drawer.png b/app/src/main/res/drawable-mdpi/ic_drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..1ed2c56ee4239ff2987568d4fdae10166650b120 GIT binary patch literal 2820 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 C*F3fW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-v21/notification_selector.xml b/app/src/main/res/drawable-v21/notification_selector.xml new file mode 100644 index 00000000..ada911a3 --- /dev/null +++ b/app/src/main/res/drawable-v21/notification_selector.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/default_album_art.png b/app/src/main/res/drawable-xhdpi/default_album_art.png new file mode 100644 index 0000000000000000000000000000000000000000..60f3f12f6ed7ab1155256a1f041c8679fb9625d1 GIT binary patch literal 27408 zcmeAS@N?(olHy`uVBq!ia0y~yV7dUp9Bd2>3_HW!=P)oZFct^7J29*~C-ahl!GXck z#WAFU@$Fsq=_2maG#(tNesAmH(V!z_e96OlxAo@yd;b>yEq?#@W}$VFax0R@MK z14~$z`M-*s=YkM&P+rimO%WssQT4*nf$=W$wBgDC-_BCf_7%KbDHd|ZSW0sVy?$F?Hmx=4euFvbgvvBx0 zDsFMd0mzH|>+dT5v^sS`vhk{_gar(JGk2(vV zRGplp+HWJKANOX(SN$b_8W|ebacV5sJuUFBV!%#^Ic0`n8u5E7HvY8iGx}aNukdNl zsYyUlI#tmp5mya0tBUYGB%>J@X1blPK2_%LC$i zF&TdL7S-Q!KL6j*;4eMbp(dbn@!yXYRbO5lc6g+8OjJ8;$yDudv;Xniaw3ZbpBz^I z(a6Ba#LB6$fZJY`P`9_%r-$|(Q)ys zg^r8e8WEq`kEE2(=U3aP^;}4Z{v>#!t!m8KEB<4-)wGIu>CQv zR~A8g3mzWID+$x*5O^`CfoWH<=2b+TzXfJ4Z7k=(FM~IFruWK=#hVWo33u$o*$j-Kk~D1 z2uSEJXpmd~FrA5|PEo*p_Tw)-8gr7_czYeDvpi~kaIl$uw)+`l+18Rd&kU1~_2`ub z$8!isY-nJrI

@eStPpVe@49x*v+NmPIN@@0^utT*rNc{Yb@;qwTZJ^Zm}-evc_S zJZ)Wi?0=3g>I)j)oj<#pqa^6TySuy3@<^EMW#$F|J)VqJdQp7 zeBM5v>!o;H#lhaF92pi471k}i4Gu?{#Pwo4?$`a!E$-Vq(?si@K*`^kOdk&#*`?Hd zzgvFR@{<0(A4yT$a&8*OKj&oOC<%68ynA-fE78W~Tt|3V0xgd3IV;7;G+DfZe^z1n z*@`23E(Ln5kKJANiTi^O6N{DNf`)f4Ql(7?HnYCFzyE%WfJAGtkNa{#1q0K@9}H6c zGydsuz1;Dk;MbRz!O<(ig%tu?8klx(%hg-{0PzeIxs>#6oeZz0ixE2BuxcE3XJL^>f|XRjMs(QLtd9r^2(M zYa9X+>l|tvdX=i~@2h?J^Z9&#>#{c<#m~-2zVp(Z%g{KHLt}yap}eImZR!HqaA1kqfEMe6N5KL-F-vnNo%Y zyFZ@PulUpG@R}*m77ibc1r75i9F^yMqAhUw zqO4ubj-3H7kM&CbWYaQYVo?+6;NMf2e%Rt}*7bF*g+yAmak^}jG0v{}eAYbe+KafVuN?=3S&x1DyV$*d+4Q)ol`}Uc-`bMtU$d`X z!6CtfiR;D19nsp2>Rd+xS6IX<``y04A+RD?VPEa7-R1e8-v-aQCb+yjgN5UX@`8qQ z>f%Z(V-(Ic_i(d3c5>`xU}RDhkYDou*Y*8o;u6PN+qlK`SezRi*qFFp@Jv!@*~OX^ zwX62`H`C9S*5`M=%XNIuY2&G27ct+a(&*@)i-uqCetCI${+Ez?b%zD4jI1TxC&C;b zcrvA*pXWQj=9A~HqNk@O&wKQqg+m~NmP%?-P$zc z=jt_Gf$iV^zFNJ07B}nbX&0)QSgJ%j`0v#Jx;%dtH&1-!)2Sxe*Ve>|Km5tU;S=b< zXzS{ruYO^*Lk+LAxqpH9!MLY3^BEdHG8qQ@&$;#S+3fsfrLV8`{#UmbRxn6mWc@Os zQFq>f84h>$nXg;b|8ga$aOH7Qu z_EvAVl#&(KJ?`4JIsLqxtHd?W1)G_;UYuJNcHB*|;Mtj(UoN`K&rD1f`Ekv&;ebBN zqvk(5i=SVL&fhyVY;Dxn?)vZSOf0{I3%K0t^Wt0|2sZARop9#$pXc`fBR~G>=mC}L zTOH0^5q$6aQBL%Is1zepuy_GW)vLB?0Xq*|T^)Y8UA}IHUd)bwoh|Q#6((#x@bB;M z+1W<->VCi7Qt z^Zfz}A4(XP7ueiAzF(-q<-vu8&M*Dze@%Y$Q&4ojfI`43hnNJ3_n9B>EfZ8QsAP0s zpq~G9rYQH3@OjnW->v3*!25Iesr^*w@>MFtN;2dLYub{J{@(rse+g z{r>-)|39c>WYmsyv;`vB^AG#+PpwAb*S^5eZh$_@&u%L{IBtrEYnJwN`} zuh;8WmoqptFf=abJW_K}zV=JtuCljLx3*?qzdY~NeU6ei2gbWUMP5Ic%#vjABM?Wvw zV$;dM$Rx~t zm5ThU99L8pG_2FSJ@xz7CnqO^(p2!S%Fk&}g?9NjFf<ewdKp9@cTGbi1B-siIu zPjCn*eAwZzC*4~8SE11qoa^mLOS)oKWA&OK6DDg_GrDXIeR?>>8}ubsZAHpGGP z?~3;OT!I}tUC(X{^w`wm*}%}q$@0kdj?G2k13!WVnYdmkT~aymk*TnJ^^E8cWl*}^ z#QJFO9a-+v=MvXm)LxK%wr1g9yNVABta-#{StklBI3y@F?nqADvpt)|ODMo$`joY& zl5I;~OelV)b)T(dK0{+3*O8wWKFX~Z)NplRjP+Wk)&Kd#MCG%gx2GC|laZCSfN}AW z>b-6YSQuGLI3rit9Gk4}zij{izwf7NgH`O(7kK~mZEh&puEDeOIP6i*TU_GD&Ak`c%rkQ;hd%Q)Z=A;e|`P3eg9wGySvNRLu1L7>Ek^4 z_kRu9GFXa4100^untAH+uHxr@zuy1<_rB!wv$LmEOXf2$GJWMdQeEG}XegG!QX~@K z@O;M9Q^~vP|JPOh|NH&&)6>(nAyITiTOiq?ht*ImgQZ9~z~T8c!H-M)?f;t0wX2=Q z0t)>QJ%MD$9!{yoJ1im>cBU@*RPm@Uq@7=WU5q0rKRP7%C>&!c*Os~b;eQNBMT(Qm z*+26O-L=5p;^-0Y*mzLp;$zo8Hd&0UUsf0={Ovt3(>VRoart_mX7G5lkA^_9OOL2j zqYR741NS8u(PY=^ZE$#i)rmY^yX-(CGyA2D$;apYwDAWOos3M6S(Dy$7`iAMXo1}OL_0qJ%-f$&r^kn_ zTksn^g0kMBNAaEHh4u5U`-VC&+J-u=e;)~|eqJ{7+r9W%8V4TUVL8TqBtj^;%WXj} zBWua+M^o3ozqhyg?Aplu`}<`Zf-F4q7EfoY=(3r?^L_+wv z^X_A561%4T+|j`Ft59%1f6Rl){&t?`d3Pj!6zNU?M-nI=)sD&Z9@xMta^bMpx|*Nu z{PKRs`{ma|)4>AWMv3C2Hy1g0f*lxbeeRx`Ui~?tENgiJ-)^ z2BxZrO;h#F&$<5oaG3w>*~p(|;GmUgbvVN-;q-)&+1aVyaarB8N#jjQ{7*8Ev@ zUhG{i(CkTqkHWL|zKtHr20Vj)25ghaOi$wM-T^-31MD zTQZ)W?ta;9_v^*tQ=;IE!BQmHVR-UN+0K;3)`c1ic27R7Q}yZP^7*qi>2>>XfQm~N zCxvC(x;J_%8+b6Xeo-*hj=$D#|8GX0taaEEpakTg;N2*(c+RPbsaah- zUW~CRw?1)M?Gv*ueH9Yt*x3dSZW}d$pSPF) zef2*mC>k8DF&U;Ey|GkqMppw~HVxW>+cPW&~wi5oLkkTS&4~_kROblP@kV zhL%YjQ`7{K!+R2?8tYg^F8n{r&xNvcKI*SenshGF*1_#xlVdt`3ZMyQe*0R{Q(gQtj|{(9}Oy zpurhBGPAFJ7wPFSr*S|>$xIoe%bGD zZ@-*1zklUc?O!!e^>d0P=}h-Vf8`G*jI3WSm;8waHN*B!yuk;`p$nuMHRdJ1xy)hX z>%jQeWbUc|y8BN3de|8G zgGZIK4{c@^V}TEv;U9R&9f{{n`Kw~OJ7dV&u8J+AIkf@`jnFjPd}{a`7850YuAm;4$mzw zRc^$t(`9V4l^L&H%xsj<-kyK|UJb*$68F+~ zGnu$vqzje)=e%F{du`*^-U#=rFB$J7rhIaYI7 z3RL*HZZllAE_(aA)v;IBtAhqz7%o4xxBhkK>6=&4GMv3idVZHLxG4)fZ^^s4X=&w= zm$HUW!+koJ3%*$7(6f^3gSM zc31c$)W0M0wE6v-%V!Md`j;pv8k9Cl1SjY8&eqNR%Av8~^~uu9!S3_5y(?Jn_BMf< zT7Ij;)~;Hx{b`xur!V^dKk1)k{{b4d)Le1;ZSM(A8!dt4*SGWc|9v6<_gW?+YsvD< zzcZe%i{F3m%>7?4;{UoW@M)aEn)LnM-F;TNI<+4ipO^3d9b0_w#l5}NvtNGTdfimO z`Nv7&nOptWtKnx=|8viL_s@~>?%g~87NxWE%b6U#q95{Sp-H2~ezUwdf#;UzXBaNt z^5N+3%?@*nrIwc+`FZj8_xJhFmbE)C2x$aq5I?)`->=t8%N+06a%n86X3_rovi$wM z+-I4(rgeqF9E}>g9<3;rsl9eeyo2AN@Lp9+frR_wWHaVohm}l*;#G6}%!-#?T6IO{ zo6vTh?~6h|6eoMF1dXSwavhQVu&5wze_d_o!|tDwOdlIx%>Vat{-sV~^;u3^OqgLrjPo{{jO{1R_{G7Ek7Yi>JVrsae3L1 z>yM-wC$S{033Zr0W#hl(P>1jN;b*kZl*8WyISLW?2D|Pt%lF^A~!AJzI(BKIfqaEsr^5f_i_CBaF}2A zWoO#bF2T)%=de4PCNU`p}!URxUV4O;$Nt}s;u z|A59cB zQAOL+&d;0s?`!;j)3vd?w>`=0uyt;{u`KC!SFVrpgvky0O~t2H6_}Ee8Tvl5ThXT$CJrnd6C)K=Q`?=Hipb5*`OYw8`4n z+1OTmQ0R_eaa366)PLw$ENd?JVX=-}oo%MZ@;uSEKB;R9Bp3FajQY+Za^ddjm7I61 zXIu{2EpoZ{@4NE--k+bH)!xeeNY6)M+twS;l5St-c%txNnU-Zr;s-;G1?>4&msIcV zD%GBAS6j8jFv7A?AYyx7?62wdKgFLfy(JSR-ch_T`F3}1fU-eWqr~LooNK#O7c{&J zvzxuXYtFul8<&XPn7cN5`?B);wb##Zus&O-t$2Km%tqd0+g1olEN!@K;kMF#PpS5T z_H^q>s`9o~B`UAC1~DZw1#bBBDty0@`Fn|u|NY=DOKzZ`?Iw|kkLQ_=J$skS#r1;k z>6d3`XTLl%(^&S^#C22DHq`z7wI%)hyi13~@{hA7%`UNWKR$U*<@cnVKEtTjeGN>z ztap}tOlU8PObUOrXzdQ;IlB){ov}CXxXkS*`E{SA#l_03&))og?r6)+#wh` z2PJP&@$C(##XF?K|9rOpzxk%wDgV^x)dHJ~k8FFDrajlcbee3Bq1CahwbD#nFY=yT zd?_t^GCA|Egto_-&oNQYUiF%93zh%z>g@j&)$dID0@D);UWYsIpDSA8ZB_Z{iO2Q) zr(Zdbgg@H<_ig^�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�PV*|l=S%D z)AOqf92giLmegJRr=!1Fs==DAA@|=R)pslzaoh_^zjvo?;&<4}u;Asb@XrOHuyI{D zVdu@8PoF%w;dR6*I`LF^?Z5Ox@dv{H=KQR)`SbnjgME)w8jWHb-C2G-ICSQV&r+H5 zlQ>(F?mX*v|FfO@{~y`QN=H1Cc1CE4JPs5p>~opb^Vw*rQm4}-&7-Q9{-->;uk)MP z?!GC*hAcbA>WR85Vw0J_d8-RVlpJ{Uoq^%g?)-ySG&qj}HTE=evN)dL5KdIQ z)xnt56`|b6AwIM9hvGpG7NKqxg_9j3flA&JO+5^QxN*)_l1-a!&&`e3ua#^+9GA!W47@5h1D7SxBBgz zj~g6#A;E5ZB*#GS*kPxQ7CM}hlTU9{TEnyYnAC=_HO#v^zb2pGSk5BU=z4(DjHTR3 zAyLlJBf=-b?}o<;1sN3+!CRc>oXjnOht!oOukhZYwoCBm5s4%t!}bUd9p!66{lfYJ z)g8`9%vFRsosHCYPGIufxFqC~&?UD^c|poAbym(c@sLuUeDYRG_9k}ADLM+;Ct9D( zeRBAT@e}DMil3sn<|a85F8LAsNF(S}mY3vHfu|-<*-~d1IfgEt6?AvS!Vp2z{wSxl z3%3QW4{To{Y*ISQ@@%u1r}xsei>6+d$`H@^p1J--$u7aWQ|0`oUkJZ+`lb7e=`WbS zl=JYmi8deaNY{{@Ex|64E?F+IeumF7$;;i6$3qOv4PVcAK9hfDerSZ&Hmw@biKn=x zdQDZEDy^j-vU1h3RrgkLhkgz{9n!xtF1U7`Tc~mHe>d_u9`{m@QFl-8KgT-vdGO8PyUV$j zi}~7GeO;%wDtoE)!rd*_s-Zv4yNvdDMa_T;rjSw=362#t!}?6!99TGea2*X~{}xO}JIWWUw(uB9K| z@NwJ8Z7a7+Zg_fw?e@BDx!Y zH{DKew)C;PC-y$^D*0L%`>^WL*SWJd&pz~a#qIU)%-&_bYrix9jLCe>d6CA&7Y?6r zeh~QdqCNNgsRr5i*VOqt`!}Xte?weiF{Ks~WZOrc( z-HVJrV}4-!iS4KT59iN|Z@uqaulzsdf7Sns49go-8Pl0N8yy>?nSTGMs!`nEXK!^o z_1mVKZykFU>~s`$>~C7z^z`6b#f%egCahKb{(Q!{iRUu;S_A4{T)V)Y_$;yea2=0& zYkO;Wn|)tFM}*Wx!|A5w7w~uqa%k{cmrdoEZ^s;PjneQ^$Wq(XB zE%MCOh`AA-Io>jsdRptu@^bdw|MT{9`=hy!xF1)G&(J>+TTr?1+@UY6%f(je3F%l( zbMWc#vzYS1Q9x^jSdZ!_;d0eJuF6BQhtiewFPdHKxZxb5CFl3aL(8{n(wm8UOe9?^Mp05k-A*loR+-^iPE^P#l9&$H9T$q+0Ijz(^k*ZKKu5}=2N#P zn9q4X|NmQ|LqZQHZCl#r9j+@I@oev!EXz$cg_XCt9*eSSw}r8-vDwkJO)K}R|LV8l z)7RDQRjcyNZj3v4s_EcX#_ZSJpLO=5U*2G_JbEWM>JG<72+!miM zT7N_1PQ_E{_WV=(XWp5zW##H4J5$c5T}pfT?4IttXufHyrazm`m&bea))wCTVO!pQ z{pNaGfBIRC+q;{$IqzJ&-)6_2)a577owc?PSrYOkt)mB z$?2cY_spF-x3*62=bePxi?&riPd;})i$DMUE`gGVmt6mMKeygveJhIR>@!u2w#o?{vP3ODqSM8(!-MR7pPJPya z@&%tS-f{oV*4ljj-^8!|{LZZXGy6}s@wUC5XJKb)`N+QL@5?{SozJJ7SDPpPZ_&Tg z>%_(M>wa8)ef`|U85b8$|1^E_{@j|Se_tJVExG>ou|vn?_MNOSHjMic_WSL@@;l|S z?-$?a_kU8OP_OYna3$jP7}zo^M^L^0kQd!rh&g3=PQ#X1sn?!oc9r!BEr4$H2fLc%Zz2H)T=zB(_H? R2@DJj44$rjF6*2UngAQMK|lZi literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/album.png b/app/src/main/res/drawable-xxhdpi/album.png new file mode 100644 index 0000000000000000000000000000000000000000..bed003af17bc93501a05743d48df678b4dd1aa9e GIT binary patch literal 962 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?FV6O3W zaSX{|y*le)_7w+_W98!9-2q-714L}XxPmk~#JLxBKU(_bQubT9y_Grsf0@7JXX?vw zwJz<5&{y*44*0U{!lO+7)k<8k_4o6HHfO5co|#f^%)igq$oT!)@2Zo}oK9Iim1)iG zx7Rp7bC?`pn!&y~LF$Cn0^WTM!UYUbSF^sF-##g^B>jNZ4MzKhwI|*8br~9)iI#-1 z&tAaWWpp&iC-&q;iG$e-IQKOL&P+0QTpJ+DGKV?;;0~YKv!Y5(4&Dh<((jf8U3ng) zC&&`PZ0~E^voNN1y=CVFWea)clZmk%&4DhazOsC~O4=V}Mg3(tRd?~Z(dT2wQ|^ni zIK-KsuP!jYS#jw2{TKiBl=dC3NsufZ3- z+O8QKHhoNy?@oyC=(_Q;)SY4JalM~5dEe6QRt6f1oH%%7XWYZ|qX#4P)~ZNePd-}o zWW%D$1rKk0oVGZA*6i#xZ7eTjuU+e_E*}lY;K_i;sw!{i|sB;2n9PF z4xVu;=gxW=DPz@Rd(@29%H5cyrn#84CI0e*xC<{PY-*qLa=+X1e@CVV&W$o%tu3He zyJVYzNSpDRg}XkrbWPZEOQSt7a@yR~P^nkTR1aiNZs==fdTJKA&Hw4{2l!uhP3KmwpeD7k}_{lh+jPg&y};Z&O|W zWSeZk+lxKx8u}`vJTFKGT)S%dG~~Nr!MSbe%uFFo9QESmstl!3f3I@QnzrS1(xXjM z1=0^h?lON2lFE{;V=j68RQQ&7dvC*JS=P46`{&PaTG{d8yK=lw%l`S1$x2x~vU_9yR z;uw;_`gB(Bx+Vjj)=s;n=hKVke^R?sb}6FZ%r@)RiMLbu-0Sxlt2rginsWAY&GK(c zz6Bh1&M~@tsBAscMZdQH3y*#C&Xo{6v&kSZT;jS?M!@0C!nXyKI-C!S8~l3ES=afX zH??6GzjY|f#VZe3KdCnD`#3BArlu0d3Re9+=TE+TYE-=1a#qkkb-reC)e}u8Vh*Lh5^NQ; z^x07DXUkx_vD#+gM-C4aHI{VVPYjhcYriVi@fqx!lzVew_%7{_E~z`y6fqsmpPCPzI~uu~PrVZWZ%V^K`BMc?k5TKI5Ko z+skF*Bz!kZG)?UM<{|y~?$+JHCJ)x^KE(6TL3`%m^JgFN6&!Gh zjrDm_^Yi$c>vb2-w$JQy@;&m=TxzQ6_RVwdt(WpR<90;gLznM;huTBh?sh_dW=lNt k6+L=PX-4kXWvAr#J??m-vvJ~21_lNOPgg&ebxsLQ02SQs$^ZZW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/default_album_art.png b/app/src/main/res/drawable-xxhdpi/default_album_art.png new file mode 100644 index 0000000000000000000000000000000000000000..00241b718a1f47c33787b434be82414c73be8b8a GIT binary patch literal 48623 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 SZe?I#VDNPHb6Mw<&;$ULM2)8a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/default_artist_image.png b/app/src/main/res/drawable-xxhdpi/default_artist_image.png new file mode 100644 index 0000000000000000000000000000000000000000..fccf8d176a6b2c4014748bbaf242879e69f0e5a7 GIT binary patch literal 37488 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 zⅅt{TeD=^n*&rd1~)M{sBVnK{-gPaU1 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 gdY}v?7DKP0wt%wYflPgg&ebxsLQ01xmmF8}}l literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/interpret.png b/app/src/main/res/drawable-xxhdpi/interpret.png new file mode 100644 index 0000000000000000000000000000000000000000..a89151b918687310c39201d6dbad5ed6350f14a7 GIT binary patch literal 845 zcmeAS@N?(olHy`uVBq!ia0y~yU@!w=4mJh`h91|fy9^8rjKx9jP7LeL$-HD>U}pDp zaSX{|eLM4Xwuqy^vGT{aUmQ#jkyjU-+}xodAuq_Uw8H$Tw*!ml7nO=mxuc8!I|%gO z+f#LUfs%%noYJHf3j`f|nm#fz`YuZOE4t&7wV!|5vR&oN4~x|J_?MSn{a%{7%XstD zQ!-mu`NwXJ%GLe&y`gl0a1C?vgUqc_wP#IboPDG%Sm!lJCTtexiDB^OTF*Dh`k=vy zr1yn(K@o)$tQpldYZ%om-88-7y1Vk{M~*A_cWgL4bMo(54YP6|EawhAGU1f$*Q2Jv zF&#E#Dy!tudX8Q4QAwI}JLy+YxuZkacJ;3l3rH>vnXk3EG;vbF;etofGRUX1z(0KO1W}$xrax zG}|p%DK5%hM$g5*P5)*pTKhXCzVNN&Vr7{t%?FnD3gsl4_+Ngnc#(CVbL z4owp3t_W`dBcN*SIn4ECvK=`dst4~{(os?X6&PSC;P&%QA;RNGtUYCG*-;FC-g&Nl% zP%e;OcsOQuddE#xwgb`+ENLZ4hP+LjyHaz}bidF0-U**7o$q-?WYaAfjtzGY+!ry^cQ90Y%Jy%W z!DY=KWtuu$gwEaUI%}@=v7u~#p?p-ahRGq-`$eoyagFvt%K0;@z4!iQU|?YIboFyt I=akR{0AK5bng9R* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/music_box.png b/app/src/main/res/drawable-xxhdpi/music_box.png new file mode 100644 index 0000000000000000000000000000000000000000..7986dc3028c6fdfbadb001627655d009abc1a52c GIT binary patch literal 606 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?FV3P23 zaSX{|eH*pg>xhGhY1Zq-0)md*+=;B*fr;LKn{&6teQ@4(Gd{lx3sA$2qFfPA5{-%}=T@7e6$ZJkz@P{NB$eviIx^Xi~W0wvB&FqnH7|iD%6O zNk->{2bK3Ml-NJXJ`mpEdSa_HyG@G3PxpkGtYRm{cf2c}RdDNu+q^=x=XSCW=4=nW zHOHww;r-i4CUaNqEI)Ee+%WIwygud| z-n%aBzI3to^$t^kemB`lbEZ-k+q}z1^h`FY$1W+{lv!!sdTCM8rZc?_Co`7cEK;5z zCOSDak44~Z`~w*amT3*84_J0E)HP(auAjpCgfaJf2v6X4%eBt?q7U?K%A9EZQTxQ) zN0~pXy^LlqdZ!y1_HAnk?+51tkzzkLb*}m~*{Xo~Q~4@C-)Y{mJ4`)|PEWhSc=FD* zs;y^~W{0VlI(*VC)$Mh-Xta{qo$2uLCNtyN9{as&7CaXd`}Es~JK%uYzQ!k<5f?9> zU#u+K7%H*zOk&5brIY912*3B}ixyizbb#d{s|Ks7obB6J+*#b*yhK=sIU;uvZ%RCT&uCMf&^;l$|IXs+Jq0Qic}@1W)>!6!=3ld!@i%v6N;v}q O1B0ilpUXO@geCwxY6<=T literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/music_box_outline.png b/app/src/main/res/drawable-xxhdpi/music_box_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..89de4fb3f8eb729ce3509a246a61c0e4fd7a40a4 GIT binary patch literal 675 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?FV2bs0 zaSX{|eLHKf*AWMSeffx8P6Yv zFIM7+%1};v=>GQTeGj32)!C=tJv9?QabW(NbLZauOD}sqDd*g4&LiLd3U-+qu&X)f zzfj9`cqVt{;I`+L=LIcKgdbGb=;FA(%Hl!bgcJ806az&(_C2uDz3sBi_u>ifIjzOM zG81g$LuO9a{lZ*rnpl2eZt2YM&mV3ESeRCsMDAWB@hq8G8-rm6u}2~r!^ zy#2jNExsyZj3{wW z8Nadv>w5|(waqQPUc~)U>9JS*x`x#rub*j#JBUk$|0-gwxb=oT`Tvv~oR*+GZp z;fvNvOR}UZ?qFA&-N6?W$FX4FVY8X3!CT50J}nNgV04O5KfJ4L&6hAshDv^>-3pyG zVKTzUHx@MrPCuCR=tQ4Xu53ink@Zhaw(;F~le=G4nwvSA{Yk>rkbAz?OEzvVePDF< zLD$*Vpz}+zULR20ykE*d;I=aJ?<>o~_&%h4J;?KVW%@z07s(e?=dPT}Y-G~XxAVky zR?EBBI=5V9ik|CRU6tomIO&^-uitj<2UckU7ej9)f4@I_W0L>!weqTm7Iwa~d@89X z@z_f4h5QBkR7KJ%{Qr8kKXvxP7flmBcg(q-#Bts9q2Bv#^-g*{ i0Ta?^JN>=;O#F1u8eX-s-WCQ11_n=8KbLh*2~7Y^)h;3c literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/play_box.png b/app/src/main/res/drawable-xxhdpi/play_box.png new file mode 100644 index 0000000000000000000000000000000000000000..dfad3ced0860e6bd36551a7b24f44b4e9416adcc GIT binary patch literal 567 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?FVEpLm z;uw;_`gWGB-(d%V*3BFj3mLSc1iV}YFBP^exN^b4{UB>l0b|yo)E5m+TmoA{zFbfg z{5uSk8KD0LqoTA7O=@5-2PDeL+U2hgn19kKYSKA zl-vjd|VMAy|Hy4!+icX ztmp3P%v@nuan5Mj(Fgr2=ceVq`6Dpr^8JXI`HilY$!9UH9)4Uj4(pc9iKDy!E7{##j`h((*9WBOYrreBzKUf%7 zCt8V5W725(oM0t>jY&i49ILjHIX`2|Go1ppC&dp{71HAD#P2a_xLAqDaxCDu8B?O# zpro=*D{|>yrTiaLnQJV+zCV9AHl*RgZmx=FXQK~@uG`695$2E-r~8HTR&c|4)hkMg_W1YryL+?`E`oiTG7PX%FWgB>%X)D8J5I;ck0qg2d|MYgd zs-0?cP-A#d^Js)|SyxF2-TIMe; zj{czeRQcSyC+kIu{v0_|{N8b8S@gd!=0DsAV&&Zn-q~s#JyGTHO`y||+cj_dPquRJ XBn_imCF>a&7#KWV{an^LB{Ts5I?DZe literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/playlist.png b/app/src/main/res/drawable-xxhdpi/playlist.png new file mode 100644 index 0000000000000000000000000000000000000000..6fea8f72ceba1cac3adeb563efb91fdc190036bd GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0y~yV6bFhU@+idV_;xdEq=+Dfq{XsILO_JVcj{ImkbPy zU7jwEAsMW1Bdz_I4MkEenuSc6V#&b%fMt%tqQy(S=b9BTM5&x}aCAM)`@)h@RNX_= z@z}wrc@y`(?lqq#FXyelZO^|wGtGDZTk_M*>eCXolfi2@y^2_RU3u<0rX|Xrns+63 zp8BSI^6afWT^>RyVZO2jMsA-Po}|g{@|~&sjC+pJzmQtZ>nmqGu}Ep)nZRiDL0&Q= zsn0t~>~BEB4#^E?);RNIuFU`MCHQIX!Jpxgf*)uTHv=+po#vllkW^q!Ss=o;!*7~UXNdh1O_kC$Nx#{@FDd#gdu#V8v!hBa$>-wB z3w~`&N&o)!Y-fsw(Jkf3qt-7^h!*V-o)po!>D40T@J<)6@ZB*xB_~Y08nf=v;mxcj Y6OtLSBf4)hFfcH9y85}Sb4q9e0HPJfuK)l5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/settings.png b/app/src/main/res/drawable-xxhdpi/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b8acd2d95264a039f4e3a8599db7e3a81eb16415 GIT binary patch literal 1182 zcmeAS@N?(olHy`uVBq!ia0y~yU@&7~U@+idV_;y2lIc9az`(#*9OUlAu-;r)JBTz`nW`cKTpqu3MB2N93ibK+q7EXz|*OI^_=dw)AsngYVyLPL3{nqoE z+M6|AmQ?Y*V6ghc9;ECM$)jk$jN?;}pBv-3A2lwQk8cW`te_szrqUa+M7Q0NAsFXdA-`&q>sSguD` zHaGens9hf@$NsIc?7`H(emtG)A}ro*71AgVvgb%q+t2bVC**yiOIkpf!<~lO<^DUr z+`U+6*->-`ePC-aHBV%Do9^o93xT<8 zB}o&n3eKJKSn5#5g*C~#O~KD^corS?v;SalC~n=de1TV6YaQQlr__Iwe&IEFTQi4} zU)pP{Q=jG)`R=hYS{E2I;lRyX9Tmc+551eNPMkgARIy}v`~^XmN}t!KN@Eu_Ezf?; z+W&Pum-S4&(4?t5r)>UvzwpY+lc5)Hh0dL4xAr{$sv|brywEb(q%;3qWpW%!>I)UEnFMWQLzEEBNg_z-lfVxBX`J8?S&S&Q+eIRTw^}L2!UFM?VLst^R z{LMaG^!Kvb_Vv<^#d@S^4xHP zuVM4;+0UG0O=jOz@!(sx!(2|eX4Pc9!Y5vqQ~d-J#4ZAbU*+c&$MYH#-R zyz{Qdvo&(^=d*#D(>wMko%2-rrYezdU^w%jSb(gGSZ1xMr>Wb~XWl*>XKwlEed@fu z+h^(O8&9M>bvwDWL~Ne4Pw+pcc|p&83Uh)uhc4r-D(!N^4RUvw{+-YKuls+&j;HmZ zn~$tHdEVoFgYtfj7&h&O@99glUhyiNOlNs9@l)3|-UG3&Q;s*-F5s<5U7x0vC>55H zthK-PEz75$)?qK^b38ebr(~q@`e1_v`&3u-$#rzTbj1BDI4|gI8YHtN3O&yJCFmb@{F{TzGG-xz$Lnv+G2ECVlu~#;qHK>( vlZhnH|1VE=A5$*m^Z2xVUt7)5FXGJ?H-`#8i9FB1z`)??>gTe~DWM4faS|jz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/songs.png b/app/src/main/res/drawable-xxhdpi/songs.png new file mode 100644 index 0000000000000000000000000000000000000000..63257b92b5dc378f7b93eb336181cc44c432e4c2 GIT binary patch literal 740 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


1s$8A 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

O`2F)x3X&{!f#SI{K5MXW2&~xDz_9Do-sSID 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 Ds92*M literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d6e10d01310606f20f1e7f9ea4f644de062651ea GIT binary patch literal 12528 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 I>zopr08SW%L;wH) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..903bd9aba1246096d5f4f09667a8a475bd1099f0 GIT binary patch literal 436 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFfi(Px;TbZ+4o j)yPexVkF;|WHY~WVXZGt4SvADz`)??>gTe~DWM4frrAaR literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..f4f713e8344614ab7b4a90204465d2006e2974e2 GIT binary patch literal 835 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFfi@*ba4!+xb^n##!wkY0oRMS5+=6t3+iZcurhjfNGLRYRTWO3+B_#d zw6>n>`<$8P$tCgcQcuP*Ffb%AFi0>kurV|+63a9be6UvTHJifr>c1f5OD~- z`u);CF}1WB zbGe3v2X`_j6cpq!6fCV2NT@0BV~Y@+Q_MJx<+)FT>p@GwgwO&%<_PgQn;FE|p35{W zJ!r|CP*~u{5FtC~F5@-!^!x_qrp@{X6c64Meeib5gEw3SKgdc;-{gO@&%0(PpVyp` R#=yY9;OXk;vd$@?2>{tB86*Gz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_repeat_grey600_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_repeat_grey600_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..237cd89665640944898bf5efe2ae9d4d8bce653f GIT binary patch literal 746 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t*ERCu~LhE&{odpFSUkOPD3#g&`1uOBr}P2FYuXW9gT%U0r-KbtMs*?!RM z%7!`ZQ>!O4FfcHK9A&`3zyoJ~-hS?H;d}W>|L;7HI5u54;GNND5rz&YZ~Meqb|7uEDmvl<8k&9YY~9`nUgw&4ZiM z#QCdc&%Dp+5&xIr(;tRM6rp6D-SzW7Y<`|4cFYg%;g8?`|7W=V6Cr>U80)u9W+|Wj zCuXwn16_^=Mur3ih8v6wVJE#CN@G;sGd%HUqzK*La{0?2^NZiD8RPzHHvB2bZu^zb(DX-(!?8}jL1DkJn?O9P;t73c!JK>F&NCm=&p%VM|G+u(BtOSG^&SN_ zhY8FICm02uFmMzxuvjuQ$uKzbH7KwX6qd_-cRgZl)BSwQ?1CBF6;KieLjwx~gG9sO bchX&3T2bP0l+XkKDMt?Q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_repeat_one_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_repeat_one_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..0d7ca9c32f2728f63b1b9bc7548615d042b9a4e0 GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFfc9jba4!+xb^mKY~C#g2DiZ7Nr^Wj=gdt_f0ykVt?lK=fZ_}%~WxxYWXxoye#PrP2| zK+B(WmK~z?DhH_=$$% z-+A3oo8O>+^VNGsj!)%|9G_k@u>72A(DWl#py^MpM$=*If0YM59p>H~`embdlAmLp zYL5b&!vto96N~~+7&wX;SS%TuWEdRz8Wh+G3jf==^Z&6d;XhTY<8Rl_M|hZrfq|KU jp@D^gL84*%-TVZPBRxljA1!8JU|{fc^>bP0l+XkKQ>_}b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_repeat_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_repeat_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..1dc6a5dda993f5b8c3b962ed1f21121567464c77 GIT binary patch literal 728 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFfavpx;TbZ+&pLlP5%A!EY~VuhQ& zeu>Z8FLCf-xsWB}M@=l|*f6BOd&lx;^B;ysDCm8K?Srew+S%`>KPzXt=yDj`=Y#FoPn%fZ^k@b&bLhCIb&#l!52K_WY?25+~ok{)DB1iwP1i zWsD8JpM(;k^Cp!ud|FN!lD}nTyT3l5~e(>^HUGG)3ACKGm-^M-HJi)+VzyJ>n1_e;+c(Ca=>#U%5#?@tN Ry$lQt44$rjF6*2UngGqR{D1%e literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_shuffle_grey600_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_shuffle_grey600_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..a5ab732defbfd36290fde0391c7e0e5790becf07 GIT binary patch literal 1428 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFtGA?x;TbZ+_WVWQl3eI>pF2J5uMH zs-lAOBY}j@q~muQl$PvZ@%CC^bm-cxS?^|k-*ta_RN2k{@jJJiDf!;#{lUc2nkRvQ zxq>Hwp`VfGfnfuq4Vyv3aR#;z5(gOMm?aJ*kf^IX5Nfi0or-ycp7hGWmq&uw5zDEhaD zlY?Q|^Yb$o6gJ%a^xXS`g@d?zdRxPC>nKg6d8P+{)G;PtT-P%msb;DS<$WF zTgR}YarZ5TQh_C>`5DX$-K7;w9dG?%DB#|{mC=dw+~c2T`tNpzcYfyQXkm2Xkb5W< zS;r9J>@J-U#&qrv!v#lo=>##Rdw&Gm61Pt|$rAZI{pPwf8^>oqE5C6|`A(>$M{$KN&X&ez-TsI@hcqr|-|h z`;j~ju|H38>=4gba?=0ntx%7m6kH`9le^gk`>R)-6xN_m2+NRV$ z>puMM*1wWxRUg{KcWsOLYme#mtmo6}-nh%m?_N;*!HeLLg$}sC} z{M-k6c|F=I7%tTOD`?to@wGpprjC=L@6TsppHun_8#?5^1wP?duV}s;F?Ys%xd!ce zi-Y`c=7mj=Y|yW_U@T#@_`_oM71U>A1oIridfiZ>FA#pWDFUU|b){B*k#|Pi@os8%uc>%&q@oxzdT@)z9|x z9#fe#EbSQ^s{Xw_e)$yRmx3h^{;)9ozHoR^#=rmlOGWGWX7nnY|D&?LX_@ecr}A?+ z<}fmBXnDudRP$MQudtl|f$8<14W6xGXi)ufK}*N}=JJJyS!#Y>U!|v$%67wVLVevK z?IdQVeScFVdQ&MBb@030`PWB>pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_shuffle_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_shuffle_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..08b8ba5a0c31d22f98d45223f40f86897472ecf1 GIT binary patch literal 1428 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFtGA?x;TbZ+_WVWQl3e>T2jXY8LUv z+sVQ0h~fdEqx_dxzX+37!`h#x6%?2x&c!n^@*a5c=cn)lhMx2Dtr=MlJo!^8 zEWqG%e!j8eV}{L7&re@a*r3;U?#~SwPAT?`h23QbSy;|9rV0n_;nJGf@TAsZCyUAZ zmaUv(A4GR=VVuG#_Ccoo>VM`E&Q(vA@0MHO9-wRgP}ESjY1L27^_;H-LeBehE^rc1 zw%2T6a-8#L3&R_hjh}>>4TNH>A3^9joa~sq+9zA7m5WRDiZN+SbygG)6qq4aTR-BKX%c}{ntmszo ztz(EdoSWNV#c}B=d&BD;OL-^EQpl}i*wHxq7Q;f8=O0U+9e;PE`^ZmrmO~7REc^w% zI`#}}7BA*C&}K}pV|cM(F|PqPWBxym!yBScr8KSiyg4V_c%H)NKQ_Nvd(}?Z*e82+?m zJS3Lq@%;H7UDeos-4$-z(oJ9XMrMApKc-r9$Vi;=_1ezNPs|C*5B6G?->NyZ(ehJ! zeYzn-=~H#(2XPA~s@Y%7R{y5zmHA0n@Pqy(@5%BXl=ilLo-AOpKVWzJ(zZ|EIj4PG zedxXZzJT2?|AcZ{MO?qPMj2$2(dXp*kAK9l{9U}hW&3K+>Goe{rph&)oqRmSJjZLM z{hSA~ek_7_j`W^w>SB2LdAX{@6Y;xS*7yB+$dX@Jq}KHRGdGjYqUhj%CM?mPPcvSb zT`$)p{KNL)>+J=#)sCtSzdj#Vl{lx*dco&k19pE?dLq;_2U!lIj7HjA?M#K zmhBtO)-7Oi`26!QXWZi}_Dvsusxnx7I`40wxu4;HQq>LjiS~0Ii25a`Ce>9jto!qj z<^IO%D91F0eSaRZTwr~0Mt*jKn1V_D-tViADl4!)I43{3L8!sL-iFDEq4&>5ju#9i zpN=akFkd(&-`v2JAgI4FT&up6Nr~a?pHR*WhLX?UIrk+@H)<&Rck%fWF2)tlnruE#-orJ^@4)o>0;K{r zh66&oggHK)_5b2mVZ#{z@9g0jo0%A0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFfgZkx;TbZ+UFu zj$pjvCZ(;hrE*%(rJFZq{LoQLPE`8vfcJm-`}#iV2FpAA&s=8cvglSGQFdTpVqoN8 zU{PQYXkeg>qs3uyDe9&Ah8+*@A2}m-xxQ)ot!t5qjOI3q|AoKA$d@iqpEEm*TVltB zY0q>P+%pSDO9%&T>l4e7*Oa zrQ_RsE%7flf2-ur-`>z@ocnqb>z2p8`!XiyAJUDy9QZRzq^8L&>e`}p{uexsZ(VVW ziS5=EC9x0F^|Aw(vhvuJ=q54#zYwXr>WAK~P~BDKO>T0l#0s1KIje77@s5dY*?L!< z2T_l*1OIlmUtjb}JtJjT)Dg~Iq8qoKNM&RWzM>?yfu(WliEKvZ)oWdK5;7NMO?*B( zEkL)(#!-3oO4m6Htc3%+=ZG5=+>biK^sci~*XzXIYgvCQjUjJt&_l`^J{ABGe zoMD~wV(Ib6{vscOtE4pY0}%WGT%?W;1W~aYlroFFEVz9sIGIs)`aCY~7rVaPIg@kH z_{9fT9(dw)U{GLSY+ztvU=m>9Acte{n1652zdWPQ7uXpX7#KWV{an^LB{Ts5qnwVN literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_skip_previous_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_skip_previous_white_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..33c0beaba34fdcd2f4d27cf83b4f38b9eefc010b GIT binary patch literal 1017 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE694rhB3_q?e>0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFfbQ-x;TbZ+=#l5kR3kC91$ zfy048fq}7sfrWu0&I88-B}e%Lh0U|8j` z`O;ygX%{zq+0gJmGpZ`$fYMoSe(o8^7kp7jXbXJ#kmHqswyn;A9bYyuJiZiM%lK^R z@=r`LQ|9ernlokIEcXeEF9jDeuJiKW%W9!*yH)JLhA$ye4W};!yEfdu6#ST@D5mO@ z{(;JuC-*UYU#k9{iD}BbJ=`2YRhzgtf~$<=92a`|?_*`rw!JAPuJ8lr>6TcJ6EVuvGmUlhBlTC%8QVtCYBBtYVq>Ufkr$(q>+feeoB{_g-FE zw~=8rYss7JMYduI$9wrq(iqFQUszn)d9gh4ousm{`-VulVwxd zmjBH>?iZNue{oCyb(U$3*oDFsr_<_1(-^&fEV=&oe#i{|hrf4R^-#WB#Bx){(Kzag zQs07VkAQBYJGOi8M;&1b6WS&Aw6e}2erpHo8qW(+Q(oQ=alW>wrr+RZneHQwDusht zf!Er9Ozi32#NY{0Q~p{e&AzVGx1hHmzB|jtGklViFg9FE`J+QDjaaE;i;hRSJImBcOxG)J{GxXuFEm3rp7QlA2|)HNls0`pki tB*rw8m9EX2;0w}CP)PO&@?~JCQe$9fXklRZ z#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7?_g0-CY>|xA&jf&%nUIS>O>_%)r1c z48n{Iv*t)JFmPV+ba4!+xb=2!ZBBT&NnS8tMWaeT#{#Kfq;;KJm< z(81EcAi&AMs363^zAcda?%1FC0n0d(0(Ge(@A-`=f>%Cm%J69 zo?7daCbD{l|9y^qbJp%TB<1HS=@4IcQfU63Gt=z%9LT!0h;uE&Tjf5@3 z;~Qru%-Awzev$mj3`UKGR%XxMyBM786TiWDp}%?KiW<9k!Kt@1xZfOMJb&ceAA`2# ziytvAUeQ}}{`oiCMZ3?0JDb@ggzB8--y0#y%70>^;-tjseNiWtTfUJqFy8j8{hy7% zKdlu_7OR#1Fq}%_U7+)afh(E!z>mu_^INQ)>>s>&`|?+dY=gWs-=#MTx}|E;lmCEEmej+tLzS}W@V^|QM#wW;3CLA;ql8? zjZ6ntbJ@hkYB20sr2~>RusyK4HsD9Q(xoStXFc5d*V4WIvY7WQ_LR>V$FA!cY45-F$1GM0`f_t7X3Zr%FKr%p-%w>}T4y}zHrtUP zCI?WOV-R3a!c3GW1FqN^RL*n066h}b++*@GNeKtDn@g@8cGS`AbG2QusO<6>-hdRz?b9FD=b2n~ z-FJA-^SNScV`k4SJvTk}iOub+w$j_eCotT*a&GhV&r>eHIpvd?zVmXh^?%uej2!E@ zvYzIhWqd9amz={^Uer-JE8Si!V6|<$z-)_z=4C(AKi9cEYyT+o$Z_qI<&~NS)+`~c zv1OmO7FFNb`K;n%X!^!5%foZGC9GPoz~({f+LOnQSDt(<`|W|b&NYwnX}z9Z%radj z%WkD_o+`)o?uFFUobY8@kCue}mFu>6aNg%~M%8`uGJ|O0$ELF$?>AD_?ERpY+7-7a z?L*8SnYgPF%sVtWf>{_BOkI1*#6RuE=bdxaKC~R$;1O+ft6}?;#rlUW68{{Q&h)Za z?yy8e4uL= zYuFin<{9ffCO&fdBmL0XOFlAV?HgCG_qHi-@33dM8m7EE{m^u(fT-Rv_eXFOoZ7p1+st)9E#<0;FfXRn7GT zyrn4mP0ZIVt}nYj$F41Mvv_;*)}kx!$t!gZ|edtGr!8?l_y;HRh}#M ziz=S}XY;uLaZ7udC(F5-{ze>^JhkJAQmx+SNSB|vzqR>Ha~_3;e?Hahx${?1*crRX zZ)#K5c}pBJQ48|o-1&sTH|B{->7LVePb>bf{k8a6-me)<4XSI@Ep(IDzLro+W3XDW zz)In>|5Mqcl3zI)7{q5fp3Go`QVOd)2=MH-w{7$0^W&uNH9noteel)vV4=hOn&_i1_!-RosD(^ z`{tAzWmLVr8}Nd!;Ks4#tuq$$sX^>$r5yu@a8Ro%|I--8HUA#G# z)8;v?aHyZRV$R17S4Dcp|v7m*F|O?Vg3jEcO7{6`uI(= z32MGlC)8%=d|EG`Zr!IJ)u-abx8U$shWsg45^t?P^Sw!`b8}C7$1}z`eWne+J~01K zxW*9lkUc;&o1yx!dOKb-uCnDS@t;} zf7T@2el|Z*`@j}+#o{^Zixt$So>w_~T-1q^!S12-8l(G#pLah0tgUuSChlZWj{Tv4 zc~1PBN73xRh%6>lF`;S3@ k^%5KQb`q@4y8cz(Hp{o#y1QW;0|Nttr>mdKI;Vst04v|pR{#J2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/logo.png b/app/src/main/res/drawable-xxxhdpi/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cac9baa1a379d786c859f9ee16049293b950d994 GIT binary patch literal 4957 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 + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_item_activated_dark.xml b/app/src/main/res/drawable/list_item_activated_dark.xml new file mode 100755 index 00000000..cc568803 --- /dev/null +++ b/app/src/main/res/drawable/list_item_activated_dark.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_item_selected.xml b/app/src/main/res/drawable/list_item_selected.xml new file mode 100755 index 00000000..f11a29f3 --- /dev/null +++ b/app/src/main/res/drawable/list_item_selected.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_item_selected_dark.xml b/app/src/main/res/drawable/list_item_selected_dark.xml new file mode 100755 index 00000000..90fd83f2 --- /dev/null +++ b/app/src/main/res/drawable/list_item_selected_dark.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_selector.xml b/app/src/main/res/drawable/list_selector.xml new file mode 100755 index 00000000..bdc63fd2 --- /dev/null +++ b/app/src/main/res/drawable/list_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_selector_dark.xml b/app/src/main/res/drawable/list_selector_dark.xml new file mode 100755 index 00000000..2edbb809 --- /dev/null +++ b/app/src/main/res/drawable/list_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/navigation_drawer_gradient.xml b/app/src/main/res/drawable/navigation_drawer_gradient.xml new file mode 100644 index 00000000..88c9a943 --- /dev/null +++ b/app/src/main/res/drawable/navigation_drawer_gradient.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transparent.xml b/app/src/main/res/drawable/transparent.xml new file mode 100755 index 00000000..6c2fdb96 --- /dev/null +++ b/app/src/main/res/drawable/transparent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_music_controller.xml b/app/src/main/res/layout-land/activity_music_controller.xml new file mode 100644 index 00000000..08ac9480 --- /dev/null +++ b/app/src/main/res/layout-land/activity_music_controller.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-v21/notification_playing.xml b/app/src/main/res/layout-v21/notification_playing.xml new file mode 100644 index 00000000..aeb95fee --- /dev/null +++ b/app/src/main/res/layout-v21/notification_playing.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-v21/notification_playing_expanded.xml b/app/src/main/res/layout-v21/notification_playing_expanded.xml new file mode 100644 index 00000000..837c5f5c --- /dev/null +++ b/app/src/main/res/layout-v21/notification_playing_expanded.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_album_detail.xml b/app/src/main/res/layout/activity_album_detail.xml new file mode 100644 index 00000000..13e29b78 --- /dev/null +++ b/app/src/main/res/layout/activity_album_detail.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_album_tag_editor.xml b/app/src/main/res/layout/activity_album_tag_editor.xml new file mode 100644 index 00000000..d1bacf7f --- /dev/null +++ b/app/src/main/res/layout/activity_album_tag_editor.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_artist_detail.xml b/app/src/main/res/layout/activity_artist_detail.xml new file mode 100644 index 00000000..c1dd2f9f --- /dev/null +++ b/app/src/main/res/layout/activity_artist_detail.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..b117c656 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_music_controller.xml b/app/src/main/res/layout/activity_music_controller.xml new file mode 100644 index 00000000..ed95eae1 --- /dev/null +++ b/app/src/main/res/layout/activity_music_controller.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_song_tag_editor.xml b/app/src/main/res/layout/activity_song_tag_editor.xml new file mode 100644 index 00000000..27cd2e70 --- /dev/null +++ b/app/src/main/res/layout/activity_song_tag_editor.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/album_tile.xml b/app/src/main/res/layout/album_tile.xml new file mode 100644 index 00000000..cfa191c4 --- /dev/null +++ b/app/src/main/res/layout/album_tile.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_file_details.xml b/app/src/main/res/layout/dialog_file_details.xml new file mode 100644 index 00000000..f7449010 --- /dev/null +++ b/app/src/main/res/layout/dialog_file_details.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml new file mode 100644 index 00000000..deffa456 --- /dev/null +++ b/app/src/main/res/layout/dialog_loading.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_playlist.xml b/app/src/main/res/layout/dialog_playlist.xml new file mode 100644 index 00000000..192dd246 --- /dev/null +++ b/app/src/main/res/layout/dialog_playlist.xml @@ -0,0 +1,11 @@ + + diff --git a/app/src/main/res/layout/fragment_albumview.xml b/app/src/main/res/layout/fragment_albumview.xml new file mode 100644 index 00000000..09c55d2a --- /dev/null +++ b/app/src/main/res/layout/fragment_albumview.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_artist_view.xml b/app/src/main/res/layout/fragment_artist_view.xml new file mode 100644 index 00000000..954ee290 --- /dev/null +++ b/app/src/main/res/layout/fragment_artist_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_drawer.xml b/app/src/main/res/layout/fragment_drawer.xml new file mode 100644 index 00000000..ca9c1070 --- /dev/null +++ b/app/src/main/res/layout/fragment_drawer.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_gridview.xml b/app/src/main/res/layout/fragment_gridview.xml new file mode 100644 index 00000000..f93082cd --- /dev/null +++ b/app/src/main/res/layout/fragment_gridview.xml @@ -0,0 +1,9 @@ + diff --git a/app/src/main/res/layout/fragment_navigation_drawer.xml b/app/src/main/res/layout/fragment_navigation_drawer.xml new file mode 100644 index 00000000..ad4ade72 --- /dev/null +++ b/app/src/main/res/layout/fragment_navigation_drawer.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + +