From 6631ccc89cd4896151073abfb486435598224f80 Mon Sep 17 00:00:00 2001 From: Ashwini Kumar Date: Thu, 19 Mar 2026 21:23:58 +0530 Subject: [PATCH 1/3] Migrate UI to Jetpack Compose and fix crash and navigation issues - Migrate activities and layouts from XML/View system to Compose - Switch from kapt to KSP for annotation processing - Fix Firebase Remote Config crash by handling fetch failures gracefully - Replace incorrect media icon with Material ArrowBack for navigation - Fix unnecessary top padding on splash screen Co-Authored-By: Claude Opus 4.6 --- app/build.gradle.kts | 67 +-- .../java/com/android/tvflix/home/HomeRobot.kt | 47 -- .../java/com/android/tvflix/home/HomeTest.kt | 48 -- .../idlingresource/LoadingIdlingResource.kt | 35 -- .../tvflix/main/MainNavigationComposeTest.kt | 55 ++ .../com/android/tvflix/shows/AllShowsRobot.kt | 18 - .../com/android/tvflix/shows/AllShowsTest.kt | 41 -- .../com/android/tvflix/utils/MatcherUtils.kt | 21 - .../android/tvflix/utils/ViewActionUtils.kt | 25 - app/src/main/AndroidManifest.xml | 19 +- .../analytics/FirebaseAnalyticsHandler.kt | 25 +- .../com/android/tvflix/config/AppConfig.kt | 23 +- .../tvflix/db/favouriteshow/FavoriteShow.kt | 1 - .../tvflix/db/favouriteshow/ShowDao.kt | 8 +- .../tvflix/favorite/FavoriteShowsActivity.kt | 103 ---- .../tvflix/favorite/FavoriteShowsAdapter.kt | 41 -- .../com/android/tvflix/home/HomeActivity.kt | 141 ----- .../tvflix/home/ShowDiffUtilCallback.kt | 40 -- .../com/android/tvflix/home/ShowsAdapter.kt | 109 ---- .../com/android/tvflix/main/MainActivity.kt | 560 ++++++++++++++++++ .../android/tvflix/shows/AllShowsActivity.kt | 99 ---- .../tvflix/shows/ShowDiffUtilItemCallback.kt | 14 - .../tvflix/shows/ShowsLoadStateAdapter.kt | 16 - .../android/tvflix/shows/ShowsPagedAdapter.kt | 50 -- .../tvflix/shows/ShowsStateViewHolder.kt | 33 -- .../android/tvflix/splash/SplashActivity.kt | 30 - .../android/tvflix/splash/SplashViewModel.kt | 8 +- .../tvflix/utils/GridItemDecoration.kt | 30 - .../main/res/drawable/favorite_gradient.xml | 12 - .../main/res/drawable/splash_background.xml | 12 - .../main/res/layout/activity_all_shows.xml | 72 --- .../res/layout/activity_favorite_shows.xml | 53 -- app/src/main/res/layout/activity_home.xml | 50 -- app/src/main/res/layout/activity_splash.xml | 8 - .../main/res/layout/all_show_list_item.xml | 113 ---- app/src/main/res/layout/loading_list_item.xml | 17 - .../res/layout/network_failure_list_item.xml | 53 -- app/src/main/res/layout/show_list_item.xml | 38 -- app/src/main/res/layout/toolbar.xml | 10 - app/src/main/res/menu/home_menu.xml | 17 - app/src/main/res/values/dimen.xml | 4 - app/src/main/res/values/styles.xml | 20 - build.gradle.kts | 23 +- buildSrc/build.gradle.kts | 7 +- buildSrc/src/main/kotlin/Dependencies.kt | 118 ++-- gradle.properties | 10 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 3 + tools/checkstyle.gradle | 8 +- tools/pmd.gradle | 8 +- 50 files changed, 786 insertions(+), 1579 deletions(-) delete mode 100644 app/src/androidTest/java/com/android/tvflix/home/HomeRobot.kt delete mode 100644 app/src/androidTest/java/com/android/tvflix/home/HomeTest.kt delete mode 100644 app/src/androidTest/java/com/android/tvflix/idlingresource/LoadingIdlingResource.kt create mode 100644 app/src/androidTest/java/com/android/tvflix/main/MainNavigationComposeTest.kt delete mode 100644 app/src/androidTest/java/com/android/tvflix/shows/AllShowsRobot.kt delete mode 100644 app/src/androidTest/java/com/android/tvflix/shows/AllShowsTest.kt delete mode 100644 app/src/androidTest/java/com/android/tvflix/utils/MatcherUtils.kt delete mode 100644 app/src/androidTest/java/com/android/tvflix/utils/ViewActionUtils.kt delete mode 100644 app/src/main/java/com/android/tvflix/favorite/FavoriteShowsActivity.kt delete mode 100644 app/src/main/java/com/android/tvflix/favorite/FavoriteShowsAdapter.kt delete mode 100644 app/src/main/java/com/android/tvflix/home/HomeActivity.kt delete mode 100644 app/src/main/java/com/android/tvflix/home/ShowDiffUtilCallback.kt delete mode 100644 app/src/main/java/com/android/tvflix/home/ShowsAdapter.kt create mode 100644 app/src/main/java/com/android/tvflix/main/MainActivity.kt delete mode 100644 app/src/main/java/com/android/tvflix/shows/AllShowsActivity.kt delete mode 100644 app/src/main/java/com/android/tvflix/shows/ShowDiffUtilItemCallback.kt delete mode 100644 app/src/main/java/com/android/tvflix/shows/ShowsLoadStateAdapter.kt delete mode 100644 app/src/main/java/com/android/tvflix/shows/ShowsPagedAdapter.kt delete mode 100644 app/src/main/java/com/android/tvflix/shows/ShowsStateViewHolder.kt delete mode 100644 app/src/main/java/com/android/tvflix/splash/SplashActivity.kt delete mode 100644 app/src/main/java/com/android/tvflix/utils/GridItemDecoration.kt delete mode 100644 app/src/main/res/drawable/favorite_gradient.xml delete mode 100644 app/src/main/res/drawable/splash_background.xml delete mode 100644 app/src/main/res/layout/activity_all_shows.xml delete mode 100644 app/src/main/res/layout/activity_favorite_shows.xml delete mode 100644 app/src/main/res/layout/activity_home.xml delete mode 100644 app/src/main/res/layout/activity_splash.xml delete mode 100644 app/src/main/res/layout/all_show_list_item.xml delete mode 100644 app/src/main/res/layout/loading_list_item.xml delete mode 100644 app/src/main/res/layout/network_failure_list_item.xml delete mode 100644 app/src/main/res/layout/show_list_item.xml delete mode 100644 app/src/main/res/layout/toolbar.xml delete mode 100644 app/src/main/res/menu/home_menu.xml delete mode 100644 app/src/main/res/values/dimen.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0550fd6..b237a40 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,8 @@ plugins { id("com.android.application") kotlin("android") - kotlin("kapt") + kotlin("plugin.compose") + id("com.google.devtools.ksp") kotlin("plugin.parcelize") id("dagger.hilt.android.plugin") id("com.google.gms.google-services") @@ -21,6 +22,7 @@ android { buildFeatures { viewBinding = true dataBinding = true + compose = true buildConfig = true } @@ -31,17 +33,11 @@ android { versionCode = Deps.Versions.app_version_code versionName = Deps.Versions.app_version_name testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - javaCompileOptions { - annotationProcessorOptions { - // Refer https://developer.android.com/jetpack/androidx/releases/room#compiler-options - arguments( - mapOf( - "room.schemaLocation" to "$projectDir/schemas", - "room.incremental" to "true", - "room.expandProjection" to "true" - ) - ) - } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.incremental", "true") + arg("room.expandProjection", "true") } } flavorDimensions.addAll(listOf("default")) @@ -76,8 +72,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } testOptions { @@ -85,17 +81,9 @@ android { animationsDisabled = true } - kapt { - useBuildCache = true - javacOptions { - // Increase the max count of errors from annotation processors. - // Default is 100. - option("-Xmaxerrs", 500) - } - } testBuildType = "debug" - packagingOptions { + packaging { resources.excludes.addAll( listOf( "META-INF/ASL2.0", @@ -110,8 +98,9 @@ android { } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } + namespace = "com.android.tvflix" } dependencies { @@ -121,13 +110,22 @@ dependencies { implementation(Deps.AndroidX.ktx_core) implementation(Deps.AndroidX.ktx_fragment) implementation(Deps.AndroidX.ktx_activity) + implementation(Deps.AndroidX.compose_activity) implementation(Deps.AndroidX.constraint_layout) - kapt(Deps.AndroidX.Lifecycle.compiler) + implementation(platform(Deps.AndroidX.Compose.bom)) + implementation(Deps.AndroidX.Compose.ui) + implementation(Deps.AndroidX.Compose.foundation) + implementation(Deps.AndroidX.Compose.material) + implementation(Deps.AndroidX.Compose.material_icons) + implementation(Deps.AndroidX.Compose.tooling_preview) + implementation(Deps.AndroidX.Compose.navigation) + implementation(Deps.AndroidX.Compose.hilt_navigation) implementation(Deps.AndroidX.Lifecycle.viewmodel) implementation(Deps.AndroidX.Paging.runtime) + implementation(Deps.AndroidX.Paging.compose) testImplementation(Deps.AndroidX.Paging.common) implementation(Deps.AndroidX.Room.runtime) - kapt(Deps.AndroidX.Room.compiler) + ksp(Deps.AndroidX.Room.compiler) testImplementation(Deps.AndroidX.Room.testing) implementation(Deps.AndroidX.Room.ktx) implementation(Deps.AndroidX.annotation) @@ -138,7 +136,8 @@ dependencies { implementation(Deps.OkHttp.logging_interceptor) implementation(Deps.Glide.runtime) implementation(Deps.Glide.okhttp_integration) - kapt(Deps.Glide.compiler) + implementation(Deps.Coil.compose) + ksp(Deps.Glide.ksp) implementation(Deps.Retrofit.main) implementation(Deps.Retrofit.moshi) @@ -154,27 +153,25 @@ dependencies { testImplementation(Deps.AndroidX.Test.core) androidTestImplementation(Deps.AndroidX.Test.runner) androidTestImplementation(Deps.AndroidX.Test.junit) - // Espresso - androidTestImplementation(Deps.AndroidX.Test.Espresso.core) - androidTestImplementation(Deps.AndroidX.Test.Espresso.contrib) - androidTestImplementation(Deps.AndroidX.Test.Espresso.idling_resource) - androidTestImplementation(Deps.AndroidX.Test.rules) + androidTestImplementation(Deps.AndroidX.Compose.test_junit4) testImplementation(Deps.Test.truth) testImplementation(Deps.Test.robolectric) testImplementation(Deps.Coroutines.test) // end-region Test implementation(Deps.Moshi.kotlin) - kapt(Deps.Moshi.codegen) + ksp(Deps.Moshi.codegen) implementation(Deps.Coroutines.core) implementation(Deps.Coroutines.android) debugImplementation(Deps.Chucker.debug) + debugImplementation(Deps.AndroidX.Compose.tooling) + debugImplementation(Deps.AndroidX.Compose.test_manifest) releaseImplementation(Deps.Chucker.release) implementation(Deps.Hilt.android) - kapt(Deps.Hilt.android_compiler) + ksp(Deps.Hilt.android_compiler) // start-region Firebase implementation(platform(Deps.Firebase.firebase_bom)) @@ -184,5 +181,3 @@ dependencies { implementation(Deps.Firebase.remote_config) // end-region Firebase } - - diff --git a/app/src/androidTest/java/com/android/tvflix/home/HomeRobot.kt b/app/src/androidTest/java/com/android/tvflix/home/HomeRobot.kt deleted file mode 100644 index 69630b1..0000000 --- a/app/src/androidTest/java/com/android/tvflix/home/HomeRobot.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.android.tvflix.home - -import android.app.Activity -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.RootMatchers.withDecorView -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import com.android.tvflix.R -import com.android.tvflix.utils.MatcherUtils -import com.android.tvflix.utils.ViewActionUtils -import org.hamcrest.Matchers.`is` -import org.hamcrest.Matchers.allOf -import org.hamcrest.Matchers.not - -fun launchHome(func: HomeRobot.() -> Unit) = HomeRobot().apply { func() } -class HomeRobot { - fun verifyHome() { - onView(withId(R.id.popular_show_header)).check(matches(isDisplayed())) - onView(allOf(withId(R.id.popular_shows), isDisplayed())) - } - - fun verifyFavorite() { - // Click on 1st item and mark as favorite - onView(withId(R.id.popular_shows)) - .perform( - actionOnItemAtPosition - (0, ViewActionUtils.clickChildViewWithId(R.id.favorite)) - ) - } - - fun verifyToast(activity: Activity) { - onView(withText(R.string.added_to_favorites)).inRoot(withDecorView(not(`is`(activity.window.decorView)))) - .check(matches(isDisplayed())) - } - - fun verifyFavoriteScreen() { - onView(withId(R.id.action_favorites)) - .perform(click()) - onView(withText(R.string.favorite_shows)).check(matches(isDisplayed())) - onView(withId(R.id.shows)) - .check(matches(MatcherUtils.withListSize(1))) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/tvflix/home/HomeTest.kt b/app/src/androidTest/java/com/android/tvflix/home/HomeTest.kt deleted file mode 100644 index 165dc55..0000000 --- a/app/src/androidTest/java/com/android/tvflix/home/HomeTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.android.tvflix.home - -import androidx.test.espresso.Espresso.pressBack -import androidx.test.espresso.IdlingRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule -import com.android.tvflix.idlingresource.LoadingIdlingResource -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@LargeTest -@RunWith(AndroidJUnit4::class) -class HomeTest { - @get:Rule - val homeActivityTestRule = ActivityTestRule(HomeActivity::class.java) - private lateinit var loadingIdlingResource: LoadingIdlingResource - - @Before - fun setUp() { - loadingIdlingResource = - LoadingIdlingResource(homeActivityTestRule.activity) - IdlingRegistry.getInstance().register(loadingIdlingResource) - } - - @Test - fun testHomePageWithFavorites() { - launchHome { - verifyHome() - // Click on add to favorites icon - verifyFavorite() - // Verify that added to favorites toast is shown - verifyToast(homeActivityTestRule.activity) - verifyFavoriteScreen() - // Verify that pressing back from favorites goes to home - pressBack() - verifyHome() - } - } - - @After - fun tearDown() { - IdlingRegistry.getInstance().unregister(loadingIdlingResource) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/tvflix/idlingresource/LoadingIdlingResource.kt b/app/src/androidTest/java/com/android/tvflix/idlingresource/LoadingIdlingResource.kt deleted file mode 100644 index c74d3c2..0000000 --- a/app/src/androidTest/java/com/android/tvflix/idlingresource/LoadingIdlingResource.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.tvflix.idlingresource - -import android.app.Activity -import android.view.View -import android.widget.ProgressBar -import androidx.test.espresso.IdlingResource -import com.android.tvflix.R - -/** - * An idling resource which checks which tells Espresso to wait until Progress Bar dismisses - */ -class LoadingIdlingResource constructor(private val activity: Activity) : IdlingResource { - private var resourceCallback: IdlingResource.ResourceCallback? = null - override fun getName(): String { - return "LoadingIdlingResource" - } - - override fun isIdleNow(): Boolean { - return if (!isProgressShowing()) { - resourceCallback?.onTransitionToIdle() - true - } else { - false - } - } - - override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { - resourceCallback = callback - } - - private fun isProgressShowing(): Boolean { - val progress = activity.findViewById(R.id.progress) - return progress.visibility == View.VISIBLE - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/tvflix/main/MainNavigationComposeTest.kt b/app/src/androidTest/java/com/android/tvflix/main/MainNavigationComposeTest.kt new file mode 100644 index 0000000..e8da11d --- /dev/null +++ b/app/src/androidTest/java/com/android/tvflix/main/MainNavigationComposeTest.kt @@ -0,0 +1,55 @@ +package com.android.tvflix.main + +import androidx.compose.ui.test.assertExists +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@LargeTest +@RunWith(AndroidJUnit4::class) +class MainNavigationComposeTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun appLaunchesAndReachesHomeShell() { + waitForHomeShell() + composeRule.onNodeWithTag("nav_shows").assertExists() + } + + @Test + fun navigateToShowsRouteFromHome() { + waitForHomeShell() + composeRule.onNodeWithTag("nav_shows").performClick() + composeRule.waitUntil(10_000) { + composeRule.onAllNodesWithTag("shows_list").fetchSemanticsNodes().isNotEmpty() || + composeRule.onAllNodesWithTag("retry").fetchSemanticsNodes().isNotEmpty() + } + } + + @Test + fun navigateToFavoritesRouteWhenFeatureEnabled() { + waitForHomeShell() + val favoritesNodeCount = composeRule.onAllNodesWithTag("nav_favorites") + .fetchSemanticsNodes().size + if (favoritesNodeCount > 0) { + composeRule.onNodeWithTag("nav_favorites").performClick() + composeRule.waitUntil(10_000) { + composeRule.onAllNodesWithTag("favorites_list").fetchSemanticsNodes().isNotEmpty() || + composeRule.onAllNodesWithTag("retry").fetchSemanticsNodes().isNotEmpty() + } + } + } + + private fun waitForHomeShell() { + composeRule.waitUntil(10_000) { + composeRule.onAllNodesWithTag("nav_shows").fetchSemanticsNodes().isNotEmpty() + } + } +} diff --git a/app/src/androidTest/java/com/android/tvflix/shows/AllShowsRobot.kt b/app/src/androidTest/java/com/android/tvflix/shows/AllShowsRobot.kt deleted file mode 100644 index 2e8fcb9..0000000 --- a/app/src/androidTest/java/com/android/tvflix/shows/AllShowsRobot.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.android.tvflix.shows - -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import com.android.tvflix.R -import com.android.tvflix.utils.MatcherUtils - -fun launchAllShows(func: AllShowsRobot.() -> Unit) = AllShowsRobot().apply { func() } -class AllShowsRobot { - fun verifyAllShows() { - onView(withText(R.string.shows)).check(matches(isDisplayed())) - onView(withId(R.id.shows)) - .check(matches(MatcherUtils.withListSize(1))) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/tvflix/shows/AllShowsTest.kt b/app/src/androidTest/java/com/android/tvflix/shows/AllShowsTest.kt deleted file mode 100644 index f37c6f9..0000000 --- a/app/src/androidTest/java/com/android/tvflix/shows/AllShowsTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.android.tvflix.shows - -import androidx.test.espresso.IdlingRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule -import com.android.tvflix.idlingresource.LoadingIdlingResource -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@LargeTest -@RunWith(AndroidJUnit4::class) -class AllShowsTest { - // @get:Rule - // val homeActivityTestRule = ActivityTestRule(HomeActivity::class.java) - @get:Rule - val allShowsActivityTestRule = ActivityTestRule(AllShowsActivity::class.java) - - private lateinit var loadingIdlingResource: LoadingIdlingResource - @Before - fun setUp() { - loadingIdlingResource = - LoadingIdlingResource(allShowsActivityTestRule.activity) - IdlingRegistry.getInstance().register(loadingIdlingResource) - } - - @Test - fun testAllShowsViaHome() { - launchAllShows { - verifyAllShows() - } - } - - @After - fun tearDown() { - IdlingRegistry.getInstance().unregister(loadingIdlingResource) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/tvflix/utils/MatcherUtils.kt b/app/src/androidTest/java/com/android/tvflix/utils/MatcherUtils.kt deleted file mode 100644 index ac8fb01..0000000 --- a/app/src/androidTest/java/com/android/tvflix/utils/MatcherUtils.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.tvflix.utils - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher - -object MatcherUtils { - fun withListSize(size: Int): Matcher { - return object : TypeSafeMatcher() { - override fun matchesSafely(view: View): Boolean { - return (view as RecyclerView).adapter!!.itemCount >= size - } - - override fun describeTo(description: Description) { - description.appendText("RecyclerView should have $size items") - } - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/tvflix/utils/ViewActionUtils.kt b/app/src/androidTest/java/com/android/tvflix/utils/ViewActionUtils.kt deleted file mode 100644 index 2bde316..0000000 --- a/app/src/androidTest/java/com/android/tvflix/utils/ViewActionUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.tvflix.utils - -import android.view.View -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import org.hamcrest.Matcher - -object ViewActionUtils { - fun clickChildViewWithId(id: Int): ViewAction { - return object : ViewAction { - override fun getConstraints(): Matcher? { - return null - } - - override fun getDescription(): String { - return "Click on a child view with id $id" - } - - override fun perform(uiController: UiController, view: View) { - val v = view.findViewById(id) - v.performClick() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb30919..992dca9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -15,8 +14,7 @@ android:theme="@style/AppTheme" tools:ignore="LockedOrientationActivity"> @@ -26,20 +24,9 @@ - - - - - \ No newline at end of file + diff --git a/app/src/main/java/com/android/tvflix/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/android/tvflix/analytics/FirebaseAnalyticsHandler.kt index 5ab66f2..4870405 100644 --- a/app/src/main/java/com/android/tvflix/analytics/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/android/tvflix/analytics/FirebaseAnalyticsHandler.kt @@ -1,32 +1,35 @@ package com.android.tvflix.analytics -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.analytics.ktx.logEvent -import com.google.firebase.ktx.Firebase +import android.content.Context +import android.os.Bundle +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton class FirebaseAnalyticsHandler @Inject -constructor() : Analytics { - private val firebaseAnalytics by lazy { Firebase.analytics } +constructor(@ApplicationContext context: Context) : Analytics { + private val firebaseAnalytics = FirebaseAnalytics.getInstance(context) + override fun sendEvent(event: Event) { - firebaseAnalytics.logEvent(event.eventName) { - param(EventParams.CONTENT_ID, event.eventParams[EventParams.CONTENT_ID] as Long) - param(EventParams.CONTENT_TITLE, event.eventParams[EventParams.CONTENT_TITLE] as String) - param( + val bundle = Bundle().apply { + putLong(EventParams.CONTENT_ID, event.eventParams[EventParams.CONTENT_ID] as Long) + putString(EventParams.CONTENT_TITLE, event.eventParams[EventParams.CONTENT_TITLE] as String) + putString( EventParams.IS_FAVORITE_MARKED, event.eventParams[EventParams.IS_FAVORITE_MARKED].toString() ) - param( + putString( EventParams.CLICK_CONTEXT, event.eventParams[EventParams.CLICK_CONTEXT] as String ) } + firebaseAnalytics.logEvent(event.eventName, bundle) } override fun setUserId(userId: String) { firebaseAnalytics.setUserId(userId) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/android/tvflix/config/AppConfig.kt b/app/src/main/java/com/android/tvflix/config/AppConfig.kt index 0859a92..b851036 100644 --- a/app/src/main/java/com/android/tvflix/config/AppConfig.kt +++ b/app/src/main/java/com/android/tvflix/config/AppConfig.kt @@ -1,10 +1,10 @@ package com.android.tvflix.config import com.android.tvflix.BuildConfig -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume @@ -19,7 +19,7 @@ constructor() { const val MIN_FETCH_INTERVAL_S = 3600L } - private val remoteConfig by lazy { Firebase.remoteConfig } + private val remoteConfig by lazy { FirebaseRemoteConfig.getInstance() } fun initialise() { val minFetchInterval: Long = if (BuildConfig.DEBUG) { @@ -27,10 +27,10 @@ constructor() { } else { MIN_FETCH_INTERVAL_S } - val remoteConfigSettings = remoteConfigSettings { - fetchTimeoutInSeconds = FETCH_TIMEOUT_S - minimumFetchIntervalInSeconds = minFetchInterval - } + val remoteConfigSettings = FirebaseRemoteConfigSettings.Builder() + .setFetchTimeoutInSeconds(FETCH_TIMEOUT_S) + .setMinimumFetchIntervalInSeconds(minFetchInterval) + .build() remoteConfig.apply { setConfigSettingsAsync(remoteConfigSettings) setDefaultsAsync(ConfigDefaults.getDefaultValues()) @@ -40,8 +40,11 @@ constructor() { suspend fun fetch(): Boolean = suspendCancellableCoroutine { continuation -> remoteConfig.fetchAndActivate().addOnSuccessListener { + Timber.i("Config fetched successfully") continuation.resume(true) - }.addOnFailureListener { exc -> continuation.resumeWithException(exc) } + }.addOnFailureListener { exc -> + Timber.i("Config fetch failed") + continuation.resumeWithException(exc) } } fun getString(key: String): String { @@ -63,4 +66,4 @@ constructor() { fun getInt(key: String): Int { return remoteConfig.getLong(key).toInt() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/android/tvflix/db/favouriteshow/FavoriteShow.kt b/app/src/main/java/com/android/tvflix/db/favouriteshow/FavoriteShow.kt index ba4ff31..ca98762 100644 --- a/app/src/main/java/com/android/tvflix/db/favouriteshow/FavoriteShow.kt +++ b/app/src/main/java/com/android/tvflix/db/favouriteshow/FavoriteShow.kt @@ -14,4 +14,3 @@ data class FavoriteShow( var rating: String?, var runtime: Int? ) - diff --git a/app/src/main/java/com/android/tvflix/db/favouriteshow/ShowDao.kt b/app/src/main/java/com/android/tvflix/db/favouriteshow/ShowDao.kt index d789a44..e56f5df 100644 --- a/app/src/main/java/com/android/tvflix/db/favouriteshow/ShowDao.kt +++ b/app/src/main/java/com/android/tvflix/db/favouriteshow/ShowDao.kt @@ -1,6 +1,10 @@ package com.android.tvflix.db.favouriteshow -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query @Dao interface ShowDao { @@ -13,7 +17,7 @@ interface ShowDao { @Delete suspend fun remove(favoriteShow: FavoriteShow) - @Query("SELECT id from favourite_shows") + @Query("SELECT id FROM favourite_shows") suspend fun getFavoriteShowIds(): List @Query("DELETE FROM favourite_shows") diff --git a/app/src/main/java/com/android/tvflix/favorite/FavoriteShowsActivity.kt b/app/src/main/java/com/android/tvflix/favorite/FavoriteShowsActivity.kt deleted file mode 100644 index d9c0967..0000000 --- a/app/src/main/java/com/android/tvflix/favorite/FavoriteShowsActivity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.android.tvflix.favorite - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.text.SpannableString -import android.text.Spanned -import android.text.style.ImageSpan -import android.view.MenuItem -import android.widget.Toast -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.GridLayoutManager -import com.android.tvflix.R -import com.android.tvflix.databinding.ActivityFavoriteShowsBinding -import com.android.tvflix.db.favouriteshow.FavoriteShow -import com.android.tvflix.utils.GridItemDecoration -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collect - -@AndroidEntryPoint -class FavoriteShowsActivity : AppCompatActivity() { - private val favoriteShowsViewModel: FavoriteShowsViewModel by viewModels() - private val binding by lazy { ActivityFavoriteShowsBinding.inflate(layoutInflater) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - setToolbar() - favoriteShowsViewModel.loadFavoriteShows() - lifecycleScope.launchWhenStarted { - favoriteShowsViewModel.favoriteShowsStateFlow.collect { setViewState(it) } - } - } - - private fun setViewState(favoriteShowState: FavoriteShowState) { - when (favoriteShowState) { - is FavoriteShowState.Loading -> binding.progress.isVisible = true - is FavoriteShowState.AllFavorites -> - showFavorites(favoriteShowState.favoriteShows) - is FavoriteShowState.Error, FavoriteShowState.Empty -> showEmptyState() - is FavoriteShowState.AddedToFavorites -> - Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT).show() - is FavoriteShowState.RemovedFromFavorites -> - Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show() - } - } - - private fun setToolbar() { - val toolbar = binding.toolbar.toolbar - setSupportActionBar(toolbar) - toolbar.setTitleTextColor(ContextCompat.getColor(this, android.R.color.white)) - toolbar.setSubtitleTextColor(ContextCompat.getColor(this, android.R.color.white)) - supportActionBar?.run { setDisplayHomeAsUpEnabled(true) } - setTitle(R.string.favorite_shows) - } - - private fun showFavorites(favoriteShows: List) { - binding.progress.isVisible = false - val layoutManager = GridLayoutManager(this, COLUMNS_COUNT) - binding.shows.layoutManager = layoutManager - val favoriteShowsAdapter = FavoriteShowsAdapter(favoriteShows.toMutableList()) - binding.shows.adapter = favoriteShowsAdapter - val spacing = resources.getDimensionPixelSize(R.dimen.show_grid_spacing) - binding.shows.addItemDecoration(GridItemDecoration(spacing, COLUMNS_COUNT)) - binding.shows.isVisible = true - } - - private fun showEmptyState() { - binding.progress.isVisible = false - val bookmarkSpan = ImageSpan(this, R.drawable.favorite_border) - val spannableString = SpannableString(getString(R.string.favorite_hint_msg)) - spannableString.setSpan( - bookmarkSpan, FAVORITE_ICON_START_OFFSET, - FAVORITE_ICON_END_OFFSET, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - binding.favoriteHint.text = spannableString - binding.favoriteHint.isVisible = true - } - - companion object { - private const val FAVORITE_ICON_START_OFFSET = 13 - private const val FAVORITE_ICON_END_OFFSET = 14 - private const val COLUMNS_COUNT = 2 - - fun start(context: Activity) { - val starter = Intent(context, FavoriteShowsActivity::class.java) - context.startActivity(starter) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return if (item.itemId == android.R.id.home) { - finish() - true - } else { - super.onOptionsItemSelected(item) - } - } -} diff --git a/app/src/main/java/com/android/tvflix/favorite/FavoriteShowsAdapter.kt b/app/src/main/java/com/android/tvflix/favorite/FavoriteShowsAdapter.kt deleted file mode 100644 index 34bdfa5..0000000 --- a/app/src/main/java/com/android/tvflix/favorite/FavoriteShowsAdapter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.android.tvflix.favorite - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.ImageView -import androidx.appcompat.content.res.AppCompatResources -import androidx.recyclerview.widget.RecyclerView -import com.android.tvflix.R -import com.android.tvflix.databinding.ShowListItemBinding -import com.android.tvflix.db.favouriteshow.FavoriteShow -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.bumptech.glide.request.RequestOptions - -class FavoriteShowsAdapter( - private val favoriteShows: MutableList -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteShowHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val showListItemBinding = ShowListItemBinding.inflate(layoutInflater, parent, false) - val holder = FavoriteShowHolder(showListItemBinding) - holder.binding.showFavoriteIcon = false - return holder - } - - override fun onBindViewHolder(holder: FavoriteShowHolder, position: Int) { - val favoriteShow = favoriteShows[position] - Glide.with(holder.itemView.context).load(favoriteShow.imageUrl) - .apply(RequestOptions.placeholderOf(R.color.grey)) - .transition(DrawableTransitionOptions.withCrossFade()) - .into(holder.binding.showImage) - } - - override fun getItemCount(): Int { - return favoriteShows.size - } - - class FavoriteShowHolder(val binding: ShowListItemBinding) : - RecyclerView.ViewHolder(binding.root) -} diff --git a/app/src/main/java/com/android/tvflix/home/HomeActivity.kt b/app/src/main/java/com/android/tvflix/home/HomeActivity.kt deleted file mode 100644 index 2b0fa4e..0000000 --- a/app/src/main/java/com/android/tvflix/home/HomeActivity.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.android.tvflix.home - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.widget.Toast -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.GridLayoutManager -import com.android.tvflix.R -import com.android.tvflix.config.FavoritesFeatureFlag -import com.android.tvflix.databinding.ActivityHomeBinding -import com.android.tvflix.domain.GetSchedulesUseCase -import com.android.tvflix.favorite.FavoriteShowsActivity -import com.android.tvflix.shows.AllShowsActivity -import com.android.tvflix.utils.GridItemDecoration -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collect -import javax.inject.Inject - -@AndroidEntryPoint -class HomeActivity : AppCompatActivity(), ShowsAdapter.Callback { - private val homeViewModel: HomeViewModel by viewModels() - private lateinit var showsAdapter: ShowsAdapter - private val binding by lazy { ActivityHomeBinding.inflate(layoutInflater) } - - @JvmField - @FavoritesFeatureFlag - @Inject - var favoritesFeatureEnable: Boolean = false - - companion object { - private const val NO_OF_COLUMNS = 2 - - @JvmStatic - fun start( - context: Activity - ) { - val intent = Intent(context, HomeActivity::class.java) - .apply { - flags = (Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - or Intent.FLAG_ACTIVITY_CLEAR_TOP) - } - context.startActivity(intent) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - setToolbar() - homeViewModel.onScreenCreated() - lifecycleScope.launchWhenStarted { - homeViewModel.homeViewStateFlow.collect { setViewState(it) } - } - } - - private fun setViewState(homeViewState: HomeViewState) { - when (homeViewState) { - is HomeViewState.Loading -> binding.progress.isVisible = true - is HomeViewState.NetworkError -> { - binding.progress.isVisible = false - showError(homeViewState.message!!) - } - is HomeViewState.Success -> { - with(binding) { - progress.isVisible = false - popularShowHeader.text = homeViewState.homeViewData.heading - popularShowHeader.isVisible = true - } - showPopularShows(homeViewState.homeViewData) - } - is HomeViewState.AddedToFavorites -> - Toast.makeText( - this, - getString(R.string.added_to_favorites, homeViewState.show.name), - Toast.LENGTH_SHORT - ).show() - is HomeViewState.RemovedFromFavorites -> - Toast.makeText( - this, - getString(R.string.removed_from_favorites, homeViewState.show.name), - Toast.LENGTH_SHORT - ).show() - } - } - - private fun setToolbar() { - val toolbar = binding.toolbar.toolbar - setSupportActionBar(toolbar) - toolbar.setTitleTextColor(ContextCompat.getColor(this, android.R.color.white)) - toolbar.setSubtitleTextColor(ContextCompat.getColor(this, android.R.color.white)) - setTitle(R.string.app_name) - } - - private fun showPopularShows(homeViewData: HomeViewData) { - val gridLayoutManager = GridLayoutManager(this, NO_OF_COLUMNS) - showsAdapter = ShowsAdapter(this, favoritesFeatureEnable) - showsAdapter.updateList(homeViewData.episodes.toMutableList()) - binding.popularShows.apply { - layoutManager = gridLayoutManager - setHasFixedSize(true) - adapter = showsAdapter - val spacing = resources.getDimensionPixelSize(R.dimen.show_grid_spacing) - addItemDecoration(GridItemDecoration(spacing, NO_OF_COLUMNS)) - } - } - - private fun showError(message: String) { - Toast.makeText(this, message, Toast.LENGTH_LONG).show() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.home_menu, menu) - menu.findItem(R.id.action_favorites).isVisible = favoritesFeatureEnable - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_shows -> { - AllShowsActivity.start(this) - true - } - R.id.action_favorites -> { - FavoriteShowsActivity.start(this) - true - } - else -> super.onOptionsItemSelected(item) - } - } - - override fun onFavoriteClicked(showViewData: HomeViewData.ShowViewData) { - homeViewModel.onFavoriteClick(showViewData) - } -} diff --git a/app/src/main/java/com/android/tvflix/home/ShowDiffUtilCallback.kt b/app/src/main/java/com/android/tvflix/home/ShowDiffUtilCallback.kt deleted file mode 100644 index 4321103..0000000 --- a/app/src/main/java/com/android/tvflix/home/ShowDiffUtilCallback.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.android.tvflix.home - -import android.os.Bundle -import androidx.recyclerview.widget.DiffUtil - -class ShowDiffUtilCallback( - private val oldList: List, - private val newList: List -) : DiffUtil.Callback() { - - override fun getOldListSize(): Int { - return oldList.size - } - - override fun getNewListSize(): Int { - return newList.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldEpisodeViewData = oldList[oldItemPosition] - val newEpisodeViewData = newList[newItemPosition] - return oldEpisodeViewData.showViewData.show.id == newEpisodeViewData.showViewData.show.id - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldEpisodeViewData = oldList[oldItemPosition] - val newEpisodeViewData = newList[newItemPosition] - return oldEpisodeViewData.showViewData.isFavoriteShow == newEpisodeViewData.showViewData.isFavoriteShow - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Bundle { - val oldEpisodeViewData = oldList[oldItemPosition] - val newEpisodeViewData = newList[newItemPosition] - val bundle = Bundle() - if (oldEpisodeViewData.showViewData.isFavoriteShow != newEpisodeViewData.showViewData.isFavoriteShow) { - bundle.putBoolean(IS_FAVORITE, newEpisodeViewData.showViewData.isFavoriteShow) - } - return bundle - } -} diff --git a/app/src/main/java/com/android/tvflix/home/ShowsAdapter.kt b/app/src/main/java/com/android/tvflix/home/ShowsAdapter.kt deleted file mode 100644 index 1801f47..0000000 --- a/app/src/main/java/com/android/tvflix/home/ShowsAdapter.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.android.tvflix.home - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.ImageView -import androidx.appcompat.content.res.AppCompatResources -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.android.tvflix.R -import com.android.tvflix.databinding.ShowListItemBinding -import com.android.tvflix.network.home.Show -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.bumptech.glide.request.RequestOptions - -const val IS_FAVORITE = "IS_FAVORITE" - -class ShowsAdapter( - private val callback: Callback, - private val favoritesFeatureEnable: Boolean -) : RecyclerView.Adapter() { - private var episodeViewDataList: MutableList = mutableListOf() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShowHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val showListItemBinding = ShowListItemBinding - .inflate(layoutInflater, parent, false) - val showHolder = ShowHolder(showListItemBinding) - showHolder.binding.showFavoriteIcon = favoritesFeatureEnable - showHolder.binding.favorite.setOnClickListener { onFavouriteIconClicked(showHolder.absoluteAdapterPosition) } - return showHolder - } - - fun updateList(newList: MutableList) { - val showDiffUtilCallback = ShowDiffUtilCallback(episodeViewDataList, newList) - val diffResult = DiffUtil.calculateDiff(showDiffUtilCallback) - episodeViewDataList = newList - diffResult.dispatchUpdatesTo(this) - } - - private fun onFavouriteIconClicked(position: Int) { - if (position != RecyclerView.NO_POSITION) { - val episodeViewData = episodeViewDataList[position] - val isFavorite = episodeViewData.showViewData.isFavoriteShow - val updatedShowViewData = - episodeViewData.showViewData.copy(isFavoriteShow = !isFavorite) - val updatedEpisodeViewData = episodeViewData.copy(showViewData = updatedShowViewData) - episodeViewDataList[position] = updatedEpisodeViewData - notifyItemChanged(position) - callback.onFavoriteClicked(episodeViewData.showViewData) - } - } - - override fun onBindViewHolder(holder: ShowHolder, position: Int) { - val episodeViewData = episodeViewDataList[position] - configureImage(holder.binding.showImage, episodeViewData.showViewData.show) - configureFavoriteIcon(holder.binding.favorite, episodeViewData.showViewData.isFavoriteShow) - } - - override fun onBindViewHolder( - holder: ShowHolder, - position: Int, - payloads: List - ) { - if (payloads.isNotEmpty()) { - val bundle = payloads[0] as Bundle - val isFavorite = bundle.getBoolean(IS_FAVORITE) - configureFavoriteIcon(holder.binding.favorite, isFavorite) - } else { - onBindViewHolder(holder, position) - } - } - - private fun configureFavoriteIcon(favoriteIcon: ImageView, favorite: Boolean) { - if (favorite) { - val favoriteDrawable = AppCompatResources - .getDrawable(favoriteIcon.context, R.drawable.favorite) - favoriteIcon.setImageDrawable(favoriteDrawable) - } else { - val unFavoriteDrawable = AppCompatResources - .getDrawable(favoriteIcon.context, R.drawable.favorite_border) - favoriteIcon.setImageDrawable(unFavoriteDrawable) - } - } - - private fun configureImage(showImage: ImageView, show: Show) { - if (show.image != null) { - Glide.with(showImage.context).load(show.image[ORIGINAL_IMAGE]) - .apply(RequestOptions.placeholderOf(R.color.grey)) - .transition(DrawableTransitionOptions.withCrossFade()) - .into(showImage) - } - } - - override fun getItemCount(): Int { - return episodeViewDataList.size - } - - class ShowHolder(val binding: ShowListItemBinding) : RecyclerView.ViewHolder(binding.root) - - interface Callback { - fun onFavoriteClicked(showViewData: HomeViewData.ShowViewData) - } - - companion object { - private const val ORIGINAL_IMAGE = "original" - } -} diff --git a/app/src/main/java/com/android/tvflix/main/MainActivity.kt b/app/src/main/java/com/android/tvflix/main/MainActivity.kt new file mode 100644 index 0000000..aafd9da --- /dev/null +++ b/app/src/main/java/com/android/tvflix/main/MainActivity.kt @@ -0,0 +1,560 @@ +package com.android.tvflix.main + +import android.widget.Toast +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.core.text.HtmlCompat +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import coil.compose.AsyncImage +import com.android.tvflix.R +import com.android.tvflix.config.FavoritesFeatureFlag +import com.android.tvflix.db.favouriteshow.FavoriteShow +import com.android.tvflix.favorite.FavoriteShowState +import com.android.tvflix.favorite.FavoriteShowsViewModel +import com.android.tvflix.home.HomeViewData +import com.android.tvflix.home.HomeViewModel +import com.android.tvflix.home.HomeViewState +import com.android.tvflix.network.home.Show +import com.android.tvflix.shows.ShowsViewModel +import com.android.tvflix.splash.SplashViewModel +import com.android.tvflix.splash.SplashViewState +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + @JvmField + @FavoritesFeatureFlag + @Inject + var favoritesFeatureEnable: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + TvFlixApp(favoritesFeatureEnable = favoritesFeatureEnable) + } + } +} + +private object Route { + const val Splash = "splash" + const val Home = "home" + const val Shows = "shows" + const val Favorites = "favorites" +} + +@Composable +private fun TvFlixApp(favoritesFeatureEnable: Boolean) { + val navController = rememberNavController() + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = backStackEntry?.destination?.route ?: Route.Splash + + Scaffold( + topBar = { + AppBar( + route = currentRoute, + favoritesFeatureEnable = favoritesFeatureEnable, + onShowsClick = { navController.navigate(Route.Shows) { launchSingleTop = true } }, + onFavoritesClick = { navController.navigate(Route.Favorites) { launchSingleTop = true } }, + onBackClick = { navController.popBackStack() } + ) + } + ) { padding -> + NavHost( + navController = navController, + startDestination = Route.Splash, + modifier = if (currentRoute == Route.Splash) Modifier else Modifier.padding(padding) + ) { + composable(Route.Splash) { + SplashRoute(navController = navController) + } + composable(Route.Home) { + HomeRoute(favoritesFeatureEnable = favoritesFeatureEnable) + } + composable(Route.Shows) { + ShowsRoute() + } + composable(Route.Favorites) { + FavoritesRoute() + } + } + } +} + +@Composable +private fun AppBar( + route: String, + favoritesFeatureEnable: Boolean, + onShowsClick: () -> Unit, + onFavoritesClick: () -> Unit, + onBackClick: () -> Unit +) { + if (route == Route.Splash) return + + TopAppBar( + title = { + Text( + text = when (route) { + Route.Shows -> stringResource(id = R.string.shows) + Route.Favorites -> stringResource(id = R.string.favorite_shows) + else -> stringResource(id = R.string.app_name) + } + ) + }, + backgroundColor = colorResource(id = R.color.colorPrimary), + contentColor = colorResource(id = R.color.white), + navigationIcon = if (route == Route.Shows || route == Route.Favorites) { + { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + } else { + null + }, + actions = { + if (route == Route.Home) { + Text( + text = stringResource(id = R.string.shows), + modifier = Modifier + .testTag("nav_shows") + .clickable(onClick = onShowsClick) + .padding(8.dp) + ) + if (favoritesFeatureEnable) { + IconButton( + onClick = onFavoritesClick, + modifier = Modifier.testTag("nav_favorites") + ) { + Icon( + painter = painterResource(id = R.drawable.favorite), + contentDescription = stringResource(id = R.string.favorite_shows), + tint = colorResource(id = R.color.colorAccent) + ) + } + } + } + } + ) +} + +@Composable +private fun SplashRoute(navController: NavHostController) { + val splashViewModel: SplashViewModel = hiltViewModel() + val splashState by splashViewModel.splashViewStateFlow.collectAsState() + + LaunchedEffect(Unit) { + splashViewModel.fetchConfig() + } + + LaunchedEffect(splashState) { + if (splashState is SplashViewState.NavigateToHome) { + navController.navigate(Route.Home) { + popUpTo(Route.Splash) { inclusive = true } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .testTag("splash_screen") + .background(colorResource(id = R.color.colorPrimaryDark)), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + painter = painterResource(id = R.drawable.ic_splash), + contentDescription = stringResource(id = R.string.app_name), + modifier = Modifier.size(96.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + CircularProgressIndicator(color = colorResource(id = R.color.colorAccent)) + } + } +} + +@Composable +private fun HomeRoute(favoritesFeatureEnable: Boolean) { + val homeViewModel: HomeViewModel = hiltViewModel() + val homeState by homeViewModel.homeViewStateFlow.collectAsState() + val context = LocalContext.current + var homeViewData by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + homeViewModel.onScreenCreated() + } + + LaunchedEffect(homeState) { + when (val state = homeState) { + is HomeViewState.Success -> homeViewData = state.homeViewData + is HomeViewState.NetworkError -> + state.message?.let { Toast.makeText(context, it, Toast.LENGTH_LONG).show() } + is HomeViewState.AddedToFavorites -> Toast + .makeText( + context, + context.getString(R.string.added_to_favorites, state.show.name), + Toast.LENGTH_SHORT + ) + .show() + is HomeViewState.RemovedFromFavorites -> Toast + .makeText( + context, + context.getString(R.string.removed_from_favorites, state.show.name), + Toast.LENGTH_SHORT + ) + .show() + HomeViewState.Loading -> Unit + } + } + + when { + homeViewData != null -> { + HomeScreen( + homeViewData = homeViewData!!, + favoritesFeatureEnable = favoritesFeatureEnable, + onFavoriteClick = { episode -> + val updatedList = homeViewData!!.episodes.map { + if (it.id == episode.id) { + it.copy( + showViewData = it.showViewData.copy( + isFavoriteShow = !it.showViewData.isFavoriteShow + ) + ) + } else { + it + } + } + homeViewData = homeViewData!!.copy(episodes = updatedList) + homeViewModel.onFavoriteClick(episode.showViewData) + } + ) + } + homeState is HomeViewState.Loading -> CenterLoader() + else -> CenterError(text = "Unable to load shows.") + } +} + +@Composable +private fun HomeScreen( + homeViewData: HomeViewData, + favoritesFeatureEnable: Boolean, + onFavoriteClick: (HomeViewData.EpisodeViewData) -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + ) { + Text( + text = homeViewData.heading, + style = MaterialTheme.typography.h6, + color = colorResource(id = R.color.white), + modifier = Modifier + .testTag("home_heading") + .padding(vertical = 8.dp) + ) + val rows = remember(homeViewData.episodes) { homeViewData.episodes.chunked(2) } + LazyColumn( + modifier = Modifier.testTag("home_list"), + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(rows.size) { rowIndex -> + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + rows[rowIndex].forEach { episode -> + ShowPosterCard( + imageUrl = episode.showViewData.show.image?.get("original"), + favoriteEnabled = favoritesFeatureEnable, + isFavorite = episode.showViewData.isFavoriteShow, + onFavoriteClick = { onFavoriteClick(episode) }, + modifier = Modifier.weight(1f) + ) + } + if (rows[rowIndex].size == 1) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } +} + +@Composable +private fun ShowPosterCard( + imageUrl: String?, + favoriteEnabled: Boolean, + isFavorite: Boolean, + onFavoriteClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(260.dp) + .clip(RoundedCornerShape(4.dp)) + ) { + AsyncImage( + model = imageUrl, + contentDescription = stringResource(id = R.string.show_image), + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + if (favoriteEnabled) { + IconButton( + onClick = onFavoriteClick, + modifier = Modifier + .padding(8.dp) + .background( + color = Color.Black.copy(alpha = 0.35f), + shape = RoundedCornerShape(20.dp) + ) + .align(Alignment.TopStart) + ) { + Icon( + painter = painterResource( + id = if (isFavorite) R.drawable.favorite else R.drawable.favorite_border + ), + contentDescription = "Favorite", + tint = colorResource(id = R.color.colorAccent) + ) + } + } + } +} + +@Composable +private fun ShowsRoute() { + val showsViewModel: ShowsViewModel = hiltViewModel() + val shows = remember(showsViewModel) { showsViewModel.shows() }.collectAsLazyPagingItems() + + when (val refresh = shows.loadState.refresh) { + is LoadState.Loading -> if (shows.itemCount == 0) CenterLoader() + is LoadState.Error -> if (shows.itemCount == 0) { + CenterError( + text = refresh.error.localizedMessage ?: "No internet connection", + showRetry = true, + onRetry = shows::retry + ) + } + is LoadState.NotLoading -> Unit + } + + if (shows.itemCount > 0) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .testTag("shows_list"), + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(shows.itemCount) { index -> + shows[index]?.let { ShowDetailsCard(show = it) } + } + if (shows.loadState.append is LoadState.Loading) { + item { CenterLoader(modifier = Modifier.fillMaxWidth()) } + } + if (shows.loadState.append is LoadState.Error) { + item { + CenterError( + text = (shows.loadState.append as LoadState.Error).error.localizedMessage + ?: "Failed to load more", + showRetry = true, + onRetry = shows::retry + ) + } + } + } + } +} + +@Composable +private fun ShowDetailsCard(show: Show) { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = 4.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .padding(8.dp) + ) { + AsyncImage( + model = show.image?.get("original"), + contentDescription = stringResource(id = R.string.show_image), + modifier = Modifier + .width(120.dp) + .height(150.dp) + .clip(RoundedCornerShape(6.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.size(8.dp)) + Column(modifier = Modifier.fillMaxWidth()) { + Text(text = show.name, style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) + show.summary?.let { + Text( + text = HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString(), + maxLines = 4, + style = MaterialTheme.typography.body2 + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (show.rating?.get("average") != null) { + stringResource(id = R.string.rating, show.rating?.get("average") ?: "") + } else { + stringResource(id = R.string.not_rated) + }, + style = MaterialTheme.typography.caption + ) + show.premiered?.let { + Text( + text = stringResource(id = R.string.premiered_on, it), + style = MaterialTheme.typography.caption + ) + } + } + } + } +} + +@Composable +private fun FavoritesRoute() { + val favoriteShowsViewModel: FavoriteShowsViewModel = hiltViewModel() + val favoriteState by favoriteShowsViewModel.favoriteShowsStateFlow.collectAsState() + + LaunchedEffect(Unit) { + favoriteShowsViewModel.loadFavoriteShows() + } + + when (val state = favoriteState) { + FavoriteShowState.Loading -> CenterLoader() + is FavoriteShowState.AllFavorites -> FavoriteShowsGrid(favoriteShows = state.favoriteShows) + FavoriteShowState.Empty -> CenterError(text = stringResource(id = R.string.favorite_hint_msg)) + is FavoriteShowState.Error -> CenterError(text = state.message) + is FavoriteShowState.AddedToFavorites -> Unit + is FavoriteShowState.RemovedFromFavorites -> Unit + } +} + +@Composable +private fun FavoriteShowsGrid(favoriteShows: List) { + val rows = remember(favoriteShows) { favoriteShows.chunked(2) } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .testTag("favorites_list") + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(rows.size) { rowIndex -> + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + rows[rowIndex].forEach { show -> + ShowPosterCard( + imageUrl = show.imageUrl, + favoriteEnabled = false, + isFavorite = false, + onFavoriteClick = {}, + modifier = Modifier.weight(1f) + ) + } + if (rows[rowIndex].size == 1) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun CenterLoader(modifier: Modifier = Modifier.fillMaxSize()) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = colorResource(id = R.color.colorAccent)) + } +} + +@Composable +private fun CenterError( + text: String, + showRetry: Boolean = false, + onRetry: (() -> Unit)? = null +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = text, color = colorResource(id = R.color.red)) + if (showRetry && onRetry != null) { + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = onRetry, + modifier = Modifier.testTag("retry") + ) { + Text(text = stringResource(id = R.string.retry)) + } + } + } + } +} diff --git a/app/src/main/java/com/android/tvflix/shows/AllShowsActivity.kt b/app/src/main/java/com/android/tvflix/shows/AllShowsActivity.kt deleted file mode 100644 index 0dcf945..0000000 --- a/app/src/main/java/com/android/tvflix/shows/AllShowsActivity.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.android.tvflix.shows - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.widget.Toast -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import androidx.paging.LoadState -import com.android.tvflix.R -import com.android.tvflix.databinding.ActivityAllShowsBinding -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest - -@AndroidEntryPoint -class AllShowsActivity : AppCompatActivity() { - private val showsViewModel: ShowsViewModel by viewModels() - private lateinit var adapter: ShowsPagedAdapter - private lateinit var showsBinding: ActivityAllShowsBinding - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - showsBinding = ActivityAllShowsBinding.inflate(LayoutInflater.from(this)) - setContentView(showsBinding.root) - setToolbar() - initAdapter() - getShows() - showsBinding.retry.setOnClickListener { adapter.retry() } - } - - private fun getShows() { - lifecycleScope.launchWhenStarted { - showsViewModel.shows().collectLatest { adapter.submitData(it) } - } - } - - private fun initAdapter() { - adapter = ShowsPagedAdapter(ShowDiffUtilItemCallback()) - showsBinding.shows.adapter = adapter.withLoadStateFooter( - footer = ShowsLoadStateAdapter { adapter.retry() } - ) - - adapter.addLoadStateListener { combinedLoadStates -> - // Handle the initial load state - when (val loadState = combinedLoadStates.source.refresh) { - is LoadState.NotLoading -> { - showsBinding.progress.isVisible = false - showsBinding.shows.isVisible = true - showsBinding.errorGroup.isVisible = false - } - is LoadState.Loading -> { - showsBinding.progress.isVisible = true - showsBinding.errorGroup.isVisible = false - } - is LoadState.Error -> { - showsBinding.progress.isVisible = false - showsBinding.errorGroup.isVisible = true - showsBinding.errorMsg.text = loadState.error.localizedMessage - } - } - - // Show message to the user when an error comes while loading the next page - val errorState = combinedLoadStates.source.append as? LoadState.Error - ?: combinedLoadStates.append as? LoadState.Error - errorState?.let { - Toast.makeText(this, errorState.error.localizedMessage, Toast.LENGTH_SHORT).show() - } - } - } - - private fun setToolbar() { - val toolbar = showsBinding.toolbar.toolbar - setSupportActionBar(toolbar) - toolbar.setTitleTextColor(ContextCompat.getColor(this, android.R.color.white)) - toolbar.setSubtitleTextColor(ContextCompat.getColor(this, android.R.color.white)) - supportActionBar!!.setDisplayHomeAsUpEnabled(true) - setTitle(R.string.shows) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return if (item.itemId == android.R.id.home) { - finish() - true - } else { - super.onOptionsItemSelected(item) - } - } - - companion object { - fun start(context: Context) { - val starter = Intent(context, AllShowsActivity::class.java) - context.startActivity(starter) - } - } -} diff --git a/app/src/main/java/com/android/tvflix/shows/ShowDiffUtilItemCallback.kt b/app/src/main/java/com/android/tvflix/shows/ShowDiffUtilItemCallback.kt deleted file mode 100644 index a0626b5..0000000 --- a/app/src/main/java/com/android/tvflix/shows/ShowDiffUtilItemCallback.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.tvflix.shows - -import androidx.recyclerview.widget.DiffUtil -import com.android.tvflix.network.home.Show - -class ShowDiffUtilItemCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Show, newItem: Show): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: Show, newItem: Show): Boolean { - return oldItem.name == newItem.name - } -} diff --git a/app/src/main/java/com/android/tvflix/shows/ShowsLoadStateAdapter.kt b/app/src/main/java/com/android/tvflix/shows/ShowsLoadStateAdapter.kt deleted file mode 100644 index 9589bdd..0000000 --- a/app/src/main/java/com/android/tvflix/shows/ShowsLoadStateAdapter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.tvflix.shows - -import android.view.ViewGroup -import androidx.paging.LoadState -import androidx.paging.LoadStateAdapter - -class ShowsLoadStateAdapter(private val retry: () -> Unit) : - LoadStateAdapter() { - override fun onBindViewHolder(holder: ShowsStateViewHolder, loadState: LoadState) { - holder.bind(loadState) - } - - override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ShowsStateViewHolder { - return ShowsStateViewHolder.create(parent, retry) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/android/tvflix/shows/ShowsPagedAdapter.kt b/app/src/main/java/com/android/tvflix/shows/ShowsPagedAdapter.kt deleted file mode 100644 index 23c50e1..0000000 --- a/app/src/main/java/com/android/tvflix/shows/ShowsPagedAdapter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.android.tvflix.shows - -import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.paging.PagingDataAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.android.tvflix.R -import com.android.tvflix.databinding.AllShowListItemBinding -import com.android.tvflix.network.home.Show -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.bumptech.glide.request.RequestOptions - -class ShowsPagedAdapter constructor( - diffCallback: DiffUtil.ItemCallback -) : PagingDataAdapter(diffCallback) { - private lateinit var context: Context - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - context = parent.context - val layoutInflater = LayoutInflater.from(context) - val showListItemBinding = AllShowListItemBinding.inflate(layoutInflater, parent, false) - return ShowHolder(showListItemBinding) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ShowHolder) { - val show = getItem(position) - holder.binding.show = show - if (show!!.rating != null) { - holder.binding.rating = show.rating!!["average"] - } - configureImage(holder, show) - } - } - - private fun configureImage(holder: ShowHolder, show: Show) { - if (show.image != null) { - Glide.with(context).load(show.image["original"]) - .apply(RequestOptions.placeholderOf(R.color.grey)) - .centerCrop() - .transition(DrawableTransitionOptions.withCrossFade()) - .into(holder.binding.showImage) - } - } - - class ShowHolder(val binding: AllShowListItemBinding) : RecyclerView.ViewHolder(binding.root) -} diff --git a/app/src/main/java/com/android/tvflix/shows/ShowsStateViewHolder.kt b/app/src/main/java/com/android/tvflix/shows/ShowsStateViewHolder.kt deleted file mode 100644 index a7a100a..0000000 --- a/app/src/main/java/com/android/tvflix/shows/ShowsStateViewHolder.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.android.tvflix.shows - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.paging.LoadState -import androidx.recyclerview.widget.RecyclerView -import com.android.tvflix.databinding.NetworkFailureListItemBinding - -class ShowsStateViewHolder( - private val binding: NetworkFailureListItemBinding, - retry: () -> Unit -) : RecyclerView.ViewHolder(binding.root) { - init { - binding.retry.setOnClickListener { retry.invoke() } - } - - fun bind(loadState: LoadState) { - if (loadState is LoadState.Error) { - binding.errorMsg.text = loadState.error.localizedMessage - } - binding.progress.isVisible = loadState is LoadState.Loading - binding.errorGroup.isVisible = loadState !is LoadState.Loading - } - - companion object { - fun create(parent: ViewGroup, retry: () -> Unit): ShowsStateViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val binding = NetworkFailureListItemBinding.inflate(layoutInflater, parent, false) - return ShowsStateViewHolder(binding, retry) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/android/tvflix/splash/SplashActivity.kt b/app/src/main/java/com/android/tvflix/splash/SplashActivity.kt deleted file mode 100644 index 01b2cdd..0000000 --- a/app/src/main/java/com/android/tvflix/splash/SplashActivity.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.tvflix.splash - -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import com.android.tvflix.R -import com.android.tvflix.home.HomeActivity -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collect - -@AndroidEntryPoint -class SplashActivity : AppCompatActivity() { - private val splashViewModel: SplashViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_splash) - splashViewModel.fetchConfig() - lifecycleScope.launchWhenStarted { - splashViewModel.splashViewStateFlow.collect { - when (it) { - is SplashViewState.Idle -> { - // do nothing - } - is SplashViewState.NavigateToHome -> HomeActivity.start(this@SplashActivity) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/android/tvflix/splash/SplashViewModel.kt b/app/src/main/java/com/android/tvflix/splash/SplashViewModel.kt index 5cb475d..210491f 100644 --- a/app/src/main/java/com/android/tvflix/splash/SplashViewModel.kt +++ b/app/src/main/java/com/android/tvflix/splash/SplashViewModel.kt @@ -24,7 +24,13 @@ constructor( val splashViewStateFlow = _splashViewStateFlow.asStateFlow() fun fetchConfig() { viewModelScope.launch(dispatcher) { - appConfig.fetch() + try { + appConfig.fetch() + } catch (_: Exception) { + // Remote config fetch can fail due to network issues or + // Firebase Installations service unavailability. Continue + // with cached/default config values. + } _splashViewStateFlow.emit(SplashViewState.NavigateToHome) } } diff --git a/app/src/main/java/com/android/tvflix/utils/GridItemDecoration.kt b/app/src/main/java/com/android/tvflix/utils/GridItemDecoration.kt deleted file mode 100644 index 0aa8d19..0000000 --- a/app/src/main/java/com/android/tvflix/utils/GridItemDecoration.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.tvflix.utils - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class GridItemDecoration(private val space: Int, private val noOfColumns: Int) : - RecyclerView.ItemDecoration() { - - override fun getItemOffsets( - outRect: Rect, view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - outRect.left = space - outRect.right = space - outRect.bottom = space - outRect.top = space - when { - parent.getChildLayoutPosition(view) % noOfColumns == 0 -> { - outRect.left = 0 - outRect.right = space - } - parent.getChildLayoutPosition(view) % noOfColumns == noOfColumns - 1 -> { - outRect.left = space - outRect.right = 0 - } - } - } -} diff --git a/app/src/main/res/drawable/favorite_gradient.xml b/app/src/main/res/drawable/favorite_gradient.xml deleted file mode 100644 index fc19973..0000000 --- a/app/src/main/res/drawable/favorite_gradient.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml deleted file mode 100644 index 7a67ec3..0000000 --- a/app/src/main/res/drawable/splash_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_all_shows.xml b/app/src/main/res/layout/activity_all_shows.xml deleted file mode 100644 index 72fec58..0000000 --- a/app/src/main/res/layout/activity_all_shows.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - -