├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ ├── java │ │ └── tech │ │ │ └── soit │ │ │ └── quiet │ │ │ ├── ExampleInstrumentedTest.kt │ │ │ ├── model │ │ │ ├── FakePlayListDetail.kt │ │ │ └── FakeUser.kt │ │ │ ├── player │ │ │ ├── MusicPlayerManagerTest.kt │ │ │ ├── QuietMusicPlayerTest.kt │ │ │ ├── core │ │ │ │ └── QuietMediaPlayerTest.kt │ │ │ └── playlist │ │ │ │ └── PlaylistTest.kt │ │ │ ├── repository │ │ │ ├── db │ │ │ │ ├── QuietDatabaseTest.kt │ │ │ │ └── dao │ │ │ │ │ └── LocalMusicDaoTest.kt │ │ │ ├── local │ │ │ │ └── LocalMusicEngineTest.kt │ │ │ └── netease │ │ │ │ └── NeteaseRepositoryTest.kt │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── cloud │ │ │ │ │ └── CloudPlayListDetailActivityTest.kt │ │ │ │ ├── local │ │ │ │ │ └── LocalScannerActivityTest.kt │ │ │ │ └── user │ │ │ │ │ └── LoginActivityTest.kt │ │ │ ├── fragment │ │ │ │ ├── home │ │ │ │ │ └── MainMusicFragmentTest.kt │ │ │ │ └── local │ │ │ │ │ ├── LocalAlbumFragmentTest.kt │ │ │ │ │ ├── LocalArtistFragmentTest.kt │ │ │ │ │ └── LocalSingleSongFragmentTest.kt │ │ │ └── service │ │ │ │ ├── MusicNotificationTest.kt │ │ │ │ └── QuietPlayerServiceTest.kt │ │ │ ├── utils │ │ │ ├── Dummy.kt │ │ │ ├── MockitoUtils.kt │ │ │ ├── component │ │ │ │ └── persistence │ │ │ │ │ ├── KeyValueTest.kt │ │ │ │ │ └── PreferenceTest.kt │ │ │ └── test │ │ │ │ ├── BaseActivityTestRule.kt │ │ │ │ ├── RecyclerViewMatcher.kt │ │ │ │ ├── Uitls.kt │ │ │ │ └── ViewModelUtil.kt │ │ │ └── viewmodel │ │ │ └── LocalMusicViewModelTest.kt │ └── res │ │ └── raw │ │ ├── summer.mp3 │ │ └── user_playlist.json │ ├── debug │ ├── AndroidManifest.xml │ └── java │ │ └── tech │ │ └── soit │ │ └── quiet │ │ └── utils │ │ └── testing │ │ ├── OpenForTesting.kt │ │ └── SingleFragmentActivity.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── tech │ │ │ └── soit │ │ │ └── quiet │ │ │ ├── AppContext.kt │ │ │ ├── model │ │ │ ├── local │ │ │ │ ├── LocalAlbum.kt │ │ │ │ └── LocalArtist.kt │ │ │ ├── po │ │ │ │ ├── NeteaseAlbum.kt │ │ │ │ ├── NeteaseArtist.kt │ │ │ │ ├── NeteaseMusic.kt │ │ │ │ ├── NeteasePlayList.kt │ │ │ │ ├── NeteasePlayListDetail.kt │ │ │ │ └── NeteaseUser.kt │ │ │ └── vo │ │ │ │ ├── Album.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── Music.kt │ │ │ │ ├── PlayList.kt │ │ │ │ ├── PlayListDetail.kt │ │ │ │ └── User.kt │ │ │ ├── player │ │ │ ├── MusicPlayerManager.kt │ │ │ ├── PlayMode.kt │ │ │ ├── QuietMusicPlayer.kt │ │ │ ├── Readme.md │ │ │ ├── core │ │ │ │ ├── IMediaPlayer.kt │ │ │ │ ├── QuietExoPlayer.kt │ │ │ │ └── QuietMediaPlayer.kt │ │ │ └── playlist │ │ │ │ └── Playlist.kt │ │ │ ├── repository │ │ │ ├── LatestPlayingRepository.kt │ │ │ ├── db │ │ │ │ ├── QuietDatabase.kt │ │ │ │ ├── dao │ │ │ │ │ ├── KeyValueDao.kt │ │ │ │ │ └── LocalMusicDao.kt │ │ │ │ └── entity │ │ │ │ │ ├── KeyValueEntity.kt │ │ │ │ │ └── LocalMusic.kt │ │ │ ├── local │ │ │ │ └── LocalMusicEngine.kt │ │ │ ├── netease │ │ │ │ ├── CloudMusicService.kt │ │ │ │ ├── CloudMusicServiceProvider.kt │ │ │ │ ├── Crypto.kt │ │ │ │ ├── NeteaseRepository.kt │ │ │ │ ├── result │ │ │ │ │ ├── CommonResultBean.kt │ │ │ │ │ ├── DailySignResultBean.kt │ │ │ │ │ ├── LoginResultBean.kt │ │ │ │ │ ├── LyricResultBean.kt │ │ │ │ │ ├── MusicDetailResultBean.kt │ │ │ │ │ ├── MusicSearchResultBean.kt │ │ │ │ │ ├── MusicUrlResultBean.kt │ │ │ │ │ ├── MvDetailResultBean.kt │ │ │ │ │ ├── PersonalFmDataResult.kt │ │ │ │ │ ├── PlaylistDetailResultBean.kt │ │ │ │ │ ├── PlaylistResultBean.kt │ │ │ │ │ ├── RecommendMvResultBean.kt │ │ │ │ │ ├── RecommendPlaylistResultBean.kt │ │ │ │ │ ├── RecommendSongResultBean.kt │ │ │ │ │ └── UserDetailResultBean.kt │ │ │ │ └── source │ │ │ │ │ └── NeteaseGlideUrl.kt │ │ │ └── setting │ │ │ │ └── LocalScannerSettingRepository.kt │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── LatestPlayListActivity.kt │ │ │ │ ├── MusicPlayerActivity.kt │ │ │ │ ├── base │ │ │ │ │ ├── BaseActivity.kt │ │ │ │ │ └── BaseListActivity.kt │ │ │ │ ├── cloud │ │ │ │ │ ├── CloudDailyRecommendActivity.kt │ │ │ │ │ ├── CloudPlayListDetailActivity.kt │ │ │ │ │ ├── TopDetailActivity.kt │ │ │ │ │ ├── adapter │ │ │ │ │ │ ├── DailyRecommendAdapter.kt │ │ │ │ │ │ └── TopDetailListAdapter.kt │ │ │ │ │ └── viewmodel │ │ │ │ │ │ └── CloudPlayListDetailViewModel.kt │ │ │ │ ├── local │ │ │ │ │ ├── LocalMusicActivity.kt │ │ │ │ │ ├── LocalMusicListActivity.kt │ │ │ │ │ ├── LocalScannerActivity.kt │ │ │ │ │ └── viewmodel │ │ │ │ │ │ └── LocalMusicListViewModel.kt │ │ │ │ ├── main │ │ │ │ │ ├── AppMainActivity.kt │ │ │ │ │ └── SplashActivity.kt │ │ │ │ └── user │ │ │ │ │ ├── LoginActivity.kt │ │ │ │ │ └── viewmodel │ │ │ │ │ └── LoginViewModel.kt │ │ │ ├── adapter │ │ │ │ ├── CloudMainAdapter.kt │ │ │ │ ├── LatestPlayListAdapter.kt │ │ │ │ ├── MusicListAdapter.kt │ │ │ │ ├── MusicListAdapter2.kt │ │ │ │ └── viewholder │ │ │ │ │ ├── BaseViewHolder.kt │ │ │ │ │ ├── CloudMainNav2ViewHolder.kt │ │ │ │ │ ├── MusicListHeaderViewHolder.kt │ │ │ │ │ ├── MusicViewHolder.kt │ │ │ │ │ ├── PlaceholderViewHolder.kt │ │ │ │ │ └── PlayListDetailViewHolder.kt │ │ │ ├── dialog │ │ │ │ └── PlayingPlaylistDialog.kt │ │ │ ├── drawable │ │ │ │ └── ArcColorDrawable.kt │ │ │ ├── fragment │ │ │ │ ├── UnimplementedFragment.kt │ │ │ │ ├── base │ │ │ │ │ └── BaseFragment.kt │ │ │ │ ├── home │ │ │ │ │ ├── MainCloudFragment.kt │ │ │ │ │ ├── MainMusicFragment.kt │ │ │ │ │ ├── cloud │ │ │ │ │ │ └── ItemPlayList.kt │ │ │ │ │ └── viewmodel │ │ │ │ │ │ ├── MainCloudViewModel.kt │ │ │ │ │ │ └── MainMusicViewModel.kt │ │ │ │ └── local │ │ │ │ │ ├── AItemViewBinder.kt │ │ │ │ │ ├── LocalAlbumFragment.kt │ │ │ │ │ ├── LocalArtistFragment.kt │ │ │ │ │ ├── LocalMusicScannerSettingFragment.kt │ │ │ │ │ └── LocalSingleSongFragment.kt │ │ │ ├── item │ │ │ │ ├── EmptyBinder.kt │ │ │ │ ├── LoadingBinder.kt │ │ │ │ ├── MusicItemBinder.kt │ │ │ │ ├── MusicListHeaderViewBinder.kt │ │ │ │ ├── SettingHeader.kt │ │ │ │ ├── SettingScannerFolderFilter.kt │ │ │ │ └── SettingSwitch.kt │ │ │ ├── service │ │ │ │ ├── MusicNotification.kt │ │ │ │ ├── NotificationRouterActivity.kt │ │ │ │ └── QuietPlayerService.kt │ │ │ └── view │ │ │ │ ├── CircleOutlineProvider.kt │ │ │ │ ├── ContentFrameLayout.kt │ │ │ │ ├── LyricView.java │ │ │ │ └── RoundRectOutlineProvider.kt │ │ │ ├── utils │ │ │ ├── Extentions.kt │ │ │ ├── MultiType.kt │ │ │ ├── MusicConverter.kt │ │ │ ├── Permissions.kt │ │ │ ├── Platform.kt │ │ │ ├── annotation │ │ │ │ ├── DisableLayoutInject.kt │ │ │ │ ├── EnableBottomController.kt │ │ │ │ └── LayoutId.kt │ │ │ ├── component │ │ │ │ ├── AppTask.kt │ │ │ │ ├── Logger.kt │ │ │ │ ├── Pictures.kt │ │ │ │ ├── network │ │ │ │ │ ├── CookieStore.kt │ │ │ │ │ ├── Inject.kt │ │ │ │ │ ├── LiveDataCallAdapter.kt │ │ │ │ │ ├── LiveDataCallAdapterFactory.kt │ │ │ │ │ ├── PersistentCookieStore.java │ │ │ │ │ ├── Retrofit.kt │ │ │ │ │ └── SerializableOkHttpCookies.java │ │ │ │ ├── persistence │ │ │ │ │ ├── KeyValue.kt │ │ │ │ │ ├── KeyValuePersistence.kt │ │ │ │ │ └── Preference.kt │ │ │ │ └── support │ │ │ │ │ ├── CenterSmoothScroller.kt │ │ │ │ │ ├── Jsons.kt │ │ │ │ │ ├── LiveData.kt │ │ │ │ │ ├── QuietViewModelProvider.kt │ │ │ │ │ ├── Resource.kt │ │ │ │ │ ├── Resources.kt │ │ │ │ │ └── Views.kt │ │ │ ├── event │ │ │ │ ├── PrimaryColorEvent.kt │ │ │ │ └── WindowInsetsEvent.kt │ │ │ └── exception │ │ │ │ └── NotLoginException.kt │ │ │ └── viewmodel │ │ │ ├── CloudViewModel.kt │ │ │ ├── LocalAlbumDetailViewModel.kt │ │ │ ├── LocalMusicScannerSettingViewModel.kt │ │ │ ├── LocalMusicViewModel.kt │ │ │ ├── LocalScannerViewModel.kt │ │ │ └── MusicControllerViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ ├── ic_launcher_foreground.xml │ │ └── mask_dark_top.xml │ │ ├── drawable │ │ ├── bg_nav_header.xml │ │ ├── ic_arrow_back_black_24dp.xml │ │ ├── ic_arrow_upward_black_24dp.xml │ │ ├── ic_chevron_right_black_24dp.xml │ │ ├── ic_clear_all_black_24dp.xml │ │ ├── ic_clear_black_24dp.xml │ │ ├── ic_close_black_24dp.xml │ │ ├── ic_cloud_black_24dp.xml │ │ ├── ic_collections_black_24dp.xml │ │ ├── ic_comment_black_24dp.xml │ │ ├── ic_date_range_black_24dp.xml │ │ ├── ic_delete_black_24dp.xml │ │ ├── ic_favorite_black_24dp.xml │ │ ├── ic_favorite_border_black_24dp.xml │ │ ├── ic_file_download_black_24dp.xml │ │ ├── ic_headset_black_24dp.xml │ │ ├── ic_history_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_link_black_24dp.xml │ │ ├── ic_menu_black_24dp.xml │ │ ├── ic_music_note_black_24dp.xml │ │ ├── ic_my_location_black_24dp.xml │ │ ├── ic_pause_black_24dp.xml │ │ ├── ic_play_arrow_black_24dp.xml │ │ ├── ic_play_circle_outline_black_24dp.xml │ │ ├── ic_radio_black_24dp.xml │ │ ├── ic_remove_black_24dp.xml │ │ ├── ic_repeat_black_24dp.xml │ │ ├── ic_repeat_one_black_24dp.xml │ │ ├── ic_search_white_24dp.xml │ │ ├── ic_select_all_black_24dp.xml │ │ ├── ic_settings_black_24dp.xml │ │ ├── ic_share_black_24dp.xml │ │ ├── ic_show_chart_black_24dp.xml │ │ ├── ic_shuffle_black_24dp.xml │ │ ├── ic_skip_next_black_24dp.xml │ │ ├── ic_skip_previous_black_24dp.xml │ │ ├── ic_today_black_24dp.xml │ │ ├── ic_volume_up_black_24dp.xml │ │ ├── mask_cloud_top_cover.xml │ │ ├── mask_play_list_detail_cover.xml │ │ └── player_gradient_mask.xml │ │ ├── layout │ │ ├── activity_app_main.xml │ │ ├── activity_base_list.xml │ │ ├── activity_cloud_daily_recommend.xml │ │ ├── activity_cloud_play_list_detail.xml │ │ ├── activity_latest_play_list.xml │ │ ├── activity_local_music.xml │ │ ├── activity_local_music_list.xml │ │ ├── activity_local_scanner.xml │ │ ├── activity_login.xml │ │ ├── activity_music_player.xml │ │ ├── base_activity_bottom_controller.xml │ │ ├── content_bottom_controller.xml │ │ ├── content_main_music_user_info.xml │ │ ├── dialog_playing_playlist.xml │ │ ├── fragment_local_scanner_setting.xml │ │ ├── fragment_local_single_song.xml │ │ ├── fragment_main_cloud.xml │ │ ├── fragment_main_music.xml │ │ ├── header_detail_none_image.xml │ │ ├── header_item_cloud_main.xml │ │ ├── header_item_cloud_top.xml │ │ ├── header_music_list.xml │ │ ├── header_play_list_detail.xml │ │ ├── home_page_local.xml │ │ ├── item_cloud_nav.xml │ │ ├── item_cloud_nav_2.xml │ │ ├── item_cloud_play_list_detail_action.xml │ │ ├── item_cloud_top_type_1.xml │ │ ├── item_cloud_top_type_2.xml │ │ ├── item_common_a.xml │ │ ├── item_empty.xml │ │ ├── item_header_daily_recommend.xml │ │ ├── item_loading.xml │ │ ├── item_main_navigation.xml │ │ ├── item_music.xml │ │ ├── item_music_1.xml │ │ ├── item_music_2.xml │ │ ├── item_placeholder.xml │ │ ├── item_play_list.xml │ │ ├── item_setting_folder_filter.xml │ │ ├── item_setting_header.xml │ │ ├── item_setting_switch.xml │ │ ├── main_content.xml │ │ ├── nav_header.xml │ │ ├── nav_login_indicator.xml │ │ ├── player_content_music_controller.xml │ │ └── player_content_music_options.xml │ │ ├── menu │ │ ├── menu_app_main.xml │ │ ├── menu_local_home_page.xml │ │ ├── menu_local_scanner.xml │ │ └── navigation_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── attr.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── backup_descriptor.xml │ │ └── network_security_config.xml │ ├── release │ └── java │ │ └── tech │ │ └── soit │ │ └── quiet │ │ └── utils │ │ └── testing │ │ └── OpenForTesting.kt │ └── test │ └── java │ └── tech │ └── soit │ └── quiet │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── main.webp ├── main_2.webp ├── playing.webp └── playlist_detail.webp └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | /.idea/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | 4 | env: 5 | global: 6 | - ANDROID_ABI=armeabi-v7a 7 | 8 | android: 9 | components: 10 | - tools 11 | - android-22 12 | - android-28 13 | - sys-img-armeabi-v7a-android-22 14 | - extra-android-m2repository 15 | - extra-android-support 16 | - extra 17 | licenses: 18 | - android-sdk-license-.+ 19 | 20 | before_install: 21 | - mkdir "$ANDROID_HOME/licenses" || true 22 | - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license" 23 | 24 | before_script: 25 | - chmod +x gradlew 26 | - echo no | android create avd --force -n test -t android-22 --abi $ANDROID_ABI 27 | - emulator -avd test -no-window & 28 | - android-wait-for-emulator 29 | - adb shell input keyevent 82 & 30 | 31 | script: 32 | - ./gradlew clean connectedAndroidTest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Quiet [![Build Status](https://travis-ci.org/boyan01/MusicPlayer.svg?branch=master)](https://travis-ci.org/boyan01/MusicPlayer) 2 | 3 | 一个简单的音乐播放器。 4 | 5 | 主要使用的 是kotlin 编写,其它框架前往 app/build.gradle 查看。 6 | 7 | ## OverView 8 | 9 | | 主页面1 | 主页面2 | 播放 | 歌单 | 10 | | :--: | :--: | :--: | :--: | 11 | | ![](./images/main.webp) | ![](./images/main_2.webp) | ![](./images/playing.webp) | ![](./images/playlist_detail.webp) | 12 | 13 | ## About 14 | 15 | only for study , non-commercial use. 16 | 17 | Thanks! 18 | 19 | ## License 20 | 21 | Apache License Version 2.0 22 | 23 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | android.enableJetifier=true -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep public class * implements com.bumptech.glide.module.GlideModule 2 | -keep public class * extends com.bumptech.glide.module.AppGlideModule 3 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { 4 | **[] $VALUES; 5 | public *; 6 | } 7 | 8 | # for DexGuard only 9 | -keepresourcexmlelements manifest/application/meta-data@value=GlideModule -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("tech.soit.quiet", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/model/FakePlayListDetail.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model 2 | 3 | import tech.soit.quiet.model.vo.Music 4 | import tech.soit.quiet.model.vo.PlayListDetail 5 | import tech.soit.quiet.model.vo.User 6 | 7 | class FakePlayListDetail( 8 | private val id: Long, 9 | private val name: String, 10 | private val coverUrl: String, 11 | private val creator: User, 12 | private val tracks: List, 13 | private val isSubscribed: Boolean, 14 | private val playCount: Int 15 | ) : PlayListDetail() { 16 | 17 | override fun getId(): Long { 18 | return id 19 | } 20 | 21 | override fun getName(): String { 22 | return name 23 | } 24 | 25 | override fun getCoverUrl(): Any { 26 | return coverUrl 27 | } 28 | 29 | override fun getCreator(): User { 30 | return creator 31 | } 32 | 33 | override fun getTracks(): List { 34 | return tracks 35 | } 36 | 37 | override fun isSubscribed(): Boolean { 38 | return isSubscribed 39 | } 40 | 41 | override fun getPlayCount(): Int { 42 | return playCount 43 | } 44 | 45 | override fun getTrackCount(): Int { 46 | return tracks.size 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/model/FakeUser.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model 2 | 3 | import tech.soit.quiet.model.vo.User 4 | 5 | class FakeUser( 6 | private val id: Long, 7 | private val nickname: String, 8 | private val avatarUrl: String 9 | ) : User() { 10 | 11 | constructor(i: Int) : this(i.toLong(), "nickname", "") 12 | 13 | override fun getId(): Long { 14 | return id 15 | } 16 | 17 | override fun getNickName(): String { 18 | return nickname 19 | } 20 | 21 | override fun getAvatarUrl(): Any { 22 | return avatarUrl 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/repository/db/QuietDatabaseTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.Observer 5 | import androidx.room.Room 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import tech.soit.quiet.repository.db.entity.LocalMusic 8 | import java.util.concurrent.CountDownLatch 9 | import java.util.concurrent.TimeUnit 10 | 11 | class QuietDatabaseTest { 12 | 13 | companion object { 14 | 15 | 16 | val DUMMY_MUSICS = listOf( 17 | LocalMusic(0, "file://test/01", "music01", "album01", "artist01"), 18 | LocalMusic(0, "file://test/02", "music02", "album02", "artist0102"), 19 | LocalMusic(0, "file://test/03", "music03", "album01", "artist01"), 20 | LocalMusic(0, "file://test/04", "music04", "album02", "artist0302"), 21 | LocalMusic(0, "file://test/05", "music05", "album01", "artist01") 22 | ) 23 | 24 | val instance 25 | get() = Room 26 | .inMemoryDatabaseBuilder( 27 | InstrumentationRegistry.getInstrumentation().context, 28 | QuietDatabase::class.java) 29 | .allowMainThreadQueries() 30 | .build() 31 | 32 | } 33 | 34 | } 35 | 36 | /** 37 | * LiveData helper 38 | */ 39 | internal fun LiveData.await(): T { 40 | val o = Array(1) { null } 41 | val countDownLatch = CountDownLatch(1) 42 | observeForever(object : Observer { 43 | override fun onChanged(t: T) { 44 | o[0] = t 45 | countDownLatch.countDown() 46 | removeObserver(this) 47 | } 48 | }) 49 | countDownLatch.await(2, TimeUnit.SECONDS) 50 | @Suppress("UNCHECKED_CAST") 51 | return o[0] as T 52 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/repository/db/dao/LocalMusicDaoTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db.dao 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import org.junit.* 6 | import org.junit.runner.RunWith 7 | import tech.soit.quiet.repository.db.QuietDatabase 8 | import tech.soit.quiet.repository.db.QuietDatabaseTest 9 | import tech.soit.quiet.repository.db.QuietDatabaseTest.Companion.DUMMY_MUSICS 10 | import tech.soit.quiet.repository.db.await 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class LocalMusicDaoTest { 14 | 15 | 16 | private lateinit var db: QuietDatabase 17 | 18 | @get:Rule 19 | val instantTaskExecutorRule = InstantTaskExecutorRule() 20 | 21 | 22 | @Before 23 | fun initDb() { 24 | db = QuietDatabaseTest.instance 25 | 26 | //insert dummy musics first 27 | DUMMY_MUSICS.forEach { 28 | db.localMusicDao().insertMusic(it) 29 | } 30 | } 31 | 32 | @After 33 | fun closeDb() { 34 | db.close() 35 | } 36 | 37 | @Test 38 | fun testEmptyQuery() { 39 | db.clearAllTables() 40 | 41 | val musics = db.localMusicDao().getAllMusics().await() 42 | Assert.assertTrue(musics.isEmpty()) 43 | } 44 | 45 | @Test 46 | fun insertMusic() { 47 | db.clearAllTables() 48 | 49 | val ids = DUMMY_MUSICS.map { 50 | db.localMusicDao().insertMusic(it) 51 | } 52 | val musics = db.localMusicDao().getAllMusics().await() 53 | ids.forEach { id -> 54 | Assert.assertTrue(musics.find { it.getId() == id } != null) 55 | } 56 | } 57 | 58 | @Test 59 | fun testFilterByArtist() { 60 | 61 | val list = db.localMusicDao().getMusicsByArtist("artist01").await() 62 | Assert.assertTrue(list.size == 3) 63 | Assert.assertNotNull(list.find { it.getTitle() == "music01" }) 64 | Assert.assertNotNull(list.find { it.getTitle() == "music03" }) 65 | Assert.assertNotNull(list.find { it.getTitle() == "music05" }) 66 | } 67 | 68 | 69 | @Test 70 | fun testFilterByAlbum() { 71 | val list = db.localMusicDao().getMusicsByAlbum("album02").await() 72 | Assert.assertTrue(list.size == 2) 73 | Assert.assertNotNull(list.find { it.getTitle() == "music02" }) 74 | Assert.assertNotNull(list.find { it.getTitle() == "music04" }) 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/repository/local/LocalMusicEngineTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.local 2 | 3 | import org.junit.Before 4 | import org.junit.Test 5 | import org.mockito.Mockito 6 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 7 | import tech.soit.quiet.utils.mock 8 | 9 | class LocalMusicEngineTest { 10 | 11 | 12 | private lateinit var engine: LocalMusicEngine 13 | 14 | private val localMusicDao = mock() 15 | 16 | @Before 17 | fun setUp() { 18 | engine = LocalMusicEngine(localMusicDao) 19 | } 20 | 21 | 22 | @Test 23 | fun scan() { 24 | engine.scan() 25 | Mockito.verifyZeroInteractions(localMusicDao) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/repository/netease/NeteaseRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.netease 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.google.gson.Gson 6 | import com.google.gson.JsonObject 7 | import kotlinx.coroutines.CompletableDeferred 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.Assert 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.mockito.Mockito 13 | import tech.soit.quiet.utils.mock 14 | 15 | @RunWith(AndroidJUnit4::class) 16 | class NeteaseRepositoryTest { 17 | 18 | 19 | private val cloudMusicService: CloudMusicService = mock() 20 | 21 | private val neteaseRepository = NeteaseRepository(cloudMusicService) 22 | 23 | @Test 24 | fun testGetUserPlayerList() = runBlocking { 25 | Mockito.`when`(cloudMusicService.userPlayList(Mockito.anyMap())).thenReturn(CompletableDeferred(getRawJsonObject("user_playlist"))) 26 | val playLists = neteaseRepository.getUserPlayerList(0) 27 | Assert.assertEquals(8 + 13, playLists.size) 28 | } 29 | 30 | /** 31 | * open /res/raw/$name.json file 32 | */ 33 | private fun getRawJsonObject(name: String): JsonObject { 34 | val resources = InstrumentationRegistry.getInstrumentation().context.resources 35 | val inputStream = resources.openRawResource(resources.getIdentifier(name, "raw", "tech.soit.quiet.test")) 36 | return Gson().fromJson(inputStream.reader(), JsonObject::class.java) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/ui/activity/cloud/CloudPlayListDetailActivityTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.cloud 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.assertion.ViewAssertions.matches 5 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 6 | import androidx.test.espresso.matcher.ViewMatchers.withId 7 | import androidx.test.ext.junit.runners.AndroidJUnit4 8 | import kotlinx.coroutines.runBlocking 9 | import org.hamcrest.Matchers.not 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | import org.mockito.Mockito 14 | import tech.soit.quiet.R 15 | import tech.soit.quiet.model.FakePlayListDetail 16 | import tech.soit.quiet.model.FakeUser 17 | import tech.soit.quiet.ui.activity.cloud.CloudPlayListDetailActivity.Companion.PARAM_ID 18 | import tech.soit.quiet.ui.activity.cloud.viewmodel.CloudPlayListDetailViewModel 19 | import tech.soit.quiet.utils.Dummy 20 | import tech.soit.quiet.utils.mock 21 | import tech.soit.quiet.utils.test.BaseActivityTestRule 22 | import tech.soit.quiet.utils.test.ViewModelUtil 23 | 24 | @RunWith(AndroidJUnit4::class) 25 | class CloudPlayListDetailActivityTest { 26 | 27 | 28 | private lateinit var viewModel: CloudPlayListDetailViewModel 29 | 30 | 31 | @get:Rule 32 | val activityRule = BaseActivityTestRule(CloudPlayListDetailActivity::class, false) 33 | 34 | @Test 35 | fun testSubscribeText() { 36 | viewModel = mock() 37 | BaseActivityTestRule.viewModelFactory = ViewModelUtil.createFor(viewModel) 38 | 39 | val login = FakeUser(1, "login", "") 40 | runBlocking { 41 | Mockito.`when`(viewModel.loadData(1000L)).thenReturn(FakePlayListDetail( 42 | 1000L, "test", "", login, Dummy.MUSICS, false, 1000 43 | )) 44 | Mockito.`when`(viewModel.getLoginUser()).thenReturn(login) 45 | } 46 | val intent = activityRule.activityIntent 47 | intent.putExtra(PARAM_ID, 1000L) 48 | activityRule.launchActivity(intent) 49 | 50 | onView(withId(R.id.textCollection)).check(matches(not(isDisplayed()))) 51 | activityRule.finishActivity() 52 | 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/ui/activity/user/LoginActivityTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.user 2 | 3 | import android.app.Instrumentation 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.action.ViewActions.click 6 | import androidx.test.espresso.action.ViewActions.replaceText 7 | import androidx.test.espresso.matcher.ViewMatchers.withId 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import androidx.test.platform.app.InstrumentationRegistry 10 | import kotlinx.coroutines.runBlocking 11 | import org.junit.Assert 12 | import org.junit.Rule 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | import org.mockito.Mockito 16 | import org.mockito.Mockito.`when` 17 | import tech.soit.quiet.R 18 | import tech.soit.quiet.model.po.NeteaseUser 19 | import tech.soit.quiet.repository.netease.NeteaseRepository 20 | import tech.soit.quiet.ui.activity.user.viewmodel.LoginViewModel 21 | import tech.soit.quiet.utils.mock 22 | import tech.soit.quiet.utils.test.BaseActivityTestRule 23 | import tech.soit.quiet.utils.test.ViewModelUtil 24 | 25 | @RunWith(AndroidJUnit4::class) 26 | class LoginActivityTest { 27 | 28 | private lateinit var viewModel: LoginViewModel 29 | 30 | @get:Rule 31 | val activityRule = BaseActivityTestRule(LoginActivity::class) { 32 | val repository = mock() 33 | 34 | runBlocking { 35 | `when`(repository.login(Mockito.anyString(), Mockito.anyString())).thenReturn(NeteaseUser(0, "test", "")) 36 | } 37 | 38 | viewModel = LoginViewModel(repository) 39 | viewModelFactory = ViewModelUtil.createFor(viewModel) 40 | } 41 | 42 | @Test 43 | fun testLogin() { 44 | 45 | val am = InstrumentationRegistry 46 | .getInstrumentation() 47 | .addMonitor("tech.soit.quiet.ui.activity.main.AppMainActivity", null, false) 48 | 49 | Instrumentation().addMonitor(am) 50 | 51 | onView(withId(R.id.editPhone)).perform(replaceText("12345678910")) 52 | onView(withId(R.id.editPassword)).perform(replaceText("password")) 53 | 54 | onView(withId(R.id.buttonLogin)).perform(click()) 55 | 56 | am.waitForActivityWithTimeout(1000) 57 | Assert.assertEquals("main activity has been launched!!", 1, am.hits) 58 | 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/Dummy.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils 2 | 3 | import kotlinx.android.parcel.Parcelize 4 | import tech.soit.quiet.model.local.LocalAlbum 5 | import tech.soit.quiet.model.local.LocalArtist 6 | import tech.soit.quiet.model.vo.Album 7 | import tech.soit.quiet.model.vo.Artist 8 | import tech.soit.quiet.model.vo.Music 9 | import tech.soit.quiet.player.core.QuietMediaPlayerTest 10 | import tech.soit.quiet.player.playlist.Playlist 11 | 12 | /** 13 | * provider dummy data 14 | */ 15 | object Dummy { 16 | 17 | @Parcelize 18 | data class DummyMusic( 19 | private val id: Long, 20 | private val title: String, 21 | private val album: String, 22 | private val artist: List 23 | ) : Music() { 24 | 25 | override fun getId(): Long { 26 | return id 27 | } 28 | 29 | override fun getTitle(): String { 30 | return title 31 | } 32 | 33 | override fun getAlbum(): Album { 34 | return LocalAlbum(album, "https://via.placeholder.com/350x150") 35 | } 36 | 37 | override fun getArtists(): List { 38 | return artist.map { LocalArtist(it) } 39 | } 40 | 41 | override fun getPlayUrl(): String { 42 | return QuietMediaPlayerTest.URI 43 | } 44 | } 45 | 46 | 47 | val MUSICS: List = listOf( 48 | DummyMusic(1, "test1", "album1", listOf("artist1", "artist2")), 49 | DummyMusic(2, "test2", "album2", listOf("artist2")), 50 | DummyMusic(3, "test3", "album1", listOf("artist1")), 51 | DummyMusic(4, "test4", "album2", listOf("artist2", "artist3")), 52 | DummyMusic(5, "test5", "album1", listOf("artist3")) 53 | ) 54 | 55 | 56 | 57 | 58 | val PLAYLIST = Playlist("test", MUSICS) 59 | 60 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/MockitoUtils.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils 2 | 3 | import org.mockito.ArgumentCaptor 4 | import org.mockito.Mockito 5 | 6 | inline fun mock(): T = Mockito.mock(T::class.java) 7 | 8 | inline fun argumentCaptor(): ArgumentCaptor = ArgumentCaptor.forClass(T::class.java) -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/component/persistence/PreferenceTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.persistence 2 | 3 | import android.preference.PreferenceManager 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Assert.assertNull 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class PreferenceTest { 14 | 15 | companion object { 16 | 17 | private const val TEST_VALUE = "modified" 18 | 19 | } 20 | 21 | private var init: String? by Preference.default() 22 | 23 | @Before 24 | fun setUp() { 25 | //clear default preferences 26 | PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext).edit() 27 | .clear() 28 | .commit() 29 | 30 | 31 | init = TEST_VALUE 32 | } 33 | 34 | @Test 35 | fun modifyNull() { 36 | var name: String? by Preference.default() 37 | assertNull(name) 38 | name = TEST_VALUE 39 | assertEquals(name, TEST_VALUE) 40 | } 41 | 42 | @Test 43 | fun modifyExisted() { 44 | val init by Preference.default() 45 | assertEquals(init, TEST_VALUE) 46 | this.init = "abc" 47 | assertEquals(init, "abc") 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/test/BaseActivityTestRule.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.test 2 | 3 | import android.content.Intent 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.test.rule.ActivityTestRule 6 | import tech.soit.quiet.ui.activity.base.BaseActivity 7 | import kotlin.reflect.KClass 8 | 9 | class BaseActivityTestRule( 10 | activityClass: KClass, 11 | launchActivity: Boolean = true, 12 | private val beforeActivityLaunched: (BaseActivityTestRule.Companion.() -> Unit)? = null 13 | ) : ActivityTestRule(activityClass.java, true, launchActivity) { 14 | 15 | companion object { 16 | 17 | var viewModelFactory: ViewModelProvider.Factory? = null 18 | 19 | /** 20 | * this function is called by [BaseActivity.onCreate] throw Reflect 21 | */ 22 | @Suppress("unused") 23 | fun injectActivity(activity: BaseActivity) { 24 | //check again 25 | val isTest = activity.intent.getBooleanExtra("isTest", false) 26 | if (!isTest) { 27 | return 28 | } 29 | viewModelFactory?.let { 30 | activity.viewModelFactory = it 31 | viewModelFactory = null 32 | } 33 | } 34 | 35 | 36 | } 37 | 38 | 39 | override fun beforeActivityLaunched() { 40 | beforeActivityLaunched?.invoke(Companion) 41 | } 42 | 43 | 44 | /** 45 | * add a default identify value 46 | */ 47 | public override fun getActivityIntent(): Intent { 48 | val intent = Intent(Intent.ACTION_MAIN) 49 | intent.putExtra("isTest", true) 50 | return intent 51 | } 52 | 53 | 54 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/test/RecyclerViewMatcher.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.test 2 | 3 | import android.content.res.Resources 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | import org.hamcrest.Description 8 | import org.hamcrest.Matcher 9 | import org.hamcrest.TypeSafeMatcher 10 | 11 | /** 12 | * taken from https://gist.github.com/baconpat/8405a88d04bd1942eb5e430d33e4faa2 13 | * license MIT 14 | */ 15 | class RecyclerViewMatcher(private val recyclerViewId: Int) { 16 | 17 | fun atPosition(position: Int): Matcher { 18 | return atPositionOnView(position, -1) 19 | } 20 | 21 | private fun atPositionOnView(position: Int, targetViewId: Int): Matcher { 22 | return object : TypeSafeMatcher() { 23 | var resources: Resources? = null 24 | var childView: View? = null 25 | 26 | override fun describeTo(description: Description) { 27 | var idDescription = Integer.toString(recyclerViewId) 28 | if (this.resources != null) { 29 | idDescription = try { 30 | this.resources!!.getResourceName(recyclerViewId) 31 | } catch (var4: Resources.NotFoundException) { 32 | "$recyclerViewId (resource name not found)" 33 | } 34 | } 35 | 36 | description.appendText("RecyclerView with id: $idDescription at position: $position") 37 | } 38 | 39 | public override fun matchesSafely(view: View): Boolean { 40 | 41 | this.resources = view.resources 42 | 43 | if (childView == null) { 44 | val recyclerView = view.rootView.findViewById(recyclerViewId) 45 | if (recyclerView?.id == recyclerViewId) { 46 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 47 | childView = viewHolder?.itemView 48 | } else { 49 | return false 50 | } 51 | } 52 | 53 | return if (targetViewId == -1) { 54 | view === childView 55 | } else { 56 | val targetView = childView?.findViewById(targetViewId) 57 | view === targetView 58 | } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/test/Uitls.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.test 2 | 3 | import kotlin.reflect.KProperty1 4 | import kotlin.reflect.full.declaredMemberProperties 5 | import kotlin.reflect.jvm.isAccessible 6 | 7 | /** 8 | * @author : summer 9 | * @date : 18-8-29 10 | */ 11 | 12 | fun Any.getPropertyValue(name: String): T { 13 | val find = this::class.declaredMemberProperties.find { it.name == name }!! 14 | 15 | @Suppress("UNCHECKED_CAST") 16 | find as KProperty1 17 | find.isAccessible = true 18 | 19 | return find.get(this) 20 | 21 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/utils/test/ViewModelUtil.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.test 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import tech.soit.quiet.utils.component.support.QuietViewModelProvider 6 | 7 | object ViewModelUtil { 8 | 9 | fun createFor(vararg models: T): ViewModelProvider.Factory { 10 | return object : ViewModelProvider.Factory { 11 | 12 | private val quiet = QuietViewModelProvider() 13 | 14 | override fun create(modelClass: Class): T { 15 | models.forEach { model -> 16 | if (modelClass.isAssignableFrom(model.javaClass)) { 17 | @Suppress("UNCHECKED_CAST") 18 | return model as T 19 | } 20 | } 21 | return quiet.create(modelClass) 22 | } 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/soit/quiet/viewmodel/LocalMusicViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.viewmodel 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import org.junit.Assert.assertNotNull 6 | import org.junit.Assert.assertTrue 7 | import org.junit.Before 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.mockito.Mockito 12 | import tech.soit.quiet.repository.db.QuietDatabaseTest 13 | import tech.soit.quiet.repository.db.QuietDatabaseTest.Companion.DUMMY_MUSICS 14 | import tech.soit.quiet.repository.db.await 15 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 16 | import tech.soit.quiet.utils.component.support.liveDataWith 17 | import tech.soit.quiet.utils.mock 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class LocalMusicViewModelTest { 21 | 22 | private lateinit var viewModel: LocalMusicViewModel 23 | 24 | private val localMusicDao = mock() 25 | 26 | @get:Rule 27 | val r = InstantTaskExecutorRule() 28 | 29 | @Before 30 | fun setUp() { 31 | viewModel = LocalMusicViewModel(localMusicDao) 32 | Mockito.`when`(localMusicDao.getAllMusics()) 33 | .thenReturn(liveDataWith(QuietDatabaseTest.DUMMY_MUSICS)) 34 | } 35 | 36 | @Test 37 | fun getAllMusics() { 38 | val list = viewModel.allMusics.await() 39 | assertTrue(list.size == DUMMY_MUSICS.size) 40 | DUMMY_MUSICS.forEach { localMusic -> 41 | assertNotNull(list.find { it.getTitle() == localMusic.getTitle() }) 42 | } 43 | } 44 | 45 | @Test 46 | fun getAllAlbums() { 47 | val list = viewModel.allAlbums.await() 48 | assertTrue(list.size == 2) 49 | assertNotNull(list.find { it.getName() == "album01" }) 50 | assertNotNull(list.find { it.getName() == "album02" }) 51 | } 52 | 53 | @Test 54 | fun getAllArtists() { 55 | val list = viewModel.allArtists.await() 56 | assertTrue(list.size == 3) 57 | assertNotNull(list.find { it.getName() == "artist01" }) 58 | assertNotNull(list.find { it.getName() == "artist0102" }) 59 | assertNotNull(list.find { it.getName() == "artist0302" }) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/summer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/androidTest/res/raw/summer.mp3 -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/debug/java/tech/soit/quiet/utils/testing/OpenForTesting.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.testing 2 | 3 | /** 4 | * This annotation allows us to open some classes for mocking purposes while they are final in 5 | * release builds. 6 | */ 7 | @Target(AnnotationTarget.ANNOTATION_CLASS) 8 | annotation class OpenClass 9 | 10 | /** 11 | * Annotate a class with [OpenForTesting] if you want it to be extendable in debug builds. 12 | */ 13 | @OpenClass 14 | @Target(AnnotationTarget.CLASS) 15 | annotation class OpenForTesting -------------------------------------------------------------------------------- /app/src/debug/java/tech/soit/quiet/utils/testing/SingleFragmentActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.testing 2 | 3 | import android.os.Bundle 4 | import tech.soit.quiet.R 5 | import tech.soit.quiet.ui.activity.base.BaseActivity 6 | import tech.soit.quiet.ui.fragment.base.BaseFragment 7 | import tech.soit.quiet.ui.view.ContentFrameLayout 8 | 9 | /** 10 | * Used for testing fragments inside a fake activity. 11 | */ 12 | class SingleFragmentActivity : BaseActivity() { 13 | 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | val content = ContentFrameLayout(this) 18 | content.id = R.id.content 19 | setContentView(content) 20 | } 21 | 22 | 23 | fun setFragment(fragment: BaseFragment) { 24 | supportFragmentManager.beginTransaction() 25 | .add(R.id.content, fragment, "test") 26 | .addToBackStack("test") 27 | .commitAllowingStateLoss() 28 | } 29 | 30 | fun replaceFragment(fragment: BaseFragment) { 31 | supportFragmentManager.beginTransaction() 32 | .replace(R.id.content, fragment) 33 | .commit() 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/AppContext.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet 2 | 3 | import android.app.Application 4 | import tech.soit.quiet.utils.component.AppTask 5 | 6 | /** 7 | * application context 8 | */ 9 | class AppContext : Application() { 10 | 11 | /** 12 | * singleton for application 13 | */ 14 | companion object : Application() 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | AppContext.attachBaseContext(this) 19 | AppContext.setTheme(R.style.AppTheme) 20 | registerActivityLifecycleCallbacks(AppTask.CallBack) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/local/LocalAlbum.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.local 2 | 3 | import kotlinx.android.parcel.Parcelize 4 | import tech.soit.quiet.model.vo.Album 5 | 6 | @Parcelize 7 | class LocalAlbum( 8 | private val name: String, 9 | private val coverImageUrl: String 10 | ) : Album() { 11 | 12 | override fun getCoverImageUrl(): Any { 13 | return coverImageUrl 14 | } 15 | 16 | override fun getName(): String { 17 | return name 18 | } 19 | 20 | override fun getId(): Long { 21 | return 0 /*local album do not have id yet*/ 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/local/LocalArtist.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.local 2 | 3 | import kotlinx.android.parcel.Parcelize 4 | import tech.soit.quiet.model.vo.Artist 5 | 6 | @Parcelize 7 | class LocalArtist( 8 | private val name: String 9 | ) : Artist() { 10 | 11 | override fun getId(): Long { 12 | return 0 /* local artist do not have id yet */ 13 | } 14 | 15 | override fun getName(): String { 16 | return name 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/po/NeteaseAlbum.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.po 2 | 3 | import com.google.gson.JsonObject 4 | import kotlinx.android.parcel.Parcelize 5 | import tech.soit.quiet.model.vo.Album 6 | import tech.soit.quiet.repository.netease.source.NeteaseGlideUrl 7 | import tech.soit.quiet.utils.string 8 | 9 | @Parcelize 10 | class NeteaseAlbum( 11 | private val id: Long, 12 | private val name: String, 13 | private val imageUrl: String 14 | ) : Album() { 15 | 16 | companion object { 17 | 18 | /** 19 | * @param al 网易云音乐API json 20 | */ 21 | fun fromJson(al: JsonObject): NeteaseAlbum { 22 | return NeteaseAlbum(al["id"].asLong, al["name"].string, al["picUrl"].string) 23 | } 24 | 25 | } 26 | 27 | override fun getCoverImageUrl(): Any { 28 | return NeteaseGlideUrl(imageUrl) 29 | } 30 | 31 | override fun getName(): String { 32 | return name 33 | } 34 | 35 | override fun getId(): Long { 36 | return id 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/po/NeteaseArtist.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.po 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonObject 5 | import kotlinx.android.parcel.Parcelize 6 | import tech.soit.quiet.model.vo.Artist 7 | import tech.soit.quiet.utils.string 8 | 9 | @Parcelize 10 | class NeteaseArtist( 11 | private val id: Long, 12 | private val name: String 13 | ) : Artist() { 14 | 15 | companion object { 16 | 17 | 18 | /** 19 | * remove json ar json object 20 | */ 21 | fun fromJson(ar: JsonArray?): List { 22 | ar ?: return emptyList() 23 | return ar.map { e -> 24 | e as JsonObject 25 | NeteaseArtist(e["id"].asLong, e["name"].string) 26 | } 27 | } 28 | 29 | } 30 | 31 | 32 | override fun getId(): Long { 33 | return id 34 | } 35 | 36 | override fun getName(): String { 37 | return name 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/po/NeteaseMusic.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.po 2 | 3 | import kotlinx.android.parcel.Parcelize 4 | import tech.soit.quiet.model.vo.Album 5 | import tech.soit.quiet.model.vo.Artist 6 | import tech.soit.quiet.model.vo.Music 7 | 8 | @Parcelize 9 | class NeteaseMusic( 10 | private val id: Long, 11 | private val title: String, 12 | private val album: NeteaseAlbum, 13 | private val artists: List 14 | ) : Music() { 15 | 16 | override fun getId(): Long { 17 | return id 18 | } 19 | 20 | override fun getTitle(): String { 21 | return title 22 | } 23 | 24 | override fun getAlbum(): Album { 25 | return album 26 | } 27 | 28 | override fun getArtists(): List { 29 | return artists 30 | } 31 | 32 | override fun getPlayUrl(): String { 33 | return "http://music.163.com/song/media/outer/url?id=$id.mp3" 34 | } 35 | 36 | override fun toString(): String { 37 | return "NeteaseMusic(id=$id, title='$title')" 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/po/NeteasePlayList.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.po 2 | 3 | import com.google.gson.JsonObject 4 | import tech.soit.quiet.model.vo.PlayList 5 | import tech.soit.quiet.repository.netease.source.NeteaseGlideUrl 6 | 7 | class NeteasePlayList(private val jsonObject: JsonObject) : PlayList() { 8 | 9 | override fun getDescription(): String { 10 | return jsonObject["description"].asString 11 | } 12 | 13 | override fun getCoverImageUrl(): NeteaseGlideUrl { 14 | return NeteaseGlideUrl(jsonObject["coverImgUrl"].asString) 15 | } 16 | 17 | override fun getTrackCount(): Int { 18 | return jsonObject["trackCount"].asInt 19 | } 20 | 21 | override fun getName(): String { 22 | return jsonObject["name"].asString 23 | } 24 | 25 | override fun getId(): Long { 26 | return jsonObject["id"].asLong 27 | } 28 | 29 | override fun getUserId(): Long { 30 | return jsonObject["userId"].asLong 31 | } 32 | 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/po/NeteaseUser.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.po 2 | 3 | import tech.soit.quiet.model.vo.User 4 | import tech.soit.quiet.repository.netease.source.NeteaseGlideUrl 5 | 6 | data class NeteaseUser( 7 | private val id: Long, 8 | private val nickname: String, 9 | private val avatarUrl: String 10 | ) : User() { 11 | 12 | override fun getNickName(): String { 13 | return nickname 14 | } 15 | 16 | override fun getAvatarUrl(): Any { 17 | return NeteaseGlideUrl(avatarUrl) 18 | } 19 | 20 | override fun getId(): Long { 21 | return id 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/vo/Album.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.vo 2 | 3 | import android.os.Parcelable 4 | import java.io.Serializable 5 | 6 | abstract class Album : Parcelable, Serializable { 7 | 8 | 9 | abstract fun getCoverImageUrl(): Any? 10 | 11 | abstract fun getName(): String 12 | 13 | abstract fun getId(): Long 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/vo/Artist.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.vo 2 | 3 | import android.os.Parcelable 4 | import java.io.Serializable 5 | 6 | abstract class Artist : Parcelable, Serializable { 7 | 8 | abstract fun getId(): Long 9 | 10 | abstract fun getName(): String 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/vo/Music.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.vo 2 | 3 | import android.os.Parcelable 4 | import java.io.Serializable 5 | 6 | abstract class Music : Parcelable, Serializable { 7 | 8 | abstract fun getId(): Long 9 | 10 | abstract fun getTitle(): String 11 | 12 | abstract fun getAlbum(): Album 13 | 14 | abstract fun getArtists(): List 15 | 16 | abstract fun getPlayUrl(): String 17 | 18 | override fun equals(other: Any?): Boolean { 19 | if (this === other) return true 20 | if (javaClass != other?.javaClass) return false 21 | other as Music 22 | 23 | if (getId() != other.getId()) return false 24 | 25 | return true 26 | } 27 | 28 | override fun hashCode(): Int { 29 | return getId().hashCode() 30 | } 31 | 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/vo/PlayList.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.vo 2 | 3 | /** 4 | * 歌单 5 | */ 6 | @Deprecated("user PlayListDetail") 7 | abstract class PlayList { 8 | 9 | abstract fun getId(): Long 10 | 11 | abstract fun getName(): String 12 | 13 | abstract fun getCoverImageUrl(): Any 14 | 15 | abstract fun getDescription(): String 16 | 17 | abstract fun getTrackCount(): Int 18 | 19 | abstract fun getUserId(): Long 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/vo/PlayListDetail.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.vo 2 | 3 | import java.io.Serializable 4 | 5 | abstract class PlayListDetail : Serializable { 6 | 7 | companion object { 8 | 9 | val NONE_TRACKS: List = ArrayList() 10 | 11 | } 12 | 13 | abstract fun getId(): Long 14 | 15 | abstract fun getName(): String 16 | 17 | abstract fun getCoverUrl(): Any 18 | 19 | abstract fun getCreator(): User 20 | 21 | abstract fun getTracks(): List 22 | 23 | abstract fun isSubscribed(): Boolean 24 | 25 | abstract fun getPlayCount(): Int 26 | 27 | abstract fun getTrackCount(): Int 28 | 29 | override fun equals(other: Any?): Boolean { 30 | if (this === other) return true 31 | if (javaClass != other?.javaClass) return false 32 | other as PlayListDetail 33 | return getId() == other.getId() 34 | } 35 | 36 | override fun hashCode(): Int { 37 | return getId().hashCode() 38 | } 39 | 40 | fun getToken(): String { 41 | return javaClass.simpleName + "-" + getId() 42 | } 43 | 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/model/vo/User.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.model.vo 2 | 3 | import java.io.Serializable 4 | 5 | abstract class User : Serializable { 6 | 7 | abstract fun getId(): Long 8 | 9 | abstract fun getNickName(): String 10 | 11 | abstract fun getAvatarUrl(): Any 12 | 13 | override fun equals(other: Any?): Boolean { 14 | if (this === other) return true 15 | if (javaClass != other?.javaClass) return false 16 | other as User 17 | return getId() == other.getId() 18 | } 19 | 20 | override fun hashCode(): Int { 21 | return getId().hashCode() 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/player/PlayMode.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.player 2 | 3 | import tech.soit.quiet.R 4 | 5 | 6 | /** 7 | * 8 | * PlayMode of MusicPlayer 9 | * 10 | * @author 杨彬 11 | */ 12 | enum class PlayMode { 13 | //随机播放 14 | Shuffle, 15 | //单曲循环 16 | Single, 17 | //列表循环 18 | Sequence; 19 | 20 | companion object { 21 | 22 | /** 23 | * safely convert enum name to instance 24 | */ 25 | fun from(name: String?) = when (name) { 26 | Shuffle.name -> Shuffle 27 | Single.name -> Single 28 | Sequence.name -> Sequence 29 | else -> Sequence 30 | } 31 | 32 | } 33 | 34 | fun next(): PlayMode = when (this) { 35 | Single -> Shuffle 36 | Shuffle -> Sequence 37 | Sequence -> Single 38 | } 39 | 40 | fun drawableRes(): Int = when (this) { 41 | Single -> R.drawable.ic_repeat_one_black_24dp 42 | Shuffle -> R.drawable.ic_shuffle_black_24dp 43 | Sequence -> R.drawable.ic_repeat_black_24dp 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/player/Readme.md: -------------------------------------------------------------------------------- 1 | 播放需要的功能有 2 | 3 | * 上/下一曲 4 | * 播放/暂停 5 | * 获取/改变当前播放中的音乐 6 | * 获取/改变当前音乐列表 7 | * 获取/改变当前音乐播放模式(单曲,顺序,随机) 8 | * 电台模式和普通音乐播放器之间切换 9 | 10 | 依据上述功能设计的播放器如 `player` 包下可见 11 | 12 | `MusicPlayerManager`以 LiveData 来提供获取播放器状态的 API ,是个单例类 13 | 14 | `QuietMusicPlayer` 提供API来改变播放器的状态(切换歌曲,暂停...) -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/player/core/IMediaPlayer.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.player.core 2 | 3 | import androidx.annotation.IntDef 4 | 5 | 6 | /** 7 | * player interface 8 | */ 9 | interface IMediaPlayer { 10 | 11 | companion object { 12 | 13 | const val IDLE = 0x0 14 | 15 | const val PLAYING = 0x1 16 | 17 | const val PAUSING = 0x2 18 | 19 | const val PREPARING = 0x3 20 | 21 | 22 | @IntDef(IDLE, PLAYING, PAUSING, PREPARING) 23 | @Target(AnnotationTarget.TYPE) 24 | annotation class PlayerState 25 | 26 | } 27 | 28 | /** 29 | * start to play uri 30 | * 31 | * if source is not ready, this call will posted until source available 32 | */ 33 | fun prepare(uri: String, playWhenReady: Boolean) 34 | 35 | /** 36 | * seek play position to [position] 37 | * 38 | * @param position millisecond 39 | */ 40 | fun seekTo(position: Long) 41 | 42 | /** 43 | * flag to change the state of player 44 | * 45 | * if set to false 46 | * [PLAYING] -> [PAUSING] 47 | * [PREPARING] -> do not play when source ready 48 | * 49 | */ 50 | var isPlayWhenReady: Boolean 51 | 52 | /** 53 | * release all source of the player 54 | * 55 | * stop play and interrupt all jobs 56 | */ 57 | fun release() 58 | 59 | /** 60 | * get current PlayerState 61 | * 62 | * @see PlayerState 63 | */ 64 | fun getState(): @PlayerState Int 65 | 66 | 67 | /** 68 | * 69 | * set a callback to listener state change 70 | * 71 | * @param callBack on state change callback, null to remove 72 | */ 73 | fun setOnStateChangeCallback(callBack: ((state: @PlayerState Int) -> Unit)?) 74 | 75 | 76 | /** 77 | * @param callBack invoke when music play complete 78 | */ 79 | fun setOnCompleteListener(callBack: (() -> Unit)?) 80 | 81 | /** 82 | * get current playing position 83 | * 84 | * if filed is not available , return 0 85 | */ 86 | fun getPosition(): Long 87 | 88 | /** 89 | * get current playing music duration 90 | * 91 | * if filed is not available , return 0 92 | * 93 | */ 94 | fun getDuration(): Long 95 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/db/QuietDatabase.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import tech.soit.quiet.AppContext 7 | import tech.soit.quiet.repository.db.dao.KeyValueDao 8 | import tech.soit.quiet.repository.db.entity.LocalMusic 9 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 10 | import tech.soit.quiet.repository.db.entity.KeyValueEntity 11 | 12 | @Database( 13 | entities = [LocalMusic::class, KeyValueEntity::class], 14 | version = 1, 15 | exportSchema = false 16 | ) 17 | abstract class QuietDatabase : RoomDatabase() { 18 | 19 | abstract fun localMusicDao(): LocalMusicDao 20 | 21 | abstract fun keyValueDao(): KeyValueDao 22 | 23 | companion object { 24 | 25 | private const val DB_NAME = "quiet.db" 26 | 27 | val instance: QuietDatabase by lazy { 28 | Room.databaseBuilder(AppContext, QuietDatabase::class.java, DB_NAME) 29 | .allowMainThreadQueries() 30 | .fallbackToDestructiveMigration() 31 | .build() 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/db/dao/KeyValueDao.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import tech.soit.quiet.repository.db.entity.KeyValueEntity 8 | import tech.soit.quiet.utils.component.persistence.KeyValuePersistence 9 | import tech.soit.quiet.utils.component.LoggerLevel 10 | import tech.soit.quiet.utils.component.log 11 | import java.lang.reflect.Type 12 | 13 | /** 14 | * 15 | * dao access object for [KeyValueEntity] 16 | * 17 | * it provide [get] and [put] to persist and restore data 18 | * 19 | * @author : summer 20 | * @date : 18-8-20 21 | */ 22 | @Dao 23 | abstract class KeyValueDao : KeyValuePersistence { 24 | 25 | @Query("select * from objects where `key` == :key") 26 | protected abstract fun findEntity(key: String): KeyValueEntity? 27 | 28 | 29 | @Insert(onConflict = OnConflictStrategy.REPLACE) 30 | protected abstract fun insert(objectWrapperEntity: KeyValueEntity) 31 | 32 | override fun get(key: String, typeofT: Type): T? { 33 | val entity = findEntity(key) ?: return null 34 | try { 35 | return entity.getValue(typeofT) 36 | } catch (e: Exception) { 37 | log(LoggerLevel.ERROR) { "parse key($key) failed : ${entity.data} " } 38 | put(key, null) 39 | } 40 | return null 41 | } 42 | 43 | override fun put(key: String, any: Any?) { 44 | try { 45 | val wrapper = KeyValueEntity(key, any) 46 | insert(wrapper) 47 | } catch (e: Exception) { 48 | e.printStackTrace() 49 | } 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/db/dao/LocalMusicDao.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db.dao 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.Dao 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import tech.soit.quiet.repository.db.entity.LocalMusic 8 | 9 | @Dao 10 | abstract class LocalMusicDao { 11 | 12 | @Query("select * from local_music") 13 | abstract fun getAllMusics(): LiveData> 14 | 15 | 16 | @Insert 17 | abstract fun insertMusic(music: LocalMusic): Long 18 | 19 | 20 | @Query("select * from local_music where artistString = :artist") 21 | abstract fun getMusicsByArtist(artist: String): LiveData> 22 | 23 | 24 | @Query("select * from local_music where albumString = :album") 25 | abstract fun getMusicsByAlbum(album: String): LiveData> 26 | 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/db/entity/KeyValueEntity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.google.gson.Gson 7 | import java.lang.reflect.Type 8 | 9 | /** 10 | * @author : summer 11 | * @date : 18-8-20 12 | */ 13 | @Entity(tableName = "objects") 14 | data class KeyValueEntity( 15 | @PrimaryKey 16 | val key: String, 17 | 18 | @ColumnInfo 19 | val data: String 20 | ) { 21 | 22 | private companion object { 23 | 24 | val GSON = Gson() 25 | 26 | } 27 | 28 | constructor(key: String, value: Any?) : this(key, GSON.toJson(value)) 29 | 30 | 31 | fun getValue(typeofT: Type): T { 32 | return GSON.fromJson(data, typeofT) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/db/entity/LocalMusic.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.db.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Ignore 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | import kotlinx.android.parcel.Parcelize 8 | import tech.soit.quiet.model.local.LocalAlbum 9 | import tech.soit.quiet.model.local.LocalArtist 10 | import tech.soit.quiet.model.vo.Album 11 | import tech.soit.quiet.model.vo.Artist 12 | import tech.soit.quiet.model.vo.Music 13 | 14 | 15 | @Entity( 16 | tableName = "local_music", 17 | indices = [ 18 | Index( 19 | value = ["fileUri"], unique = true 20 | ), 21 | Index( 22 | value = ["albumString"], unique = false 23 | ), 24 | Index( 25 | value = ["artistString"], unique = false 26 | )] 27 | ) 28 | @Parcelize 29 | data class LocalMusic( 30 | @PrimaryKey(autoGenerate = true) 31 | private val id: Long, 32 | 33 | /** 34 | * file path; 35 | * unique uri, start with file:// 36 | */ 37 | val fileUri: String, 38 | 39 | /** 40 | * song title 41 | */ 42 | private val title: String, 43 | 44 | /** 45 | * song album 46 | */ 47 | val albumString: String, 48 | 49 | /** 50 | * artist 51 | */ 52 | val artistString: String 53 | 54 | ) : Music() { 55 | 56 | override fun getId(): Long { 57 | return id 58 | } 59 | 60 | @Ignore 61 | override fun getAlbum(): Album { 62 | return LocalAlbum(albumString, "") 63 | } 64 | 65 | @Ignore 66 | override fun getArtists(): List { 67 | return listOf(LocalArtist(artistString)) 68 | } 69 | 70 | override fun getTitle(): String { 71 | return title 72 | } 73 | 74 | @Ignore 75 | override fun getPlayUrl(): String { 76 | return fileUri 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/result/CommonResultBean.kt: -------------------------------------------------------------------------------- 1 | package tech.summerly.quiet.data.netease.result 2 | 3 | /** 4 | * Created by Summerly on 2017/10/2. 5 | * Desc: 6 | */ 7 | class CommonResultBean(val code: Int) -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/result/DailySignResultBean.kt: -------------------------------------------------------------------------------- 1 | package tech.summerly.quiet.data.netease.result 2 | 3 | /** 4 | * author : SUMMERLY 5 | * e-mail : yangbinyhbn@gmail.com 6 | * time : 2017/8/24 7 | * desc : 签到成功 {"point":3,"code":200} 8 | * 重复签到 : {"code":-2,"msg":"重复签到"} 9 | */ 10 | data class DailySignResultBean( 11 | val code: Int, 12 | val point: Int?, //经验点数 13 | val msg: String? 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/result/LyricResultBean.kt: -------------------------------------------------------------------------------- 1 | package tech.summerly.quiet.data.netease.result 2 | 3 | /** 4 | * author : SUMMERLY 5 | * e-mail : yangbinyhbn@gmail.com 6 | * time : 2017/8/23 7 | * desc : 8 | */ 9 | data class LyricResultBean( 10 | val lrc: LrcBean, 11 | val klyric: LrcBean, 12 | val tlyric: LrcBean, 13 | val code: Int 14 | // val sgc: Boolean, 15 | // val sfy: Boolean, 16 | // val qfy: Boolean 17 | ) { 18 | data class LrcBean(val version: Int, 19 | val lyric: String?) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/result/MusicSearchResultBean.kt: -------------------------------------------------------------------------------- 1 | package tech.summerly.quiet.data.netease.result 2 | 3 | /** 4 | * author : SUMMERLY 5 | * e-mail : yangbinyhbn@gmail.com 6 | * time : 2017/8/23 7 | * desc : 8 | */ 9 | data class MusicSearchResultBean(val result: ResultBean, 10 | val code: Long) { 11 | data class ResultBean(val songs: List?, 12 | val songCount: Int) 13 | 14 | data class SongsBean(val id: Long, 15 | val name: String, 16 | val artists: List?, 17 | val album: AlbumBean?, 18 | val duration: Long, 19 | val copyrightId: Long, 20 | val status: Long, 21 | val alias: List, 22 | val rtype: Long, 23 | val ftype: Long, 24 | val mvid: Long, 25 | val fee: Long, 26 | var rUrl: Any?) 27 | 28 | 29 | data class ArtistsBean(val id: Long, 30 | val name: String, 31 | var picUrl: String?, 32 | val alias: List, 33 | val albumSize: Long, 34 | val picId: Long, 35 | val img1v1Url: String, 36 | val img1v1: Long, 37 | var trans: Any?) 38 | 39 | data class AlbumBean(val id: Long, 40 | val name: String, 41 | val artist: ArtistBean, 42 | val publishTime: Long, 43 | val size: Long, 44 | val copyrightId: Long, 45 | val status: Long, 46 | val picId: Long) 47 | 48 | data class ArtistBean(val id: Long, 49 | val name: String, 50 | var picUrl: Any?, 51 | val alias: List, 52 | val albumSize: Long, 53 | val picId: Long, 54 | val img1v1Url: String, 55 | val img1v1: Long, 56 | var trans: Any?) 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/result/MusicUrlResultBean.kt: -------------------------------------------------------------------------------- 1 | package tech.summerly.quiet.data.netease.result 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | 7 | /** 8 | * author : SUMMERLY 9 | * e-mail : yangbinyhbn@gmail.com 10 | * time : 2017/8/23 11 | * desc : 12 | */ 13 | data class MusicUrlResultBean( 14 | 15 | @SerializedName("data") 16 | @Expose 17 | val data: List?, 18 | 19 | @SerializedName("code") 20 | @Expose 21 | val code: Int 22 | ) { 23 | data class Datum( 24 | @SerializedName("id") 25 | @Expose 26 | var id: Long, 27 | 28 | @SerializedName("url") 29 | @Expose 30 | var url: String?, 31 | 32 | @SerializedName("br") 33 | @Expose 34 | var bitrate: Int, 35 | 36 | @SerializedName("size") 37 | @Expose 38 | var size: Long, 39 | 40 | @SerializedName("md5") 41 | @Expose 42 | var md5: String, 43 | 44 | @SerializedName("type") 45 | @Expose 46 | var type: String? 47 | 48 | // @SerializedName("code") 49 | // @Expose 50 | // var code: Long? = null, 51 | // @SerializedName("expi") 52 | // @Expose 53 | // var expi: Long? = null, 54 | // @SerializedName("gain") 55 | // @Expose 56 | // var gain: Double? = null, 57 | // @SerializedName("fee") 58 | // @Expose 59 | // var fee: Long? = null, 60 | // @SerializedName("uf") 61 | // @Expose 62 | // var uf: Any? = null, 63 | // @SerializedName("payed") 64 | // @Expose 65 | // var payed: Long? = null, 66 | // @SerializedName("flag") 67 | // @Expose 68 | // var flag: Long? = null, 69 | // @SerializedName("canExtend") 70 | // @Expose 71 | // var canExtend: Boolean? = null 72 | 73 | ) 74 | } 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/result/PlaylistDetailResultBean.kt: -------------------------------------------------------------------------------- 1 | package tech.summerly.quiet.data.netease.result 2 | 3 | /** 4 | * author : SUMMERLY 5 | * e-mail : yangbinyhbn@gmail.com 6 | * time : 2017/8/25 7 | * desc : 8 | */ 9 | data class PlaylistDetailResultBean( 10 | val playlist: Playlist?, 11 | val code: Int) { 12 | 13 | data class Playlist( 14 | val subscribed: Boolean, 15 | val tracks: List?, 16 | val coverImgUrl: String, 17 | val trackCount: Int, 18 | val playCount: Int, 19 | val trackUpdateTime: Long, 20 | val updateTime: Long, 21 | val name: String, 22 | val id: Long 23 | ) 24 | 25 | data class Track( 26 | val name: String?, 27 | val id: Long, 28 | val ar: List?, 29 | val pop: Int, 30 | val crbt: String, 31 | val al: Album?, 32 | val mv: Long, 33 | val no: Long, 34 | val dt: Int 35 | ) 36 | 37 | data class Artist( 38 | val id: Long, 39 | val name: String? 40 | ) 41 | 42 | data class Album( 43 | val id: Long, 44 | val name: String?, 45 | val picUrl: String? 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/repository/netease/source/NeteaseGlideUrl.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.repository.netease.source 2 | 3 | import com.bumptech.glide.load.model.GlideUrl 4 | import com.bumptech.glide.load.model.Headers 5 | 6 | /** 7 | * 获取唯一标识符作缓存KEY 8 | */ 9 | class NeteaseGlideUrl( 10 | url: String, 11 | headers: Headers = Headers.DEFAULT 12 | ) : GlideUrl(url, headers) { 13 | 14 | override fun getCacheKey(): String { 15 | return super.getCacheKey().substringAfterLast('/') 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/LatestPlayListActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity 2 | 3 | import android.os.Bundle 4 | import androidx.core.view.doOnLayout 5 | import androidx.core.view.updatePadding 6 | import androidx.lifecycle.Observer 7 | import kotlinx.android.synthetic.main.activity_latest_play_list.* 8 | import tech.soit.quiet.R 9 | import tech.soit.quiet.repository.LatestPlayingRepository 10 | import tech.soit.quiet.ui.activity.base.BaseActivity 11 | import tech.soit.quiet.ui.adapter.LatestPlayListAdapter 12 | import tech.soit.quiet.utils.annotation.EnableBottomController 13 | import tech.soit.quiet.utils.annotation.LayoutId 14 | import tech.soit.quiet.utils.component.support.dimen 15 | 16 | @LayoutId(R.layout.activity_latest_play_list) 17 | @EnableBottomController 18 | class LatestPlayListActivity : BaseActivity() { 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | layoutRoot.setOnApplyWindowInsetsListener { _, insets -> 23 | toolbar.updatePadding(top = toolbar.paddingTop + insets.systemWindowInsetTop) 24 | insets.consumeSystemWindowInsets() 25 | } 26 | toolbar.setNavigationOnClickListener { 27 | onBackPressed() 28 | } 29 | val adapter = LatestPlayListAdapter() 30 | recyclerView.adapter = adapter 31 | 32 | LatestPlayingRepository.getInstance().getLatestPlayMusic().observe(this, Observer { musics -> 33 | adapter.showLatestPlayList(musics) 34 | }) 35 | 36 | layoutRoot.doOnLayout { 37 | adapter.placeholderHeight = layoutRoot.height - 38 | (toolbar.height + dimen(R.dimen.height_header_music_list).toInt()) 39 | } 40 | 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/base/BaseListActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.base 2 | 3 | import android.os.Bundle 4 | import androidx.core.view.isVisible 5 | import androidx.core.view.updatePadding 6 | import kotlinx.android.synthetic.main.activity_base_list.* 7 | import tech.soit.quiet.R 8 | import tech.soit.quiet.utils.annotation.LayoutId 9 | 10 | 11 | /** 12 | * the base activity which holder a recycler view 13 | * 14 | */ 15 | @LayoutId(R.layout.activity_base_list) 16 | abstract class BaseListActivity : BaseActivity() { 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | 21 | //toolbar setup 22 | layoutRoot.setOnApplyWindowInsetsListener { _, insets -> 23 | toolbar.updatePadding(top = toolbar.paddingTop + insets.systemWindowInsetTop) 24 | insets.consumeSystemWindowInsets() 25 | } 26 | toolbar.setNavigationOnClickListener { onBackPressed() } 27 | 28 | buttonRetry.setOnClickListener { onRetryButtonClicked() } 29 | } 30 | 31 | 32 | /** 33 | * 显示载入动画 34 | */ 35 | protected open fun setLoading() { 36 | recyclerView.isVisible = false 37 | progressBar.isVisible = true 38 | buttonRetry.isVisible = false 39 | } 40 | 41 | /** 42 | * 失败 43 | */ 44 | protected open fun setFailed() { 45 | recyclerView.isVisible = false 46 | progressBar.isVisible = false 47 | buttonRetry.isVisible = true 48 | } 49 | 50 | /** 51 | * load success 52 | */ 53 | protected open fun setSuccess() { 54 | recyclerView.isVisible = true 55 | progressBar.isVisible = false 56 | buttonRetry.isVisible = false 57 | } 58 | 59 | /** 60 | * 重试按钮的点击回调 61 | */ 62 | protected open fun onRetryButtonClicked() { 63 | setLoading() 64 | } 65 | 66 | 67 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/cloud/CloudDailyRecommendActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.cloud 2 | 3 | import android.os.Bundle 4 | import kotlinx.android.synthetic.main.activity_cloud_daily_recommend.* 5 | import kotlinx.coroutines.launch 6 | import tech.soit.quiet.R 7 | import tech.soit.quiet.repository.netease.NeteaseRepository 8 | import tech.soit.quiet.ui.activity.base.BaseActivity 9 | import tech.soit.quiet.ui.activity.cloud.adapter.DailyRecommendAdapter 10 | import tech.soit.quiet.ui.adapter.MusicListAdapter 11 | import tech.soit.quiet.utils.annotation.EnableBottomController 12 | import tech.soit.quiet.utils.annotation.LayoutId 13 | import tech.soit.quiet.utils.component.log 14 | import tech.soit.quiet.utils.exception.NotLoginException 15 | import tech.soit.quiet.utils.setEmpty 16 | import tech.soit.quiet.utils.setLoading 17 | 18 | /** 19 | * 每日推荐歌曲activity 20 | */ 21 | @LayoutId(R.layout.activity_cloud_daily_recommend) 22 | @EnableBottomController 23 | class CloudDailyRecommendActivity : BaseActivity() { 24 | 25 | private val neteaseRepository by lazyViewModel() 26 | 27 | private lateinit var adapter: MusicListAdapter 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | 32 | adapter = DailyRecommendAdapter() 33 | recyclerView.adapter = adapter 34 | 35 | toolbar.setNavigationOnClickListener { 36 | onBackPressed() 37 | } 38 | 39 | launch { 40 | val user = neteaseRepository.getLoginUser() 41 | if (user == null) { 42 | log { "未登录" } 43 | return@launch 44 | } 45 | adapter.setLoading() 46 | 47 | val list = try { 48 | neteaseRepository.recommendSongs() 49 | } catch (notLogin: NotLoginException) { 50 | //需要登录 51 | return@launch 52 | } catch (e: Exception) { 53 | e.printStackTrace() 54 | return@launch 55 | } 56 | 57 | if (list.isEmpty()) { 58 | adapter.setEmpty() 59 | } else { 60 | adapter.showList(list, false, false) 61 | } 62 | 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/cloud/TopDetailActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.cloud 2 | 3 | import android.os.Bundle 4 | import androidx.recyclerview.widget.GridLayoutManager 5 | import kotlinx.android.synthetic.main.activity_base_list.* 6 | import kotlinx.coroutines.launch 7 | import tech.soit.quiet.R 8 | import tech.soit.quiet.repository.netease.NeteaseRepository 9 | import tech.soit.quiet.ui.activity.base.BaseListActivity 10 | import tech.soit.quiet.ui.activity.cloud.adapter.TopDetailListAdapter 11 | import tech.soit.quiet.ui.activity.cloud.adapter.TopDetailListAdapter.Companion.INDEX_GLOBAL 12 | import tech.soit.quiet.ui.activity.cloud.adapter.TopDetailListAdapter.Companion.INDEX_OFFICIAL 13 | import tech.soit.quiet.utils.annotation.EnableBottomController 14 | import tech.soit.quiet.utils.component.log 15 | 16 | /** 17 | * 各个排行榜数据 18 | */ 19 | @EnableBottomController 20 | class TopDetailActivity : BaseListActivity() { 21 | 22 | private val neteaseRepository by lazyViewModel() 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | toolbar.setTitle(R.string.leader_board) 27 | 28 | val gridLayoutManager = GridLayoutManager(this, 3) 29 | gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 30 | 31 | override fun getSpanSize(position: Int): Int { 32 | return if (position in INDEX_OFFICIAL..INDEX_GLOBAL) 3 else 1 33 | } 34 | 35 | } 36 | recyclerView.layoutManager = gridLayoutManager 37 | loadData() 38 | } 39 | 40 | private fun loadData() { 41 | setLoading() 42 | launch { 43 | try { 44 | val detail = neteaseRepository.toplistDetail() 45 | recyclerView.adapter = TopDetailListAdapter(detail) 46 | setSuccess() 47 | } catch (e: Exception) { 48 | log { e.printStackTrace();"error" } 49 | setFailed() 50 | } 51 | } 52 | } 53 | 54 | override fun onRetryButtonClicked() { 55 | super.onRetryButtonClicked() 56 | loadData() 57 | } 58 | 59 | 60 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/cloud/adapter/DailyRecommendAdapter.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.cloud.adapter 2 | 3 | import android.widget.TextView 4 | import tech.soit.quiet.R 5 | import tech.soit.quiet.model.vo.Music 6 | import tech.soit.quiet.ui.adapter.MusicListAdapter 7 | import tech.soit.quiet.utils.KItemViewBinder 8 | import tech.soit.quiet.utils.KViewHolder 9 | import tech.soit.quiet.utils.TypeLayoutRes 10 | import tech.soit.quiet.utils.withBinder 11 | import java.util.* 12 | 13 | class DailyRecommendAdapter : MusicListAdapter(TOKEN) { 14 | companion object { 15 | private const val TOKEN = "netease_daily_recommend" 16 | } 17 | 18 | init { 19 | withBinder(DailyHeaderBinder()) 20 | } 21 | 22 | override fun buildShowList(musics: List, isShowSubscribeButton: Boolean, isSubscribed: Boolean): ArrayList { 23 | val list = super.buildShowList(musics, isShowSubscribeButton, isSubscribed) 24 | 25 | //添加推荐头 26 | list.add(0, DailyRecommendHeader) 27 | return list 28 | } 29 | 30 | 31 | object DailyRecommendHeader 32 | 33 | @TypeLayoutRes(R.layout.item_header_daily_recommend) 34 | class DailyHeaderBinder : KItemViewBinder() { 35 | 36 | override fun onBindViewHolder(holder: KViewHolder, item: DailyRecommendHeader) { 37 | holder.itemView.findViewById(R.id.textDay).apply { 38 | text = Calendar.getInstance().get(Calendar.DAY_OF_MONTH).toString() 39 | } 40 | } 41 | 42 | } 43 | 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/cloud/viewmodel/CloudPlayListDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.cloud.viewmodel 2 | 3 | import tech.soit.quiet.model.vo.PlayListDetail 4 | import tech.soit.quiet.utils.testing.OpenForTesting 5 | import tech.soit.quiet.viewmodel.CloudViewModel 6 | 7 | @OpenForTesting 8 | class CloudPlayListDetailViewModel : CloudViewModel() { 9 | 10 | suspend fun loadData(id: Long): PlayListDetail? { 11 | return try { 12 | repository.playListDetail(id) 13 | } catch (e: Exception) { 14 | e.printStackTrace() 15 | null 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/local/viewmodel/LocalMusicListViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.local.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import tech.soit.quiet.model.vo.Album 6 | import tech.soit.quiet.model.vo.Artist 7 | import tech.soit.quiet.model.vo.Music 8 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 9 | import tech.soit.quiet.repository.db.entity.LocalMusic 10 | import tech.soit.quiet.utils.testing.OpenForTesting 11 | 12 | @OpenForTesting 13 | class LocalMusicListViewModel( 14 | private val dao: LocalMusicDao 15 | ) : ViewModel() { 16 | 17 | 18 | /** 19 | * get local music by artist 20 | */ 21 | fun getMusicListByArtist(artist: Artist): LiveData> { 22 | @Suppress("UNCHECKED_CAST") 23 | return dao.getMusicsByArtist(artist.getName()) as LiveData> 24 | } 25 | 26 | 27 | /** 28 | * get local music by album 29 | */ 30 | fun getMusicListByAlbum(album: Album): LiveData> { 31 | @Suppress("UNCHECKED_CAST") 32 | return dao.getMusicsByAlbum(album.getName()) as LiveData> 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/main/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.main 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import tech.soit.quiet.ui.activity.base.BaseActivity 6 | 7 | /** 8 | * application splash 9 | */ 10 | class SplashActivity : BaseActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | startActivity(Intent(this, AppMainActivity::class.java)) 15 | finish() 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/user/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.user 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.Toast 7 | import androidx.lifecycle.ViewModel 8 | import kotlinx.android.synthetic.main.activity_login.* 9 | import tech.soit.quiet.R 10 | import tech.soit.quiet.ui.activity.base.BaseActivity 11 | import tech.soit.quiet.ui.activity.main.AppMainActivity 12 | import tech.soit.quiet.ui.activity.user.viewmodel.LoginViewModel 13 | import tech.soit.quiet.utils.annotation.LayoutId 14 | import tech.soit.quiet.utils.component.support.QuietViewModelProvider 15 | import tech.soit.quiet.utils.component.support.setOnClickListenerAsync 16 | 17 | /** 18 | * 暂时先这样吧,登陆界面能跑就行了 19 | */ 20 | @LayoutId(R.layout.activity_login) 21 | class LoginActivity : BaseActivity() { 22 | 23 | init { 24 | viewModelFactory = object : QuietViewModelProvider() { 25 | override fun createViewModel(modelClass: Class): ViewModel { 26 | if (modelClass == LoginViewModel::class.java) { 27 | return LoginViewModel() 28 | } 29 | return super.createViewModel(modelClass) 30 | } 31 | } 32 | } 33 | 34 | private val loginViewModel by lazyViewModel() 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | 39 | buttonLogin.setOnClickListenerAsync { 40 | val phone = editPhone.text?.toString() 41 | val password = editPassword.text?.toString() 42 | val (isSuccess, msg) = loginViewModel.login(phone, password) 43 | if (isSuccess) { 44 | startActivity(Intent(this@LoginActivity, AppMainActivity::class.java)) 45 | setResult(Activity.RESULT_OK) 46 | finish() 47 | } else { 48 | Toast.makeText(this@LoginActivity, msg!!, Toast.LENGTH_SHORT).show() 49 | } 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/activity/user/viewmodel/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.activity.user.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import tech.soit.quiet.repository.netease.NeteaseRepository 5 | import tech.soit.quiet.utils.testing.OpenForTesting 6 | 7 | @OpenForTesting 8 | class LoginViewModel( 9 | private val neteaseRepository: NeteaseRepository = NeteaseRepository.instance 10 | ) : ViewModel() { 11 | 12 | /** 13 | * login to netease 14 | */ 15 | suspend fun login(phone: String?, password: String?): LoginResult { 16 | if (phone == null || password == null) { 17 | return LoginResult(false, "空输入") 18 | } 19 | return try { 20 | neteaseRepository.login(phone, password) 21 | LoginResult(true, null) 22 | } catch (e: Exception) { 23 | LoginResult(false, e.message ?: "Unknown") 24 | } 25 | } 26 | 27 | 28 | data class LoginResult(val isSuccess: Boolean, 29 | val msg: String?) 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/adapter/LatestPlayListAdapter.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.adapter 2 | 3 | import tech.soit.quiet.model.vo.Music 4 | import tech.soit.quiet.ui.adapter.viewholder.MusicListHeaderViewHolder 5 | 6 | class LatestPlayListAdapter : MusicListAdapter2() { 7 | 8 | companion object { 9 | 10 | private const val TOKEN = "latest-play-list" 11 | 12 | } 13 | 14 | 15 | override val token: String get() = TOKEN 16 | 17 | override val musicIndexOffset: Int get() = 1 18 | 19 | override val trackCount: Int get() = musics.size 20 | 21 | override fun getItemCount(): Int { 22 | return if (isPreviewMode) 2 else musics.size + 1 23 | } 24 | 25 | fun showLatestPlayList(musics: List?) { 26 | if (musics.isNullOrEmpty()) { 27 | preview() 28 | } else { 29 | showList(musics) 30 | } 31 | } 32 | 33 | override fun getItemViewType(position: Int): Int { 34 | return when { 35 | position == 0 -> TYPE_HEADER 36 | isPreviewMode -> TYPE_HEADER 37 | else -> TYPE_MUSIC 38 | } 39 | } 40 | 41 | override fun bindHeader(holder: MusicListHeaderViewHolder) { 42 | holder.setHeader(trackCount, false, false) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/adapter/viewholder/BaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.adapter.viewholder 2 | 3 | import android.view.View 4 | import androidx.annotation.ColorInt 5 | import tech.soit.quiet.utils.KViewHolder 6 | 7 | open class BaseViewHolder(itemView: View) : KViewHolder(itemView) { 8 | 9 | 10 | open fun applyPrimaryColor(@ColorInt colorPrimary: Int) { 11 | 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/adapter/viewholder/CloudMainNav2ViewHolder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.adapter.viewholder 2 | 3 | import android.view.View 4 | import androidx.core.view.isVisible 5 | import kotlinx.android.synthetic.main.item_cloud_nav_2.view.* 6 | import tech.soit.quiet.R 7 | import tech.soit.quiet.ui.view.RoundRectOutlineProvider 8 | import tech.soit.quiet.utils.annotation.LayoutId 9 | import tech.soit.quiet.utils.component.ImageLoader 10 | import tech.soit.quiet.utils.component.support.px 11 | 12 | @LayoutId(R.layout.item_cloud_nav_2) 13 | class CloudMainNav2ViewHolder(itemView: View) : BaseViewHolder(itemView) { 14 | 15 | companion object { 16 | 17 | private val imageOutline = RoundRectOutlineProvider(3.px.toFloat()) 18 | 19 | } 20 | 21 | init { 22 | itemView.run { 23 | imageCover.outlineProvider = imageOutline 24 | imageCover.clipToOutline = true 25 | } 26 | } 27 | 28 | fun set(title: String, image: Any) = itemView.run { 29 | textTitle.text = title 30 | ImageLoader.with(this).load(image).into(imageCover) 31 | } 32 | 33 | fun setPlayCount(playCount: Long) = itemView.run { 34 | 35 | val str: String = if (playCount > 10000) { 36 | "%d万".format(playCount / 10000) 37 | } else { 38 | playCount.toString() 39 | } 40 | textRightTop.text = str 41 | } 42 | 43 | 44 | fun setIsRightTopVisible(show: Boolean) { 45 | itemView.layoutRightTop.isVisible = show 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/adapter/viewholder/MusicListHeaderViewHolder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.adapter.viewholder 2 | 3 | import android.view.View 4 | import androidx.core.view.isGone 5 | import kotlinx.android.synthetic.main.header_music_list.view.* 6 | import tech.soit.quiet.R 7 | import tech.soit.quiet.utils.component.support.setOnClickListenerAsync 8 | import tech.soit.quiet.utils.component.support.string 9 | 10 | class MusicListHeaderViewHolder(itemView: View) : BaseViewHolder(itemView) { 11 | 12 | 13 | fun setHeader(count: Int, 14 | showSubscribeButton: Boolean, 15 | subscribed: Boolean) = with(itemView) { 16 | textCollection.isGone = !showSubscribeButton 17 | textCollection.setOnClickListener { 18 | //TODO 19 | } 20 | if (!subscribed) { 21 | textCollection.setText(R.string.add_to_collection) 22 | } else { 23 | textCollection.setText(R.string.collected) 24 | } 25 | textMusicCount.text = string(R.string.template_music_list_header_subtitle, count) 26 | } 27 | 28 | /** 29 | * 设置按钮监听事件 30 | * 31 | * @param subscribed 收藏按钮被点击的监听,返回Boolean表示修改的结果,true按钮点击后被收藏 32 | * @param playAll 播放全部歌曲按钮被点击的监听 33 | */ 34 | fun setListener(subscribed: suspend () -> Boolean, 35 | playAll: () -> Unit) = with(itemView) { 36 | textCollection.setOnClickListenerAsync { 37 | if (subscribed()) { 38 | textCollection.setText(R.string.collected) 39 | } else { 40 | textCollection.setText(R.string.add_to_collection) 41 | } 42 | } 43 | setOnClickListener { 44 | playAll() 45 | } 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/adapter/viewholder/MusicViewHolder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.adapter.viewholder 2 | 3 | import android.view.View 4 | import androidx.core.view.isInvisible 5 | import kotlinx.android.synthetic.main.item_music_1.view.* 6 | import tech.soit.quiet.R 7 | import tech.soit.quiet.model.vo.Music 8 | import tech.soit.quiet.utils.annotation.LayoutId 9 | 10 | @LayoutId(R.layout.item_music_1) 11 | class MusicViewHolder(itemView: View) : BaseViewHolder(itemView) { 12 | 13 | 14 | fun bind(data: Music, position: Int): Unit = with(itemView) { 15 | //音乐序号 16 | textPosition.text = position.toString() 17 | 18 | text_item_title.text = data.getTitle() 19 | text_item_subtitle.text = data.getArtists().joinToString("/") { it.getName() } 20 | text_item_subtitle_2.text = data.getAlbum().getName() 21 | } 22 | 23 | fun setPlaying(playing: Boolean) = with(itemView) { 24 | iconPlaying.isInvisible = !playing 25 | textPosition.isInvisible = playing 26 | } 27 | 28 | 29 | /** 30 | * @param play 播放当前歌曲 31 | * @param showOptions 展示更多选项 32 | */ 33 | fun setListener( 34 | play: () -> Unit, 35 | showOptions: () -> Unit 36 | ) = with(itemView) { 37 | setOnClickListener { 38 | play() 39 | } 40 | iconMore.setOnClickListener { 41 | showOptions() 42 | } 43 | } 44 | 45 | 46 | override fun applyPrimaryColor(colorPrimary: Int) { 47 | itemView.divider_subtitle.setBackgroundColor(colorPrimary) 48 | itemView.iconPlaying.setColorFilter(colorPrimary) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/adapter/viewholder/PlaceholderViewHolder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.adapter.viewholder 2 | 3 | import android.view.View 4 | import kotlinx.android.synthetic.main.item_placeholder.view.* 5 | import tech.soit.quiet.R 6 | import tech.soit.quiet.utils.annotation.LayoutId 7 | 8 | /** 9 | * RecyclerView 空白占位 10 | */ 11 | @LayoutId(R.layout.item_placeholder) 12 | class PlaceholderViewHolder(itemView: View) : BaseViewHolder(itemView) { 13 | 14 | 15 | override fun applyPrimaryColor(colorPrimary: Int) { 16 | itemView.progressBar.indeterminateDrawable.setTint(colorPrimary) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/fragment/UnimplementedFragment.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.fragment 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.Gravity 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.FrameLayout 10 | import android.widget.TextView 11 | import tech.soit.quiet.ui.fragment.base.BaseFragment 12 | 13 | @Suppress("unused") 14 | /** 15 | * 16 | * fragment placeholder 17 | * 18 | * @author YangBin 19 | * @date 2018/8/12 20 | */ 21 | class UnimplementedFragment : BaseFragment() { 22 | 23 | override fun onCreateView2(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 24 | val layout = FrameLayout(inflater.context) 25 | 26 | val text = TextView(inflater.context) 27 | @SuppressLint("SetTextI18n") 28 | text.text = "Unimplemented" 29 | 30 | layout.addView(text, FrameLayout.LayoutParams( 31 | FrameLayout.LayoutParams.WRAP_CONTENT, 32 | FrameLayout.LayoutParams.WRAP_CONTENT, 33 | Gravity.CENTER)) 34 | 35 | return layout 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/fragment/home/cloud/ItemPlayList.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.fragment.home.cloud 2 | 3 | import android.content.Intent 4 | import android.view.View 5 | import kotlinx.android.synthetic.main.item_play_list.view.* 6 | import tech.soit.quiet.R 7 | import tech.soit.quiet.model.vo.PlayListDetail 8 | import tech.soit.quiet.ui.activity.cloud.CloudPlayListDetailActivity 9 | import tech.soit.quiet.ui.view.RoundRectOutlineProvider 10 | import tech.soit.quiet.utils.KItemViewBinder 11 | import tech.soit.quiet.utils.KViewHolder 12 | import tech.soit.quiet.utils.TypeLayoutRes 13 | import tech.soit.quiet.utils.component.ImageLoader 14 | import tech.soit.quiet.utils.component.support.px 15 | import tech.soit.quiet.utils.component.support.string 16 | 17 | /** 18 | * cloud fragment 主页面的歌单列表 item 19 | */ 20 | @TypeLayoutRes(R.layout.item_play_list) 21 | class PlayListViewBinder : KItemViewBinder() { 22 | 23 | private val outlineProvider = RoundRectOutlineProvider(3.px.toFloat()) 24 | 25 | override fun onViewCreated(view: View) { 26 | view.imageCover.apply { 27 | outlineProvider = this@PlayListViewBinder.outlineProvider 28 | clipToOutline = true 29 | } 30 | } 31 | 32 | override fun onBindViewHolder(holder: KViewHolder, item: PlayListDetail) { 33 | with(holder.itemView) { 34 | ImageLoader.with(this).load(item.getCoverUrl()).into(imageCover) 35 | textTitle.text = item.getName() 36 | textSubTitle.text = string(R.string.template_item_play_list_count, item.getTrackCount()) 37 | setOnClickListener { 38 | val intent = Intent(context, CloudPlayListDetailActivity::class.java) 39 | intent.putExtra("id", item.getId()) 40 | intent.putExtra(CloudPlayListDetailActivity.PARAM_PLAY_LIST, item) 41 | context.startActivity(intent) 42 | } 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/fragment/home/viewmodel/MainCloudViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.fragment.home.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class MainCloudViewModel() : ViewModel() { 6 | 7 | private val userId: Long = TODO() 8 | 9 | fun getPlaylists() { 10 | 11 | } 12 | 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/fragment/home/viewmodel/MainMusicViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.fragment.home.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import tech.soit.quiet.repository.netease.NeteaseRepository 5 | import tech.soit.quiet.utils.testing.OpenForTesting 6 | 7 | @OpenForTesting 8 | class MainMusicViewModel : ViewModel() { 9 | 10 | fun getNeteaseRepository(): NeteaseRepository { 11 | return NeteaseRepository.instance 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/fragment/local/AItemViewBinder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.fragment.local 2 | 3 | import android.graphics.drawable.ColorDrawable 4 | import androidx.annotation.DrawableRes 5 | import kotlinx.android.synthetic.main.item_common_a.view.* 6 | import tech.soit.quiet.R 7 | import tech.soit.quiet.utils.KItemViewBinder 8 | import tech.soit.quiet.utils.KViewHolder 9 | import tech.soit.quiet.utils.TypeLayoutRes 10 | import tech.soit.quiet.utils.component.ImageLoader 11 | import tech.soit.quiet.utils.component.support.attrValue 12 | 13 | @TypeLayoutRes(R.layout.item_common_a) 14 | class AItemViewBinder( 15 | private val onClick: ((position: Int) -> Unit)? = null, 16 | private val onLongClick: ((position: Int) -> Boolean)? = null 17 | ) : KItemViewBinder() { 18 | 19 | 20 | override fun onBindViewHolder(holder: KViewHolder, item: AItem) = with(holder.itemView) { 21 | when { 22 | item.imageResource != 0 -> image.setImageResource(item.imageResource) 23 | item.imageUrl != null -> ImageLoader.with(image).load(item.imageUrl).into(image) 24 | else -> image.setImageDrawable(ColorDrawable(context.attrValue(R.attr.colorPrimary))) 25 | } 26 | textTitle.text = item.title 27 | textCaption.text = item.caption 28 | onClick?.let { action -> setOnClickListener { action(holder.adapterPosition) } } 29 | onLongClick?.let { action -> setOnLongClickListener { action(holder.adapterPosition) } } 30 | Unit 31 | } 32 | 33 | } 34 | 35 | 36 | class AItem( 37 | val title: String, 38 | val caption: String 39 | ) { 40 | 41 | constructor(@DrawableRes image: Int, title: String, caption: String) : this(title, caption) { 42 | imageResource = image 43 | } 44 | 45 | constructor(image: String, title: String, caption: String) : this(title, caption) { 46 | imageUrl = image 47 | } 48 | 49 | var imageResource: Int = 0 50 | private set 51 | 52 | var imageUrl: String? = null 53 | private set 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/item/EmptyBinder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.item 2 | 3 | import tech.soit.quiet.R 4 | import tech.soit.typed.adapter.TypedAdapter 5 | import tech.soit.typed.adapter.TypedBinder 6 | import tech.soit.typed.adapter.ViewHolder 7 | import tech.soit.typed.adapter.annotation.TypeLayoutResource 8 | 9 | @TypeLayoutResource(R.layout.item_empty) 10 | @Deprecated("") 11 | class EmptyViewBinder : TypedBinder() { 12 | 13 | override fun onBindViewHolder(holder: ViewHolder, item: Empty) { 14 | //do nothing 15 | } 16 | 17 | } 18 | 19 | @Deprecated("") 20 | object Empty 21 | 22 | 23 | /** 24 | * shortcut to register Empty 25 | */ 26 | fun TypedAdapter.withEmptyBinder(): TypedAdapter { 27 | return withBinder(Empty::class, EmptyViewBinder()) 28 | } 29 | 30 | fun TypedAdapter.submitEmpty() { 31 | submit(listOf(Empty)) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/item/LoadingBinder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.item 2 | 3 | import tech.soit.quiet.R 4 | import tech.soit.typed.adapter.TypedAdapter 5 | import tech.soit.typed.adapter.TypedBinder 6 | import tech.soit.typed.adapter.ViewHolder 7 | import tech.soit.typed.adapter.annotation.TypeLayoutResource 8 | 9 | /** 10 | * show a load view for RecyclerView 11 | */ 12 | @TypeLayoutResource(R.layout.item_loading) 13 | @Deprecated("") 14 | class LoadingViewBinder : TypedBinder() { 15 | 16 | override fun onBindViewHolder(holder: ViewHolder, item: Loading) { 17 | //do nothing 18 | } 19 | } 20 | 21 | /** 22 | * object for [LoadingViewBinder] 23 | */ 24 | @Deprecated("") 25 | object Loading 26 | 27 | 28 | /** 29 | * shortcut to register Loading 30 | */ 31 | fun TypedAdapter.withLoadingBinder(): TypedAdapter { 32 | return withBinder(Loading::class, LoadingViewBinder()) 33 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/item/MusicListHeaderViewBinder.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.item 2 | 3 | import androidx.core.view.isGone 4 | import kotlinx.android.synthetic.main.header_music_list.view.* 5 | import tech.soit.quiet.R 6 | import tech.soit.quiet.utils.KItemViewBinder 7 | import tech.soit.quiet.utils.KViewHolder 8 | import tech.soit.quiet.utils.TypeLayoutRes 9 | import tech.soit.quiet.utils.component.support.setOnClickListenerAsync 10 | import tech.soit.quiet.utils.component.support.string 11 | 12 | class ItemMusicListHeader( 13 | val count: Int, 14 | val isShowSubscribeButton: Boolean, 15 | var isSubscribed: Boolean 16 | ) 17 | 18 | 19 | @TypeLayoutRes(R.layout.header_music_list) 20 | class MusicListHeaderViewBinder( 21 | private val onClicked: () -> Unit, 22 | private val onCollectionClicked: (suspend () -> Unit)? = null 23 | ) : KItemViewBinder() { 24 | 25 | override fun onBindViewHolder(holder: KViewHolder, item: ItemMusicListHeader) { 26 | with(holder.itemView) { 27 | textCollection.isGone = !item.isShowSubscribeButton 28 | textCollection.setOnClickListenerAsync { 29 | onCollectionClicked?.invoke() 30 | } 31 | if (!item.isSubscribed) { 32 | textCollection.setText(R.string.add_to_collection) 33 | } else { 34 | textCollection.setText(R.string.collected) 35 | } 36 | textMusicCount.text = string(R.string.template_item_play_list_count, item.count) 37 | setOnClickListener { 38 | onClicked() 39 | } 40 | } 41 | 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/item/SettingHeader.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.item 2 | 3 | import kotlinx.android.synthetic.main.item_setting_header.view.* 4 | import tech.soit.quiet.R 5 | import tech.soit.typed.adapter.TypedBinder 6 | import tech.soit.typed.adapter.ViewHolder 7 | import tech.soit.typed.adapter.annotation.TypeLayoutResource 8 | 9 | data class SettingHeader( 10 | val title: String 11 | ) 12 | 13 | @TypeLayoutResource(R.layout.item_setting_header) 14 | class SettingHeaderBinder : TypedBinder() { 15 | override fun onBindViewHolder(holder: ViewHolder, item: SettingHeader) { 16 | holder.itemView.textHeader.text = item.title 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/item/SettingScannerFolderFilter.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.item 2 | 3 | import kotlinx.android.synthetic.main.item_setting_folder_filter.view.* 4 | import tech.soit.quiet.R 5 | import tech.soit.typed.adapter.TypedBinder 6 | import tech.soit.typed.adapter.ViewHolder 7 | import tech.soit.typed.adapter.annotation.TypeLayoutResource 8 | import java.io.File 9 | 10 | /** 11 | * @param path 目录路径 12 | * @param isChecked 是否被标记为过滤 13 | */ 14 | data class SettingScannerFolderFilter( 15 | val path: String, 16 | val isChecked: Boolean 17 | ) { 18 | 19 | val name: String get() = path.substringAfterLast(File.separator) 20 | 21 | val parent: String get() = path.substringBeforeLast(File.separator) 22 | 23 | } 24 | 25 | 26 | /** 27 | * for [tech.soit.quiet.ui.fragment.local.LocalMusicScannerSettingFragment] 28 | */ 29 | @TypeLayoutResource(R.layout.item_setting_folder_filter) 30 | class SettingScannerFolderFilterBinder : TypedBinder() { 31 | 32 | override fun onBindViewHolder(holder: ViewHolder, item: SettingScannerFolderFilter) = with(holder.itemView) { 33 | checkbox.isChecked = item.isChecked 34 | textTitle.text = item.name 35 | textSubTitle.text = item.parent 36 | setOnClickListener { 37 | //TODO 38 | } 39 | Unit 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/item/SettingSwitch.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.item 2 | 3 | import kotlinx.android.synthetic.main.item_setting_switch.view.* 4 | import tech.soit.quiet.R 5 | import tech.soit.typed.adapter.TypedBinder 6 | import tech.soit.typed.adapter.ViewHolder 7 | import tech.soit.typed.adapter.annotation.TypeLayoutResource 8 | 9 | data class SettingSwitch( 10 | val key: String, 11 | val title: String, 12 | val isChecked: Boolean, 13 | val subTitle: String? = null 14 | ) 15 | 16 | 17 | @TypeLayoutResource(R.layout.item_setting_switch) 18 | class SettingSwitchBinder : TypedBinder() { 19 | 20 | override fun onBindViewHolder(holder: ViewHolder, item: SettingSwitch) { 21 | holder.itemView.switch1.apply { 22 | text = item.title 23 | isChecked = item.isChecked 24 | } 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/service/NotificationRouterActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.service 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import tech.soit.quiet.player.MusicPlayerManager 6 | import tech.soit.quiet.player.playlist.Playlist 7 | import tech.soit.quiet.ui.activity.MusicPlayerActivity 8 | import tech.soit.quiet.ui.activity.base.BaseActivity 9 | import tech.soit.quiet.ui.activity.main.AppMainActivity 10 | 11 | 12 | /** 13 | * RouterActivity for Notification to open player fragment 14 | * 15 | * @author YangBin 16 | * @date 2018/8/25 17 | */ 18 | class NotificationRouterActivity : BaseActivity() { 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | val pl = MusicPlayerManager.musicPlayer.playlist.token 23 | 24 | if (pl == Playlist.TOKEN_FM) { 25 | //TODO 26 | startActivity(Intent(this, AppMainActivity::class.java)) 27 | } else { 28 | startActivity(Intent(this, MusicPlayerActivity::class.java)) 29 | } 30 | finish() 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/view/CircleOutlineProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.view 2 | 3 | import android.graphics.Outline 4 | import android.view.View 5 | import android.view.ViewOutlineProvider 6 | 7 | /** 8 | * set view to clip to Oval 9 | * 10 | * View will clip to Circle when it is Square 11 | */ 12 | class CircleOutlineProvider : ViewOutlineProvider() { 13 | 14 | override fun getOutline(view: View, outline: Outline) { 15 | outline.setOval(0, 0, view.width, view.height) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/view/ContentFrameLayout.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.view 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.drawable.ColorDrawable 6 | import android.graphics.drawable.Drawable 7 | import android.util.AttributeSet 8 | import android.view.WindowInsets 9 | import android.widget.FrameLayout 10 | import androidx.core.util.ObjectsCompat 11 | import tech.soit.quiet.R 12 | import tech.soit.quiet.utils.component.support.attrValue 13 | 14 | 15 | /** 16 | * ContentFrameLayout do not consume [WindowInsets] , is usually used as RootLayout 17 | */ 18 | class ContentFrameLayout @JvmOverloads constructor( 19 | context: Context, 20 | attrs: AttributeSet? = null, 21 | defStyleAttr: Int = 0, 22 | defStyleRes: Int = 0 23 | ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { 24 | 25 | 26 | var statusBarBackground: Drawable = ColorDrawable(context.attrValue(R.attr.colorPrimaryDark)) 27 | 28 | private var mDrawStatusBarBackground: Boolean = false 29 | 30 | private var mLastInsets: WindowInsets? = null 31 | 32 | override fun onDraw(canvas: Canvas) { 33 | super.onDraw(canvas) 34 | if (mDrawStatusBarBackground) { 35 | val inset = mLastInsets?.systemWindowInsetTop ?: 0 36 | if (inset > 0) { 37 | statusBarBackground.setBounds(0, 0, width, inset) 38 | statusBarBackground.draw(canvas) 39 | } 40 | } 41 | } 42 | 43 | 44 | /** 45 | * not consume [WindowInsets] 46 | */ 47 | override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { 48 | if (fitsSystemWindows && !ObjectsCompat.equals(mLastInsets, insets)) { 49 | mLastInsets = insets 50 | mDrawStatusBarBackground = insets.systemWindowInsetTop > 0 51 | setWillNotDraw(!mDrawStatusBarBackground && background == null) 52 | requestLayout() 53 | } 54 | super.dispatchApplyWindowInsets(WindowInsets(insets)) 55 | return insets 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/ui/view/RoundRectOutlineProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.ui.view 2 | 3 | import android.graphics.Outline 4 | import android.view.View 5 | import android.view.ViewOutlineProvider 6 | 7 | class RoundRectOutlineProvider(private val radius: Float) : ViewOutlineProvider() { 8 | 9 | override fun getOutline(view: View, outline: Outline) { 10 | outline.setRoundRect(0, 0, view.width, view.height, radius) 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/Extentions.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils 2 | 3 | import com.google.gson.JsonElement 4 | import com.google.gson.JsonNull 5 | import tech.soit.quiet.model.vo.Artist 6 | import tech.soit.quiet.model.vo.Music 7 | 8 | /** 9 | * the separate of artists 10 | */ 11 | private const val ARTIST_SEPARATOR = "/" 12 | 13 | /** 14 | * convert a List of Artist to String 15 | */ 16 | fun List.getString(): String = joinToString(ARTIST_SEPARATOR) { it.getName() } 17 | 18 | /** 19 | * music artist and album info 20 | */ 21 | val Music.subTitle: String 22 | get() = getArtists().getString() + " - " + getAlbum().getName() 23 | 24 | 25 | /** 26 | * if music was marked as Favorite 27 | * TODO 28 | */ 29 | val Music.isFavorite: Boolean 30 | get() = false 31 | 32 | 33 | val JsonElement.string: String 34 | get() { 35 | if (this is JsonNull) { 36 | return "null" 37 | } 38 | return try { 39 | asString 40 | } catch (e: Exception) { 41 | toString() 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/MusicConverter.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils 2 | 3 | import androidx.core.net.toUri 4 | import com.mpatric.mp3agic.Mp3File 5 | import tech.soit.quiet.repository.db.entity.LocalMusic 6 | import tech.soit.quiet.utils.component.log 7 | import java.io.File 8 | 9 | /** 10 | * 11 | * convert file to [LocalMusic] 12 | * 13 | * @author 伯言 14 | */ 15 | object MusicConverter { 16 | 17 | private const val DEFAULT_ARTIST = "未知歌手" 18 | private const val DEFAULT_ALBUM = "未知专辑" 19 | 20 | private const val DEFAULT_DURATION = 0L 21 | private const val DEFAULT_BITRATE = 0 22 | 23 | fun scanFileToMusic(file: File): LocalMusic? { 24 | if (!file.exists()) { 25 | return null 26 | } 27 | val mp3File: Mp3File? 28 | try { 29 | mp3File = Mp3File(file) 30 | } catch (e: Exception) { 31 | log { "can not convert file to Mp3File : ${file.path}" } 32 | return null 33 | } 34 | 35 | val title = mp3File.title(file) 36 | val artist = mp3File.artist() 37 | val album = mp3File.album() 38 | return LocalMusic(0, file.toUri().toString(), title, album, artist) 39 | } 40 | 41 | private fun Mp3File?.album(): String { 42 | this ?: return DEFAULT_ALBUM 43 | return id3v2Tag?.album ?: id3v1Tag?.album ?: DEFAULT_ALBUM 44 | } 45 | 46 | private fun Mp3File?.artist(): String { 47 | this ?: return DEFAULT_ARTIST 48 | return id3v2Tag?.artist ?: id3v1Tag?.artist ?: DEFAULT_ARTIST 49 | } 50 | 51 | private fun Mp3File?.title(file: File): String = this?.id3v2Tag?.title 52 | ?: this?.id3v1Tag?.title 53 | ?: file.nameWithoutExtension 54 | 55 | 56 | private fun Mp3File?.artWork(): ByteArray? = this?.id3v2Tag?.albumImage 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/Platform.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils 2 | 3 | import android.content.Context 4 | import android.os.storage.StorageManager 5 | import tech.soit.quiet.AppContext 6 | import java.lang.reflect.Array 7 | import java.lang.reflect.InvocationTargetException 8 | 9 | /** 10 | * @param is_removable true返回外置存储卡路径 false返回内置存储卡的路径 11 | */ 12 | fun getStoragePath(is_removable: Boolean): String? { 13 | 14 | val mStorageManager = AppContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager 15 | val storageVolumeClazz: Class<*>? 16 | try { 17 | storageVolumeClazz = Class.forName("android.os.storage.StorageVolume") 18 | val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList") 19 | val getPath = storageVolumeClazz!!.getMethod("getPath") 20 | val isRemovable = storageVolumeClazz.getMethod("isRemovable") 21 | val result = getVolumeList.invoke(mStorageManager) 22 | val length = Array.getLength(result) 23 | for (i in 0 until length) { 24 | val storageVolumeElement = Array.get(result, i) 25 | val path = getPath.invoke(storageVolumeElement) as String 26 | val removable = isRemovable.invoke(storageVolumeElement) as Boolean 27 | if (is_removable == removable) { 28 | return path 29 | } 30 | } 31 | } catch (e: ClassNotFoundException) { 32 | e.printStackTrace() 33 | } catch (e: InvocationTargetException) { 34 | e.printStackTrace() 35 | } catch (e: NoSuchMethodException) { 36 | e.printStackTrace() 37 | } catch (e: IllegalAccessException) { 38 | e.printStackTrace() 39 | } 40 | 41 | return null 42 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/annotation/DisableLayoutInject.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.annotation 2 | 3 | /** 4 | * annotated an activity which do not need [LayoutId] to create UI 5 | */ 6 | @Target(AnnotationTarget.CLASS) 7 | annotation class DisableLayoutInject -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/annotation/EnableBottomController.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.annotation 2 | 3 | /** 4 | * add this annotation to enable BottomController to Activity which extends BaseActivity 5 | */ 6 | @Target(AnnotationTarget.CLASS) 7 | annotation class EnableBottomController -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/annotation/LayoutId.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.annotation 2 | 3 | import androidx.annotation.LayoutRes 4 | import tech.soit.quiet.ui.fragment.base.BaseFragment 5 | import java.lang.annotation.Inherited 6 | 7 | /** 8 | * bind the fragment's layout id to fragment by annotation 9 | * this binder process will be invoke in [BaseFragment.onCreateView] 10 | * 11 | * this annotation will be ignore, if your override [BaseFragment.onCreateView2] and return non null 12 | * 13 | * @param value the id of fragment layout 14 | * @param translucent if false , it will set a background for this fragment' root view 15 | * if true , do nothing. 16 | */ 17 | @Target(AnnotationTarget.CLASS) 18 | @Inherited 19 | annotation class LayoutId( 20 | @LayoutRes val value: Int, 21 | val translucent: Boolean = true 22 | ) -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/Logger.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component 2 | 3 | import android.util.Log 4 | import tech.soit.quiet.BuildConfig 5 | 6 | /** 7 | * Created by summer on 17-12-17 8 | */ 9 | 10 | 11 | private val DEBUG get() = BuildConfig.DEBUG 12 | 13 | private const val TAG = "QUIET" 14 | 15 | fun logError(error: Throwable?) { 16 | error ?: return 17 | if (DEBUG) { 18 | error.printStackTrace() 19 | } 20 | } 21 | 22 | fun log(level: LoggerLevel = LoggerLevel.INFO, lazyMessage: () -> Any?) { 23 | if (DEBUG) { 24 | //TODO logger 调整 25 | val traceElement = Exception().stackTrace[2] 26 | val traceInfo = with(traceElement) { 27 | val source = if (isNativeMethod) { 28 | "(Native Method)" 29 | } else if (fileName != null && lineNumber >= 0) { 30 | "($fileName:$lineNumber)" 31 | } else if (fileName != null) { 32 | "($fileName)" 33 | } else { 34 | "(Unknown Source)" 35 | } 36 | source + className.substringAfterLast('.') + "." + methodName 37 | } 38 | val tag = traceElement.className.substringAfterLast('.') 39 | val message = "$traceInfo: ${lazyMessage().toString()}" 40 | logByAndroid(message, level, tag) 41 | } 42 | } 43 | 44 | private fun logByAndroid(message: String, level: LoggerLevel, tag: String = TAG) = when (level) { 45 | LoggerLevel.DEBUG -> Log.d(tag, message) 46 | LoggerLevel.INFO -> Log.i(tag, message) 47 | LoggerLevel.WARN -> Log.w(tag, message) 48 | LoggerLevel.ERROR -> Log.e(tag, message) 49 | } 50 | 51 | enum class LoggerLevel { 52 | DEBUG, 53 | INFO, 54 | WARN, 55 | ERROR 56 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/Pictures.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.graphics.PorterDuff 7 | import androidx.annotation.ColorInt 8 | import androidx.palette.graphics.Palette 9 | import com.bumptech.glide.annotation.GlideModule 10 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool 11 | import com.bumptech.glide.module.AppGlideModule 12 | import jp.wasabeef.glide.transformations.BitmapTransformation 13 | import kotlinx.coroutines.GlobalScope 14 | import kotlinx.coroutines.async 15 | import tech.soit.quiet.R 16 | import tech.soit.quiet.utils.component.support.color 17 | import java.security.MessageDigest 18 | 19 | 20 | @GlideModule(glideName = "ImageLoader") 21 | class MyAppGlideModule : AppGlideModule() 22 | 23 | 24 | /** 25 | * generate palette for bitmap 26 | */ 27 | fun Bitmap.generatePalette() = GlobalScope.async { 28 | Palette.from(this@generatePalette).generate() 29 | } 30 | 31 | fun Palette.getMuteSwatch(night: Boolean = false): Palette.Swatch { 32 | return if (night) { 33 | darkMutedSwatch ?: Palette.Swatch(color(R.color.color_primary_dark), 100) 34 | } else { 35 | mutedSwatch ?: Palette.Swatch(color(R.color.color_primary), 100) 36 | } 37 | } 38 | 39 | 40 | class ColorMaskTransformation( 41 | @ColorInt private val color: Int 42 | ) : BitmapTransformation() { 43 | 44 | companion object { 45 | private const val ID = "ColorMaskTransformation" 46 | } 47 | 48 | override fun hashCode(): Int { 49 | return ID.hashCode() + color * 10 50 | } 51 | 52 | override fun equals(other: Any?): Boolean { 53 | return other is ColorMaskTransformation && other.color == this.color 54 | } 55 | 56 | override fun updateDiskCacheKey(messageDigest: MessageDigest) { 57 | messageDigest.update((ID + color).toByteArray()) 58 | } 59 | 60 | override fun transform(context: Context, pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { 61 | Canvas(toTransform).drawColor(color, PorterDuff.Mode.SRC_OVER) 62 | return toTransform 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/network/LiveDataCallAdapter.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.network 2 | 3 | import androidx.lifecycle.LiveData 4 | import retrofit2.Call 5 | import retrofit2.CallAdapter 6 | import retrofit2.Callback 7 | import retrofit2.Response 8 | import tech.soit.quiet.utils.component.support.Resource 9 | import java.lang.reflect.Type 10 | import java.util.concurrent.atomic.AtomicBoolean 11 | 12 | class LiveDataCallAdapter( 13 | private val responseType: Type 14 | ) : CallAdapter>> { 15 | override fun adapt(call: Call): LiveData> { 16 | 17 | return object : LiveData>() { 18 | private val started = AtomicBoolean(false) 19 | 20 | override fun onActive() { 21 | super.onActive() 22 | if (started.compareAndSet(false, true)) { 23 | call.enqueue(object : Callback { 24 | override fun onFailure(call: Call, t: Throwable) { 25 | postValue(Resource.error(t.message ?: "")) 26 | } 27 | 28 | override fun onResponse(call: Call, response: Response) { 29 | val body = response.body() 30 | postValue(Resource.success(body)) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | } 37 | } 38 | 39 | override fun responseType(): Type { 40 | return responseType 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/network/LiveDataCallAdapterFactory.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.network 2 | 3 | import androidx.lifecycle.LiveData 4 | import retrofit2.CallAdapter 5 | import retrofit2.Retrofit 6 | import java.lang.reflect.Type 7 | 8 | class LiveDataCallAdapterFactory : CallAdapter.Factory() { 9 | override fun get( 10 | returnType: Type, 11 | annotations: Array, 12 | retrofit: Retrofit 13 | ): CallAdapter<*, *>? { 14 | if (getRawType(returnType) != LiveData::class.java){ 15 | return null 16 | } 17 | TODO() 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/network/Retrofit.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.network 2 | 3 | import retrofit2.Call 4 | import retrofit2.Callback 5 | import retrofit2.Response 6 | 7 | 8 | /** 9 | * @see Call.enqueue 10 | */ 11 | fun Call.enqueue(callBack: ICallBack.() -> Unit) { 12 | val instance = ICallBack() 13 | instance.callBack() 14 | enqueue(object : Callback { 15 | override fun onFailure(call: Call, t: Throwable) { 16 | instance.onFailure?.invoke(call, t) 17 | } 18 | 19 | override fun onResponse(call: Call, response: Response) { 20 | instance.onResponse?.invoke(call, response) 21 | } 22 | }) 23 | } 24 | 25 | class ICallBack { 26 | 27 | var onResponse: ((call: Call, response: Response) -> Unit)? = null 28 | 29 | var onFailure: ((call: Call, t: Throwable) -> Unit)? = null 30 | 31 | fun onResponse(onResponse: (call: Call, response: Response) -> Unit) { 32 | this.onResponse = onResponse 33 | } 34 | 35 | fun onFailure(onFailure: (call: Call, t: Throwable) -> Unit) { 36 | this.onFailure = onFailure 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/persistence/KeyValue.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.persistence 2 | 3 | import android.util.Base64 4 | import tech.soit.quiet.repository.db.QuietDatabase 5 | import java.io.* 6 | import java.lang.Exception 7 | import java.lang.reflect.Type 8 | 9 | /** 10 | * 11 | * @see KeyValuePersistence 12 | * 13 | * @author : summer 14 | * @date : 18-8-20 15 | */ 16 | object KeyValue : KeyValuePersistence { 17 | 18 | private val impl: KeyValuePersistence get() = QuietDatabase.instance.keyValueDao() 19 | 20 | override fun get(key: String, typeofT: Type): T? { 21 | return impl.get(key, typeofT) 22 | } 23 | 24 | override fun put(key: String, any: Any?) { 25 | impl.put(key, any) 26 | } 27 | 28 | fun objectToString(obj: Serializable?): String? { 29 | obj ?: return null 30 | return try { 31 | val bos = ByteArrayOutputStream() 32 | val out = ObjectOutputStream(bos) 33 | out.writeObject(obj) 34 | out.flush() 35 | 36 | val string = Base64.encodeToString(bos.toByteArray(), Base64.DEFAULT) 37 | out.close() 38 | bos.close() 39 | 40 | string 41 | } catch (e: Exception) { 42 | e.printStackTrace() 43 | null 44 | } 45 | } 46 | 47 | fun objectFromString(str: String?): T? { 48 | str ?: return null 49 | @Suppress("UNCHECKED_CAST") 50 | return try { 51 | val bis = ByteArrayInputStream(Base64.decode(str, Base64.DEFAULT)) 52 | val i = ObjectInputStream(bis) 53 | val any = i.readObject() 54 | any as T 55 | } catch (e: Exception) { 56 | e.printStackTrace() 57 | null 58 | } 59 | } 60 | 61 | } 62 | 63 | /** 64 | * @see KeyValuePersistence.get 65 | */ 66 | inline fun KeyValuePersistence.get(key: String): T? { 67 | return get(key, T::class.java) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/persistence/KeyValuePersistence.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.persistence 2 | 3 | import java.lang.reflect.Type 4 | 5 | /** 6 | * 7 | * save key value pair to persistence 8 | * 9 | * @author : summer 10 | * @date : 18-8-20 11 | */ 12 | interface KeyValuePersistence { 13 | 14 | 15 | /** 16 | * get value by key, might be null if value is empty or parse failed 17 | */ 18 | fun get(key: String, typeofT: Type): T? 19 | 20 | 21 | /** 22 | * save key and value 23 | * 24 | * @param any null to remove the value 25 | * 26 | */ 27 | fun put(key: String, any: Any?) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/CenterSmoothScroller.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.LinearSmoothScroller 5 | 6 | /** 7 | * 8 | * a smooth scroller make RecyclerView could scroll to center 9 | * 10 | * @author : summer 11 | * @date : 18-9-1 12 | */ 13 | class CenterSmoothScroller(context: Context) : LinearSmoothScroller(context) { 14 | 15 | override fun calculateDtToFit( 16 | viewStart: Int, 17 | viewEnd: Int, 18 | boxStart: Int, 19 | boxEnd: Int, 20 | snapPreference: Int): Int { 21 | return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/Jsons.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | import com.google.gson.JsonElement 4 | import com.google.gson.JsonNull 5 | import java.math.BigDecimal 6 | import java.math.BigInteger 7 | 8 | inline fun JsonElement.value(): T? { 9 | if (this is JsonNull) { 10 | return null 11 | } 12 | 13 | return try { 14 | val a: Any? = when (T::class.java) { 15 | Boolean::class.java -> asBoolean 16 | Short::class.java -> asShort 17 | Char::class.java -> asCharacter 18 | String::class.java -> asString 19 | Float::class.java -> asFloat 20 | Double::class.java -> asDouble 21 | BigDecimal::class.java -> asBigDecimal 22 | BigInteger::class.java -> asBigDecimal 23 | Number::class.java -> asNumber 24 | else -> null 25 | } 26 | a as? T 27 | } catch (e: Exception) { 28 | null 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/LiveData.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | import androidx.lifecycle.* 4 | 5 | 6 | /** 7 | * @see Transformations.map 8 | */ 9 | fun LiveData.map(function: (T?) -> R?): LiveData { 10 | return Transformations.map(this, function) 11 | } 12 | 13 | /** 14 | * the same as [map],but ignore [function] when received a null object 15 | */ 16 | fun LiveData.mapNonNull(function: (T) -> R): LiveData { 17 | return map { t: T? -> 18 | t ?: return@map null 19 | return@map function(t) 20 | } 21 | } 22 | 23 | 24 | /** 25 | * @see Transformations.switchMap 26 | */ 27 | fun LiveData.switchMap(function: (T) -> LiveData?): LiveData { 28 | return Transformations.switchMap(this, function) 29 | } 30 | 31 | 32 | /** 33 | * observe LiveData but filter null change 34 | * 35 | * @see LiveData.observe 36 | */ 37 | fun LiveData.observeNonNull(lifecycleOwner: LifecycleOwner, observer: (T) -> Unit) { 38 | observe(lifecycleOwner, Observer { 39 | if (it != null) { 40 | observer(it) 41 | } 42 | }) 43 | } 44 | 45 | 46 | /** 47 | * create MutableLiveData with initial value 48 | */ 49 | fun liveDataWith(initial: T): MutableLiveData { 50 | val liveData = MutableLiveData() 51 | liveData.postValue(initial)//use post to fit any thread 52 | return liveData 53 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/QuietViewModelProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import tech.soit.quiet.AppContext 6 | import tech.soit.quiet.repository.db.QuietDatabase 7 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 8 | import tech.soit.quiet.repository.netease.NeteaseRepository 9 | 10 | open class QuietViewModelProvider : ViewModelProvider.AndroidViewModelFactory(AppContext) { 11 | 12 | protected val database get() = QuietDatabase.instance 13 | 14 | final override fun create(modelClass: Class): T { 15 | @Suppress("UNCHECKED_CAST") 16 | return createViewModel(modelClass as Class) as T 17 | } 18 | 19 | /** 20 | * create view model by [modelClass] 21 | */ 22 | open fun createViewModel(modelClass: Class): ViewModel { 23 | try { 24 | val constructor = modelClass.getConstructor(LocalMusicDao::class.java) 25 | return constructor.newInstance(database.localMusicDao()) 26 | } catch (e: Exception) { 27 | 28 | } 29 | try { 30 | val constructor = modelClass.getConstructor(QuietDatabase::class.java) 31 | return constructor.newInstance(database) 32 | } catch (e: Exception) { 33 | 34 | } 35 | //网易资源实例只有一个 36 | if (modelClass.isAssignableFrom(NeteaseRepository::class.java)) { 37 | return NeteaseRepository.instance 38 | } 39 | return super.create(modelClass) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/Resource.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | /** 4 | * A generic class that holds a value with its loading status. 5 | * 6 | * @author : summer 7 | * @date : 18-8-29 8 | */ 9 | data class Resource( 10 | val status: Status, 11 | val data: T?, 12 | val message: String? 13 | ) { 14 | 15 | 16 | companion object { 17 | 18 | fun success(data: T?): Resource { 19 | return Resource(Status.SUCCESS, data, null) 20 | } 21 | 22 | fun error(msg: String?, data: T? = null): Resource { 23 | return Resource(Status.ERROR, data, msg) 24 | } 25 | 26 | fun error(throwable: Throwable): Resource { 27 | return error(throwable.message ?: "Unknown exception") 28 | } 29 | 30 | @Deprecated("do not use loading") 31 | fun loading(data: T? = null): Resource { 32 | return Resource(Status.LOADING, data, null) 33 | } 34 | 35 | } 36 | 37 | /** 38 | * get data 39 | */ 40 | fun requireData(): T { 41 | return data ?: throw NullPointerException("data is null") 42 | } 43 | 44 | } 45 | 46 | /** 47 | * Status of a resource that is provided to the UI. 48 | * 49 | * 50 | * These are usually created by the Repository classes where they return 51 | * `LiveData>` to pass back the latest data to the UI with its fetch status. 52 | */ 53 | enum class Status { 54 | SUCCESS, 55 | ERROR, 56 | LOADING 57 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/Resources.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.util.TypedValue 6 | import android.view.View 7 | import androidx.annotation.* 8 | import androidx.core.content.ContextCompat 9 | import tech.soit.quiet.AppContext 10 | 11 | /** 12 | * @see Context.getString 13 | */ 14 | fun string(@StringRes stringId: Int): String = AppContext.getString(stringId) 15 | 16 | /** 17 | * @see Context.getString 18 | */ 19 | fun string(@StringRes stringId: Int, vararg formatArgs: Any) = string(stringId).format(*formatArgs) 20 | 21 | /** 22 | * @see Context.getColor 23 | */ 24 | fun color(@ColorRes colorId: Int) = ContextCompat.getColor(AppContext, colorId) 25 | 26 | 27 | /** 28 | * @see Context.getDrawable 29 | */ 30 | fun drawable(@DrawableRes id: Int, @ColorInt tint: Int = 0) = AppContext.getDrawable(id)!!.also { 31 | if (tint != 0) { 32 | it.setTint(tint) 33 | } 34 | } 35 | 36 | /** 37 | * @see android.content.res.Resources.getDimension 38 | */ 39 | fun dimen(@DimenRes id: Int) = AppContext.resources.getDimension(id) 40 | 41 | /** 42 | * get theme data by Attr id 43 | */ 44 | fun Context.attrValue(@AttrRes id: Int): Int { 45 | val value = TypedValue() 46 | if (theme.resolveAttribute(id, value, true)) { 47 | value.data 48 | return value.data 49 | } else { 50 | error("can not attribute for : $id") 51 | } 52 | } 53 | 54 | 55 | fun View.attrValue(@AttrRes id: Int): Int { 56 | return context.attrValue(id) 57 | } 58 | 59 | /** 60 | * px to dp 61 | */ 62 | val Int.dp: Int 63 | get() = (this / Resources.getSystem().displayMetrics.density).toInt() 64 | 65 | /** 66 | * dp to px 67 | */ 68 | val Int.px: Int 69 | get() = (this * Resources.getSystem().displayMetrics.density).toInt() -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/component/support/Views.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.component.support 2 | 3 | import android.view.View 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.launch 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | /** 10 | * add suspend function support for clicked callback 11 | */ 12 | fun View.setOnClickListenerAsync( 13 | scope: CoroutineScope = context as? CoroutineScope ?: ContextScope(Dispatchers.Main), 14 | listener: suspend View.() -> Unit) { 15 | 16 | val l = CoroutineOnClickListener(scope, listener) 17 | setOnClickListener(l) 18 | } 19 | 20 | 21 | private class CoroutineOnClickListener( 22 | private val scope: CoroutineScope, 23 | private val suspendListener: suspend View.() -> Unit 24 | ) : View.OnClickListener { 25 | 26 | private var isClicked = false 27 | 28 | override fun onClick(v: View) { 29 | if (isClicked) { 30 | return 31 | } 32 | scope.launch { 33 | isClicked = true 34 | suspendListener(v) 35 | isClicked = false 36 | } 37 | } 38 | 39 | 40 | } 41 | 42 | private class ContextScope(override val coroutineContext: CoroutineContext) : CoroutineScope 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/event/PrimaryColorEvent.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.event 2 | 3 | import androidx.annotation.ColorInt 4 | 5 | /** 6 | * primary color 改变事件 7 | */ 8 | class PrimaryColorEvent(@ColorInt val color: Int) -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/event/WindowInsetsEvent.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.event 2 | 3 | import android.view.WindowInsets 4 | 5 | class WindowInsetsEvent(val insets: WindowInsets) -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/utils/exception/NotLoginException.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.exception 2 | 3 | import java.lang.Exception 4 | 5 | class NotLoginException : Exception() -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/viewmodel/CloudViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import tech.soit.quiet.model.vo.User 5 | import tech.soit.quiet.repository.netease.NeteaseRepository 6 | import tech.soit.quiet.utils.testing.OpenForTesting 7 | 8 | @OpenForTesting 9 | abstract class CloudViewModel : ViewModel() { 10 | 11 | protected val repository: NeteaseRepository get() = NeteaseRepository.instance 12 | 13 | /** 14 | * current user 15 | */ 16 | fun getLoginUser(): User? { 17 | return repository.getLoginUser() 18 | } 19 | 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/viewmodel/LocalAlbumDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import tech.soit.quiet.model.vo.Album 7 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 8 | import tech.soit.quiet.repository.db.entity.LocalMusic 9 | import tech.soit.quiet.utils.component.support.switchMap 10 | import tech.soit.quiet.utils.testing.OpenForTesting 11 | 12 | @OpenForTesting 13 | class LocalAlbumDetailViewModel(localMusicDao: LocalMusicDao) : ViewModel() { 14 | 15 | protected val _album = MutableLiveData() 16 | 17 | final val album get() = _album 18 | 19 | /** 20 | * the musics associated with [album] 21 | */ 22 | val musics: LiveData> = album.switchMap { album -> 23 | album ?: return@switchMap null 24 | return@switchMap localMusicDao.getMusicsByAlbum(album.getName()) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/viewmodel/LocalMusicScannerSettingViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import tech.soit.quiet.repository.setting.LocalScannerSettingRepository 6 | import tech.soit.quiet.ui.item.SettingScannerFolderFilter 7 | import tech.soit.quiet.utils.component.support.Resource 8 | import tech.soit.quiet.utils.component.support.map 9 | import tech.soit.quiet.utils.testing.OpenForTesting 10 | 11 | @OpenForTesting 12 | class LocalMusicScannerSettingViewModel : ViewModel() { 13 | 14 | fun setFilterByDuration(boolean: Boolean) { 15 | LocalScannerSettingRepository.setFilterByDuration(boolean) 16 | } 17 | 18 | fun isFilterByDuration() = LocalScannerSettingRepository.isFilterByDuration() 19 | 20 | fun getFileFilterData(): LiveData>> { 21 | return LocalScannerSettingRepository.getFolderFilterData() 22 | .map { 23 | if (it == null) { 24 | return@map Resource.loading>() 25 | } else { 26 | return@map Resource.success(it) 27 | } 28 | } 29 | } 30 | 31 | fun updateFilterData(settingScannerFolderFilter: SettingScannerFolderFilter) { 32 | LocalScannerSettingRepository.editFilterData { 33 | put(settingScannerFolderFilter) 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/viewmodel/LocalMusicViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import tech.soit.quiet.model.vo.Album 6 | import tech.soit.quiet.model.vo.Artist 7 | import tech.soit.quiet.model.vo.Music 8 | import tech.soit.quiet.repository.db.dao.LocalMusicDao 9 | import tech.soit.quiet.utils.component.support.mapNonNull 10 | import tech.soit.quiet.utils.testing.OpenForTesting 11 | 12 | 13 | /** 14 | * view model tracks LocalMusicRepository 15 | */ 16 | @OpenForTesting 17 | class LocalMusicViewModel constructor( 18 | private val localMusicDao: LocalMusicDao 19 | ) : ViewModel() { 20 | 21 | /** 22 | * local total musics 23 | */ 24 | val allMusics: LiveData> 25 | @Suppress("UNCHECKED_CAST") 26 | get() = localMusicDao.getAllMusics() as LiveData> 27 | 28 | 29 | /** 30 | * local total albums 31 | */ 32 | val allAlbums: LiveData> 33 | get() = allMusics.mapNonNull { musics -> 34 | musics.asSequence().map { it.getAlbum() }.distinctBy { it.getName() }.toList() 35 | } 36 | 37 | /** 38 | * local total artist 39 | */ 40 | val allArtists: LiveData> 41 | get() = allMusics.mapNonNull { musics -> 42 | musics.flatMap { it.getArtists() }.distinctBy { it.getName() } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/soit/quiet/viewmodel/MusicControllerViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import tech.soit.quiet.model.vo.Music 6 | import tech.soit.quiet.player.MusicPlayerManager 7 | import tech.soit.quiet.utils.testing.OpenForTesting 8 | 9 | /** 10 | * view model for MusicController 11 | */ 12 | @OpenForTesting 13 | class MusicControllerViewModel : ViewModel() { 14 | 15 | /** 16 | * pause if playing 17 | * play if not playing 18 | * 19 | * @see tech.soit.quiet.player.QuietMusicPlayer.playPause 20 | */ 21 | fun pauseOrPlay() { 22 | MusicPlayerManager.musicPlayer.playPause() 23 | } 24 | 25 | /** 26 | * @see tech.soit.quiet.player.QuietMusicPlayer.playPrevious 27 | */ 28 | fun playPrevious() { 29 | MusicPlayerManager.musicPlayer.playPrevious() 30 | } 31 | 32 | /** 33 | * @see tech.soit.quiet.player.QuietMusicPlayer.playNext 34 | */ 35 | fun playNext() { 36 | MusicPlayerManager.musicPlayer.playNext() 37 | } 38 | 39 | /** 40 | * @see tech.soit.quiet.player.QuietMusicPlayer.quiet 41 | */ 42 | fun quiet() { 43 | MusicPlayerManager.musicPlayer.quiet() 44 | } 45 | 46 | /** 47 | * @see MusicPlayerManager.playingMusic 48 | */ 49 | val playingMusic: LiveData 50 | get() = MusicPlayerManager.playingMusic 51 | 52 | 53 | /** 54 | * @see MusicPlayerManager.playerState 55 | */ 56 | val playerState: LiveData 57 | get() = MusicPlayerManager.playerState 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/mask_dark_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_nav_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_upward_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear_all_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cloud_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_collections_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_comment_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_date_range_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_border_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_download_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_headset_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_link_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_music_note_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_my_location_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_circle_outline_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_radio_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repeat_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_repeat_one_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_select_all_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_show_chart_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shuffle_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_next_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_skip_previous_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_today_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_up_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mask_cloud_top_cover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mask_play_list_detail_cover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/player_gradient_mask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_app_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_base_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 24 | 25 | 31 | 32 | 39 | 40 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_cloud_daily_recommend.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 23 | 24 | 25 | 26 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_cloud_play_list_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_latest_play_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_local_music.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 17 | 18 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_local_music_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 13 | 14 | 15 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 25 | 26 | 27 | 28 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/base_activity_bottom_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_main_music_user_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_local_scanner_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 19 | 20 | 21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_local_single_song.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_cloud.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/header_detail_none_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | 19 | 20 | 28 | 29 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/header_item_cloud_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 22 | 23 | 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/header_item_cloud_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_page_local.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_cloud_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 24 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_cloud_play_list_detail_action.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_cloud_top_type_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 32 | 33 | 34 | 35 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_common_a.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 19 | 20 | 21 | 32 | 33 | 40 | 41 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_setting_header.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_setting_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_login_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/player_content_music_options.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 26 | 27 | 35 | 36 | 37 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_app_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_local_home_page.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_local_scanner.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/navigation_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #4CAF50 5 | #81C784 6 | #388E3C 7 | 8 | #DD2C00 9 | 10 | #ff3d44 11 | 12 | #dd000000 13 | #8a000000 14 | #60000000 15 | @color/color_text_disable 16 | 17 | #ffffff 18 | #B2ffffff 19 | 20 | #1e000000 21 | 22 | 23 | #bf797979 24 | 25 | #FAFAFA 26 | #EEEEEE 27 | #ffffff 28 | 29 | #00000000 30 | #4effffff 31 | #4e000000 32 | 33 | 34 | @android:color/transparent 35 | 36 | @color/color_accent 37 | 38 | #565656 39 | #9e9e9e 40 | 41 | #4CAF50 42 | 43 | #939393 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50dp 4 | 50dp 5 | 270dp 6 | 64dp 7 | 120dp 8 | 60dp 9 | 42dp 10 | 130dp 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_descriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/release/java/tech/soit/quiet/utils/testing/OpenForTesting.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet.utils.testing 2 | 3 | /** 4 | * Annotate a class with [OpenForTesting] if you want it to be extendable in debug builds. 5 | */ 6 | @Target(AnnotationTarget.CLASS) 7 | annotation class OpenForTesting -------------------------------------------------------------------------------- /app/src/test/java/tech/soit/quiet/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package tech.soit.quiet 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | maven { url "https://kotlin.bintray.com/kotlinx" } 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.2.1' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" 15 | classpath "org.jetbrains.kotlinx:kotlinx-gradle-serialization-plugin:$serialization_version" 16 | classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$nav_version" 17 | 18 | } 19 | 20 | 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | google() 26 | jcenter() 27 | maven { url "https://kotlin.bintray.com/kotlinx" } 28 | maven { url "https://dl.bintray.com/summerly/maven" } 29 | } 30 | } 31 | 32 | task clean(type: Delete) { 33 | delete rootProject.buildDir 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #org.gradle.jvmargs=-Xmx1536m \ 2 | #-DsocksProxyHost=127.0.0.1 -DsocksProxyPort=1080 3 | #for app 4 | app_version_code=1 5 | app_version_name=1.0.0 6 | #for build 7 | compile_sdk=android-28 8 | min_sdk=21 9 | target_sdk=28 10 | # for lib 11 | kotlin_version=1.3.10 12 | coroutine_version=1.0.0 13 | serialization_version=0.3 14 | nav_version=1.0.0-alpha05 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 27 20:16:09 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 7 | -------------------------------------------------------------------------------- /images/main.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/images/main.webp -------------------------------------------------------------------------------- /images/main_2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/images/main_2.webp -------------------------------------------------------------------------------- /images/playing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/images/playing.webp -------------------------------------------------------------------------------- /images/playlist_detail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyan01/MusicPlayer/0ba7211c39bf6d144dcda8f79a0d10f4e57b2208/images/playlist_detail.webp -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------