diff --git a/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt b/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt index a4880fd46..16503f283 100644 --- a/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt @@ -5,28 +5,15 @@ package org.mozilla.fenix.migration import android.content.Intent -import android.graphics.Rect import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.annotation.DimenRes import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.activity_migration.* -import kotlinx.android.synthetic.main.migration_list_item.view.* import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.migration.AbstractMigrationProgressActivity import mozilla.components.support.migration.AbstractMigrationService -import mozilla.components.support.migration.Migration -import mozilla.components.support.migration.Migration.Bookmarks -import mozilla.components.support.migration.Migration.History -import mozilla.components.support.migration.Migration.Logins -import mozilla.components.support.migration.Migration.Settings import mozilla.components.support.migration.MigrationResults import mozilla.components.support.migration.state.MigrationAction import mozilla.components.support.migration.state.MigrationProgress @@ -97,91 +84,10 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() { migration_button.setBackgroundResource(R.drawable.migration_button_background) migration_button_progress_bar.visibility = View.INVISIBLE // Keep the results list up-to-date. - statusAdapter.submitList(results.toItemList()) + statusAdapter.updateData(results) } override fun onMigrationStateChanged(progress: MigrationProgress, results: MigrationResults) { - statusAdapter.submitList(results.toItemList()) - } -} - -// These are the only items we want to show migrating in the UI. -internal val whiteList = linkedMapOf( - Settings to R.string.settings_title, - History to R.string.preferences_sync_history, - Bookmarks to R.string.preferences_sync_bookmarks, - Logins to R.string.migration_text_passwords -) - -internal fun MigrationResults.toItemList() = whiteList.keys - .map { - if (containsKey(it)) { - MigrationItem(it, getValue(it).success) - } else { - MigrationItem(it) - } - } - -internal data class MigrationItem(val migration: Migration, val status: Boolean = false) - -internal class MigrationStatusAdapter : - ListAdapter(DiffCallback) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - override fun getItemViewType(position: Int): Int = R.layout.migration_list_item - - class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - private val context = view.context - private val title = view.migration_item_name - private val status = view.migration_status_image - - fun bind(item: MigrationItem) { - // Get the resource ID for the item. - val migrationText = whiteList[item.migration]?.run { - context.getString(this) - } ?: "" - title.text = migrationText - status.visibility = if (item.status) View.VISIBLE else View.INVISIBLE - status.contentDescription = context.getString(R.string.migration_icon_description) - } - } - - private object DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: MigrationItem, newItem: MigrationItem) = - oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName - - override fun areContentsTheSame(oldItem: MigrationItem, newItem: MigrationItem) = - oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName && - oldItem.status == newItem.status - } -} - -internal class MigrationStatusItemDecoration( - @DimenRes private val spacing: Int -) : RecyclerView.ItemDecoration() { - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - val position = parent.getChildViewHolder(view).adapterPosition - val itemCount = state.itemCount - - outRect.left = spacing - outRect.right = spacing - outRect.top = spacing - outRect.bottom = if (position == itemCount - 1) spacing else 0 + statusAdapter.updateData(results) } } diff --git a/app/src/main/java/org/mozilla/fenix/migration/MigrationStatusAdapter.kt b/app/src/main/java/org/mozilla/fenix/migration/MigrationStatusAdapter.kt new file mode 100644 index 000000000..39ddc3405 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/migration/MigrationStatusAdapter.kt @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.migration + +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.Px +import androidx.core.view.isInvisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.migration_list_item.view.* +import mozilla.components.support.migration.Migration +import mozilla.components.support.migration.MigrationResults +import org.mozilla.fenix.R + +internal data class MigrationItem( + val migration: Migration, + val status: Boolean = false +) + +// These are the only items we want to show migrating in the UI. +internal val whiteList = linkedMapOf( + Migration.Settings to R.string.settings_title, + Migration.History to R.string.preferences_sync_history, + Migration.Bookmarks to R.string.preferences_sync_bookmarks, + Migration.Logins to R.string.migration_text_passwords +) + +internal class MigrationStatusAdapter : + ListAdapter(DiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.migration_list_item, parent, false) + + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + /** + * Filter the [results] to only include items in [whiteList] and update the adapter. + */ + fun updateData(results: MigrationResults) { + val itemList = whiteList.keys.map { + if (results.containsKey(it)) { + MigrationItem(it, results.getValue(it).success) + } else { + MigrationItem(it) + } + } + submitList(itemList) + } + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val context = view.context + private val title = view.migration_item_name + private val status = view.migration_status_image + + fun bind(item: MigrationItem) { + // Get the resource ID for the item. + val migrationText = whiteList[item.migration]?.let { + context.getString(it) + }.orEmpty() + title.text = migrationText + status.isInvisible = !item.status + status.contentDescription = context.getString(R.string.migration_icon_description) + } + } + + private object DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: MigrationItem, newItem: MigrationItem) = + oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName + + override fun areContentsTheSame(oldItem: MigrationItem, newItem: MigrationItem) = + oldItem.migration.javaClass.simpleName == newItem.migration.javaClass.simpleName && + oldItem.status == newItem.status + } +} + +internal class MigrationStatusItemDecoration( + @Px private val spacing: Int +) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildViewHolder(view).adapterPosition + val itemCount = state.itemCount + + outRect.left = spacing + outRect.right = spacing + outRect.top = spacing + outRect.bottom = if (position == itemCount - 1) spacing else 0 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt b/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt index 1b5268272..3821091e2 100644 --- a/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt +++ b/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt @@ -1,6 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this -file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.fenix.migration @@ -15,7 +15,8 @@ import org.mozilla.fenix.components.metrics.MetricController class MigrationTelemetryListener( private val metrics: MetricController, - private val store: MigrationStore + private val store: MigrationStore, + private val logger: Logger = Logger("MigrationTelemetryListener") ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -23,7 +24,7 @@ class MigrationTelemetryListener( // Observe for migration completed. store.flowScoped { flow -> flow.collect { state -> - Logger("MigrationTelemetryListener").debug("Migration state: ${state.progress}") + logger.debug("Migration state: ${state.progress}") if (state.progress == MigrationProgress.COMPLETED) { metrics.track(Event.FennecToFenixMigrated) } diff --git a/app/src/test/java/org/mozilla/fenix/migration/MigrationStatusAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/migration/MigrationStatusAdapterTest.kt new file mode 100644 index 000000000..f0012ff31 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/migration/MigrationStatusAdapterTest.kt @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.migration + +import android.view.View +import android.widget.FrameLayout +import kotlinx.android.synthetic.main.migration_list_item.view.* +import mozilla.components.support.migration.Migration +import mozilla.components.support.migration.MigrationRun +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class MigrationStatusAdapterTest { + + private lateinit var adapter: MigrationStatusAdapter + + @Before + fun setup() { + adapter = MigrationStatusAdapter() + } + + @Test + fun `getItemCount should return the number of items in whitelist`() { + assertEquals(0, adapter.itemCount) + + adapter.updateData(mapOf( + Migration.Addons to MigrationRun(0, success = true), + Migration.Settings to MigrationRun(0, success = true), + Migration.Bookmarks to MigrationRun(0, success = false) + )) + assertEquals(4, adapter.itemCount) + } + + @Test + fun `creates and binds viewholder`() { + adapter.updateData(mapOf( + Migration.History to MigrationRun(0, success = true) + )) + + val holder1 = adapter.createViewHolder(FrameLayout(testContext), 0) + val holder2 = adapter.createViewHolder(FrameLayout(testContext), 0) + adapter.bindViewHolder(holder1, 0) + adapter.bindViewHolder(holder2, 1) + + assertEquals("Settings", holder1.itemView.migration_item_name.text) + assertEquals(View.INVISIBLE, holder1.itemView.migration_status_image.visibility) + + assertEquals("History", holder2.itemView.migration_item_name.text) + assertEquals(View.VISIBLE, holder2.itemView.migration_status_image.visibility) + assertEquals("Migration completed", holder2.itemView.migration_status_image.contentDescription) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/migration/MigrationTelemetryListenerTest.kt b/app/src/test/java/org/mozilla/fenix/migration/MigrationTelemetryListenerTest.kt new file mode 100644 index 000000000..1c6c65a13 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/migration/MigrationTelemetryListenerTest.kt @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.migration + +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import io.mockk.verifyOrder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.migration.state.MigrationAction +import mozilla.components.support.migration.state.MigrationStore +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController + +@ExperimentalCoroutinesApi +class MigrationTelemetryListenerTest { + + private val testDispatcher = TestCoroutineDispatcher() + + @get:Rule + val coroutinesTestRule = MainCoroutineRule(testDispatcher) + + @MockK(relaxed = true) private lateinit var metrics: MetricController + @MockK(relaxed = true) private lateinit var logger: Logger + private lateinit var store: MigrationStore + private lateinit var listener: MigrationTelemetryListener + + @Before + fun setup() { + MockKAnnotations.init(this) + store = MigrationStore() + listener = MigrationTelemetryListener( + metrics = metrics, + store = store, + logger = logger + ) + } + + @Test + fun `progress state is logged`() = testDispatcher.runBlockingTest { + listener.start() + store.dispatch(MigrationAction.Started).joinBlocking() + store.dispatch(MigrationAction.Completed).joinBlocking() + store.dispatch(MigrationAction.Clear).joinBlocking() + + verifyOrder { + logger.debug("Migration state: MIGRATING") + logger.debug("Migration state: COMPLETED") + logger.debug("Migration state: NONE") + } + } + + @Test + fun `metrics are logged when migration is completed`() = testDispatcher.runBlockingTest { + listener.start() + store.dispatch(MigrationAction.Completed).joinBlocking() + + verify { metrics.track(Event.FennecToFenixMigrated) } + } +}