From 77a4e5c6d3b647b7249a89f054f65d4d1f3acf99 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Thu, 3 Feb 2022 17:17:03 -0300
Subject: [PATCH] Allow swiping between OnboardingFragments

---
 .../mailbox/android/ui/OnboardingActivity.kt  | 35 ++++++++-
 .../mailbox/android/ui/OnboardingFragment.kt  | 74 ++++++++++---------
 .../mailbox/android/ui/OnboardingViewModel.kt | 43 +++++++++++
 mailbox-android/src/main/res/anim/enter.xml   | 11 ---
 mailbox-android/src/main/res/anim/exit.xml    |  6 --
 .../src/main/res/anim/pop_enter.xml           |  6 --
 .../src/main/res/anim/pop_exit.xml            | 11 ---
 .../res/layout-land/fragment_onboarding.xml   |  4 +-
 .../main/res/layout/activity_onboarding.xml   |  8 +-
 .../main/res/layout/fragment_onboarding.xml   |  4 +-
 .../main/res/navigation/nav_onboarding.xml    | 51 -------------
 mailbox-android/src/main/res/values/attrs.xml |  6 --
 .../src/main/res/values/strings.xml           | 16 ++--
 13 files changed, 130 insertions(+), 145 deletions(-)
 create mode 100644 mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingViewModel.kt
 delete mode 100644 mailbox-android/src/main/res/anim/enter.xml
 delete mode 100644 mailbox-android/src/main/res/anim/exit.xml
 delete mode 100644 mailbox-android/src/main/res/anim/pop_enter.xml
 delete mode 100644 mailbox-android/src/main/res/anim/pop_exit.xml
 delete mode 100644 mailbox-android/src/main/res/navigation/nav_onboarding.xml
 delete mode 100644 mailbox-android/src/main/res/values/attrs.xml

diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingActivity.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingActivity.kt
index acfbeaf7..6bf93093 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingActivity.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingActivity.kt
@@ -20,13 +20,44 @@
 package org.briarproject.mailbox.android.ui
 
 import android.os.Bundle
+import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
-import org.briarproject.mailbox.R
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import androidx.viewpager2.widget.ViewPager2
+import org.briarproject.mailbox.databinding.ActivityOnboardingBinding
 
 class OnboardingActivity : AppCompatActivity() {
 
+    private val viewModel: OnboardingViewModel by viewModels()
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_onboarding)
+        val ui = ActivityOnboardingBinding.inflate(layoutInflater)
+        setContentView(ui.root)
+
+        ui.pager.adapter = OnboardingAdapter(this)
+        ui.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+            override fun onPageSelected(position: Int) {
+                super.onPageSelected(position)
+                viewModel.selectPage(position)
+            }
+        })
+        viewModel.currentPage.observe(this) { position ->
+            ui.pager.setCurrentItem(position, true)
+        }
     }
+
+    class OnboardingAdapter(activity: AppCompatActivity) : FragmentStateAdapter(activity) {
+        override fun getItemCount(): Int = 5
+        override fun createFragment(position: Int): Fragment = when (position) {
+            0 -> Onboarding0Fragment()
+            1 -> Onboarding1Fragment()
+            2 -> Onboarding2Fragment()
+            3 -> Onboarding3Fragment()
+            4 -> FinishFragment()
+            else -> error("Unexpected OnboardingFragment: $position")
+        }
+    }
+
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt
index c332dede..2181adda 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingFragment.kt
@@ -28,22 +28,34 @@ import androidx.annotation.StringRes
 import androidx.core.content.ContextCompat.getColorStateList
 import androidx.core.widget.ImageViewCompat
 import androidx.fragment.app.Fragment
-import androidx.navigation.NavController
-import androidx.navigation.fragment.findNavController
+import androidx.fragment.app.activityViewModels
 import org.briarproject.mailbox.R
 import org.briarproject.mailbox.databinding.FragmentOnboardingBinding
 
