diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4970cc6..1025875 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(libs.androidx.navigation.uiKtx) implementation(libs.androidx.coreKtx) + implementation(libs.androidx.splashScreen) implementation(libs.androidx.appcompat) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d2163d..db9ef8d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" - android:theme="@style/AppTheme" + android:theme="@style/Theme.App.Starting" tools:ignore="UnusedAttribute"> = callback } } -fun Activity.setSystemUiVisibility(isVisible: Boolean, logger: Logger) { - logger.v { "setSystemUiVisibility() called with: isVisible = $isVisible" } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible, logger) - else setSystemUiVisibilityV1(isVisible, logger) -} - -@Suppress("DEPRECATION") -private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean, logger: Logger) { - logger.v { "setSystemUiVisibilityV1() called with: isVisible = $isVisible" } - window.decorView.systemUiVisibility = if (isVisible) 0 else View.SYSTEM_UI_FLAG_FULLSCREEN -} - -@RequiresApi(Build.VERSION_CODES.R) -private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean, logger: Logger) { - logger.v { "setSystemUiVisibilityV30() called with: isVisible = $isVisible" } - val systemBars = WindowInsets.Type.systemBars() - window.insetsController?.apply { if (isVisible) show(systemBars) else hide(systemBars) } - ?: logger.w { "setSystemUiVisibilityV30: insets controller is null" } -} - -fun AppCompatActivity.setActionBarVisibility(isVisible: Boolean, logger: Logger) { - logger.v { "setActionBarVisibility() called with: isVisible = $isVisible" } - supportActionBar?.apply { if (isVisible) show() else hide() } - ?: logger.w { "setActionBarVisibility: action bar is null" } -} - fun TextView.textChangesFlow(logger: Logger): Flow = callbackFlow { logger.v { "textChangesFlow() called" } val textWatcher = doAfterTextChanged { @@ -116,4 +86,13 @@ fun SharedPreferences.prefsChangeFlow( sendValue() registerOnSharedPreferenceChangeListener(listener) awaitClose { unregisterOnSharedPreferenceChangeListener(listener) } +} + +fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { + observe(lifecycleOwner, object : Observer { + override fun onChanged(value: T) { + removeObserver(this) + observer.onChanged(value) + } + }) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index e346641..c98aedf 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -6,13 +6,16 @@ import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.isVisible -import androidx.navigation.findNavController +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.MainActivityBinding +import gq.kirmanak.mealient.extensions.observeOnce import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject @@ -23,21 +26,39 @@ class MainActivity : AppCompatActivity() { private val viewModel by viewModels() private val title: String by lazy { getString(R.string.app_name) } private val uiState: MainActivityUiState get() = viewModel.uiState + private val navController: NavController + get() = binding.navHost.getFragment().navController @Inject lateinit var logger: Logger override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } + splashScreen.setKeepOnScreenCondition { viewModel.startDestination.value == null } binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) + configureToolbar() + configureNavGraph() + viewModel.uiStateLive.observe(this, ::onUiStateChange) + binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) + } + + private fun configureNavGraph() { + viewModel.startDestination.observeOnce(this) { + logger.d { "configureNavGraph: received destination" } + val graph = navController.navInflater.inflate(R.navigation.nav_graph) + graph.setStartDestination(it) + navController.setGraph(graph, intent.extras) + } + } + + private fun configureToolbar() { setSupportActionBar(binding.toolbar) binding.toolbar.setNavigationIcon(R.drawable.ic_toolbar) binding.toolbar.setNavigationOnClickListener { binding.drawer.open() } setToolbarRoundCorner() - viewModel.uiStateLive.observe(this, ::onUiStateChange) - binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) } private fun onNavigationItemSelected(menuItem: MenuItem): Boolean { @@ -102,7 +123,7 @@ class MainActivity : AppCompatActivity() { private fun navigateDeepLink(deepLink: String) { logger.v { "navigateDeepLink() called with: deepLink = $deepLink" } - findNavController(binding.navHost.id).navigate(deepLink.toUri()) + navController.navigate(deepLink.toUri()) } companion object { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt index 0a47762..7375832 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt @@ -2,7 +2,10 @@ package gq.kirmanak.mealient.ui.activity import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.auth.AuthRepo +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo +import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -13,6 +16,8 @@ import javax.inject.Inject class MainActivityViewModel @Inject constructor( private val authRepo: AuthRepo, private val logger: Logger, + private val disclaimerStorage: DisclaimerStorage, + private val serverInfoRepo: ServerInfoRepo, ) : ViewModel() { private val _uiState = MutableLiveData(MainActivityUiState()) @@ -22,10 +27,21 @@ class MainActivityViewModel @Inject constructor( get() = checkNotNull(_uiState.value) { "UiState must not be null" } private set(value) = _uiState.postValue(value) + private val _startDestination = MutableLiveData() + val startDestination: LiveData = _startDestination + init { authRepo.isAuthorizedFlow .onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } } .launchIn(viewModelScope) + + viewModelScope.launch { + _startDestination.value = when { + !disclaimerStorage.isDisclaimerAccepted() -> R.id.disclaimerFragment + serverInfoRepo.getUrl() == null -> R.id.baseURLFragment + else -> R.id.recipesFragment + } + } } fun updateUiState(updater: (MainActivityUiState) -> MainActivityUiState) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt deleted file mode 100644 index f2978b1..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt +++ /dev/null @@ -1,53 +0,0 @@ -package gq.kirmanak.mealient.ui.splash - -import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.NavDirections -import androidx.navigation.fragment.findNavController -import dagger.hilt.android.AndroidEntryPoint -import gq.kirmanak.mealient.R -import gq.kirmanak.mealient.extensions.setActionBarVisibility -import gq.kirmanak.mealient.extensions.setSystemUiVisibility -import gq.kirmanak.mealient.logging.Logger -import javax.inject.Inject - -@AndroidEntryPoint -class SplashFragment : Fragment(R.layout.fragment_splash) { - - private val viewModel by viewModels() - - @Inject - lateinit var logger: Logger - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } - viewModel.nextDestination.observe(this, ::onNextDestination) - } - - private fun onNextDestination(navDirections: NavDirections) { - logger.v { "onNextDestination() called with: navDirections = $navDirections" } - findNavController().navigate(navDirections) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } - changeFullscreenState(true) - } - - override fun onDestroyView() { - super.onDestroyView() - logger.v { "onDestroyView() called" } - changeFullscreenState(false) - } - - private fun changeFullscreenState(isFullscreen: Boolean) { - logger.v { "changeFullscreenState() called with: isFullscreen = $isFullscreen" } - (activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen, logger) - activity?.setSystemUiVisibility(!isFullscreen, logger) - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt deleted file mode 100644 index 399f860..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -package gq.kirmanak.mealient.ui.splash - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.navigation.NavDirections -import dagger.hilt.android.lifecycle.HiltViewModel -import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo -import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SplashViewModel @Inject constructor( - private val disclaimerStorage: DisclaimerStorage, - private val serverInfoRepo: ServerInfoRepo, -) : ViewModel() { - private val _nextDestination = MutableLiveData() - val nextDestination: LiveData = _nextDestination - - init { - viewModelScope.launch { - delay(1000) - _nextDestination.value = when { - !disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment() - serverInfoRepo.getUrl() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment() - else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment() - } - } - } -} diff --git a/app/src/main/res/drawable-v31/ic_splash_screen.xml b/app/src/main/res/drawable-v31/ic_splash_screen.xml new file mode 100644 index 0000000..3881f89 --- /dev/null +++ b/app/src/main/res/drawable-v31/ic_splash_screen.xml @@ -0,0 +1,465 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_background.xml rename to app/src/main/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/drawable/ic_splash_screen.xml b/app/src/main/res/drawable/ic_splash_screen.xml new file mode 100644 index 0000000..454093a --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_screen.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_splash_screen_background.xml b/app/src/main/res/drawable/ic_splash_screen_background.xml index e3007b4..dcd8800 100644 --- a/app/src/main/res/drawable/ic_splash_screen_background.xml +++ b/app/src/main/res/drawable/ic_splash_screen_background.xml @@ -1,55 +1,31 @@ - - - - - - - - - - - - - - - - + xmlns:aapt="http://schemas.android.com/aapt" + android:width="288dp" + android:height="288dp" + android:viewportWidth="288" + android:viewportHeight="288"> + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_splash_screen_foreground.xml b/app/src/main/res/drawable/ic_splash_screen_foreground.xml new file mode 100644 index 0000000..98a9788 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_screen_foreground.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml deleted file mode 100644 index 79cb841..0000000 --- a/app/src/main/res/layout/fragment_splash.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index e788a45..f5b97f5 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -33,8 +33,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" - app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:navGraph="@navigation/nav_graph" /> + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + tools:ignore="InvalidNavigation"> - - - - - - + + + \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/Versions.kt b/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/Versions.kt index 7f705a9..db4359f 100644 --- a/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/Versions.kt +++ b/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/Versions.kt @@ -6,7 +6,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.getByType object Versions { - const val MIN_SDK_VERSION = 23 + const val MIN_SDK_VERSION = 26 const val TARGET_SDK_VERSION = 33 const val COMPILE_SDK_VERSION = 33 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 718bc8f..31cbf30 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ appcompat = "1.5.1" contraintLayout = "2.1.4" # https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout swipeRefreshLayout = "1.1.0" +# https://developer.android.com/jetpack/androidx/releases/core +splashScreen = "1.0.0" # https://developer.android.com/jetpack/androidx/releases/lifecycle lifecycle = "2.5.1" # https://github.com/square/retrofit/tags @@ -118,6 +120,7 @@ androidx-coreKtx = { group = "androidx.core", name = "core-ktx", version.ref = " androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-constraintLayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "contraintLayout" } androidx-swipeRefreshLayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swipeRefreshLayout" } +androidx-splashScreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" } androidx-paging-runtimeKtx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging" } androidx-paging-commonKtx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "paging" }