├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── koducation │ │ └── androidcourse101 │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── koducation │ │ │ └── androidcourse101 │ │ │ ├── SpotifyRadioApp.kt │ │ │ ├── data │ │ │ ├── Resource.kt │ │ │ ├── Status.kt │ │ │ ├── local │ │ │ │ ├── FavoriteDataSource.kt │ │ │ │ ├── FavoriteRadioDao.kt │ │ │ │ ├── ListConverter.kt │ │ │ │ ├── SpotifyRadioDatabase.kt │ │ │ │ └── entity │ │ │ │ │ └── FavoriteRadioEntity.kt │ │ │ └── remote │ │ │ │ ├── RadioDataSource.kt │ │ │ │ ├── RadioService.kt │ │ │ │ ├── RadioServiceProvider.kt │ │ │ │ └── model │ │ │ │ ├── Radio.kt │ │ │ │ └── Stream.kt │ │ │ ├── di │ │ │ ├── ActivityModule.kt │ │ │ ├── AppComponent.kt │ │ │ ├── AppModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── FragmentModule.kt │ │ │ └── viewmodel │ │ │ │ ├── ViewModelFactory.kt │ │ │ │ ├── ViewModelKey.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── player │ │ │ ├── PlayerState.kt │ │ │ ├── PlayerViewModel.kt │ │ │ └── PlayerViewState.kt │ │ │ └── ui │ │ │ ├── favorites │ │ │ ├── FavoriteFragment.kt │ │ │ ├── FavoriteFragmentViewModel.kt │ │ │ ├── FavoriteRadioItemViewState.kt │ │ │ ├── FavoriteRadiosAdapter.kt │ │ │ └── di │ │ │ │ └── FavoriteFragmentModule.kt │ │ │ ├── main │ │ │ ├── BottomPlayerViewState.kt │ │ │ ├── BottomPlayerViewStateProducer.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityViewModel.kt │ │ │ ├── MainPagerAdapter.kt │ │ │ └── di │ │ │ │ └── MainActivityModule.kt │ │ │ ├── radios │ │ │ ├── RadiosAdapter.kt │ │ │ ├── RadiosFragment.kt │ │ │ ├── RadiosFragmentViewModel.kt │ │ │ ├── RadiosFragmentViewState.kt │ │ │ ├── RadiosItemViewState.kt │ │ │ ├── RadiosPageCombiner.kt │ │ │ └── di │ │ │ │ └── RadiosFragmentModule.kt │ │ │ └── util │ │ │ └── binding │ │ │ └── ImageLoadingBindingAdapter.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_favorite.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_pause.xml │ │ └── ic_play.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_favorites.xml │ │ ├── fragment_radios.xml │ │ ├── include_bottom_player.xml │ │ ├── item_favorite_radio.xml │ │ └── item_radio.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 │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── koducation │ └── androidcourse101 │ └── PlayerViewStateTest.kt ├── art ├── course_cover.png ├── fragment_ui_favorites.png └── fragment_ui_radios.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | # Uncomment the following line in case you need and you don't have the release build type files in your app 17 | # release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | # Android Studio 3 in .gitignore file. 47 | .idea/caches 48 | .idea/modules.xml 49 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 50 | .idea/navEditor.xml 51 | 52 | # Keystore files 53 | # Uncomment the following lines if you do not want to check your keystore files in. 54 | #*.jks 55 | #*.keystore 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | 60 | # Google Services (e.g. APIs or Firebase) 61 | # google-services.json 62 | 63 | # Freeline 64 | freeline.py 65 | freeline/ 66 | freeline_project_description.json 67 | 68 | # fastlane 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | fastlane/readme.md 74 | 75 | # Version control 76 | vcs.xml 77 | 78 | # lint 79 | lint/intermediates/ 80 | lint/generated/ 81 | lint/outputs/ 82 | lint/tmp/ 83 | # lint/reports/ -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Complete Android App Development Course 101 2 | 3 | ## Still working on this course records and code. Will be announced when it is finished. 4 | 5 | ## Susbcribe to [youtube channel](http://bit.ly/2MwvAUy) to get notified when videos are uploaded. 6 | 7 | In this course you will develop Spotify Clone App. 8 | You will have a modern development skill in this course. 9 | 10 | ![](https://raw.githubusercontent.com/Koducation/AndroidCourse101/master/art/course_cover.png) 11 | 12 | # Note 13 | I will be update the [wiki](https://github.com/Koducation/AndroidCourse101/wiki) page as I produce content. 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | # Uncomment the following line in case you need and you don't have the release build type files in your app 17 | # release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | # Android Studio 3 in .gitignore file. 47 | .idea/caches 48 | .idea/modules.xml 49 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 50 | .idea/navEditor.xml 51 | 52 | # Keystore files 53 | # Uncomment the following lines if you do not want to check your keystore files in. 54 | #*.jks 55 | #*.keystore 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | 60 | # Google Services (e.g. APIs or Firebase) 61 | # google-services.json 62 | 63 | # Freeline 64 | freeline.py 65 | freeline/ 66 | freeline_project_description.json 67 | 68 | # fastlane 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots 72 | fastlane/test_output 73 | fastlane/readme.md 74 | 75 | # Version control 76 | vcs.xml 77 | 78 | # lint 79 | lint/intermediates/ 80 | lint/generated/ 81 | lint/outputs/ 82 | lint/tmp/ 83 | # lint/reports/ -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | apply plugin: 'kotlin-kapt' 8 | 9 | android { 10 | compileSdkVersion 29 11 | buildToolsVersion "29.0.2" 12 | defaultConfig { 13 | applicationId "com.koducation.androidcourse101" 14 | minSdkVersion 16 15 | targetSdkVersion 29 16 | versionCode 1 17 | versionName "1.0" 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables.useSupportLibrary = true 20 | } 21 | buildTypes { 22 | 23 | debug { 24 | buildConfigField "String", "SERVER_URL", "\"https://radiofm-4b467.firebaseio.com/\"" 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | 29 | release { 30 | buildConfigField "String", "SERVER_URL", "\"https://radiofm-4b467.firebaseio.com/\"" 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | 36 | dataBinding { 37 | enabled = true 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility 1.8 42 | targetCompatibility 1.8 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation fileTree(dir: 'libs', include: ['*.jar']) 48 | 49 | //Kotlin 50 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 51 | implementation 'androidx.core:core-ktx:1.1.0' 52 | 53 | //Android 54 | implementation 'androidx.appcompat:appcompat:1.1.0' 55 | implementation 'com.google.android.material:material:1.1.0-alpha10' 56 | 57 | //Architecture Components 58 | implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" 59 | kapt "androidx.lifecycle:lifecycle-compiler:2.1.0" 60 | 61 | //Room 62 | implementation "androidx.room:room-runtime:2.2.0" 63 | implementation "androidx.room:room-rxjava2:2.2.0" 64 | kapt "androidx.room:room-compiler:2.2.0" 65 | 66 | //Network 67 | implementation 'com.squareup.retrofit2:retrofit:2.6.1' 68 | implementation 'com.squareup.retrofit2:converter-gson:2.6.1' 69 | 70 | //RxJava & RxAndroid 71 | implementation 'io.reactivex.rxjava2:rxjava:2.2.13' 72 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 73 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0' 74 | 75 | //Tools 76 | implementation 'com.squareup.picasso:picasso:2.71828' 77 | 78 | //Dagger 79 | implementation 'com.google.dagger:dagger:2.25.2' 80 | kapt 'com.google.dagger:dagger-compiler:2.25.2' 81 | implementation 'com.google.dagger:dagger-android:2.22.1' 82 | implementation 'com.google.dagger:dagger-android-support:2.22.1' 83 | kapt 'com.google.dagger:dagger-android-processor:2.22.1' 84 | 85 | //Player 86 | implementation 'com.google.android.exoplayer:exoplayer:2.10.1' 87 | 88 | //Test 89 | testImplementation 'junit:junit:4.12' 90 | testImplementation 'com.google.truth:truth:1.0' 91 | androidTestImplementation 'androidx.test:runner:1.2.0' 92 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 93 | } 94 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/koducation/androidcourse101/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.koducation.androidcourse101", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/SpotifyRadioApp.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101 2 | 3 | import com.koducation.androidcourse101.di.DaggerAppComponent 4 | import dagger.android.AndroidInjector 5 | import dagger.android.DaggerApplication 6 | 7 | class SpotifyRadioApp : DaggerApplication() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | } 12 | 13 | override fun applicationInjector(): AndroidInjector { 14 | return DaggerAppComponent.factory().create(this) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data 2 | 3 | data class Resource(val status: Status, val data: T?, val message: String?) { 4 | companion object { 5 | fun success(data: T?): Resource { 6 | return Resource(Status.SUCCESS, data, null) 7 | } 8 | 9 | fun error(msg: String): Resource { 10 | return Resource(Status.ERROR, null, msg) 11 | } 12 | 13 | fun loading(): Resource { 14 | return Resource(Status.LOADING, null, null) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/Status.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data 2 | 3 | enum class Status { 4 | SUCCESS, 5 | ERROR, 6 | LOADING 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/local/FavoriteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.local 2 | 3 | import com.koducation.androidcourse101.data.local.entity.FavoriteRadioEntity 4 | import com.koducation.androidcourse101.data.remote.model.Radio 5 | import io.reactivex.BackpressureStrategy 6 | import io.reactivex.Completable 7 | import io.reactivex.Flowable 8 | import io.reactivex.schedulers.Schedulers 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | @Singleton 13 | class FavoriteDataSource @Inject constructor(val favoriteRadioDao: FavoriteRadioDao) { 14 | 15 | fun getFavoriteList(): Flowable> { 16 | return Flowable.create( 17 | { emitter -> 18 | emitter.onNext(arrayListOf()) 19 | 20 | favoriteRadioDao.getFavoriteRadios() 21 | .subscribeOn(Schedulers.io()) 22 | .subscribe { 23 | emitter.onNext(it) 24 | } 25 | }, BackpressureStrategy.BUFFER 26 | ) 27 | } 28 | 29 | fun addToFavorite(radio: Radio): Completable { 30 | return Completable.create { 31 | val favoriteRadioEntity = FavoriteRadioEntity( 32 | radio.id, 33 | radio.band, 34 | radio.city, 35 | radio.country, 36 | radio.dial, 37 | radio.genres, 38 | radio.language, 39 | radio.listenerCount, 40 | radio.logo_big, 41 | radio.logo_small, 42 | radio.radioName, 43 | radio.spotUrl, 44 | radio.streams, 45 | radio.website 46 | ) 47 | favoriteRadioDao.insertFavorite(favoriteRadioEntity) 48 | } 49 | } 50 | 51 | fun removeFromFavorite(radioId: Int): Completable { 52 | return Completable.create { 53 | favoriteRadioDao.removeFavorite(radioId) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/local/FavoriteRadioDao.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.local 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import com.koducation.androidcourse101.data.local.entity.FavoriteRadioEntity 7 | import io.reactivex.Flowable 8 | 9 | @Dao 10 | abstract class FavoriteRadioDao { 11 | 12 | @Query("SELECT * FROM favorite_radios") 13 | abstract fun getFavoriteRadios(): Flowable> 14 | 15 | @Insert 16 | abstract fun insertFavorite(favoriteRadioEntity: FavoriteRadioEntity) 17 | 18 | @Query("DELETE FROM favorite_radios WHERE id=:radioId") 19 | abstract fun removeFavorite(radioId: Int) 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/local/ListConverter.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.local 2 | 3 | import androidx.room.TypeConverter 4 | import com.google.gson.reflect.TypeToken 5 | import com.google.gson.Gson 6 | import com.koducation.androidcourse101.data.remote.model.Stream 7 | 8 | class ListConverter { 9 | 10 | @TypeConverter 11 | fun stringListToJson(stringList: List?): String { 12 | if (stringList.isNullOrEmpty()) { 13 | return "" 14 | } 15 | 16 | val gson = Gson() 17 | val type = object : TypeToken>() {}.type 18 | return gson.toJson(stringList, type) 19 | } 20 | 21 | @TypeConverter 22 | fun jsonToStringList(stringListJson: String): List { 23 | val gson = Gson() 24 | val type = object : TypeToken>() {}.type 25 | return gson.fromJson(stringListJson, type) 26 | } 27 | 28 | @TypeConverter 29 | fun streamsToJson(streams: List?): String { 30 | if (streams == null) { 31 | return "" 32 | } 33 | val gson = Gson() 34 | val type = object : TypeToken>() {}.type 35 | return gson.toJson(streams, type) 36 | } 37 | 38 | @TypeConverter 39 | fun jsonToStreams(streamsJson: String): List { 40 | val gson = Gson() 41 | val type = object : TypeToken>() {}.type 42 | return gson.fromJson(streamsJson, type) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/local/SpotifyRadioDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.local 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.koducation.androidcourse101.data.local.entity.FavoriteRadioEntity 7 | 8 | @Database(entities = [FavoriteRadioEntity::class], version = 1) 9 | @TypeConverters(ListConverter::class) 10 | abstract class SpotifyRadioDatabase : RoomDatabase() { 11 | abstract fun getFavoriteRadiosDao(): FavoriteRadioDao 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/local/entity/FavoriteRadioEntity.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.local.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import androidx.room.TypeConverters 7 | import com.koducation.androidcourse101.data.remote.model.Stream 8 | 9 | @TypeConverters() 10 | @Entity(tableName = "favorite_radios") 11 | data class FavoriteRadioEntity( 12 | @PrimaryKey @ColumnInfo(name = "id") val id: Int, 13 | @ColumnInfo(name = "band") val band: String?, 14 | @ColumnInfo(name = "city") val city: String?, 15 | @ColumnInfo(name = "country") val country: String?, 16 | @ColumnInfo(name = "dial") val dial: String?, 17 | @ColumnInfo(name = "genres") val genres: List?, 18 | @ColumnInfo(name = "language") val language: String?, 19 | @ColumnInfo(name = "listenerCount") val listenerCount: Int = 0, 20 | @ColumnInfo(name = "logo_big") val logo_big: String?, 21 | @ColumnInfo(name = "logo_small") val logo_small: String?, 22 | @ColumnInfo(name = "radioName") val radioName: String?, 23 | @ColumnInfo(name = "spotUrl") val spotUrl: String?, 24 | @ColumnInfo(name = "streams") val streams: List?, 25 | @ColumnInfo(name = "website") val website: String? 26 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/remote/RadioDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.remote 2 | 3 | import com.koducation.androidcourse101.data.Resource 4 | import com.koducation.androidcourse101.data.remote.model.Radio 5 | import io.reactivex.Observable 6 | import io.reactivex.schedulers.Schedulers 7 | import javax.inject.Inject 8 | 9 | class RadioDataSource @Inject constructor(val radioServiceProvider: RadioServiceProvider) { 10 | 11 | fun fetchPopularRadios(): Observable>> { 12 | return Observable.create { emitter -> 13 | 14 | emitter.onNext(Resource.loading()) 15 | 16 | radioServiceProvider 17 | .radioService 18 | .popularRadios() 19 | .subscribeOn(Schedulers.io()) 20 | .subscribe( 21 | { 22 | emitter.onNext( 23 | Resource.success( 24 | it 25 | ) 26 | ) 27 | emitter.onComplete() 28 | }, 29 | { 30 | emitter.onNext( 31 | Resource.error( 32 | it.message ?: "Error" 33 | ) 34 | ) 35 | emitter.onComplete() 36 | } 37 | ) 38 | } 39 | } 40 | 41 | fun fetchLocationRadios(): Observable>> { 42 | return Observable.create { emitter -> 43 | 44 | emitter.onNext(Resource.loading()) 45 | 46 | radioServiceProvider 47 | .radioService 48 | .locationRadios() 49 | .subscribeOn(Schedulers.io()) 50 | .subscribe( 51 | { 52 | emitter.onNext( 53 | Resource.success( 54 | it 55 | ) 56 | ) 57 | emitter.onComplete() 58 | }, 59 | { 60 | emitter.onNext( 61 | Resource.error( 62 | it.message ?: "Error" 63 | ) 64 | ) 65 | emitter.onComplete() 66 | }) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/remote/RadioService.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.remote 2 | 3 | import com.koducation.androidcourse101.data.remote.model.Radio 4 | import io.reactivex.Single 5 | import retrofit2.http.GET 6 | 7 | interface RadioService { 8 | 9 | @GET("popularRadios.json") 10 | fun popularRadios(): Single> 11 | 12 | @GET("locationRadios.json") 13 | fun locationRadios(): Single> 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/remote/RadioServiceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.remote 2 | 3 | import com.koducation.androidcourse101.BuildConfig 4 | import retrofit2.Retrofit 5 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 6 | import retrofit2.converter.gson.GsonConverterFactory 7 | import javax.inject.Inject 8 | 9 | class RadioServiceProvider @Inject constructor() { 10 | 11 | private val retrofit = Retrofit.Builder() 12 | .addConverterFactory(GsonConverterFactory.create()) 13 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 14 | .baseUrl(BuildConfig.SERVER_URL) 15 | .build() 16 | 17 | val radioService = retrofit.create(RadioService::class.java) 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/remote/model/Radio.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.remote.model 2 | 3 | data class Radio( 4 | val band: String?, 5 | val city: String?, 6 | val country: String?, 7 | val dial: String?, 8 | val genres: List?, 9 | val id: Int, 10 | val language: String?, 11 | val listenerCount: Int = 0, 12 | val logo_big: String?, 13 | val logo_small: String?, 14 | val radioName: String?, 15 | val spotUrl: String?, 16 | val streams: List?, 17 | val website: String? 18 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/data/remote/model/Stream.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.data.remote.model 2 | 3 | data class Stream ( 4 | val id : Int, 5 | val is_external : Boolean, 6 | val isfile : Boolean, 7 | val rate : Int, 8 | val works : Boolean, 9 | val url: String? 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di 2 | 3 | import com.koducation.androidcourse101.ui.main.MainActivity 4 | import com.koducation.androidcourse101.ui.main.di.MainActivityModule 5 | import dagger.Module 6 | import dagger.android.ContributesAndroidInjector 7 | 8 | @Module 9 | abstract class ActivityModule { 10 | 11 | @ContributesAndroidInjector(modules = [MainActivityModule::class]) 12 | abstract fun provideMainActivity(): MainActivity 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di 2 | 3 | import com.koducation.androidcourse101.SpotifyRadioApp 4 | import com.koducation.androidcourse101.di.viewmodel.ViewModelModule 5 | import dagger.Component 6 | import dagger.android.AndroidInjector 7 | import dagger.android.support.AndroidSupportInjectionModule 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | @Component( 12 | modules = [ 13 | AndroidSupportInjectionModule::class, 14 | ActivityModule::class, 15 | FragmentModule::class, 16 | AppModule::class, 17 | DatabaseModule::class, 18 | ViewModelModule::class] 19 | ) 20 | interface AppComponent : AndroidInjector { 21 | 22 | @Component.Factory 23 | abstract class Factory : AndroidInjector.Factory 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.koducation.androidcourse101.SpotifyRadioApp 6 | import dagger.Module 7 | import dagger.Provides 8 | 9 | @Module 10 | class AppModule { 11 | 12 | @Provides 13 | fun provideAppC(app: SpotifyRadioApp): Application { 14 | return app 15 | } 16 | 17 | @Provides 18 | fun provideAppContext(app: SpotifyRadioApp): Context { 19 | return app 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.koducation.androidcourse101.data.local.FavoriteRadioDao 6 | import com.koducation.androidcourse101.data.local.SpotifyRadioDatabase 7 | import dagger.Module 8 | import dagger.Provides 9 | 10 | @Module 11 | class DatabaseModule { 12 | 13 | @Provides 14 | fun provideDatabase(context: Context): SpotifyRadioDatabase { 15 | return Room.databaseBuilder(context, SpotifyRadioDatabase::class.java, "spotify-database") 16 | .build() 17 | } 18 | 19 | @Provides 20 | fun provideFavoriteDao(database: SpotifyRadioDatabase): FavoriteRadioDao { 21 | return database.getFavoriteRadiosDao() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/FragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di 2 | 3 | import com.koducation.androidcourse101.ui.favorites.FavoriteFragment 4 | import com.koducation.androidcourse101.ui.favorites.di.FavoriteFragmentModule 5 | import com.koducation.androidcourse101.ui.radios.RadiosFragment 6 | import com.koducation.androidcourse101.ui.radios.di.RadiosFragmentModule 7 | import dagger.Module 8 | import dagger.android.ContributesAndroidInjector 9 | 10 | @Module 11 | abstract class FragmentModule { 12 | 13 | @ContributesAndroidInjector(modules = [FavoriteFragmentModule::class]) 14 | abstract fun provideFavoriteFragment(): FavoriteFragment 15 | 16 | @ContributesAndroidInjector(modules = [RadiosFragmentModule::class]) 17 | abstract fun provideRadiosFragment(): RadiosFragment 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/viewmodel/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | import javax.inject.Singleton 8 | 9 | class ViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>) : 10 | ViewModelProvider.Factory { 11 | 12 | override fun create(modelClass: Class): T = 13 | viewModels[modelClass]?.get() as T 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/viewmodel/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 8 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) 9 | @MapKey 10 | internal annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/di/viewmodel/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.di.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.koducation.androidcourse101.player.PlayerViewModel 6 | import com.koducation.androidcourse101.ui.favorites.FavoriteFragmentViewModel 7 | import com.koducation.androidcourse101.ui.main.MainActivityViewModel 8 | import com.koducation.androidcourse101.ui.radios.RadiosFragmentViewModel 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.multibindings.IntoMap 12 | 13 | @Module 14 | abstract class ViewModelModule { 15 | 16 | @Binds 17 | internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory 18 | 19 | @Binds 20 | @IntoMap 21 | @ViewModelKey(MainActivityViewModel::class) 22 | internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel 23 | 24 | @Binds 25 | @IntoMap 26 | @ViewModelKey(RadiosFragmentViewModel::class) 27 | internal abstract fun bindRadiosFragmentViewModel(viewModel: RadiosFragmentViewModel): ViewModel 28 | 29 | @Binds 30 | @IntoMap 31 | @ViewModelKey(FavoriteFragmentViewModel::class) 32 | internal abstract fun bindFavoriteFragmentViewModel(viewModel: FavoriteFragmentViewModel): ViewModel 33 | 34 | @Binds 35 | @IntoMap 36 | @ViewModelKey(PlayerViewModel::class) 37 | internal abstract fun bindPlayerViewModel(viewModel: PlayerViewModel): ViewModel 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/player/PlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.player 2 | 3 | enum class PlayerState { 4 | PLAYING, BUFFERING, PAUSED, ERROR 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/player/PlayerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.player 2 | 3 | import android.app.Application 4 | import android.net.Uri 5 | import androidx.lifecycle.AndroidViewModel 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MutableLiveData 8 | import com.google.android.exoplayer2.ExoPlaybackException 9 | import com.google.android.exoplayer2.ExoPlayer 10 | import com.google.android.exoplayer2.ExoPlayerFactory 11 | import com.google.android.exoplayer2.Player 12 | import com.google.android.exoplayer2.source.MediaSource 13 | import com.google.android.exoplayer2.source.ProgressiveMediaSource 14 | import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory 15 | import com.google.android.exoplayer2.util.Util 16 | import javax.inject.Inject 17 | 18 | class PlayerViewModel @Inject constructor(val app: Application) : AndroidViewModel(app) { 19 | 20 | private val playerViewStateLiveData = MutableLiveData() 21 | 22 | private val mediaPlayer = ExoPlayerFactory.newSimpleInstance(app.applicationContext) 23 | 24 | private val dataSourceFactory = DefaultDataSourceFactory(app.applicationContext, getUserAgent()) 25 | 26 | private var currentUri: Uri? = null 27 | 28 | init { 29 | mediaPlayer.addListener(object : Player.EventListener { 30 | override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { 31 | val playerState = when { 32 | playbackState == ExoPlayer.STATE_READY && mediaPlayer.playWhenReady -> PlayerState.PLAYING 33 | playbackState == ExoPlayer.STATE_READY && mediaPlayer.playWhenReady.not() -> PlayerState.PAUSED 34 | playbackState == ExoPlayer.STATE_BUFFERING -> PlayerState.BUFFERING 35 | else -> PlayerState.ERROR 36 | } 37 | 38 | playerViewStateLiveData.value = PlayerViewState(playerState) 39 | } 40 | 41 | override fun onPlayerError(error: ExoPlaybackException?) { 42 | playerViewStateLiveData.value = PlayerViewState(PlayerState.ERROR) 43 | } 44 | }) 45 | } 46 | 47 | fun getPlayerViewStateLiveData(): LiveData { 48 | return playerViewStateLiveData 49 | } 50 | 51 | fun start(uri: Uri) { 52 | if (uri == currentUri) { 53 | return 54 | } 55 | 56 | currentUri = uri 57 | val mediaSource = createMediaSource(uri) 58 | mediaPlayer.playWhenReady = true 59 | mediaPlayer.prepare(mediaSource) 60 | } 61 | 62 | fun resume() { 63 | mediaPlayer.playWhenReady = true 64 | } 65 | 66 | fun stop() { 67 | mediaPlayer.playWhenReady = false 68 | } 69 | 70 | fun isPlaying() = 71 | mediaPlayer.playbackState == ExoPlayer.STATE_READY && mediaPlayer.playWhenReady 72 | 73 | override fun onCleared() { 74 | super.onCleared() 75 | mediaPlayer.stop() 76 | mediaPlayer.release() 77 | } 78 | 79 | private fun createMediaSource(uri: Uri): MediaSource { 80 | return ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri) 81 | } 82 | 83 | private fun getUserAgent(): String { 84 | return Util.getUserAgent(app.applicationContext, "spotifyRadioApp") 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/player/PlayerViewState.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.player 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import androidx.core.content.ContextCompat 6 | import com.koducation.androidcourse101.R 7 | import com.koducation.androidcourse101.player.PlayerState.* 8 | 9 | data class PlayerViewState(val playerState: PlayerState) { 10 | 11 | fun getPlayerStateText(context: Context): String { 12 | return when (playerState) { 13 | PLAYING -> context.getString(R.string.player_status_now_playing) 14 | BUFFERING -> context.getString(R.string.player_status_buffering) 15 | PAUSED -> context.getString(R.string.player_status_paused) 16 | ERROR -> context.getString(R.string.player_status_error) 17 | } 18 | } 19 | 20 | fun getPlayPauseIcon(context: Context): Drawable? { 21 | return when (playerState) { 22 | PLAYING -> ContextCompat.getDrawable(context, R.drawable.ic_pause) 23 | else -> ContextCompat.getDrawable(context, R.drawable.ic_play) 24 | } 25 | } 26 | 27 | fun getPlayPauseIconClicable(): Boolean { 28 | return when (playerState) { 29 | PLAYING -> true 30 | PAUSED -> true 31 | else -> false 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/favorites/FavoriteFragment.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.favorites 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.databinding.DataBindingUtil 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.Observer 11 | import androidx.lifecycle.ViewModelProvider 12 | import androidx.lifecycle.ViewModelProviders 13 | import com.koducation.androidcourse101.R 14 | import com.koducation.androidcourse101.databinding.FragmentFavoritesBinding 15 | import dagger.android.support.DaggerFragment 16 | import javax.inject.Inject 17 | 18 | class FavoriteFragment : DaggerFragment() { 19 | 20 | private lateinit var binding: FragmentFavoritesBinding 21 | 22 | @Inject 23 | lateinit var favoriteRadiosAdapter: FavoriteRadiosAdapter 24 | 25 | private lateinit var viewModel: FavoriteFragmentViewModel 26 | 27 | @Inject 28 | lateinit var viewModelFactory: ViewModelProvider.Factory 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | viewModel = 33 | ViewModelProviders.of(this, viewModelFactory).get(FavoriteFragmentViewModel::class.java) 34 | } 35 | 36 | override fun onCreateView( 37 | inflater: LayoutInflater, 38 | container: ViewGroup?, 39 | savedInstanceState: Bundle? 40 | ): View? { 41 | 42 | binding = DataBindingUtil.inflate(inflater, R.layout.fragment_favorites, container, false) 43 | binding.recyclerViewFavorites.adapter = favoriteRadiosAdapter 44 | return binding.root 45 | } 46 | 47 | override fun onActivityCreated(savedInstanceState: Bundle?) { 48 | super.onActivityCreated(savedInstanceState) 49 | 50 | favoriteRadiosAdapter.onFavoriteClicked = { 51 | viewModel.removeFromFavorites(it.getRadio().id) 52 | } 53 | 54 | viewModel.getFavoriteRadiosLiveData().observe(this, Observer { 55 | favoriteRadiosAdapter.updateFavoriteList(it) 56 | }) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/favorites/FavoriteFragmentViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.favorites 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import com.koducation.androidcourse101.SpotifyRadioApp 8 | import com.koducation.androidcourse101.data.local.FavoriteDataSource 9 | import com.koducation.androidcourse101.data.local.entity.FavoriteRadioEntity 10 | import io.reactivex.android.schedulers.AndroidSchedulers 11 | import io.reactivex.disposables.CompositeDisposable 12 | import io.reactivex.schedulers.Schedulers 13 | import javax.inject.Inject 14 | 15 | class FavoriteFragmentViewModel @Inject constructor( 16 | val app: Application, 17 | val favoriteDataSource: FavoriteDataSource 18 | ) : AndroidViewModel(app) { 19 | 20 | private val favoriteViewStateListLiveData = MutableLiveData>() 21 | 22 | private val compositeDisposable = CompositeDisposable() 23 | 24 | init { 25 | compositeDisposable.add(favoriteDataSource 26 | .getFavoriteList() 27 | .map { mapToViewState(it) } 28 | .subscribeOn(Schedulers.io()) 29 | .observeOn(AndroidSchedulers.mainThread()) 30 | .subscribe { 31 | favoriteViewStateListLiveData.value = it 32 | }) 33 | } 34 | 35 | fun getFavoriteRadiosLiveData(): LiveData> = 36 | favoriteViewStateListLiveData 37 | 38 | fun removeFromFavorites(radioId: Int) { 39 | favoriteDataSource.removeFromFavorite(radioId) 40 | .subscribeOn(Schedulers.io()) 41 | .observeOn(AndroidSchedulers.mainThread()) 42 | .subscribe() 43 | } 44 | 45 | private fun mapToViewState(entityList: List): List { 46 | val viewStateList = arrayListOf() 47 | entityList.forEach { viewStateList.add(FavoriteRadioItemViewState(it)) } 48 | return viewStateList 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/favorites/FavoriteRadioItemViewState.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.favorites 2 | 3 | import com.koducation.androidcourse101.data.local.entity.FavoriteRadioEntity 4 | 5 | data class FavoriteRadioItemViewState(private val favoriteRadioEntity: FavoriteRadioEntity) { 6 | 7 | fun getRadioName(): String { 8 | return favoriteRadioEntity.radioName ?: "" 9 | } 10 | 11 | fun getRadioBand(): String { 12 | return favoriteRadioEntity.band ?: "" 13 | } 14 | 15 | fun getRadioImageUrl(): String = favoriteRadioEntity.logo_small ?: "" 16 | 17 | fun getRadio(): FavoriteRadioEntity = favoriteRadioEntity 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/favorites/FavoriteRadiosAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.favorites 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.koducation.androidcourse101.R 8 | import com.koducation.androidcourse101.databinding.ItemFavoriteRadioBinding 9 | import javax.inject.Inject 10 | 11 | class FavoriteRadiosAdapter @Inject constructor(): 12 | RecyclerView.Adapter() { 13 | 14 | private val favoriteViewStateList = arrayListOf() 15 | 16 | var onFavoriteClicked: ((FavoriteRadioItemViewState) -> Unit)? = null 17 | 18 | fun updateFavoriteList(favoriteViewStateList: List) { 19 | this.favoriteViewStateList.clear() 20 | this.favoriteViewStateList.addAll(favoriteViewStateList) 21 | notifyDataSetChanged() 22 | } 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteRadioItemViewHolder = 25 | FavoriteRadioItemViewHolder.create(parent, onFavoriteClicked) 26 | 27 | override fun getItemCount(): Int = favoriteViewStateList.size 28 | 29 | override fun onBindViewHolder(holder: FavoriteRadioItemViewHolder, position: Int) = 30 | holder.bind(favoriteViewStateList[position]) 31 | 32 | class FavoriteRadioItemViewHolder( 33 | private val binding: ItemFavoriteRadioBinding, 34 | private val onFavoriteClicked: ((FavoriteRadioItemViewState) -> Unit)? 35 | ) : 36 | RecyclerView.ViewHolder(binding.root) { 37 | 38 | init { 39 | binding.imageViewFavoriteAction.setOnClickListener { 40 | onFavoriteClicked?.invoke(binding.viewState!!) 41 | } 42 | } 43 | 44 | fun bind(favoriteRadioItemViewState: FavoriteRadioItemViewState) { 45 | binding.viewState = favoriteRadioItemViewState 46 | binding.executePendingBindings() 47 | } 48 | 49 | companion object { 50 | 51 | fun create( 52 | parent: ViewGroup, 53 | onFavoriteClicked: ((FavoriteRadioItemViewState) -> Unit)? 54 | ): FavoriteRadioItemViewHolder { 55 | val binding: ItemFavoriteRadioBinding = DataBindingUtil.inflate( 56 | LayoutInflater.from(parent.context), 57 | R.layout.item_favorite_radio, 58 | parent, 59 | false 60 | ) 61 | return FavoriteRadioItemViewHolder(binding, onFavoriteClicked) 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/favorites/di/FavoriteFragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.favorites.di 2 | 3 | import dagger.Module 4 | 5 | @Module 6 | class FavoriteFragmentModule { 7 | 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/main/BottomPlayerViewState.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.main 2 | 3 | import android.content.Context 4 | import androidx.core.content.ContextCompat 5 | import com.koducation.androidcourse101.R 6 | import com.koducation.androidcourse101.data.remote.model.Radio 7 | 8 | data class BottomPlayerViewState(val radio: Radio, val isFavorited: Boolean) { 9 | 10 | fun getFavoriteColor(context: Context): Int { 11 | return if (isFavorited) { 12 | ContextCompat.getColor(context, R.color.colorFavoriteEnabled) 13 | } else { 14 | ContextCompat.getColor(context, R.color.colorFavoriteDisabled) 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/main/BottomPlayerViewStateProducer.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.main 2 | 3 | import com.koducation.androidcourse101.data.local.entity.FavoriteRadioEntity 4 | import com.koducation.androidcourse101.data.remote.model.Radio 5 | import io.reactivex.functions.BiFunction 6 | 7 | class BottomPlayerViewStateProducer : 8 | BiFunction, BottomPlayerViewState> { 9 | 10 | override fun apply( 11 | selectedRadio: Radio, 12 | favoriteList: List 13 | ): BottomPlayerViewState { 14 | 15 | favoriteList.forEach { 16 | if (it.id == selectedRadio.id) { 17 | return BottomPlayerViewState(selectedRadio, true) 18 | } 19 | } 20 | 21 | return BottomPlayerViewState(selectedRadio, false) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.main 2 | 3 | import android.net.Uri 4 | import android.os.Bundle 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.fragment.app.FragmentStatePagerAdapter 7 | import androidx.lifecycle.Observer 8 | import androidx.lifecycle.ViewModelProvider 9 | import androidx.lifecycle.ViewModelProviders 10 | import com.koducation.androidcourse101.R 11 | import com.koducation.androidcourse101.databinding.ActivityMainBinding 12 | import com.koducation.androidcourse101.player.PlayerViewModel 13 | import dagger.android.support.DaggerAppCompatActivity 14 | import javax.inject.Inject 15 | 16 | class MainActivity : DaggerAppCompatActivity() { 17 | 18 | private lateinit var binding: ActivityMainBinding 19 | 20 | private lateinit var mainViewModel: MainActivityViewModel 21 | 22 | private lateinit var playerViewModel: PlayerViewModel 23 | 24 | @Inject 25 | lateinit var viewModelFactory: ViewModelProvider.Factory 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 30 | 31 | binding.viewPager.adapter = MainPagerAdapter( 32 | context = this, 33 | fm = supportFragmentManager, 34 | behavior = FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 35 | ) 36 | 37 | binding.tabLayout.setupWithViewPager(binding.viewPager) 38 | 39 | binding.layoutBottomPlayer.imageViewFavorite.setOnClickListener { 40 | if (mainViewModel.isFavorited()) { 41 | mainViewModel.removeFromFavorite() 42 | } else { 43 | mainViewModel.addToFavorite() 44 | } 45 | } 46 | 47 | binding.layoutBottomPlayer.imageViewPlayPause.setOnClickListener { 48 | if (playerViewModel.isPlaying()) { 49 | playerViewModel.stop() 50 | } else { 51 | playerViewModel.resume() 52 | } 53 | } 54 | 55 | mainViewModel = 56 | ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java) 57 | 58 | playerViewModel = 59 | ViewModelProviders.of(this, viewModelFactory).get(PlayerViewModel::class.java) 60 | 61 | mainViewModel.getBottomPlayerViewStateLiveData().observe(this, Observer { 62 | playerViewModel.start(Uri.parse(it.radio.streams?.find { it.url != null }?.url)) 63 | binding.viewState = it 64 | binding.executePendingBindings() 65 | }) 66 | 67 | playerViewModel.getPlayerViewStateLiveData().observe(this, Observer { 68 | binding.playerViewState = it 69 | binding.executePendingBindings() 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/main/MainActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.main 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import com.koducation.androidcourse101.SpotifyRadioApp 8 | import com.koducation.androidcourse101.data.local.FavoriteDataSource 9 | import com.koducation.androidcourse101.data.remote.model.Radio 10 | import io.reactivex.BackpressureStrategy 11 | import io.reactivex.Completable 12 | import io.reactivex.Flowable 13 | import io.reactivex.android.schedulers.AndroidSchedulers 14 | import io.reactivex.disposables.CompositeDisposable 15 | import io.reactivex.schedulers.Schedulers 16 | import io.reactivex.subjects.BehaviorSubject 17 | import javax.inject.Inject 18 | 19 | class MainActivityViewModel @Inject constructor(app: Application, val favoriteDataSource: FavoriteDataSource) : AndroidViewModel(app) { 20 | 21 | private val bottomPlayerViewStateLiveData = MutableLiveData() 22 | 23 | private val currentSelectedRadioSubject = BehaviorSubject.create() 24 | 25 | private val disposable = CompositeDisposable() 26 | 27 | init { 28 | disposable.add( 29 | Flowable 30 | .combineLatest( 31 | currentSelectedRadioSubject.toFlowable(BackpressureStrategy.BUFFER), 32 | favoriteDataSource.getFavoriteList(), 33 | BottomPlayerViewStateProducer() 34 | ) 35 | .subscribeOn(Schedulers.io()) 36 | .observeOn(AndroidSchedulers.mainThread()) 37 | .subscribe { bottomPlayerViewStateLiveData.value = it } 38 | ) 39 | } 40 | 41 | fun getBottomPlayerViewStateLiveData(): LiveData = 42 | bottomPlayerViewStateLiveData 43 | 44 | fun setCurrentPlayingRadio(radio: Radio) { 45 | currentSelectedRadioSubject.onNext(radio) 46 | } 47 | 48 | fun addToFavorite() { 49 | bottomPlayerViewStateLiveData.value?.let { 50 | favoriteDataSource.addToFavorite(it.radio) 51 | .subscribeOn(Schedulers.io()) 52 | .observeOn(AndroidSchedulers.mainThread()) 53 | .subscribe() 54 | } 55 | } 56 | 57 | fun removeFromFavorite() { 58 | bottomPlayerViewStateLiveData.value?.let { 59 | favoriteDataSource.removeFromFavorite(it.radio.id) 60 | .subscribeOn(Schedulers.io()) 61 | .observeOn(AndroidSchedulers.mainThread()) 62 | .subscribe() 63 | } 64 | } 65 | 66 | fun isFavorited(): Boolean { 67 | return bottomPlayerViewStateLiveData.value?.isFavorited ?: false 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/main/MainPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.main 2 | 3 | import android.content.Context 4 | import androidx.fragment.app.Fragment 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.fragment.app.FragmentStatePagerAdapter 7 | import com.koducation.androidcourse101.R 8 | import com.koducation.androidcourse101.ui.favorites.FavoriteFragment 9 | import com.koducation.androidcourse101.ui.radios.RadiosFragment 10 | import java.lang.IllegalStateException 11 | 12 | class MainPagerAdapter(context: Context, fm: FragmentManager, behavior: Int) : 13 | FragmentStatePagerAdapter(fm, behavior) { 14 | 15 | private val tabLayoutTexts: Array = context.resources.getStringArray(R.array.tabs) 16 | 17 | override fun getItem(position: Int): Fragment { 18 | return when (position) { 19 | POSITION_RADIOS -> RadiosFragment() 20 | POSITION_FAVORITES -> FavoriteFragment() 21 | else -> throw IllegalStateException("Undefined position $position. Max count is $TAB_COUNT") 22 | } 23 | } 24 | 25 | override fun getCount(): Int = TAB_COUNT 26 | 27 | override fun getPageTitle(position: Int): CharSequence? = tabLayoutTexts[position].toUpperCase() 28 | 29 | companion object { 30 | 31 | private const val TAB_COUNT = 2 32 | 33 | private const val POSITION_RADIOS = 0 34 | private const val POSITION_FAVORITES = 1 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/main/di/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.main.di 2 | 3 | import com.koducation.androidcourse101.ui.main.MainActivity 4 | import dagger.Module 5 | import dagger.Provides 6 | 7 | @Module 8 | class MainActivityModule { 9 | 10 | @Provides 11 | fun provideActivityName(mainActivity: MainActivity): String { 12 | return mainActivity.javaClass.name 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/RadiosAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.koducation.androidcourse101.data.remote.model.Radio 7 | import com.koducation.androidcourse101.databinding.ItemRadioBinding 8 | import javax.inject.Inject 9 | 10 | class RadiosAdapter @Inject constructor(): RecyclerView.Adapter() { 11 | 12 | private val radiosList = arrayListOf() 13 | 14 | var radioItemClicked: ((Radio) -> Unit)? = null 15 | 16 | fun setRadioList(radiosList: List) { 17 | this.radiosList.clear() 18 | this.radiosList.addAll(radiosList) 19 | notifyDataSetChanged() 20 | } 21 | 22 | override fun getItemCount(): Int = radiosList.size 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadioItemViewHolder = 25 | RadioItemViewHolder.create(parent, radioItemClicked) 26 | 27 | override fun onBindViewHolder(holder: RadioItemViewHolder, position: Int) = 28 | holder.bind(radiosList[position]) 29 | 30 | class RadioItemViewHolder( 31 | private val binding: ItemRadioBinding, 32 | private val radioItemClicked: ((Radio) -> Unit)? 33 | ) : 34 | RecyclerView.ViewHolder(binding.root) { 35 | 36 | init { 37 | binding.root.setOnClickListener { 38 | radioItemClicked?.invoke(binding.viewState?.radio!!) 39 | } 40 | } 41 | 42 | fun bind(radioItem: Radio) { 43 | binding.viewState = RadiosItemViewState(radioItem) 44 | binding.executePendingBindings() 45 | } 46 | 47 | companion object { 48 | 49 | fun create( 50 | parent: ViewGroup, 51 | radioItemClicked: ((Radio) -> Unit)? 52 | ): RadioItemViewHolder { 53 | val binding = ItemRadioBinding.inflate(LayoutInflater.from(parent.context)) 54 | return RadioItemViewHolder(binding, radioItemClicked) 55 | } 56 | } 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/RadiosFragment.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.lifecycle.Observer 9 | import androidx.lifecycle.ViewModelProvider 10 | import androidx.lifecycle.ViewModelProviders 11 | import com.koducation.androidcourse101.R 12 | import com.koducation.androidcourse101.databinding.FragmentRadiosBinding 13 | import com.koducation.androidcourse101.ui.main.MainActivityViewModel 14 | import dagger.android.support.DaggerFragment 15 | import javax.inject.Inject 16 | import javax.inject.Named 17 | 18 | class RadiosFragment : DaggerFragment() { 19 | 20 | @Named("popularAdapter") 21 | @Inject 22 | lateinit var popularRadiosAdapter: RadiosAdapter 23 | 24 | @Named("locationAdapter") 25 | @Inject 26 | lateinit var locationRadiosAdapter: RadiosAdapter 27 | 28 | private lateinit var binding: FragmentRadiosBinding 29 | 30 | private lateinit var viewModel: RadiosFragmentViewModel 31 | 32 | private lateinit var sharedViewModel: MainActivityViewModel 33 | 34 | @Inject 35 | lateinit var viewModelFactory: ViewModelProvider.Factory 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | viewModel = 40 | ViewModelProviders.of(this, viewModelFactory).get(RadiosFragmentViewModel::class.java) 41 | } 42 | 43 | override fun onCreateView( 44 | inflater: LayoutInflater, 45 | container: ViewGroup?, 46 | savedInstanceState: Bundle? 47 | ): View? { 48 | binding = DataBindingUtil.inflate(inflater, R.layout.fragment_radios, container, false) 49 | 50 | binding.recyclerViewPopularRadios.adapter = popularRadiosAdapter 51 | binding.recyclerViewLocationRadios.adapter = locationRadiosAdapter 52 | 53 | binding.buttonReloadLocationRadios.setOnClickListener { viewModel.loadRadiosPage() } 54 | binding.buttonReloadPopularRadios.setOnClickListener { viewModel.loadRadiosPage() } 55 | 56 | return binding.root 57 | } 58 | 59 | override fun onActivityCreated(savedInstanceState: Bundle?) { 60 | super.onActivityCreated(savedInstanceState) 61 | 62 | sharedViewModel = ViewModelProviders.of(activity!!).get(MainActivityViewModel::class.java) 63 | 64 | viewModel.getRadiosLiveData().observe(this, Observer { 65 | popularRadiosAdapter.setRadioList(it.getPopularRadios()) 66 | locationRadiosAdapter.setRadioList(it.getLocationRadios()) 67 | 68 | binding.viewState = it 69 | binding.executePendingBindings() 70 | }) 71 | 72 | popularRadiosAdapter.radioItemClicked = { 73 | sharedViewModel.setCurrentPlayingRadio(it) 74 | } 75 | 76 | locationRadiosAdapter.radioItemClicked = { 77 | sharedViewModel.setCurrentPlayingRadio(it) 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/RadiosFragmentViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.koducation.androidcourse101.data.remote.RadioDataSource 8 | import io.reactivex.Observable 9 | import io.reactivex.android.schedulers.AndroidSchedulers 10 | import io.reactivex.schedulers.Schedulers 11 | import javax.inject.Inject 12 | 13 | class RadiosFragmentViewModel @Inject constructor(val radioDataSource: RadioDataSource) : 14 | ViewModel() { 15 | 16 | private val radiosLiveData: MutableLiveData = MutableLiveData() 17 | 18 | init { 19 | loadRadiosPage() 20 | } 21 | 22 | fun getRadiosLiveData(): LiveData = radiosLiveData 23 | 24 | @SuppressLint("CheckResult") 25 | fun loadRadiosPage() { 26 | Observable 27 | .combineLatest( 28 | radioDataSource.fetchPopularRadios(), 29 | radioDataSource.fetchLocationRadios(), 30 | RadiosPageCombiner() 31 | ) 32 | .subscribeOn(Schedulers.io()) 33 | .observeOn(AndroidSchedulers.mainThread()) 34 | .subscribe { radiosLiveData.value = it } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/RadiosFragmentViewState.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios 2 | 3 | import android.view.View 4 | import com.koducation.androidcourse101.data.Resource 5 | import com.koducation.androidcourse101.data.Status.* 6 | import com.koducation.androidcourse101.data.remote.model.Radio 7 | 8 | data class RadiosFragmentViewState( 9 | private val popularRadioResource: Resource>, 10 | private val locationRadioResource: Resource> 11 | ) { 12 | 13 | fun getPopularRadios() = popularRadioResource.data ?: arrayListOf() 14 | 15 | fun getLocationRadios() = locationRadioResource.data ?: arrayListOf() 16 | 17 | fun getPopularRadiosLoadingVisibility(): Int { 18 | return when (popularRadioResource.status) { 19 | LOADING -> View.VISIBLE 20 | else -> View.GONE 21 | } 22 | } 23 | 24 | fun getLocationRadiosLoadingVisibility(): Int { 25 | return when (locationRadioResource.status) { 26 | LOADING -> View.VISIBLE 27 | else -> View.GONE 28 | } 29 | } 30 | 31 | fun getPopularRadiosReloadButtonVisibility(): Int{ 32 | return when (popularRadioResource.status) { 33 | ERROR -> View.VISIBLE 34 | else -> View.GONE 35 | } 36 | } 37 | 38 | fun getLocationRadiosReloadButtonVisibility(): Int{ 39 | return when (locationRadioResource.status) { 40 | ERROR -> View.VISIBLE 41 | else -> View.GONE 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/RadiosItemViewState.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios 2 | 3 | import com.koducation.androidcourse101.data.remote.model.Radio 4 | 5 | data class RadiosItemViewState(val radio: Radio) { 6 | 7 | fun getRadioName(): String { 8 | return radio.radioName ?: "" 9 | } 10 | 11 | fun getRadioBand(): String { 12 | return radio.band ?: "" 13 | } 14 | 15 | fun getRadioImageUrl(): String { 16 | return radio.logo_big ?: "" 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/RadiosPageCombiner.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios 2 | 3 | import com.koducation.androidcourse101.data.Resource 4 | import com.koducation.androidcourse101.data.remote.model.Radio 5 | import io.reactivex.functions.BiFunction 6 | 7 | class RadiosPageCombiner : 8 | BiFunction>, Resource>, RadiosFragmentViewState> { 9 | 10 | override fun apply( 11 | popularRadios: Resource>, 12 | locationRadios: Resource> 13 | ): RadiosFragmentViewState { 14 | return RadiosFragmentViewState(popularRadios, locationRadios) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/radios/di/RadiosFragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.radios.di 2 | 3 | import com.koducation.androidcourse101.ui.radios.RadiosAdapter 4 | import com.koducation.androidcourse101.ui.radios.RadiosFragment 5 | import dagger.Module 6 | import dagger.Provides 7 | import javax.inject.Named 8 | 9 | @Module 10 | class RadiosFragmentModule { 11 | 12 | @Provides 13 | @Named("locationAdapter") 14 | fun provideLocationAdapter(): RadiosAdapter { 15 | return RadiosAdapter() 16 | } 17 | 18 | @Provides 19 | @Named("popularAdapter") 20 | fun providePopularAdapter(): RadiosAdapter { 21 | return RadiosAdapter() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/koducation/androidcourse101/ui/util/binding/ImageLoadingBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101.ui.util.binding 2 | 3 | import android.content.res.ColorStateList 4 | import android.graphics.drawable.Drawable 5 | import android.widget.ImageView 6 | import androidx.core.widget.ImageViewCompat 7 | import androidx.databinding.BindingAdapter 8 | import com.squareup.picasso.Picasso 9 | 10 | @BindingAdapter("url") 11 | fun loadImage(imageView: ImageView, url: String?) { 12 | if (url.isNullOrEmpty()) { 13 | return 14 | } 15 | Picasso.get().load(url).into(imageView) 16 | } 17 | 18 | @BindingAdapter("tintImage") 19 | fun tintImage(imageView: ImageView, color: Int) { 20 | ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(color)) 21 | } 22 | 23 | @BindingAdapter("vector") 24 | fun loadVector(imageView: ImageView, drawable: Drawable?) { 25 | imageView.setImageDrawable(drawable) 26 | } -------------------------------------------------------------------------------- /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/ic_favorite.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 23 | 24 | 29 | 30 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 52 | 53 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_favorites.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_radios.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 16 | 17 | 22 | 23 | 30 | 31 | 35 | 36 | 37 | 46 | 47 | 53 | 54 | 62 | 63 | 64 | 65 | 72 | 73 | 77 | 78 | 87 | 88 | 94 | 95 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /app/src/main/res/layout/include_bottom_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 21 | 22 | 23 | 30 | 31 | 32 | 41 | 42 | 49 | 50 | 58 | 59 | 60 | 61 | 62 | 73 | 74 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_favorite_radio.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 24 | 25 | 32 | 33 | 41 | 42 | 51 | 52 | 53 | 54 | 55 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_radio.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 21 | 22 | 27 | 28 | 39 | 40 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /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/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #333333 4 | #1E1E1E 5 | #1BB954 6 | 7 | #ffffff 8 | #FF544D 9 | 10 | #6D6D6D 11 | @color/colorRed 12 | 13 | #242424 14 | 15 | #242424 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12sp 5 | 14sp 6 | 16sp 7 | 18sp 8 | 9 | @dimen/textSizeMedium 10 | @dimen/textSizeRegular 11 | 12 | 4dp 13 | 16dp 14 | 15 | 16dp 16 | 8dp 17 | 18 | 64dp 19 | 20 | 64dp 21 | 48dp 22 | 24dp 23 | 24 | 108dp 25 | 26 | 60dp 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidCourse101 3 | 4 | 5 | Radios 6 | Favorites 7 | 8 | 9 | Popular Radios 10 | Location Radios 11 | 12 | Reload 13 | 14 | Buffering… 15 | Now Playing… 16 | Paused 17 | Error. 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/koducation/androidcourse101/PlayerViewStateTest.kt: -------------------------------------------------------------------------------- 1 | package com.koducation.androidcourse101 2 | 3 | import com.google.common.truth.Truth 4 | import com.koducation.androidcourse101.player.PlayerState 5 | import com.koducation.androidcourse101.player.PlayerViewState 6 | import org.junit.Test 7 | 8 | class PlayerViewStateTest { 9 | 10 | @Test 11 | fun `given player PLAYING state, when getPlayPauseIconClicable is called, then return true`() { 12 | 13 | // Given 14 | val playerState = PlayerState.PLAYING 15 | val givenViewState = PlayerViewState(playerState) 16 | 17 | // When 18 | val actualResult = givenViewState.getPlayPauseIconClicable() 19 | 20 | // Then 21 | Truth.assertThat(actualResult).isTrue() 22 | } 23 | 24 | @Test 25 | fun `given player PAUSED state, when getPlayPauseIconClicable is called, then return true`() { 26 | 27 | // Given 28 | val playerState = PlayerState.PAUSED 29 | val givenViewState = PlayerViewState(playerState) 30 | 31 | // When 32 | val actualResult = givenViewState.getPlayPauseIconClicable() 33 | 34 | // Then 35 | Truth.assertThat(actualResult).isTrue() 36 | } 37 | 38 | @Test 39 | fun `given player ERROR state, when getPlayPauseIconClicable is called, then return false`() { 40 | 41 | // Given 42 | val playerState = PlayerState.ERROR 43 | val givenViewState = PlayerViewState(playerState) 44 | 45 | // When 46 | val actualResult = givenViewState.getPlayPauseIconClicable() 47 | 48 | // Then 49 | Truth.assertThat(actualResult).isFalse() 50 | } 51 | 52 | @Test 53 | fun `given player BUFFERING state, when getPlayPauseIconClicable is called, then return false`() { 54 | 55 | // Given 56 | val playerState = PlayerState.BUFFERING 57 | val givenViewState = PlayerViewState(playerState) 58 | 59 | // When 60 | val actualResult = givenViewState.getPlayPauseIconClicable() 61 | 62 | // Then 63 | Truth.assertThat(actualResult).isFalse() 64 | } 65 | } -------------------------------------------------------------------------------- /art/course_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/art/course_cover.png -------------------------------------------------------------------------------- /art/fragment_ui_favorites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/art/fragment_ui_favorites.png -------------------------------------------------------------------------------- /art/fragment_ui_radios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/art/fragment_ui_radios.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.50' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.5.0' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Koducation/AndroidCourse101/1bc90a5b24ab969068fbbff276b7c46e8d53d400/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Oct 06 17:03:40 EET 2019 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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name='AndroidCourse101' 3 | --------------------------------------------------------------------------------