+class Onboarding0Fragment : OnboardingFragment(
+    number = 0,
+    icon = R.mipmap.ic_launcher_round,
+    title = R.string.onboarding_0_title,
+    description = R.string.onboarding_0_description,
+    topButtonAction = { viewModel ->
+        viewModel.selectPage(1)
+    },
+    bottomButtonText = R.string.button_skip_intro,
+    bottomButtonAction = {
+        requireActivity().supportFinishAfterTransition()
+    },
+)
+
 class Onboarding1Fragment : OnboardingFragment(
     number = 1,
     icon = R.mipmap.ic_launcher_round,
     title = R.string.onboarding_1_title,
     description = R.string.onboarding_1_description,
-    topButtonAction = { nav ->
-        nav.navigate(R.id.action_onboarding_1_to_2)
+    topButtonAction = { viewModel ->
+        viewModel.selectPage(2)
     },
-    bottomButtonText = R.string.button_skip_intro,
-    bottomButtonAction = {
-        requireActivity().supportFinishAfterTransition()
+    bottomButtonAction = { viewModel ->
+        viewModel.selectPage(0)
     },
 )
 
@@ -52,11 +64,11 @@ class Onboarding2Fragment : OnboardingFragment(
     icon = R.mipmap.ic_launcher_round,
     title = R.string.onboarding_2_title,
     description = R.string.onboarding_2_description,
-    topButtonAction = { nav ->
-        nav.navigate(R.id.action_onboarding_2_to_3)
+    topButtonAction = { viewModel ->
+        viewModel.selectPage(3)
     },
-    bottomButtonAction = { nav ->
-        nav.popBackStack()
+    bottomButtonAction = { viewModel ->
+        viewModel.selectPage(1)
     },
 )
 
@@ -65,24 +77,11 @@ class Onboarding3Fragment : OnboardingFragment(
     icon = R.mipmap.ic_launcher_round,
     title = R.string.onboarding_3_title,
     description = R.string.onboarding_3_description,
-    topButtonAction = { nav ->
-        nav.navigate(R.id.action_onboarding_3_to_4)
+    topButtonAction = { viewModel ->
+        viewModel.selectPage(4) // finishes activity
     },
-    bottomButtonAction = { nav ->
-        nav.popBackStack()
-    },
-)
-
-class Onboarding4Fragment : OnboardingFragment(
-    number = 4,
-    icon = R.mipmap.ic_launcher_round,
-    title = R.string.onboarding_4_title,
-    description = R.string.onboarding_4_description,
-    topButtonAction = {
-        requireActivity().supportFinishAfterTransition()
-    },
-    bottomButtonAction = { nav ->
-        nav.popBackStack()
+    bottomButtonAction = { viewModel ->
+        viewModel.selectPage(2)
     },
 )
 
@@ -96,10 +95,11 @@ abstract class OnboardingFragment(
     private val description: Int,
     @StringRes
     private val bottomButtonText: Int = R.string.button_back,
-    private val topButtonAction: Fragment.(NavController) -> Unit,
-    private val bottomButtonAction: Fragment.(NavController) -> Unit,
+    private val topButtonAction: Fragment.(OnboardingViewModel) -> Unit,
+    private val bottomButtonAction: Fragment.(OnboardingViewModel) -> Unit,
 ) : Fragment() {
 
+    private val viewModel: OnboardingViewModel by activityViewModels()
     private var _ui: FragmentOnboardingBinding? = null
 
     /**
@@ -121,17 +121,16 @@ abstract class OnboardingFragment(
         ui.title.setText(title)
         ui.description.setText(description)
         listOf(ui.bullet1, ui.bullet2, ui.bullet3, ui.bullet4).forEachIndexed { i, imageView ->
-            val color = if (i + 1 <= number) R.color.briar_green else R.color.briar_night
+            val color = if (i <= number) R.color.briar_green else R.color.briar_night
             val tintList = getColorStateList(requireContext(), color)
             ImageViewCompat.setImageTintList(imageView, tintList)
         }
-        val nav = findNavController()
         ui.topButton.setOnClickListener {
-            topButtonAction(nav)
+            topButtonAction(viewModel)
         }
         ui.bottomButton.setText(bottomButtonText)
         ui.bottomButton.setOnClickListener {
-            bottomButtonAction(nav)
+            bottomButtonAction(viewModel)
         }
     }
 
@@ -141,3 +140,10 @@ abstract class OnboardingFragment(
     }
 
 }
+
+class FinishFragment : Fragment() {
+    override fun onResume() {
+        super.onResume()
+        requireActivity().supportFinishAfterTransition()
+    }
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingViewModel.kt
new file mode 100644
index 00000000..c33a87e5
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/OnboardingViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ *     Briar Mailbox
+ *     Copyright (C) 2021-2022  The Briar Project
+ *
+ *     This program is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU Affero General Public License as
+ *     published by the Free Software Foundation, either version 3 of the
+ *     License, or (at your option) any later version.
+ *
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU Affero General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Affero General Public License
+ *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.briarproject.mailbox.android.ui
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.distinctUntilChanged
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class OnboardingViewModel @Inject constructor(
+    app: Application,
+    handle: SavedStateHandle,
+) : AndroidViewModel(app) {
+
+    private val _currentPage = handle.getLiveData("currentPage", 0)
+    val currentPage: LiveData<Int> = _currentPage.distinctUntilChanged() // prevent infinite loop
+
+    fun selectPage(position: Int) {
+        _currentPage.value = position
+    }
+
+}
diff --git a/mailbox-android/src/main/res/anim/enter.xml b/mailbox-android/src/main/res/anim/enter.xml
deleted file mode 100644
index 32b84f2d..00000000
--- a/mailbox-android/src/main/res/anim/enter.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android">
-
-	<!-- slide in from right -->
-	<translate
-		android:duration="@integer/animationSpeed"
-		android:fromXDelta="100%p"
-		android:interpolator="@android:interpolator/decelerate_quad"
-		android:toXDelta="0" />
-
-</set>
diff --git a/mailbox-android/src/main/res/anim/exit.xml b/mailbox-android/src/main/res/anim/exit.xml
deleted file mode 100644
index 205435cc..00000000
--- a/mailbox-android/src/main/res/anim/exit.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<alpha xmlns:android="http://schemas.android.com/apk/res/android"
-	android:duration="@integer/animationSpeed"
-	android:fromAlpha="1.0"
-	android:interpolator="@android:interpolator/accelerate_quad"
-	android:toAlpha="0.0" />
diff --git a/mailbox-android/src/main/res/anim/pop_enter.xml b/mailbox-android/src/main/res/anim/pop_enter.xml
deleted file mode 100644
index 4b3a8d60..00000000
--- a/mailbox-android/src/main/res/anim/pop_enter.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<alpha xmlns:android="http://schemas.android.com/apk/res/android"
-    android:duration="@integer/animationSpeed"
-    android:fromAlpha="0.0"
-    android:interpolator="@android:interpolator/decelerate_quad"
-    android:toAlpha="1.0" />
diff --git a/mailbox-android/src/main/res/anim/pop_exit.xml b/mailbox-android/src/main/res/anim/pop_exit.xml
deleted file mode 100644
index 2c514d82..00000000
--- a/mailbox-android/src/main/res/anim/pop_exit.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <!-- slide out to right -->
-    <translate
-        android:duration="@integer/animationSpeed"
-        android:fromXDelta="0"
-        android:interpolator="@android:interpolator/accelerate_quad"
-        android:toXDelta="100%p" />
-
-</set>
diff --git a/mailbox-android/src/main/res/layout-land/fragment_onboarding.xml b/mailbox-android/src/main/res/layout-land/fragment_onboarding.xml
index e5492eec..365e2e40 100644
--- a/mailbox-android/src/main/res/layout-land/fragment_onboarding.xml
+++ b/mailbox-android/src/main/res/layout-land/fragment_onboarding.xml
@@ -37,7 +37,7 @@
         app:layout_constraintStart_toStartOf="@+id/guideline"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_chainStyle="packed"
-        tools:text="@string/onboarding_1_title" />
+        tools:text="@string/onboarding_0_title" />
 
     <TextView
         android:id="@+id/description"
@@ -49,7 +49,7 @@
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="@+id/guideline"
         app:layout_constraintTop_toBottomOf="@+id/title"
-        tools:text="@string/onboarding_1_description" />
+        tools:text="@string/onboarding_0_description" />
 
     <ImageView
         android:id="@+id/bullet1"
diff --git a/mailbox-android/src/main/res/layout/activity_onboarding.xml b/mailbox-android/src/main/res/layout/activity_onboarding.xml
index 24bf07ef..4aca0f57 100644
--- a/mailbox-android/src/main/res/layout/activity_onboarding.xml
+++ b/mailbox-android/src/main/res/layout/activity_onboarding.xml
@@ -1,11 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/fragmentContainer"
-    android:name="androidx.navigation.fragment.NavHostFragment"
+    android:id="@+id/pager"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    app:defaultNavHost="true"
-    app:navGraph="@navigation/nav_onboarding"
     tools:context=".android.ui.OnboardingActivity" />
diff --git a/mailbox-android/src/main/res/layout/fragment_onboarding.xml b/mailbox-android/src/main/res/layout/fragment_onboarding.xml
index 2c07cce9..af06d824 100644
--- a/mailbox-android/src/main/res/layout/fragment_onboarding.xml
+++ b/mailbox-android/src/main/res/layout/fragment_onboarding.xml
@@ -30,7 +30,7 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/icon"
         app:layout_constraintVertical_chainStyle="packed"
-        tools:text="@string/onboarding_1_title" />
+        tools:text="@string/onboarding_0_title" />
 
     <TextView
         android:id="@+id/description"
@@ -43,7 +43,7 @@
         app:layout_constraintHorizontal_bias="0.0"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/title"
-        tools:text="@string/onboarding_1_description" />
+        tools:text="@string/onboarding_0_description" />
 
     <ImageView
         android:id="@+id/bullet1"
diff --git a/mailbox-android/src/main/res/navigation/nav_onboarding.xml b/mailbox-android/src/main/res/navigation/nav_onboarding.xml
deleted file mode 100644
index 74a5c755..00000000
--- a/mailbox-android/src/main/res/navigation/nav_onboarding.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<navigation xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/nav_onboarding"
-    app:startDestination="@id/onboarding1Fragment">
-    <fragment
-        android:id="@+id/onboarding1Fragment"
-        android:name="org.briarproject.mailbox.android.ui.Onboarding1Fragment"
-        android:label="Onboarding1Fragment"
-        tools:layout="@layout/fragment_onboarding">
-        <action
-            android:id="@+id/action_onboarding_1_to_2"
-            app:destination="@id/onboarding2Fragment"
-            app:enterAnim="@anim/enter"
-            app:exitAnim="@anim/exit"
-            app:popEnterAnim="@anim/pop_enter"
-            app:popExitAnim="@anim/pop_exit" />
-    </fragment>
-    <fragment
-        android:id="@+id/onboarding2Fragment"
-        android:name="org.briarproject.mailbox.android.ui.Onboarding2Fragment"
-        android:label="Onboarding2Fragment"
-        tools:layout="@layout/fragment_onboarding">
-        <action
-            android:id="@+id/action_onboarding_2_to_3"
-            app:destination="@id/onboarding3Fragment"
-            app:enterAnim="@anim/enter"
-            app:exitAnim="@anim/exit"
-            app:popEnterAnim="@anim/pop_enter"
-            app:popExitAnim="@anim/pop_exit" />
-    </fragment>
-    <fragment
-        android:id="@+id/onboarding3Fragment"
-        android:name="org.briarproject.mailbox.android.ui.Onboarding3Fragment"
-        android:label="Onboarding3Fragment"
-        tools:layout="@layout/fragment_onboarding">
-        <action
-            android:id="@+id/action_onboarding_3_to_4"
-            app:destination="@id/onboarding4Fragment"
-            app:enterAnim="@anim/enter"
-            app:exitAnim="@anim/exit"
-            app:popEnterAnim="@anim/pop_enter"
-            app:popExitAnim="@anim/pop_exit" />
-    </fragment>
-    <fragment
-        android:id="@+id/onboarding4Fragment"
-        android:name="org.briarproject.mailbox.android.ui.Onboarding4Fragment"
-        android:label="Onboarding4Fragment"
-        tools:layout="@layout/fragment_onboarding" />
-</navigation>
diff --git a/mailbox-android/src/main/res/values/attrs.xml b/mailbox-android/src/main/res/values/attrs.xml
deleted file mode 100644
index 3704cec4..00000000
--- a/mailbox-android/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
-    <integer name="animationSpeed">@android:integer/config_mediumAnimTime</integer>
-
-</resources>
diff --git a/mailbox-android/src/main/res/values/strings.xml b/mailbox-android/src/main/res/values/strings.xml
index f895cbf1..094a7371 100644
--- a/mailbox-android/src/main/res/values/strings.xml
+++ b/mailbox-android/src/main/res/values/strings.xml
@@ -10,14 +10,14 @@
     <string name="button_skip_intro">Skip Intro</string>
     <string name="button_back">Back</string>
 
-    <string name="onboarding_1_title">Stay Reachable</string>
-    <string name="onboarding_1_description">The Mailbox helps you to stay in touch with your contacts and synchronizes your messages to Briar.</string>
-    <string name="onboarding_2_title">Use a spare device</string>
-    <string name="onboarding_2_description">Install the mailbox app on a spare phone or tablet and leave it connected to power and WiFi. </string>
-    <string name="onboarding_3_title">Message delivery</string>
-    <string name="onboarding_3_description">When your Mailbox is online, your contacts can leave messages for you even if your Briar is offline. The Mailbox will receive the messages, and forward them to Briar when you come online.</string>
-    <string name="onboarding_4_title">Encrypted</string>
-    <string name="onboarding_4_description">Messages going through the Mailbox are encrypted and cannot be read by the Mailbox or this phone. The Mailbox simply forwards them to your Briar Messenger phone.</string>
+    <string name="onboarding_0_title">Stay Reachable</string>
+    <string name="onboarding_0_description">The Mailbox helps you to stay in touch with your contacts and synchronizes your messages to Briar.</string>
+    <string name="onboarding_1_title">Use a spare device</string>
+    <string name="onboarding_1_description">Install the mailbox app on a spare phone or tablet and leave it connected to power and WiFi. </string>
+    <string name="onboarding_2_title">Message delivery</string>
+    <string name="onboarding_2_description">When your Mailbox is online, your contacts can leave messages for you even if your Briar is offline. The Mailbox will receive the messages, and forward them to Briar when you come online.</string>
+    <string name="onboarding_3_title">Encrypted</string>
+    <string name="onboarding_3_description">Messages going through the Mailbox are encrypted and cannot be read by the Mailbox or this phone. The Mailbox simply forwards them to your Briar Messenger phone.</string>
 
     <!-- TODO: We might want to copy string from don't kill me lib,
           so translation memory can auto-translate most of them. -->
-- 
GitLab