From dee5363da088ce8d28fa49759022035b0851586b Mon Sep 17 00:00:00 2001 From: odifek Date: Sun, 7 Aug 2022 15:51:35 +0200 Subject: [PATCH] #16: fix incorrect backstack. Enable transition animations --- .../techbeloved/hymnbook/HymnbookActivity.kt | 67 ++++---- .../sheetmusic/SheetMusicListingFragment.kt | 44 ----- .../sheetmusic/SheetMusicListingViewModel.kt | 58 ------- .../sheetmusic/SheetMusicPagerFragment.kt | 156 ------------------ app/src/main/res/anim/slide_in_left.xml | 25 +++ app/src/main/res/anim/slide_in_right.xml | 25 +++ app/src/main/res/anim/slide_left.xml | 10 ++ app/src/main/res/anim/slide_out_left.xml | 25 +++ app/src/main/res/anim/slide_out_right.xml | 25 +++ app/src/main/res/anim/slide_right.xml | 10 ++ app/src/main/res/anim/wait.xml | 4 + app/src/main/res/navigation/navigation.xml | 102 ++++++++---- .../SheetMusicListingViewModelTest.kt | 75 --------- 13 files changed, 228 insertions(+), 398 deletions(-) delete mode 100644 app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingFragment.kt delete mode 100644 app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingViewModel.kt delete mode 100644 app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicPagerFragment.kt create mode 100644 app/src/main/res/anim/slide_in_left.xml create mode 100644 app/src/main/res/anim/slide_in_right.xml create mode 100644 app/src/main/res/anim/slide_left.xml create mode 100644 app/src/main/res/anim/slide_out_left.xml create mode 100644 app/src/main/res/anim/slide_out_right.xml create mode 100644 app/src/main/res/anim/slide_right.xml create mode 100644 app/src/main/res/anim/wait.xml delete mode 100644 app/src/test/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingViewModelTest.kt diff --git a/app/src/main/java/com/techbeloved/hymnbook/HymnbookActivity.kt b/app/src/main/java/com/techbeloved/hymnbook/HymnbookActivity.kt index 4ec568e..4fbe2c2 100644 --- a/app/src/main/java/com/techbeloved/hymnbook/HymnbookActivity.kt +++ b/app/src/main/java/com/techbeloved/hymnbook/HymnbookActivity.kt @@ -55,11 +55,11 @@ class HymnbookActivity : AppCompatActivity() { navController.addOnDestinationChangedListener { _, destination, _ -> // Hide the bottom navigation in detail view when (destination.id) { - R.id.detailPagerFragment, - R.id.sheetMusicPagerFragment -> if (binding.bottomNavigationMain.isVisible) { + R.id.detailPagerFragment -> if (binding.bottomNavigationMain.isVisible) { binding.bottomNavigationMain.visibility = View.INVISIBLE } - else -> if (!binding.bottomNavigationMain.isVisible) binding.bottomNavigationMain.visibility = View.VISIBLE + else -> if (!binding.bottomNavigationMain.isVisible) binding.bottomNavigationMain.visibility = + View.VISIBLE } } @@ -73,7 +73,9 @@ class HymnbookActivity : AppCompatActivity() { } override fun onOptionsItemSelected(item: MenuItem): Boolean { - return (navController.currentDestination?.id != item.itemId && item.onNavDestinationSelected(navController)) + return (navController.currentDestination?.id != item.itemId && item.onNavDestinationSelected( + navController + )) || super.onOptionsItemSelected(item) } @@ -82,42 +84,45 @@ class HymnbookActivity : AppCompatActivity() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val rxPreferences = RxSharedPreferences.create(sharedPreferences) - val nightModePreference: Preference = rxPreferences.getBoolean(getString(R.string.pref_key_enable_night_mode), false) + val nightModePreference: Preference = + rxPreferences.getBoolean(getString(R.string.pref_key_enable_night_mode), false) val disposable = nightModePreference.asObservable() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { enable -> - - // If already in night mode, do nothing, and otherwise - when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { - Configuration.UI_MODE_NIGHT_NO -> { - if (enable) delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES - else delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - - } - Configuration.UI_MODE_NIGHT_YES -> { - if (!enable) delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } - Configuration.UI_MODE_NIGHT_UNDEFINED -> { - if (!enable) delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { enable -> + + // If already in night mode, do nothing, and otherwise + when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_NO -> { + if (enable) delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + else delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + + } + Configuration.UI_MODE_NIGHT_YES -> { + if (!enable) delegate.localNightMode = + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + Configuration.UI_MODE_NIGHT_UNDEFINED -> { + if (!enable) delegate.localNightMode = + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } } + } disposables.add(disposable) } private fun handleDynamicLinks(intent: Intent) { FirebaseDynamicLinks.getInstance() - .getDynamicLink(intent) - .addOnSuccessListener(this) { pendingDynamicLinkData -> - // Get deep link from result (may be null if no link is found) - val deepLink = pendingDynamicLinkData?.link - if (deepLink != null) { - navController.safeNavigate(deepLink) - } - + .getDynamicLink(intent) + .addOnSuccessListener(this) { pendingDynamicLinkData -> + // Get deep link from result (may be null if no link is found) + val deepLink = pendingDynamicLinkData?.link + if (deepLink != null) { + navController.safeNavigate(deepLink) } - .addOnFailureListener(this) { e -> Timber.w(e, "getDynamicLink:onFailure") } + + } + .addOnFailureListener(this) { e -> Timber.w(e, "getDynamicLink:onFailure") } } override fun onDestroy() { diff --git a/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingFragment.kt b/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingFragment.kt deleted file mode 100644 index 6ffdd9a..0000000 --- a/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.techbeloved.hymnbook.sheetmusic - -import android.view.View -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import androidx.navigation.fragment.findNavController -import com.techbeloved.hymnbook.hymnlisting.BaseHymnListingFragment -import com.techbeloved.hymnbook.hymnlisting.HymnItemModel -import com.techbeloved.hymnbook.usecases.Lce -import com.techbeloved.hymnbook.utils.safeNavigate -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber - -@AndroidEntryPoint -class SheetMusicListingFragment : BaseHymnListingFragment() { - override var title: String - get() = "Sheet Music" - set(value) {} - - private val navController by lazy { findNavController() } - - override fun navigateToHymnDetail(view: View, item: HymnItemModel) { - navController.safeNavigate(SheetMusicListingFragmentDirections.actionSheetMusicListingToSheetMusicPagerFragment(item.id)) - } - - - private val viewModel: SheetMusicListingViewModel by viewModels() - - override fun observeViewModel() { - // Monitor data - viewModel.hymnTitlesLce.observe(viewLifecycleOwner, Observer { - Timber.i("Receiving items") - when (it) { - is Lce.Loading -> showLoadingProgress(it.loading) - is Lce.Content -> displayContent(it.content) - is Lce.Error -> showLoadingProgress(false) // Possibly show error message - } - }) - } - - override fun loadHymnTitles(sortBy: Int) { - viewModel.loadHymnTitlesFromRepo(sortBy) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingViewModel.kt b/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingViewModel.kt deleted file mode 100644 index 696f2f4..0000000 --- a/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicListingViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.techbeloved.hymnbook.sheetmusic - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.techbeloved.hymnbook.hymndetail.BY_NUMBER -import com.techbeloved.hymnbook.hymndetail.SortBy -import com.techbeloved.hymnbook.hymnlisting.TitleItem -import com.techbeloved.hymnbook.usecases.Lce -import com.techbeloved.hymnbook.utils.SchedulerProvider -import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.ObservableTransformer -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.subjects.PublishSubject -import timber.log.Timber -import javax.inject.Inject - -@HiltViewModel -class SheetMusicListingViewModel @Inject constructor(private val useCases: HymnUseCases, - private val schedulerProvider: SchedulerProvider) : ViewModel() { - - private val sortBySubject = PublishSubject.create() - private val disposables: CompositeDisposable = CompositeDisposable() - private val hymnTitlesDataLce: MutableLiveData>> = MutableLiveData() - val hymnTitlesLce: LiveData>> - get() = hymnTitlesDataLce - - init { - loadData() - } - - fun loadHymnTitlesFromRepo(@SortBy sortBy: Int = BY_NUMBER) { - sortBySubject.onNext(sortBy) - } - - private fun loadData() { - sortBySubject.distinctUntilChanged() - .switchMap { sortBy -> - useCases.hymnSheetMusicTitles(sortBy) - } - .compose(getViewState()) - .startWith(Lce.Loading(true)) - .observeOn(schedulerProvider.ui()) - .subscribe({ hymnTitlesDataLce.value = it }, { Timber.w(it, "Error!") }) - .run { disposables.add(this) } - - } - - - override fun onCleared() { - super.onCleared() - if (!disposables.isDisposed) disposables.dispose() - } - - private fun getViewState(): ObservableTransformer> = ObservableTransformer { upstream -> - upstream.map { Lce.Content(it) } - } -} diff --git a/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicPagerFragment.kt b/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicPagerFragment.kt deleted file mode 100644 index d22eab8..0000000 --- a/app/src/main/java/com/techbeloved/hymnbook/sheetmusic/SheetMusicPagerFragment.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.techbeloved.hymnbook.sheetmusic - -import android.annotation.SuppressLint -import android.app.ProgressDialog -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.app.ShareCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import com.google.android.material.snackbar.Snackbar -import com.techbeloved.hymnbook.R -import com.techbeloved.hymnbook.hymndetail.BaseDetailPagerFragment -import com.techbeloved.hymnbook.hymndetail.EXTRA_CURRENT_ITEM_ID -import com.techbeloved.hymnbook.hymndetail.ShareStatus -import com.techbeloved.hymnbook.usecases.Lce -import com.techbeloved.hymnbook.utils.DepthPageTransformer -import com.techbeloved.hymnbook.utils.MINIMUM_VERSION_FOR_SHARE_LINK -import com.techbeloved.hymnbook.utils.WCCRM_LOGO_URL -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber - -@AndroidEntryPoint -class SheetMusicPagerFragment : BaseDetailPagerFragment() { - private val viewModel: SheetMusicPagerViewModel by viewModels() - private lateinit var detailPagerAdapter: DetailPagerAdapter - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Restore current index - if (savedInstanceState != null) { - currentHymnId = savedInstanceState.getInt(EXTRA_CURRENT_ITEM_ID, 1) - } else { - val args = arguments?.let { SheetMusicPagerFragmentArgs.fromBundle(it) } - currentHymnId = args?.hymnId ?: 1 - } - - viewModel.hymnIndicesLive.observe(this, Observer { - when (it) { - is Lce.Loading -> showProgressLoading(it.loading) - is Lce.Content -> { - initializeViewPager(it.content, currentHymnId) - updateHymnItems(it.content) - } - is Lce.Error -> showContentError(it.error) - } - }) - viewModel.loadIndices() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - detailPagerAdapter = DetailPagerAdapter(childFragmentManager) - binding.viewpagerHymnDetail.adapter = detailPagerAdapter - binding.viewpagerHymnDetail.setPageTransformer(true, DepthPageTransformer()) - - viewModel.shareLinkStatus.observe(viewLifecycleOwner, { shareStatus -> - when (shareStatus) { - ShareStatus.Loading -> showShareLoadingDialog() - is ShareStatus.Success -> showShareOptionsChooser(shareStatus.shareLink) - is ShareStatus.Error -> { - showShareError(shareStatus.error) - } - ShareStatus.None -> { - cancelProgressDialog() - } - } - }) - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(EXTRA_CURRENT_ITEM_ID, currentHymnId) - super.onSaveInstanceState(outState) - } - - override fun initiateContentSharing() { - viewModel.requestShareLink( - currentHymnId, - getString(R.string.about_app), - MINIMUM_VERSION_FOR_SHARE_LINK, - WCCRM_LOGO_URL) - } - - private fun showShareError(error: Throwable) { - Timber.w(error) - Snackbar.make(requireView().rootView, "Failure creating share content", Snackbar.LENGTH_SHORT).show() - } - - private fun showShareOptionsChooser(shareLink: String) { - ShareCompat.IntentBuilder.from(requireActivity()).apply { - setChooserTitle(getString(R.string.share_hymn)) - setType("text/plain") - setText(shareLink) - }.startChooser() - - } - - private var progressDialog: ProgressDialog? = null - private fun showShareLoadingDialog() { - progressDialog = ProgressDialog.show(requireContext(), "Share hymn", "Working") - progressDialog?.setCancelable(true) - } - - private fun cancelProgressDialog() { - progressDialog?.cancel() - } - - private fun initializeViewPager(hymnIndices: List, initialIndex: Int) { - Timber.i("Initializing viewPager with index: $initialIndex") - //showProgressLoading(false) - - detailPagerAdapter.submitList(hymnIndices) - // initialIndex represents the hymn number, where as the adapter uses a zero based index - // Which implies that when the indices is sorted by titles, the correct detail won't be shown. - // So we just need to find the index from the list of hymn indices - - val indexToLoad = hymnIndices.indexOf(initialIndex) - binding.viewpagerHymnDetail.currentItem = indexToLoad - } - - @SuppressLint("WrongConstant") - inner class DetailPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - private val hymnIndices = mutableListOf() - override fun getItem(position: Int): Fragment { - val detailFragment = SheetMusicDetailFragment() - return if (position < hymnIndices.size) { - detailFragment.init(hymnIndices[position]) - detailFragment - } else { - detailFragment.init(1) - detailFragment - } - } - - override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) { - super.setPrimaryItem(container, position, `object`) - updateCurrentItemId(hymnIndices[position]) - } - - override fun getCount(): Int { - return hymnIndices.size - } - - fun submitList(hymnIndices: List) { - this.hymnIndices.clear() - this.hymnIndices.addAll(hymnIndices) - notifyDataSetChanged() - } - - } -} \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..2e6bfab --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..cc3919c --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_left.xml b/app/src/main/res/anim/slide_left.xml new file mode 100644 index 0000000..fa86a92 --- /dev/null +++ b/app/src/main/res/anim/slide_left.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..f304d1a --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..448f510 --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_right.xml b/app/src/main/res/anim/slide_right.xml new file mode 100644 index 0000000..d283f64 --- /dev/null +++ b/app/src/main/res/anim/slide_right.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/wait.xml b/app/src/main/res/anim/wait.xml new file mode 100644 index 0000000..d955e6d --- /dev/null +++ b/app/src/main/res/anim/wait.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml index a587c4d..8f3001c 100644 --- a/app/src/main/res/navigation/navigation.xml +++ b/app/src/main/res/navigation/navigation.xml @@ -12,7 +12,40 @@ tools:layout="@layout/fragment_song_listing"> + app:destination="@id/detailPagerFragment" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right"/> + + + + + + + + + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right" /> + app:destination="@id/settingsActivity" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right"/> + app:destination="@id/aboutFragment" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right"/> - - - - - - + app:destination="@id/hymnListingFragment2" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right" /> + app:destination="@id/hymnListingFragment2" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right"/> + app:destination="@id/openSourceLicensesFragment" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right"/> + app:destination="@id/acknowledgementFragment" + app:enterAnim="@anim/slide_left" + app:exitAnim="@anim/wait" + app:popEnterAnim="@anim/wait" + app:popExitAnim="@anim/slide_right"/> - - // endregion constants ------------------------------------------------------------------------- - - // region helper fields ------------------------------------------------------------------------ - @Mock - private lateinit var hymnUseCases: HymnUseCases - - private lateinit var subject: SheetMusicListingViewModel - - private lateinit var schedulerProvider: SchedulerProvider - - @Mock - private lateinit var titlesLceObserver: Observer>> - - @get:Rule - val rule = InstantTaskExecutorRule() - - // endregion helper fields --------------------------------------------------------------------- - - @Before - fun setUp() { - titles = listOf( - TitleItem(1, "title1", "subtitle1", "description1"), - TitleItem(2, "title2", "subtitle2", "description2"), - TitleItem(3, "title3", "subtitle3", "description3") - ) - - schedulerProvider = ImmediateSchedulerProvider() - - subject = SheetMusicListingViewModel(hymnUseCases, schedulerProvider) - - - } - - @Test - fun getHymnTitles_success_returnsAllTitlesWithSheetMusic() { - // Setup - whenever(hymnUseCases.hymnSheetMusicTitles()).thenReturn(Observable.just(titles)) - subject.hymnTitlesLce.observeForever(titlesLceObserver) - // Execute - subject.loadHymnTitlesFromRepo() - - //Verify - - inOrder(titlesLceObserver) { - verify(titlesLceObserver).onChanged(Lce.Loading(true)) - verify(titlesLceObserver).onChanged(Lce.Content(titles)) - verifyNoMoreInteractions(titlesLceObserver) - } - } -} \ No newline at end of file