├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_splash.png │ │ │ │ ├── splash_background.xml │ │ │ │ ├── favorite_gradient.xml │ │ │ │ ├── favorite.xml │ │ │ │ └── favorite_border.xml │ │ │ ├── values │ │ │ │ ├── dimen.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── layout │ │ │ │ ├── activity_splash.xml │ │ │ │ ├── toolbar.xml │ │ │ │ ├── loading_list_item.xml │ │ │ │ ├── show_list_item.xml │ │ │ │ ├── activity_home.xml │ │ │ │ ├── activity_favorite_shows.xml │ │ │ │ ├── network_failure_list_item.xml │ │ │ │ ├── activity_all_shows.xml │ │ │ │ └── all_show_list_item.xml │ │ │ └── menu │ │ │ │ └── home_menu.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── android │ │ │ │ └── tvflix │ │ │ │ ├── analytics │ │ │ │ ├── Analytics.kt │ │ │ │ ├── Event.kt │ │ │ │ └── FirebaseAnalyticsHandler.kt │ │ │ │ ├── splash │ │ │ │ ├── SplashViewState.kt │ │ │ │ ├── SplashActivity.kt │ │ │ │ └── SplashViewModel.kt │ │ │ │ ├── di │ │ │ │ ├── DaggerTypes.kt │ │ │ │ ├── AppBindingModule.kt │ │ │ │ ├── CoroutinesQualifiers.kt │ │ │ │ ├── AppModule.kt │ │ │ │ └── CoroutinesDispatcherModule.kt │ │ │ │ ├── config │ │ │ │ ├── FeatureFlagAnnotations.kt │ │ │ │ ├── ConfigDefaults.kt │ │ │ │ ├── FeatureFlagModule.kt │ │ │ │ └── AppConfig.kt │ │ │ │ ├── network │ │ │ │ ├── RetrofitExt.kt │ │ │ │ ├── home │ │ │ │ │ ├── Episode.kt │ │ │ │ │ └── Show.kt │ │ │ │ ├── TvFlixApi.kt │ │ │ │ ├── TvFlixApiModule.kt │ │ │ │ ├── ApiInterceptor.kt │ │ │ │ ├── InterceptorModule.kt │ │ │ │ └── NetworkModule.kt │ │ │ │ ├── db │ │ │ │ ├── favouriteshow │ │ │ │ │ ├── FavoriteShow.kt │ │ │ │ │ └── ShowDao.kt │ │ │ │ ├── TvFlixDatabase.kt │ │ │ │ └── TvFlixDbModule.kt │ │ │ │ ├── home │ │ │ │ ├── HomeViewState.kt │ │ │ │ ├── HomeViewData.kt │ │ │ │ ├── ShowDiffUtilCallback.kt │ │ │ │ ├── ShowsAdapter.kt │ │ │ │ ├── HomeActivity.kt │ │ │ │ └── HomeViewModel.kt │ │ │ │ ├── model │ │ │ │ └── respositores │ │ │ │ │ └── SchedulesRepository.kt │ │ │ │ ├── utils │ │ │ │ ├── Extensions.kt │ │ │ │ └── GridItemDecoration.kt │ │ │ │ ├── shows │ │ │ │ ├── ShowDiffUtilItemCallback.kt │ │ │ │ ├── ShowsLoadStateAdapter.kt │ │ │ │ ├── ShowsViewModel.kt │ │ │ │ ├── ShowsRepository.kt │ │ │ │ ├── ShowsStateViewHolder.kt │ │ │ │ ├── ShowsPagingSource.kt │ │ │ │ ├── ShowsPagedAdapter.kt │ │ │ │ └── AllShowsActivity.kt │ │ │ │ ├── favorite │ │ │ │ ├── FavoriteShowState.kt │ │ │ │ ├── FavoriteShowsAdapter.kt │ │ │ │ ├── FavoriteShowsRepository.kt │ │ │ │ ├── FavoriteShowsViewModel.kt │ │ │ │ └── FavoriteShowsActivity.kt │ │ │ │ ├── TvFlixApplication.kt │ │ │ │ └── domain │ │ │ │ ├── AddToFavoritesUseCase.kt │ │ │ │ ├── RemoveFromFavoritesUseCase.kt │ │ │ │ ├── GetFavoriteShowsUseCase.kt │ │ │ │ ├── GetSchedulesUseCase.kt │ │ │ │ └── SuspendUseCase.kt │ │ └── AndroidManifest.xml │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── android │ │ │ └── tvflix │ │ │ ├── utils │ │ │ ├── MatcherUtils.kt │ │ │ └── ViewActionUtils.kt │ │ │ ├── shows │ │ │ ├── AllShowsRobot.kt │ │ │ └── AllShowsTest.kt │ │ │ ├── idlingresource │ │ │ └── LoadingIdlingResource.kt │ │ │ └── home │ │ │ ├── HomeTest.kt │ │ │ └── HomeRobot.kt │ └── test │ │ └── java │ │ └── com │ │ └── android │ │ └── tvflix │ │ ├── utils │ │ ├── MainCoroutineRule.kt │ │ └── TestUtil.kt │ │ ├── favorites │ │ └── FavoritesRepositoryTest.kt │ │ └── home │ │ └── HomeViewModelTest.kt ├── google-services.json ├── proguard-rules.pro └── build.gradle.kts ├── _config.yml ├── settings.gradle.kts ├── release-notes.txt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── workflows │ └── android.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tools ├── pmd.gradle ├── checkstyle.gradle ├── rules-findbugs.xml ├── rules-pmd.xml └── rules-checkstyle.xml ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── gradle.properties ├── gradlew.bat ├── CODE_OF_CONDUCT.md ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") 2 | rootProject.name = "TvFlix" 3 | -------------------------------------------------------------------------------- /release-notes.txt: -------------------------------------------------------------------------------- 1 | v2.2.2 2 | Refactoring to move towards clean code architecture 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactivedroid/TvFlix/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactivedroid/TvFlix/HEAD/app/src/main/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactivedroid/TvFlix/HEAD/app/src/main/res/drawable/ic_splash.png -------------------------------------------------------------------------------- /app/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/analytics/Analytics.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.analytics 2 | 3 | interface Analytics { 4 | fun sendEvent(event: Event) 5 | fun setUserId(userId: String) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/splash/SplashViewState.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.splash 2 | 3 | sealed class SplashViewState { 4 | object Idle : SplashViewState() 5 | object NavigateToHome : SplashViewState() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/di/DaggerTypes.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.di 2 | 3 | /** 4 | * Sugar over multibindings that helps with Kotlin wildcards. 5 | */ 6 | typealias DaggerSet = @JvmSuppressWildcards Set 7 | typealias DaggerMap = @JvmSuppressWildcards Map -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 08 19:26:01 IST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/config/FeatureFlagAnnotations.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.config 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @Target( 7 | AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, 8 | AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD 9 | ) 10 | annotation class FavoritesFeatureFlag -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/config/ConfigDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.config 2 | 3 | object ConfigDefaults { 4 | fun getDefaultValues(): Map { 5 | return hashMapOf(ConfigKeys.FEATURE_FAVORITES_ENABLE to false) 6 | } 7 | 8 | object ConfigKeys { 9 | const val FEATURE_FAVORITES_ENABLE = "feature_favorites_enable" 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/RetrofitExt.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network 2 | 3 | import okhttp3.Call 4 | import okhttp3.Request 5 | import retrofit2.Retrofit 6 | 7 | @PublishedApi 8 | internal inline fun Retrofit.Builder.callFactory( 9 | crossinline body: (Request) -> Call 10 | ) = callFactory(object : Call.Factory { 11 | override fun newCall(request: Request): Call = body(request) 12 | }) -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: set up JDK 1.11 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.11 16 | - name: Build with Gradle 17 | run: ./gradlew assembleDebug 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/favorite_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/analytics/Event.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.analytics 2 | 3 | data class Event(val eventName: String, val eventParams: Map) 4 | 5 | object EventParams { 6 | const val CONTENT_ID = "content_id" 7 | const val CONTENT_TITLE = "content_title" 8 | const val IS_FAVORITE_MARKED = "is_favorite_marked" 9 | const val CLICK_CONTEXT = "CLICK_CONTEXT" 10 | } 11 | 12 | object EventNames { 13 | const val CLICK = "click" 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/db/favouriteshow/FavoriteShow.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.db.favouriteshow 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "favourite_shows") 7 | data class FavoriteShow( 8 | @PrimaryKey 9 | val id: Long, 10 | val name: String, 11 | val premiered: String?, 12 | val imageUrl: String?, 13 | var summary: String?, 14 | var rating: String?, 15 | var runtime: Int? 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/home/HomeViewState.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.home 2 | 3 | import com.android.tvflix.network.home.Show 4 | 5 | sealed class HomeViewState { 6 | data class NetworkError(val message: String?) : HomeViewState() 7 | object Loading : HomeViewState() 8 | data class Success(val homeViewData: HomeViewData) : HomeViewState() 9 | data class AddedToFavorites(val show: Show) : HomeViewState() 10 | data class RemovedFromFavorites(val show: Show) : HomeViewState() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/model/respositores/SchedulesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.model.respositores 2 | 3 | import com.android.tvflix.network.TvFlixApi 4 | import com.android.tvflix.network.home.Episode 5 | import javax.inject.Inject 6 | 7 | class SchedulesRepository 8 | @Inject 9 | constructor(private val tvFlixApi: TvFlixApi) { 10 | suspend fun getSchedule(country: String, currentDate: String): List { 11 | return tvFlixApi.getCurrentSchedule(country, currentDate) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.utils 2 | 3 | import com.android.tvflix.db.favouriteshow.FavoriteShow 4 | import com.android.tvflix.network.home.Show 5 | 6 | fun Show.toFavoriteShow(): FavoriteShow { 7 | return FavoriteShow( 8 | id = id, 9 | name = name, 10 | premiered = premiered, 11 | imageUrl = image!!["original"], 12 | summary = summary, 13 | rating = rating!!["average"], 14 | runtime = runtime!! 15 | ) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/config/FeatureFlagModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.config 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | 8 | @InstallIn(SingletonComponent::class) 9 | @Module 10 | object FeatureFlagModule { 11 | @FavoritesFeatureFlag 12 | @Provides 13 | fun provideFavoritesFeatureFlag(appConfig: AppConfig): Boolean { 14 | return appConfig.getBoolean(ConfigDefaults.ConfigKeys.FEATURE_FAVORITES_ENABLE) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/home/Episode.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network.home 2 | 3 | import android.os.Parcelable 4 | import com.squareup.moshi.JsonClass 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | @JsonClass(generateAdapter = true) 9 | data class Episode( 10 | val show: Show, 11 | val id: Long, 12 | val url: String?, 13 | val name: String?, 14 | val season: Int?, 15 | val number: Int?, 16 | val airdate: String?, 17 | val airtime: String?, 18 | val runtime: Int? 19 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/di/AppBindingModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.di 2 | 3 | import com.android.tvflix.analytics.Analytics 4 | import com.android.tvflix.analytics.FirebaseAnalyticsHandler 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @InstallIn(SingletonComponent::class) 11 | @Module 12 | abstract class AppBindingModule { 13 | @Binds 14 | abstract fun bindFirebaseAnalytics(firebaseAnalyticsHandler: FirebaseAnalyticsHandler): Analytics 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowDiffUtilItemCallback.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.android.tvflix.network.home.Show 5 | 6 | class ShowDiffUtilItemCallback : DiffUtil.ItemCallback() { 7 | override fun areItemsTheSame(oldItem: Show, newItem: Show): Boolean { 8 | return oldItem.id == newItem.id 9 | } 10 | 11 | override fun areContentsTheSame(oldItem: Show, newItem: Show): Boolean { 12 | return oldItem.name == newItem.name 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/db/TvFlixDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.android.tvflix.db.favouriteshow.FavoriteShow 6 | import com.android.tvflix.db.favouriteshow.ShowDao 7 | 8 | @Database(entities = [FavoriteShow::class], version = 3, exportSchema = false) 9 | abstract class TvFlixDatabase : RoomDatabase() { 10 | 11 | abstract fun showDao(): ShowDao 12 | 13 | companion object { 14 | const val DATABASE_NAME = "tvflix.db" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/di/CoroutinesQualifiers.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Retention(AnnotationRetention.BINARY) 6 | @Qualifier 7 | internal annotation class DefaultDispatcher 8 | 9 | @Retention(AnnotationRetention.BINARY) 10 | @Qualifier 11 | internal annotation class IoDispatcher 12 | 13 | @Retention(AnnotationRetention.BINARY) 14 | @Qualifier 15 | internal annotation class MainDispatcher 16 | 17 | @Retention(AnnotationRetention.BINARY) 18 | @Qualifier 19 | internal annotation class MainImmediateDispatcher 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/TvFlixApi.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network 2 | 3 | import com.android.tvflix.network.home.Episode 4 | import com.android.tvflix.network.home.Show 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface TvFlixApi { 9 | @GET("/schedule") 10 | suspend fun getCurrentSchedule( 11 | @Query("country") country: String, 12 | @Query("date") date: String 13 | ): List 14 | 15 | @GET("/shows") 16 | suspend fun getShows(@Query("page") pageNumber: Int): List 17 | } 18 | -------------------------------------------------------------------------------- /tools/pmd.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'pmd' 2 | 3 | task pmd(type: Pmd) { 4 | description 'Identifying potential problems mainly dead code, duplicated code, cyclomatic complexity and overcomplicated expressions' 5 | group 'verification' 6 | ruleSetFiles = files("$project.rootDir/tools/rules-pmd.xml") 7 | source = fileTree('src/main/java') 8 | include '**/*.java' 9 | exclude '**/gen/**' 10 | 11 | reports { 12 | xml.enabled = false 13 | html.enabled = true 14 | html.destination file("$project.buildDir/outputs/pmd/pmd.html") 15 | } 16 | } -------------------------------------------------------------------------------- /tools/checkstyle.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'checkstyle' 2 | 3 | task checkstyle(type: Checkstyle) { 4 | description 'Check code standard' 5 | group 'verification' 6 | configFile file("${project.rootDir}/tools/rules-checkstyle.xml") 7 | source fileTree('src/main/java') 8 | include '**/*.java' 9 | exclude '**/gen/**' 10 | 11 | classpath = files() 12 | showViolations true 13 | 14 | reports { 15 | xml.enabled = true 16 | html.enabled = true 17 | html.destination file("$project.buildDir/outputs/checkstyle/checkstyle.html") 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/TvFlixApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import retrofit2.Retrofit 8 | import javax.inject.Singleton 9 | 10 | @InstallIn(SingletonComponent::class) 11 | @Module 12 | object TvFlixApiModule { 13 | @Provides 14 | @Singleton 15 | fun provideTvFlixApi( 16 | retrofit: Retrofit 17 | ): TvFlixApi { 18 | return retrofit.create(TvFlixApi::class.java) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @InstallIn(SingletonComponent::class) 12 | @Module(includes = [AppBindingModule::class]) 13 | object AppModule { 14 | @Provides 15 | @Singleton 16 | fun provideContext(application: Application): Context { 17 | return application 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/menu/home_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/favorite.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loading_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/favorite/FavoriteShowState.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.favorite 2 | 3 | import com.android.tvflix.db.favouriteshow.FavoriteShow 4 | 5 | sealed class FavoriteShowState { 6 | data class AllFavorites(val favoriteShows: List) : FavoriteShowState() 7 | data class Error(val message: String) : FavoriteShowState() 8 | object Loading : FavoriteShowState() 9 | object Empty : FavoriteShowState() 10 | data class AddedToFavorites(val show: FavoriteShow) : FavoriteShowState() 11 | data class RemovedFromFavorites(val show: FavoriteShow) : FavoriteShowState() 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ff393939 4 | #1e1e1e 5 | #DCC213 6 | #FFFFFF 7 | #ff303030 8 | #ff212121 9 | #387eed 10 | #cacdd1 11 | #1f000000 12 | #212121 13 | #E82C0C 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowsLoadStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import android.view.ViewGroup 4 | import androidx.paging.LoadState 5 | import androidx.paging.LoadStateAdapter 6 | 7 | class ShowsLoadStateAdapter(private val retry: () -> Unit) : 8 | LoadStateAdapter() { 9 | override fun onBindViewHolder(holder: ShowsStateViewHolder, loadState: LoadState) { 10 | holder.bind(loadState) 11 | } 12 | 13 | override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ShowsStateViewHolder { 14 | return ShowsStateViewHolder.create(parent, retry) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/db/favouriteshow/ShowDao.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.db.favouriteshow 2 | 3 | import androidx.room.* 4 | 5 | @Dao 6 | interface ShowDao { 7 | @Query("SELECT * FROM favourite_shows") 8 | suspend fun allFavouriteShows(): List 9 | 10 | @Insert(onConflict = OnConflictStrategy.REPLACE) 11 | suspend fun insert(favoriteShow: FavoriteShow) 12 | 13 | @Delete 14 | suspend fun remove(favoriteShow: FavoriteShow) 15 | 16 | @Query("SELECT id from favourite_shows") 17 | suspend fun getFavoriteShowIds(): List 18 | 19 | @Query("DELETE FROM favourite_shows") 20 | suspend fun clear() 21 | } 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to **TvMaze** 2 | 3 | * *Issues* - If you have a question, or you think you've discovered an issue, then find/file an issue BEFORE starting work to fix/implement it. 4 | * *Enhancement* - If you want to contribute to the repository, go ahead, any kind of patches are encouraged, and may be submitted by forking this project and submitting a pull request. If you have something big in mind, or any architectural change, please raise an issue first to discuss it. 5 | 6 | 👍🎉🚀Please note that any piece of contribution will be cherished and TvMaze will love to have it. At the end, it is all about helping the Android Community to thrive for better development👍🎉🚀 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/home/HomeViewData.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.home 2 | 3 | import com.android.tvflix.network.home.Show 4 | 5 | data class HomeViewData(val heading: String, val episodes: List) { 6 | data class EpisodeViewData( 7 | val id: Long, 8 | val showViewData: ShowViewData, 9 | val url: String?, 10 | val name: String?, 11 | val season: Int?, 12 | val number: Int?, 13 | val airdate: String?, 14 | val airtime: String?, 15 | val runtime: Int? 16 | ) 17 | 18 | data class ShowViewData( 19 | val show: Show, 20 | val isFavoriteShow: Boolean 21 | ) 22 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/TvFlixApplication.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix 2 | 3 | import android.app.Application 4 | import com.android.tvflix.config.AppConfig 5 | import com.google.firebase.FirebaseApp 6 | import dagger.hilt.android.HiltAndroidApp 7 | import timber.log.Timber 8 | import javax.inject.Inject 9 | 10 | @HiltAndroidApp 11 | class TvFlixApplication : Application() { 12 | @Inject 13 | lateinit var appConfig: AppConfig 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | FirebaseApp.initializeApp(this) 18 | if (BuildConfig.DEBUG) { 19 | Timber.plant(Timber.DebugTree()) 20 | } 21 | appConfig.initialise() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import androidx.paging.PagingData 6 | import androidx.paging.cachedIn 7 | import com.android.tvflix.network.home.Show 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.Flow 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class ShowsViewModel @Inject 14 | constructor( 15 | private val showsRepository: ShowsRepository 16 | ) : ViewModel() { 17 | fun shows(): Flow> { 18 | return showsRepository.getShows() 19 | .cachedIn(viewModelScope) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/domain/AddToFavoritesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.domain 2 | 3 | import com.android.tvflix.db.favouriteshow.FavoriteShow 4 | import com.android.tvflix.di.IoDispatcher 5 | import com.android.tvflix.favorite.FavoriteShowsRepository 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import javax.inject.Inject 8 | 9 | class AddToFavoritesUseCase @Inject 10 | constructor( 11 | private val favoriteShowsRepository: FavoriteShowsRepository, 12 | @IoDispatcher ioDispatcher: CoroutineDispatcher 13 | ) : SuspendUseCase(ioDispatcher) { 14 | override suspend fun execute(parameters: FavoriteShow) { 15 | favoriteShowsRepository.insertIntoFavorites(parameters) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/favorite_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/utils/MatcherUtils.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.utils 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import org.hamcrest.Description 6 | import org.hamcrest.Matcher 7 | import org.hamcrest.TypeSafeMatcher 8 | 9 | object MatcherUtils { 10 | fun withListSize(size: Int): Matcher { 11 | return object : TypeSafeMatcher() { 12 | override fun matchesSafely(view: View): Boolean { 13 | return (view as RecyclerView).adapter!!.itemCount >= size 14 | } 15 | 16 | override fun describeTo(description: Description) { 17 | description.appendText("RecyclerView should have $size items") 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # User-specific configurations 35 | .idea/ 36 | *.iml 37 | 38 | # OS-specific files 39 | .DS_Store 40 | .DS_Store? 41 | ._* 42 | .Spotlight-V100 43 | .Trashes 44 | ehthumbs.db 45 | Thumbs.db 46 | 47 | *.hprof 48 | tvflix-*.json 49 | tester-groups.txt -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/domain/RemoveFromFavoritesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.domain 2 | 3 | import com.android.tvflix.db.favouriteshow.FavoriteShow 4 | import com.android.tvflix.di.IoDispatcher 5 | import com.android.tvflix.favorite.FavoriteShowsRepository 6 | import com.android.tvflix.network.home.Show 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import javax.inject.Inject 9 | 10 | class RemoveFromFavoritesUseCase 11 | @Inject 12 | constructor( 13 | private val favoriteShowsRepository: FavoriteShowsRepository, 14 | @IoDispatcher ioDispatcher: CoroutineDispatcher 15 | ) : SuspendUseCase(ioDispatcher) { 16 | override suspend fun execute(parameters: FavoriteShow) { 17 | favoriteShowsRepository.removeFromFavorites(parameters) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/shows/AllShowsRobot.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.assertion.ViewAssertions.matches 5 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 6 | import androidx.test.espresso.matcher.ViewMatchers.withId 7 | import androidx.test.espresso.matcher.ViewMatchers.withText 8 | import com.android.tvflix.R 9 | import com.android.tvflix.utils.MatcherUtils 10 | 11 | fun launchAllShows(func: AllShowsRobot.() -> Unit) = AllShowsRobot().apply { func() } 12 | class AllShowsRobot { 13 | fun verifyAllShows() { 14 | onView(withText(R.string.shows)).check(matches(isDisplayed())) 15 | onView(withId(R.id.shows)) 16 | .check(matches(MatcherUtils.withListSize(1))) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import com.android.tvflix.network.home.Show 7 | import kotlinx.coroutines.flow.Flow 8 | import javax.inject.Inject 9 | 10 | class ShowsRepository 11 | @Inject 12 | constructor(private val showsPagingSource: ShowsPagingSource) { 13 | fun getShows(): Flow> { 14 | return Pager( 15 | config = PagingConfig( 16 | pageSize = SHOWS_PAGE_SIZE, 17 | enablePlaceholders = true 18 | ), 19 | pagingSourceFactory = { showsPagingSource } 20 | ).flow 21 | } 22 | 23 | companion object { 24 | const val SHOWS_PAGE_SIZE = 20 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/domain/GetFavoriteShowsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.domain 2 | 3 | import com.android.tvflix.db.favouriteshow.FavoriteShow 4 | import com.android.tvflix.di.IoDispatcher 5 | import com.android.tvflix.favorite.FavoriteShowsRepository 6 | import com.android.tvflix.home.HomeViewModel 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import java.text.SimpleDateFormat 9 | import java.util.* 10 | import javax.inject.Inject 11 | 12 | class GetFavoriteShowsUseCase 13 | @Inject 14 | constructor( 15 | private val favoriteShowsRepository: FavoriteShowsRepository, 16 | @IoDispatcher ioDispatcher: CoroutineDispatcher 17 | ) : SuspendUseCase>(ioDispatcher) { 18 | override suspend fun execute(parameters: Unit): List { 19 | return favoriteShowsRepository.allFavoriteShowIds() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/utils/ViewActionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.utils 2 | 3 | import android.view.View 4 | import androidx.test.espresso.UiController 5 | import androidx.test.espresso.ViewAction 6 | import org.hamcrest.Matcher 7 | 8 | object ViewActionUtils { 9 | fun clickChildViewWithId(id: Int): ViewAction { 10 | return object : ViewAction { 11 | override fun getConstraints(): Matcher? { 12 | return null 13 | } 14 | 15 | override fun getDescription(): String { 16 | return "Click on a child view with id $id" 17 | } 18 | 19 | override fun perform(uiController: UiController, view: View) { 20 | val v = view.findViewById(id) 21 | v.performClick() 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/ApiInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | import java.io.IOException 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class ApiInterceptor @Inject constructor() : Interceptor { 11 | 12 | @Throws(IOException::class) 13 | override fun intercept(chain: Interceptor.Chain): Response { 14 | val original = chain.request() 15 | val originalHttpUrl = original.url 16 | 17 | val url = originalHttpUrl.newBuilder() 18 | .addQueryParameter("api_key", "") 19 | .build() 20 | 21 | val requestBuilder = original.newBuilder() 22 | .url(url) 23 | 24 | val request = requestBuilder.build() 25 | return chain.proceed(request) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TvFlix 3 | Popular shows in %s tonight 4 | Schedule for %s 5 | Shows 6 | Retry 7 | Rating: %s 8 | Premiered on: %s 9 | Not Rated 10 | \"%s\" removed from favorites 11 | \"%s\" added to favorites 12 | Favorite Shows 13 | Click on the   icon \nto mark the show as favorite 14 | Show image 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/InterceptorModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network 2 | 3 | import com.chuckerteam.chucker.api.ChuckerInterceptor 4 | import dagger.Binds 5 | import dagger.Module 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import dagger.multibindings.IntoSet 9 | import okhttp3.Interceptor 10 | import okhttp3.logging.HttpLoggingInterceptor 11 | 12 | @InstallIn(SingletonComponent::class) 13 | @Module 14 | abstract class InterceptorModule { 15 | @Binds 16 | @IntoSet 17 | abstract fun bindApiInterceptor(apiInterceptor: ApiInterceptor): Interceptor 18 | 19 | @Binds 20 | @IntoSet 21 | abstract fun bindHttpLoggingInterceptor(httpLoggingInterceptor: HttpLoggingInterceptor): Interceptor 22 | 23 | @Binds 24 | @IntoSet 25 | abstract fun bindChuckerInterceptor(chuckerInterceptor: ChuckerInterceptor): Interceptor 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/di/CoroutinesDispatcherModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | 10 | @InstallIn(SingletonComponent::class) 11 | @Module 12 | internal object CoroutinesDispatcherModule { 13 | @DefaultDispatcher 14 | @Provides 15 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 16 | 17 | @IoDispatcher 18 | @Provides 19 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 20 | 21 | @MainDispatcher 22 | @Provides 23 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main 24 | 25 | @MainImmediateDispatcher 26 | @Provides 27 | fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate 28 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/utils/GridItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.utils 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class GridItemDecoration(private val space: Int, private val noOfColumns: Int) : 8 | RecyclerView.ItemDecoration() { 9 | 10 | override fun getItemOffsets( 11 | outRect: Rect, view: View, 12 | parent: RecyclerView, 13 | state: RecyclerView.State 14 | ) { 15 | outRect.left = space 16 | outRect.right = space 17 | outRect.bottom = space 18 | outRect.top = space 19 | when { 20 | parent.getChildLayoutPosition(view) % noOfColumns == 0 -> { 21 | outRect.left = 0 22 | outRect.right = space 23 | } 24 | parent.getChildLayoutPosition(view) % noOfColumns == noOfColumns - 1 -> { 25 | outRect.left = space 26 | outRect.right = 0 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/db/TvFlixDbModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.db 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.android.tvflix.db.favouriteshow.ShowDao 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | import javax.inject.Singleton 13 | 14 | @InstallIn(SingletonComponent::class) 15 | @Module 16 | object TvFlixDbModule { 17 | @Singleton 18 | @Provides 19 | fun provideTvFlixDb(@ApplicationContext context: Context): TvFlixDatabase { 20 | return Room.databaseBuilder( 21 | context, 22 | TvFlixDatabase::class.java, TvFlixDatabase.DATABASE_NAME 23 | ) 24 | .fallbackToDestructiveMigration() 25 | .allowMainThreadQueries() 26 | .build() 27 | } 28 | 29 | @Singleton 30 | @Provides 31 | fun provideShowDao(tvFlixDatabase: TvFlixDatabase): ShowDao { 32 | return tvFlixDatabase.showDao() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ashwini Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "352317035381", 4 | "project_id": "tvflix-b45cd", 5 | "storage_bucket": "tvflix-b45cd.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:352317035381:android:d68e4d8178aaa27e62793c", 11 | "android_client_info": { 12 | "package_name": "com.android.tvflix" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "352317035381-gknu987p38ks98itjcds99pehar6539u.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyDomD9NnOHReomJXzki3akzw9M_JZ--DyU" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "352317035381-gknu987p38ks98itjcds99pehar6539u.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.splash 2 | 3 | import android.os.Bundle 4 | import androidx.activity.viewModels 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.lifecycleScope 7 | import com.android.tvflix.R 8 | import com.android.tvflix.home.HomeActivity 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import kotlinx.coroutines.flow.collect 11 | 12 | @AndroidEntryPoint 13 | class SplashActivity : AppCompatActivity() { 14 | private val splashViewModel: SplashViewModel by viewModels() 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.activity_splash) 18 | splashViewModel.fetchConfig() 19 | lifecycleScope.launchWhenStarted { 20 | splashViewModel.splashViewStateFlow.collect { 21 | when (it) { 22 | is SplashViewState.Idle -> { 23 | // do nothing 24 | } 25 | is SplashViewState.NavigateToHome -> HomeActivity.start(this@SplashActivity) 26 | } 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /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=-Xmx4096m -XX\:MaxPermSize\=4096m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8 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 | # Google Play and Firebase dependencies are migrating to androidX. This is to fix the duplicate class resolution build error to use the androidX libraries 15 | android.useAndroidX=true 16 | # All kapt to use workers 17 | kapt.use.worker.api=true 18 | kapt.incremental.apt=true 19 | android.databinding.incremental=true 20 | kapt.include.compile.classpath=false 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/splash/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.splash 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.android.tvflix.config.AppConfig 6 | import com.android.tvflix.di.IoDispatcher 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.CoroutineDispatcher 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class SplashViewModel 16 | @Inject 17 | constructor( 18 | private val appConfig: AppConfig, 19 | @IoDispatcher private val dispatcher: CoroutineDispatcher 20 | ) : ViewModel() { 21 | private val _splashViewStateFlow = MutableStateFlow(SplashViewState.Idle) 22 | 23 | // Represents _splashViewStateFlow mutable state flow as a read-only state flow. 24 | val splashViewStateFlow = _splashViewStateFlow.asStateFlow() 25 | fun fetchConfig() { 26 | viewModelScope.launch(dispatcher) { 27 | appConfig.fetch() 28 | _splashViewStateFlow.emit(SplashViewState.NavigateToHome) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/analytics/FirebaseAnalyticsHandler.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.analytics 2 | 3 | import com.google.firebase.analytics.ktx.analytics 4 | import com.google.firebase.analytics.ktx.logEvent 5 | import com.google.firebase.ktx.Firebase 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class FirebaseAnalyticsHandler 11 | @Inject 12 | constructor() : Analytics { 13 | private val firebaseAnalytics by lazy { Firebase.analytics } 14 | override fun sendEvent(event: Event) { 15 | firebaseAnalytics.logEvent(event.eventName) { 16 | param(EventParams.CONTENT_ID, event.eventParams[EventParams.CONTENT_ID] as Long) 17 | param(EventParams.CONTENT_TITLE, event.eventParams[EventParams.CONTENT_TITLE] as String) 18 | param( 19 | EventParams.IS_FAVORITE_MARKED, 20 | event.eventParams[EventParams.IS_FAVORITE_MARKED].toString() 21 | ) 22 | param( 23 | EventParams.CLICK_CONTEXT, 24 | event.eventParams[EventParams.CLICK_CONTEXT] as String 25 | ) 26 | } 27 | } 28 | 29 | override fun setUserId(userId: String) { 30 | firebaseAnalytics.setUserId(userId) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/idlingresource/LoadingIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.idlingresource 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.widget.ProgressBar 6 | import androidx.test.espresso.IdlingResource 7 | import com.android.tvflix.R 8 | 9 | /** 10 | * An idling resource which checks which tells Espresso to wait until Progress Bar dismisses 11 | */ 12 | class LoadingIdlingResource constructor(private val activity: Activity) : IdlingResource { 13 | private var resourceCallback: IdlingResource.ResourceCallback? = null 14 | override fun getName(): String { 15 | return "LoadingIdlingResource" 16 | } 17 | 18 | override fun isIdleNow(): Boolean { 19 | return if (!isProgressShowing()) { 20 | resourceCallback?.onTransitionToIdle() 21 | true 22 | } else { 23 | false 24 | } 25 | } 26 | 27 | override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { 28 | resourceCallback = callback 29 | } 30 | 31 | private fun isProgressShowing(): Boolean { 32 | val progress = activity.findViewById(R.id.progress) 33 | return progress.visibility == View.VISIBLE 34 | } 35 | } -------------------------------------------------------------------------------- /tools/rules-findbugs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/domain/GetSchedulesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.domain 2 | 3 | import com.android.tvflix.di.IoDispatcher 4 | import com.android.tvflix.model.respositores.SchedulesRepository 5 | import com.android.tvflix.network.home.Episode 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import java.text.SimpleDateFormat 8 | import java.util.* 9 | import javax.inject.Inject 10 | 11 | 12 | class GetSchedulesUseCase 13 | @Inject 14 | constructor( 15 | private val schedulesRepository: SchedulesRepository, 16 | @IoDispatcher ioDispatcher: CoroutineDispatcher 17 | ) : 18 | SuspendUseCase>(ioDispatcher) { 19 | override suspend fun execute(parameters: Unit): List { 20 | return schedulesRepository.getSchedule(country = COUNTRY, currentDate = currentDate) 21 | } 22 | 23 | companion object { 24 | private const val QUERY_DATE_FORMAT = "yyyy-MM-dd" 25 | const val COUNTRY = "US" 26 | private val currentDate: String 27 | get() { 28 | val simpleDateFormat = SimpleDateFormat(QUERY_DATE_FORMAT, Locale.US) 29 | val calendar = Calendar.getInstance() 30 | return simpleDateFormat.format(calendar.time) 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/domain/SuspendUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.domain 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.withContext 5 | 6 | /** 7 | * Executes business logic synchronously or asynchronously using Coroutines. 8 | * 9 | * The [execute] method of [SuspendUseCase] is a suspend function 10 | */ 11 | abstract class SuspendUseCase(private val coroutineDispatcher: CoroutineDispatcher) { 12 | 13 | /** Executes the use case asynchronously and returns a [Result]. 14 | * 15 | * @return a [Result]. 16 | * 17 | * @param parameters the input parameters to run the use case with 18 | */ 19 | val tag: String = this.javaClass.simpleName 20 | 21 | suspend operator fun invoke(parameters: P): R { 22 | // Moving all use case's executions to the injected dispatcher 23 | // In production code, this is usually the Default dispatcher (background thread) 24 | // In tests, this becomes a TestCoroutineDispatcher 25 | return withContext(coroutineDispatcher) { 26 | execute(parameters) 27 | } 28 | } 29 | 30 | /** 31 | * Override this to set the code to be executed. 32 | */ 33 | @Throws(RuntimeException::class) 34 | protected abstract suspend fun execute(parameters: P): R 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowsStateViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.core.view.isVisible 6 | import androidx.paging.LoadState 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.android.tvflix.databinding.NetworkFailureListItemBinding 9 | 10 | class ShowsStateViewHolder( 11 | private val binding: NetworkFailureListItemBinding, 12 | retry: () -> Unit 13 | ) : RecyclerView.ViewHolder(binding.root) { 14 | init { 15 | binding.retry.setOnClickListener { retry.invoke() } 16 | } 17 | 18 | fun bind(loadState: LoadState) { 19 | if (loadState is LoadState.Error) { 20 | binding.errorMsg.text = loadState.error.localizedMessage 21 | } 22 | binding.progress.isVisible = loadState is LoadState.Loading 23 | binding.errorGroup.isVisible = loadState !is LoadState.Loading 24 | } 25 | 26 | companion object { 27 | fun create(parent: ViewGroup, retry: () -> Unit): ShowsStateViewHolder { 28 | val layoutInflater = LayoutInflater.from(parent.context) 29 | val binding = NetworkFailureListItemBinding.inflate(layoutInflater, parent, false) 30 | return ShowsStateViewHolder(binding, retry) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /tools/rules-pmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Custom ruleset for TvMaze Android application 8 | 9 | .*/R.java 10 | .*/gen/.* 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/show_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 22 | 23 | 29 | 30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/shows/AllShowsTest.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import androidx.test.espresso.IdlingRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import androidx.test.filters.LargeTest 6 | import androidx.test.rule.ActivityTestRule 7 | import com.android.tvflix.idlingresource.LoadingIdlingResource 8 | import org.junit.After 9 | import org.junit.Before 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | 14 | @LargeTest 15 | @RunWith(AndroidJUnit4::class) 16 | class AllShowsTest { 17 | // @get:Rule 18 | // val homeActivityTestRule = ActivityTestRule(HomeActivity::class.java) 19 | @get:Rule 20 | val allShowsActivityTestRule = ActivityTestRule(AllShowsActivity::class.java) 21 | 22 | private lateinit var loadingIdlingResource: LoadingIdlingResource 23 | @Before 24 | fun setUp() { 25 | loadingIdlingResource = 26 | LoadingIdlingResource(allShowsActivityTestRule.activity) 27 | IdlingRegistry.getInstance().register(loadingIdlingResource) 28 | } 29 | 30 | @Test 31 | fun testAllShowsViaHome() { 32 | launchAllShows { 33 | verifyAllShows() 34 | } 35 | } 36 | 37 | @After 38 | fun tearDown() { 39 | IdlingRegistry.getInstance().unregister(loadingIdlingResource) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/test/java/com/android/tvflix/utils/MainCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.utils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.runBlockingTest 9 | import kotlinx.coroutines.test.setMain 10 | import org.junit.rules.TestWatcher 11 | import org.junit.runner.Description 12 | 13 | /** 14 | * Sets the main coroutines dispatcher to a [TestCoroutineScope] for unit testing. A 15 | * [TestCoroutineScope] provides control over the execution of coroutines. 16 | * 17 | */ 18 | @ExperimentalCoroutinesApi 19 | class MainCoroutineRule( 20 | val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 21 | ) : TestWatcher() { 22 | 23 | override fun starting(description: Description?) { 24 | super.starting(description) 25 | Dispatchers.setMain(testDispatcher) 26 | } 27 | 28 | override fun finished(description: Description?) { 29 | super.finished(description) 30 | Dispatchers.resetMain() 31 | testDispatcher.cleanupTestCoroutines() 32 | } 33 | } 34 | 35 | @ExperimentalCoroutinesApi 36 | fun MainCoroutineRule.runBlockingTest(block: suspend () -> Unit) = 37 | this.testDispatcher.runBlockingTest { 38 | block() 39 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 15 | 16 | 24 | 25 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/network/home/Show.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.network.home 2 | 3 | import android.os.Parcelable 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | import kotlinx.parcelize.Parcelize 7 | 8 | @Parcelize 9 | @JsonClass(generateAdapter = true) 10 | data class Show( 11 | val id: Long, 12 | val url: String?, 13 | val name: String, 14 | val type: String?, 15 | val language: String?, 16 | val genres: List?, 17 | val status: String?, 18 | val runtime: Int?, 19 | val premiered: String?, 20 | val officialSite: String?, 21 | @Json(name = "network") val airChannel: Channel?, 22 | val webChannel: Channel?, 23 | val image: Map?, 24 | @Json(name = "externals") val externalInfo: ExternalInfo?, 25 | val summary: String?, 26 | val rating: Map? 27 | ) : Parcelable { 28 | @Parcelize 29 | @JsonClass(generateAdapter = true) 30 | data class Channel( 31 | val id: Long?, 32 | val name: String?, 33 | val country: Country? 34 | ) : Parcelable { 35 | @Parcelize 36 | @JsonClass(generateAdapter = true) 37 | data class Country( 38 | val name: String, 39 | val code: String, 40 | val timezone: String 41 | ) : Parcelable 42 | } 43 | 44 | @Parcelize 45 | @JsonClass(generateAdapter = true) 46 | data class ExternalInfo( 47 | val tvrage: String?, 48 | val thetvdb: Long?, 49 | val imdb: String? 50 | ) : Parcelable 51 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/home/HomeTest.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.home 2 | 3 | import androidx.test.espresso.Espresso.pressBack 4 | import androidx.test.espresso.IdlingRegistry 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import androidx.test.filters.LargeTest 7 | import androidx.test.rule.ActivityTestRule 8 | import com.android.tvflix.idlingresource.LoadingIdlingResource 9 | import org.junit.After 10 | import org.junit.Before 11 | import org.junit.Rule 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | 15 | @LargeTest 16 | @RunWith(AndroidJUnit4::class) 17 | class HomeTest { 18 | @get:Rule 19 | val homeActivityTestRule = ActivityTestRule(HomeActivity::class.java) 20 | private lateinit var loadingIdlingResource: LoadingIdlingResource 21 | 22 | @Before 23 | fun setUp() { 24 | loadingIdlingResource = 25 | LoadingIdlingResource(homeActivityTestRule.activity) 26 | IdlingRegistry.getInstance().register(loadingIdlingResource) 27 | } 28 | 29 | @Test 30 | fun testHomePageWithFavorites() { 31 | launchHome { 32 | verifyHome() 33 | // Click on add to favorites icon 34 | verifyFavorite() 35 | // Verify that added to favorites toast is shown 36 | verifyToast(homeActivityTestRule.activity) 37 | verifyFavoriteScreen() 38 | // Verify that pressing back from favorites goes to home 39 | pressBack() 40 | verifyHome() 41 | } 42 | } 43 | 44 | @After 45 | fun tearDown() { 46 | IdlingRegistry.getInstance().unregister(loadingIdlingResource) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/home/ShowDiffUtilCallback.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.home 2 | 3 | import android.os.Bundle 4 | import androidx.recyclerview.widget.DiffUtil 5 | 6 | class ShowDiffUtilCallback( 7 | private val oldList: List, 8 | private val newList: List 9 | ) : DiffUtil.Callback() { 10 | 11 | override fun getOldListSize(): Int { 12 | return oldList.size 13 | } 14 | 15 | override fun getNewListSize(): Int { 16 | return newList.size 17 | } 18 | 19 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 20 | val oldEpisodeViewData = oldList[oldItemPosition] 21 | val newEpisodeViewData = newList[newItemPosition] 22 | return oldEpisodeViewData.showViewData.show.id == newEpisodeViewData.showViewData.show.id 23 | } 24 | 25 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 26 | val oldEpisodeViewData = oldList[oldItemPosition] 27 | val newEpisodeViewData = newList[newItemPosition] 28 | return oldEpisodeViewData.showViewData.isFavoriteShow == newEpisodeViewData.showViewData.isFavoriteShow 29 | } 30 | 31 | override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Bundle { 32 | val oldEpisodeViewData = oldList[oldItemPosition] 33 | val newEpisodeViewData = newList[newItemPosition] 34 | val bundle = Bundle() 35 | if (oldEpisodeViewData.showViewData.isFavoriteShow != newEpisodeViewData.showViewData.isFavoriteShow) { 36 | bundle.putBoolean(IS_FAVORITE, newEpisodeViewData.showViewData.isFavoriteShow) 37 | } 38 | return bundle 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowsPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.android.tvflix.network.TvFlixApi 6 | import com.android.tvflix.network.home.Show 7 | import retrofit2.HttpException 8 | import java.io.IOException 9 | import javax.inject.Inject 10 | 11 | class ShowsPagingSource 12 | @Inject 13 | constructor(private val tvFlixApi: TvFlixApi) : PagingSource() { 14 | companion object { 15 | const val SHOWS_STARTING_INDEX = 1 16 | } 17 | 18 | override suspend fun load(params: LoadParams): LoadResult { 19 | val position = params.key ?: SHOWS_STARTING_INDEX 20 | return try { 21 | val showsList = tvFlixApi.getShows(position) 22 | LoadResult.Page( 23 | data = showsList, 24 | prevKey = if (position == SHOWS_STARTING_INDEX) null else position - 1, 25 | nextKey = if (showsList.isEmpty()) null else position + 1 26 | ) 27 | } catch (exception: IOException) { 28 | LoadResult.Error(exception) 29 | } catch (exception: HttpException) { 30 | LoadResult.Error(exception) 31 | } 32 | } 33 | 34 | override fun getRefreshKey(state: PagingState): Int? { 35 | // We need to get the previous key (or next key if previous is null) of the page 36 | // that was closest to the most recently accessed index. 37 | // Anchor position is the most recently accessed index 38 | return state.anchorPosition?.let { anchorPosition -> 39 | state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) 40 | ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 36 | 39 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/favorite/FavoriteShowsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.favorite 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import android.widget.ImageView 6 | import androidx.appcompat.content.res.AppCompatResources 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.android.tvflix.R 9 | import com.android.tvflix.databinding.ShowListItemBinding 10 | import com.android.tvflix.db.favouriteshow.FavoriteShow 11 | import com.bumptech.glide.Glide 12 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 13 | import com.bumptech.glide.request.RequestOptions 14 | 15 | class FavoriteShowsAdapter( 16 | private val favoriteShows: MutableList 17 | ) : RecyclerView.Adapter() { 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteShowHolder { 20 | val layoutInflater = LayoutInflater.from(parent.context) 21 | val showListItemBinding = ShowListItemBinding.inflate(layoutInflater, parent, false) 22 | val holder = FavoriteShowHolder(showListItemBinding) 23 | holder.binding.showFavoriteIcon = false 24 | return holder 25 | } 26 | 27 | override fun onBindViewHolder(holder: FavoriteShowHolder, position: Int) { 28 | val favoriteShow = favoriteShows[position] 29 | Glide.with(holder.itemView.context).load(favoriteShow.imageUrl) 30 | .apply(RequestOptions.placeholderOf(R.color.grey)) 31 | .transition(DrawableTransitionOptions.withCrossFade()) 32 | .into(holder.binding.showImage) 33 | } 34 | 35 | override fun getItemCount(): Int { 36 | return favoriteShows.size 37 | } 38 | 39 | class FavoriteShowHolder(val binding: ShowListItemBinding) : 40 | RecyclerView.ViewHolder(binding.root) 41 | } 42 | -------------------------------------------------------------------------------- /tools/rules-checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/favorite/FavoriteShowsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.favorite 2 | 3 | import com.android.tvflix.db.favouriteshow.FavoriteShow 4 | import com.android.tvflix.db.favouriteshow.ShowDao 5 | import com.android.tvflix.network.home.Show 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | class FavoriteShowsRepository @Inject 11 | constructor(private val showDao: ShowDao) { 12 | 13 | suspend fun allFavoriteShows(): List { 14 | return showDao.allFavouriteShows() 15 | } 16 | 17 | suspend fun insertShowIntoFavorites(show: Show) { 18 | val favoriteShow = FavoriteShow( 19 | id = show.id, 20 | name = show.name, 21 | premiered = show.premiered, 22 | imageUrl = show.image!!["original"], 23 | summary = show.summary, 24 | rating = show.rating!!["average"], 25 | runtime = show.runtime!! 26 | ) 27 | showDao.insert(favoriteShow) 28 | } 29 | 30 | suspend fun removeShowFromFavorites(show: Show) { 31 | val favoriteShow = FavoriteShow( 32 | id = show.id, 33 | name = show.name, 34 | premiered = show.premiered, 35 | imageUrl = show.image!!["original"], 36 | summary = show.summary, 37 | rating = show.rating!!["average"], 38 | runtime = show.runtime!! 39 | ) 40 | showDao.remove(favoriteShow) 41 | } 42 | 43 | suspend fun insertIntoFavorites(favoriteShow: FavoriteShow) { 44 | showDao.insert(favoriteShow) 45 | } 46 | 47 | suspend fun removeFromFavorites(favoriteShow: FavoriteShow) { 48 | showDao.remove(favoriteShow) 49 | } 50 | 51 | suspend fun allFavoriteShowIds(): List { 52 | return showDao.getFavoriteShowIds() 53 | } 54 | 55 | suspend fun clearAll() { 56 | showDao.clear() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/tvflix/home/HomeRobot.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.home 2 | 3 | import android.app.Activity 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.action.ViewActions.click 6 | import androidx.test.espresso.assertion.ViewAssertions.matches 7 | import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition 8 | import androidx.test.espresso.matcher.RootMatchers.withDecorView 9 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 10 | import androidx.test.espresso.matcher.ViewMatchers.withId 11 | import androidx.test.espresso.matcher.ViewMatchers.withText 12 | import com.android.tvflix.R 13 | import com.android.tvflix.utils.MatcherUtils 14 | import com.android.tvflix.utils.ViewActionUtils 15 | import org.hamcrest.Matchers.`is` 16 | import org.hamcrest.Matchers.allOf 17 | import org.hamcrest.Matchers.not 18 | 19 | fun launchHome(func: HomeRobot.() -> Unit) = HomeRobot().apply { func() } 20 | class HomeRobot { 21 | fun verifyHome() { 22 | onView(withId(R.id.popular_show_header)).check(matches(isDisplayed())) 23 | onView(allOf(withId(R.id.popular_shows), isDisplayed())) 24 | } 25 | 26 | fun verifyFavorite() { 27 | // Click on 1st item and mark as favorite 28 | onView(withId(R.id.popular_shows)) 29 | .perform( 30 | actionOnItemAtPosition 31 | (0, ViewActionUtils.clickChildViewWithId(R.id.favorite)) 32 | ) 33 | } 34 | 35 | fun verifyToast(activity: Activity) { 36 | onView(withText(R.string.added_to_favorites)).inRoot(withDecorView(not(`is`(activity.window.decorView)))) 37 | .check(matches(isDisplayed())) 38 | } 39 | 40 | fun verifyFavoriteScreen() { 41 | onView(withId(R.id.action_favorites)) 42 | .perform(click()) 43 | onView(withText(R.string.favorite_shows)).check(matches(isDisplayed())) 44 | onView(withId(R.id.shows)) 45 | .check(matches(MatcherUtils.withListSize(1))) 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/shows/ShowsPagedAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.shows 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.paging.PagingDataAdapter 7 | import androidx.recyclerview.widget.DiffUtil 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.android.tvflix.R 10 | import com.android.tvflix.databinding.AllShowListItemBinding 11 | import com.android.tvflix.network.home.Show 12 | import com.bumptech.glide.Glide 13 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 14 | import com.bumptech.glide.request.RequestOptions 15 | 16 | class ShowsPagedAdapter constructor( 17 | diffCallback: DiffUtil.ItemCallback 18 | ) : PagingDataAdapter(diffCallback) { 19 | private lateinit var context: Context 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 22 | context = parent.context 23 | val layoutInflater = LayoutInflater.from(context) 24 | val showListItemBinding = AllShowListItemBinding.inflate(layoutInflater, parent, false) 25 | return ShowHolder(showListItemBinding) 26 | } 27 | 28 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 29 | if (holder is ShowHolder) { 30 | val show = getItem(position) 31 | holder.binding.show = show 32 | if (show!!.rating != null) { 33 | holder.binding.rating = show.rating!!["average"] 34 | } 35 | configureImage(holder, show) 36 | } 37 | } 38 | 39 | private fun configureImage(holder: ShowHolder, show: Show) { 40 | if (show.image != null) { 41 | Glide.with(context).load(show.image["original"]) 42 | .apply(RequestOptions.placeholderOf(R.color.grey)) 43 | .centerCrop() 44 | .transition(DrawableTransitionOptions.withCrossFade()) 45 | .into(holder.binding.showImage) 46 | } 47 | } 48 | 49 | class ShowHolder(val binding: AllShowListItemBinding) : RecyclerView.ViewHolder(binding.root) 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/config/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.config 2 | 3 | import com.android.tvflix.BuildConfig 4 | import com.google.firebase.ktx.Firebase 5 | import com.google.firebase.remoteconfig.ktx.remoteConfig 6 | import com.google.firebase.remoteconfig.ktx.remoteConfigSettings 7 | import kotlinx.coroutines.suspendCancellableCoroutine 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | import kotlin.coroutines.resume 11 | import kotlin.coroutines.resumeWithException 12 | 13 | @Singleton 14 | class AppConfig 15 | @Inject 16 | constructor() { 17 | companion object { 18 | const val FETCH_TIMEOUT_S = 20L 19 | const val MIN_FETCH_INTERVAL_S = 3600L 20 | } 21 | 22 | private val remoteConfig by lazy { Firebase.remoteConfig } 23 | 24 | fun initialise() { 25 | val minFetchInterval: Long = if (BuildConfig.DEBUG) { 26 | 0 27 | } else { 28 | MIN_FETCH_INTERVAL_S 29 | } 30 | val remoteConfigSettings = remoteConfigSettings { 31 | fetchTimeoutInSeconds = FETCH_TIMEOUT_S 32 | minimumFetchIntervalInSeconds = minFetchInterval 33 | } 34 | remoteConfig.apply { 35 | setConfigSettingsAsync(remoteConfigSettings) 36 | setDefaultsAsync(ConfigDefaults.getDefaultValues()) 37 | } 38 | } 39 | 40 | suspend fun fetch(): Boolean = 41 | suspendCancellableCoroutine { continuation -> 42 | remoteConfig.fetchAndActivate().addOnSuccessListener { 43 | continuation.resume(true) 44 | }.addOnFailureListener { exc -> continuation.resumeWithException(exc) } 45 | } 46 | 47 | fun getString(key: String): String { 48 | return remoteConfig.getString(key) 49 | } 50 | 51 | fun getBoolean(key: String): Boolean { 52 | return remoteConfig.getBoolean(key) 53 | } 54 | 55 | fun getDouble(key: String): Double { 56 | return remoteConfig.getDouble(key) 57 | } 58 | 59 | fun getLong(key: String): Long { 60 | return remoteConfig.getLong(key) 61 | } 62 | 63 | fun getInt(key: String): Int { 64 | return remoteConfig.getLong(key).toInt() 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 15 | 16 | 28 | 29 | 39 | 40 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/tvflix/favorite/FavoriteShowsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.favorite 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.android.tvflix.db.favouriteshow.FavoriteShow 6 | import com.android.tvflix.di.IoDispatcher 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.CoroutineDispatcher 9 | import kotlinx.coroutines.CoroutineExceptionHandler 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.launch 13 | import timber.log.Timber 14 | import java.util.* 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class FavoriteShowsViewModel @Inject 19 | constructor( 20 | private val favoriteShowsRepository: FavoriteShowsRepository, 21 | // Inject coroutineDispatcher to facilitate Unit Testing 22 | @IoDispatcher private val ioDispatcher: CoroutineDispatcher, 23 | ) : ViewModel() { 24 | private val _favoriteShowsStateFlow = 25 | MutableStateFlow(FavoriteShowState.Loading) 26 | 27 | // Represents _favoriteShowsStateFlow mutable state flow as a read-only state flow. 28 | val favoriteShowsStateFlow = _favoriteShowsStateFlow.asStateFlow() 29 | 30 | private lateinit var removedFromFavoriteShows: List 31 | 32 | fun loadFavoriteShows() { 33 | val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> 34 | onError(exception) 35 | } 36 | 37 | viewModelScope.launch(ioDispatcher + coroutineExceptionHandler) { 38 | val favoriteShows = favoriteShowsRepository.allFavoriteShows() 39 | 40 | removedFromFavoriteShows = ArrayList(favoriteShows.size) 41 | if (favoriteShows.isNotEmpty()) { 42 | _favoriteShowsStateFlow.emit(FavoriteShowState.AllFavorites(favoriteShows)) 43 | } else { 44 | _favoriteShowsStateFlow.emit(FavoriteShowState.Empty) 45 | } 46 | } 47 | } 48 | 49 | private fun onError(throwable: Throwable) { 50 | _favoriteShowsStateFlow.value = 51 | FavoriteShowState.Error( 52 | throwable.localizedMessage ?: "Some error occurred. Please try again." 53 | ) 54 | Timber.d(throwable) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/test/java/com/android/tvflix/favorites/FavoritesRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.android.tvflix.favorites 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import androidx.test.core.app.ApplicationProvider 6 | import com.android.tvflix.db.TvFlixDatabase 7 | import com.android.tvflix.db.favouriteshow.ShowDao 8 | import com.android.tvflix.favorite.FavoriteShowsRepository 9 | import com.android.tvflix.utils.TestUtil 10 | import com.google.common.truth.Truth.assertThat 11 | import kotlinx.coroutines.runBlocking 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Test 15 | import org.junit.runner.RunWith 16 | import org.robolectric.RobolectricTestRunner 17 | 18 | @RunWith(RobolectricTestRunner::class) 19 | class FavoritesRepositoryTest { 20 | private lateinit var favoriteShowsRepository: FavoriteShowsRepository 21 | private lateinit var tvFlixDb: TvFlixDatabase 22 | private lateinit var showDao: ShowDao 23 | 24 | @Before 25 | fun setup() { 26 | val context = ApplicationProvider.getApplicationContext() 27 | tvFlixDb = Room.inMemoryDatabaseBuilder( 28 | context, TvFlixDatabase::class.java 29 | ).allowMainThreadQueries().build() 30 | showDao = tvFlixDb.showDao() 31 | favoriteShowsRepository = FavoriteShowsRepository(showDao) 32 | } 33 | 34 | @Test 35 | fun testShowInsertionInDb() { 36 | runBlocking { 37 | favoriteShowsRepository.insertIntoFavorites(TestUtil.getFakeShow()) 38 | val favShows = favoriteShowsRepository.allFavoriteShows() 39 | assertThat(favShows.isNotEmpty()).isTrue() 40 | } 41 | } 42 | 43 | @Test 44 | fun testRemoveFromDb() { 45 | runBlocking { 46 | favoriteShowsRepository.clearAll() 47 | assertThat(favoriteShowsRepository.allFavoriteShows().isEmpty()).isTrue() 48 | } 49 | } 50 | 51 | @Test 52 | fun testFavoriteShows() { 53 | runBlocking { 54 | val fakeShow = TestUtil.getFakeShow() 55 | favoriteShowsRepository.insertIntoFavorites(fakeShow) 56 | val favoriteShows = favoriteShowsRepository.allFavoriteShows() 57 | assertThat(favoriteShows[0] == fakeShow).isTrue() 58 | } 59 | } 60 | 61 | @After 62 | fun release() { 63 | tvFlixDb.close() 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_favorite_shows.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 15 | 16 | 27 | 28 | 37 | 38 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/network_failure_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 20 | 21 | 28 | 29 | 42 | 43 |