From fdd7400ccce73cb4912ca766a03b26eed283c692 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Tue, 5 Nov 2019 17:30:04 -0800 Subject: [PATCH] Use ViewModel for share fragment --- app/build.gradle | 6 +- .../mozilla/fenix/share/ShareController.kt | 6 +- .../org/mozilla/fenix/share/ShareFragment.kt | 154 ++--------------- .../mozilla/fenix/share/ShareInteractor.kt | 4 +- .../mozilla/fenix/share/ShareToAppsView.kt | 6 +- .../org/mozilla/fenix/share/ShareViewModel.kt | 162 ++++++++++++++++++ .../share/listadapters/AppShareAdapter.kt | 43 ++--- .../fenix/share/viewholders/AppViewHolder.kt | 25 +-- app/src/main/res/layout/share_to_apps.xml | 1 + .../fenix/share/ShareControllerTest.kt | 4 +- .../fenix/share/ShareInteractorTest.kt | 4 +- .../mozilla/fenix/share/ShareViewModelTest.kt | 133 ++++++++++++++ .../share/listadapters/AppShareAdapterTest.kt | 9 +- architecture/build.gradle | 3 +- buildSrc/src/main/java/Dependencies.kt | 2 +- 15 files changed, 353 insertions(+), 209 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt create mode 100644 app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index 5aed36c49..ec3088c88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,11 +10,12 @@ apply from: "$project.rootDir/automation/gradle/versionCode.gradle" apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'com.google.android.gms.oss-licenses-plugin' -import com.android.build.gradle.internal.tasks.AppPreBuildTask + +import com.android.build.OutputFile import org.gradle.internal.logging.text.StyledTextOutput.Style import org.gradle.internal.logging.text.StyledTextOutputFactory + import static org.gradle.api.tasks.testing.TestResult.ResultType -import com.android.build.OutputFile android { compileSdkVersion 28 @@ -413,6 +414,7 @@ dependencies { implementation Deps.androidx_navigation_fragment implementation Deps.androidx_navigation_ui implementation Deps.androidx_recyclerview + implementation Deps.androidx_lifecycle_livedata implementation Deps.androidx_lifecycle_runtime implementation Deps.androidx_lifecycle_viewmodel implementation Deps.androidx_core diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt index fa367707e..54071e80b 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -26,7 +26,7 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav -import org.mozilla.fenix.share.listadapters.AndroidShareOption +import org.mozilla.fenix.share.listadapters.AppShareOption /** * [ShareFragment] controller. @@ -36,7 +36,7 @@ import org.mozilla.fenix.share.listadapters.AndroidShareOption interface ShareController { fun handleReauth() fun handleShareClosed() - fun handleShareToApp(app: AndroidShareOption.App) + fun handleShareToApp(app: AppShareOption) fun handleAddNewDevice() fun handleShareToDevice(device: Device) fun handleShareToAllDevices(devices: List) @@ -72,7 +72,7 @@ class DefaultShareController( dismiss() } - override fun handleShareToApp(app: AndroidShareOption.App) { + override fun handleShareToApp(app: AppShareOption) { val intent = Intent(ACTION_SEND).apply { putExtra(EXTRA_TEXT, getShareText()) type = "text/plain" diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt index 90c645a8e..407e7a311 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt @@ -5,97 +5,38 @@ package org.mozilla.fenix.share import android.content.Context -import android.content.Intent -import android.content.Intent.ACTION_SEND -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.pm.ResolveInfo -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkRequest import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.WorkerThread import androidx.appcompat.app.AppCompatDialogFragment -import androidx.core.content.getSystemService -import androidx.lifecycle.lifecycleScope +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory +import androidx.lifecycle.observe import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_share.view.* -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import mozilla.components.concept.sync.DeviceCapability import mozilla.components.feature.sendtab.SendTabUseCases -import mozilla.components.service.fxa.manager.FxaAccountManager import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbarPresenter -import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.share.listadapters.AndroidShareOption -import org.mozilla.fenix.share.listadapters.SyncShareOption -@Suppress("TooManyFunctions") class ShareFragment : AppCompatDialogFragment() { + + private val viewModel: ShareViewModel by viewModels { + AndroidViewModelFactory(requireActivity().application) + } private lateinit var shareInteractor: ShareInteractor private lateinit var shareCloseView: ShareCloseView private lateinit var shareToAccountDevicesView: ShareToAccountDevicesView private lateinit var shareToAppsView: ShareToAppsView - private lateinit var appsListDeferred: Deferred> - private lateinit var devicesListDeferred: Deferred> - private var connectivityManager: ConnectivityManager? = null - - private val networkCallback = object : ConnectivityManager.NetworkCallback() { - override fun onLost(network: Network?) = reloadDevices() - override fun onAvailable(network: Network?) = reloadDevices() - - private fun reloadDevices() { - context?.let { context -> - val fxaAccountManager = context.components.backgroundServices.accountManager - lifecycleScope.launch { - fxaAccountManager.authenticatedAccount() - ?.deviceConstellation() - ?.refreshDevicesAsync() - ?.await() - - val devicesShareOptions = buildDeviceList(fxaAccountManager) - shareToAccountDevicesView.setShareTargets(devicesShareOptions) - } - } - } - } override fun onAttach(context: Context) { super.onAttach(context) - - connectivityManager = context.getSystemService() - val networkRequest = NetworkRequest.Builder().build() - connectivityManager?.registerNetworkCallback(networkRequest, networkCallback) - - // Start preparing the data as soon as we have a valid Context - appsListDeferred = lifecycleScope.async(IO) { - val shareIntent = Intent(ACTION_SEND).apply { - type = "text/plain" - flags = FLAG_ACTIVITY_NEW_TASK - } - val shareAppsActivities = getIntentActivities(shareIntent, context) - buildAppsList(shareAppsActivities, context) - } - - devicesListDeferred = lifecycleScope.async(IO) { - val fxaAccountManager = context.components.backgroundServices.accountManager - buildDeviceList(fxaAccountManager) - } - } - - override fun onDetach() { - connectivityManager?.unregisterNetworkCallback(networkCallback) - super.onDetach() + viewModel.loadDevicesAndApps() } override fun onCreate(savedInstanceState: Bundle?) { @@ -149,84 +90,11 @@ class ShareFragment : AppCompatDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - // Start with some invisible views so the share menu height doesn't jump later - shareToAppsView.setShareTargets( - listOf(AndroidShareOption.Invisible, AndroidShareOption.Invisible) - ) - - lifecycleScope.launch { - val devicesShareOptions = devicesListDeferred.await() + viewModel.devicesList.observe(viewLifecycleOwner) { devicesShareOptions -> shareToAccountDevicesView.setShareTargets(devicesShareOptions) - val appsToShareTo = appsListDeferred.await() - shareToAppsView.setShareTargets(appsToShareTo) } - } - - @WorkerThread - private fun getIntentActivities(shareIntent: Intent, context: Context): List? { - return context.packageManager.queryIntentActivities(shareIntent, 0) - } - - /** - * Returns a list of apps that can be shared to. - * @param intentActivities List of activities from [getIntentActivities]. - */ - @WorkerThread - private fun buildAppsList( - intentActivities: List?, - context: Context - ): List { - return intentActivities - .orEmpty() - .filter { it.activityInfo.packageName != context.packageName } - .map { resolveInfo -> - AndroidShareOption.App( - resolveInfo.loadLabel(context.packageManager).toString(), - resolveInfo.loadIcon(context.packageManager), - resolveInfo.activityInfo.packageName, - resolveInfo.activityInfo.name - ) - } - } - - /** - * Builds list of options to display in the top row of the share sheet. - * This will primarily include devices that tabs can be sent to, but also options - * for reconnecting the account or sending to all devices. - */ - private fun buildDeviceList(accountManager: FxaAccountManager): List { - val activeNetwork = connectivityManager?.activeNetworkInfo - val account = accountManager.authenticatedAccount() - - return when { - // No network - activeNetwork?.isConnected != true -> listOf(SyncShareOption.Offline) - // No account signed in - account == null -> listOf(SyncShareOption.SignIn) - // Account needs to be re-authenticated - accountManager.accountNeedsReauth() -> listOf(SyncShareOption.Reconnect) - // Signed in - else -> { - val shareableDevices = account.deviceConstellation().state() - ?.otherDevices - .orEmpty() - .filter { it.capabilities.contains(DeviceCapability.SEND_TAB) } - - val list = mutableListOf() - if (shareableDevices.isEmpty()) { - // Show add device button if there are no devices - list.add(SyncShareOption.AddNewDevice) - } - - shareableDevices.mapTo(list) { SyncShareOption.SingleDevice(it) } - - if (shareableDevices.size > 1) { - // Show send all button if there are multiple devices - list.add(SyncShareOption.SendAll(shareableDevices)) - } - list - } + viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo -> + shareToAppsView.setShareTargets(appsToShareTo) } } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt b/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt index b2b8c01da..64a45831c 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt @@ -5,7 +5,7 @@ package org.mozilla.fenix.share import mozilla.components.concept.sync.Device -import org.mozilla.fenix.share.listadapters.AndroidShareOption +import org.mozilla.fenix.share.listadapters.AppShareOption /** * Interactor for the share screen. @@ -37,7 +37,7 @@ class ShareInteractor( controller.handleShareToAllDevices(devices) } - override fun onShareToApp(appToShareTo: AndroidShareOption.App) { + override fun onShareToApp(appToShareTo: AppShareOption) { controller.handleShareToApp(appToShareTo) } } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareToAppsView.kt b/app/src/main/java/org/mozilla/fenix/share/ShareToAppsView.kt index b0f8b976f..7a15297fd 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareToAppsView.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareToAppsView.kt @@ -10,14 +10,14 @@ import android.view.ViewGroup import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.share_to_apps.* import org.mozilla.fenix.R -import org.mozilla.fenix.share.listadapters.AndroidShareOption import org.mozilla.fenix.share.listadapters.AppShareAdapter +import org.mozilla.fenix.share.listadapters.AppShareOption /** * Callbacks for possible user interactions on the [ShareCloseView] */ interface ShareToAppsInteractor { - fun onShareToApp(appToShareTo: AndroidShareOption.App) + fun onShareToApp(appToShareTo: AppShareOption) } class ShareToAppsView( @@ -34,7 +34,7 @@ class ShareToAppsView( appsList.adapter = adapter } - fun setShareTargets(targets: List) { + fun setShareTargets(targets: List) { progressBar.visibility = View.GONE appsList.visibility = View.VISIBLE diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt b/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt new file mode 100644 index 000000000..e673a3e1c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt @@ -0,0 +1,162 @@ +/* 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.share + +import android.app.Application +import android.content.Context +import android.content.Intent +import android.content.pm.ResolveInfo +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import androidx.core.content.getSystemService +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import mozilla.components.concept.sync.DeviceCapability +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.share.listadapters.AppShareOption +import org.mozilla.fenix.share.listadapters.SyncShareOption + +class ShareViewModel(application: Application) : AndroidViewModel(application) { + + private val connectivityManager by lazy { application.getSystemService() } + private val fxaAccountManager = application.components.backgroundServices.accountManager + + private val devicesListLiveData = MutableLiveData>(emptyList()) + private val appsListLiveData = MutableLiveData>(emptyList()) + + @VisibleForTesting + internal val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onLost(network: Network?) = reloadDevices() + override fun onAvailable(network: Network?) = reloadDevices() + + private fun reloadDevices() { + viewModelScope.launch(IO) { + fxaAccountManager.authenticatedAccount() + ?.deviceConstellation() + ?.refreshDevicesAsync() + ?.await() + + val devicesShareOptions = buildDeviceList(fxaAccountManager) + devicesListLiveData.postValue(devicesShareOptions) + } + } + } + + /** + * List of devices and sync-related share options. + */ + val devicesList: LiveData> get() = devicesListLiveData + /** + * List of applications that can be shared to. + */ + val appsList: LiveData> get() = appsListLiveData + + /** + * Load a list of devices and apps into [devicesList] and [appsList]. + * Should be called when a fragment is attached so the data can be fetched early. + */ + fun loadDevicesAndApps() { + val networkRequest = NetworkRequest.Builder().build() + connectivityManager?.registerNetworkCallback(networkRequest, networkCallback) + + // Start preparing the data as soon as we have a valid Context + viewModelScope.launch(IO) { + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + val shareAppsActivities = getIntentActivities(shareIntent, getApplication()) + val apps = buildAppsList(shareAppsActivities, getApplication()) + appsListLiveData.postValue(apps) + } + + viewModelScope.launch(IO) { + val devices = buildDeviceList(fxaAccountManager) + devicesListLiveData.postValue(devices) + } + } + + override fun onCleared() { + connectivityManager?.unregisterNetworkCallback(networkCallback) + } + + @WorkerThread + private fun getIntentActivities(shareIntent: Intent, context: Context): List? { + return context.packageManager.queryIntentActivities(shareIntent, 0) + } + + /** + * Returns a list of apps that can be shared to. + * @param intentActivities List of activities from [getIntentActivities]. + */ + @VisibleForTesting + @WorkerThread + internal fun buildAppsList( + intentActivities: List?, + context: Context + ): List { + return intentActivities + .orEmpty() + .filter { it.activityInfo.packageName != context.packageName } + .map { resolveInfo -> + AppShareOption( + resolveInfo.loadLabel(context.packageManager).toString(), + resolveInfo.loadIcon(context.packageManager), + resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name + ) + } + } + + /** + * Builds list of options to display in the top row of the share sheet. + * This will primarily include devices that tabs can be sent to, but also options + * for reconnecting the account or sending to all devices. + */ + @VisibleForTesting + @WorkerThread + internal fun buildDeviceList(accountManager: FxaAccountManager): List { + val activeNetwork = connectivityManager?.activeNetworkInfo + val account = accountManager.authenticatedAccount() + + return when { + // No network + activeNetwork?.isConnected != true -> listOf(SyncShareOption.Offline) + // No account signed in + account == null -> listOf(SyncShareOption.SignIn) + // Account needs to be re-authenticated + accountManager.accountNeedsReauth() -> listOf(SyncShareOption.Reconnect) + // Signed in + else -> { + val shareableDevices = account.deviceConstellation().state() + ?.otherDevices + .orEmpty() + .filter { it.capabilities.contains(DeviceCapability.SEND_TAB) } + + val list = mutableListOf() + if (shareableDevices.isEmpty()) { + // Show add device button if there are no devices + list.add(SyncShareOption.AddNewDevice) + } + + shareableDevices.mapTo(list) { SyncShareOption.SingleDevice(it) } + + if (shareableDevices.size > 1) { + // Show send all button if there are multiple devices + list.add(SyncShareOption.SendAll(shareableDevices)) + } + list + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/share/listadapters/AppShareAdapter.kt b/app/src/main/java/org/mozilla/fenix/share/listadapters/AppShareAdapter.kt index 1a585688b..6c722dd5e 100644 --- a/app/src/main/java/org/mozilla/fenix/share/listadapters/AppShareAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/share/listadapters/AppShareAdapter.kt @@ -17,7 +17,7 @@ import org.mozilla.fenix.share.viewholders.AppViewHolder */ class AppShareAdapter( private val interactor: ShareToAppsInteractor -) : ListAdapter(DiffCallback) { +) : ListAdapter(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { val view = LayoutInflater.from(parent.context) @@ -30,37 +30,26 @@ class AppShareAdapter( holder.bind(getItem(position)) } - private object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AndroidShareOption, newItem: AndroidShareOption) = - when (oldItem) { - AndroidShareOption.Invisible -> oldItem === newItem - is AndroidShareOption.App -> - newItem is AndroidShareOption.App && oldItem.packageName == newItem.packageName - } + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AppShareOption, newItem: AppShareOption) = + oldItem.packageName == newItem.packageName - @Suppress("DiffUtilEquals") - override fun areContentsTheSame(oldItem: AndroidShareOption, newItem: AndroidShareOption) = + override fun areContentsTheSame(oldItem: AppShareOption, newItem: AppShareOption) = oldItem == newItem } } /** * Represents an app that can be shared to. + * + * @property name Name of the app. + * @property icon Icon representing the share target. + * @property packageName Package of the app. + * @property activityName Activity that will be shared to. */ -sealed class AndroidShareOption { - object Invisible : AndroidShareOption() - /** - * Represents an app that can be shared to. - * - * @property name Name of the app. - * @property icon Icon representing the share target. - * @property packageName Package of the app. - * @property activityName Activity that will be shared to. - */ - data class App( - val name: String, - val icon: Drawable, - val packageName: String, - val activityName: String - ) : AndroidShareOption() -} +data class AppShareOption( + val name: String, + val icon: Drawable, + val packageName: String, + val activityName: String +) diff --git a/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt b/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt index bdf098cd3..52644dbe7 100644 --- a/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/share/viewholders/AppViewHolder.kt @@ -6,43 +6,32 @@ package org.mozilla.fenix.share.viewholders import android.view.View import androidx.annotation.VisibleForTesting -import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.app_share_list_item.view.* import org.mozilla.fenix.R -import org.mozilla.fenix.lib.Do import org.mozilla.fenix.share.ShareToAppsInteractor -import org.mozilla.fenix.share.listadapters.AndroidShareOption +import org.mozilla.fenix.share.listadapters.AppShareOption class AppViewHolder( itemView: View, @VisibleForTesting val interactor: ShareToAppsInteractor ) : RecyclerView.ViewHolder(itemView) { - private var application: AndroidShareOption? = null + private var application: AppShareOption? = null init { itemView.setOnClickListener { - Do exhaustive when (val app = application) { - AndroidShareOption.Invisible, null -> { /* no-op */ } - is AndroidShareOption.App -> interactor.onShareToApp(app) + application?.let { app -> + interactor.onShareToApp(app) } } } - fun bind(item: AndroidShareOption) { + fun bind(item: AppShareOption) { application = item - when (item) { - AndroidShareOption.Invisible -> { - itemView.isInvisible = true - } - is AndroidShareOption.App -> { - itemView.isInvisible = false - itemView.appName.text = item.name - itemView.appIcon.setImageDrawable(item.icon) - } - } + itemView.appName.text = item.name + itemView.appIcon.setImageDrawable(item.icon) } companion object { diff --git a/app/src/main/res/layout/share_to_apps.xml b/app/src/main/res/layout/share_to_apps.xml index b21e0a446..d44d1f5af 100644 --- a/app/src/main/res/layout/share_to_apps.xml +++ b/app/src/main/res/layout/share_to_apps.xml @@ -39,6 +39,7 @@ android:id="@+id/appsList" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="160dp" android:layout_marginBottom="8dp" android:clipToPadding="false" android:orientation="horizontal" diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt index b8a2c0388..cff245abc 100644 --- a/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt @@ -40,7 +40,7 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav -import org.mozilla.fenix.share.listadapters.AndroidShareOption +import org.mozilla.fenix.share.listadapters.AppShareOption import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -84,7 +84,7 @@ class ShareControllerTest { fun `handleShareToApp should start a new sharing activity and close this`() { val appPackageName = "package" val appClassName = "activity" - val appShareOption = AndroidShareOption.App("app", mockk(), appPackageName, appClassName) + val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName) val shareIntent = slot() // Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt index f59e488da..3afac05d6 100644 --- a/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/share/ShareInteractorTest.kt @@ -8,7 +8,7 @@ import io.mockk.mockk import io.mockk.verify import mozilla.components.concept.sync.Device import org.junit.Test -import org.mozilla.fenix.share.listadapters.AndroidShareOption +import org.mozilla.fenix.share.listadapters.AppShareOption class ShareInteractorTest { private val controller = mockk(relaxed = true) @@ -62,7 +62,7 @@ class ShareInteractorTest { @Test fun onShareToApp() { - val app = mockk() + val app = mockk() interactor.onShareToApp(app) diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt new file mode 100644 index 000000000..cd025c32b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt @@ -0,0 +1,133 @@ +/* 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.share + +import android.app.Application +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.net.ConnectivityManager +import androidx.core.content.getSystemService +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import mozilla.components.service.fxa.manager.FxaAccountManager +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.TestApplication +import org.mozilla.fenix.ext.application +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.share.listadapters.AppShareOption +import org.mozilla.fenix.share.listadapters.SyncShareOption +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +class ShareViewModelTest { + + private val packageName = "org.mozilla.fenix" + private lateinit var application: Application + private lateinit var packageManager: PackageManager + private lateinit var connectivityManager: ConnectivityManager + private lateinit var fxaAccountManager: FxaAccountManager + private lateinit var viewModel: ShareViewModel + + @Before + fun setup() { + application = spyk(testContext.application) + packageManager = mockk(relaxed = true) + connectivityManager = mockk(relaxed = true) + fxaAccountManager = mockk(relaxed = true) + + every { application.packageName } returns packageName + every { application.packageManager } returns packageManager + every { application.getSystemService() } returns connectivityManager + every { application.components.backgroundServices.accountManager } returns fxaAccountManager + + viewModel = ShareViewModel(application) + } + + @Test + fun `liveData should be initialized as empty list`() { + assertEquals(emptyList(), viewModel.devicesList.value) + assertEquals(emptyList(), viewModel.appsList.value) + } + + @Test + fun `loadDevicesAndApps registers networkCallback`() = runBlocking { + viewModel.loadDevicesAndApps() + + verify { connectivityManager.registerNetworkCallback(any(), eq(viewModel.networkCallback)) } + } + + @Test + fun `buildAppsList transforms ResolveInfo list`() { + assertEquals(emptyList(), viewModel.buildAppsList(null, application)) + + val icon1: Drawable = mockk() + val icon2: Drawable = mockk() + + val info = listOf( + createResolveInfo("App 0", icon1, "package 0", "activity 0"), + createResolveInfo("Self", mockk(), packageName, "activity self"), + createResolveInfo("App 1", icon2, "package 1", "activity 1") + ) + val apps = listOf( + AppShareOption("App 0", icon1, "package 0", "activity 0"), + AppShareOption("App 1", icon2, "package 1", "activity 1") + ) + assertEquals(apps, viewModel.buildAppsList(info, application)) + } + + @Test + fun `buildDevicesList returns offline option`() { + every { connectivityManager.activeNetworkInfo.isConnected } returns false + assertEquals(listOf(SyncShareOption.Offline), viewModel.buildDeviceList(fxaAccountManager)) + + every { connectivityManager.activeNetworkInfo } returns null + assertEquals(listOf(SyncShareOption.Offline), viewModel.buildDeviceList(fxaAccountManager)) + } + + @Test + fun `buildDevicesList returns sign-in option`() { + every { connectivityManager.activeNetworkInfo.isConnected } returns true + every { fxaAccountManager.authenticatedAccount() } returns null + + assertEquals(listOf(SyncShareOption.SignIn), viewModel.buildDeviceList(fxaAccountManager)) + } + + @Test + fun `buildDevicesList returns reconnect option`() { + every { connectivityManager.activeNetworkInfo.isConnected } returns true + every { fxaAccountManager.authenticatedAccount() } returns mockk() + every { fxaAccountManager.accountNeedsReauth() } returns true + + assertEquals(listOf(SyncShareOption.Reconnect), viewModel.buildDeviceList(fxaAccountManager)) + } + + private fun createResolveInfo( + label: String, + icon: Drawable, + packageName: String, + name: String + ): ResolveInfo { + val info = ResolveInfo().apply { + activityInfo = ActivityInfo() + activityInfo.packageName = packageName + activityInfo.name = name + } + val spy = spyk(info) + every { spy.loadLabel(packageManager) } returns label + every { spy.loadIcon(packageManager) } returns icon + return spy + } +} diff --git a/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt index e2cc09cf9..a2816dd31 100644 --- a/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/share/listadapters/AppShareAdapterTest.kt @@ -14,7 +14,6 @@ import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import io.mockk.verifyOrder -import kotlinx.android.synthetic.main.app_share_list_item.view.* import mozilla.components.support.test.robolectric.testContext import org.junit.Test import org.junit.runner.RunWith @@ -28,11 +27,11 @@ import org.robolectric.annotation.Config @Config(application = TestApplication::class) class AppShareAdapterTest { - private val appOptions = mutableListOf( - AndroidShareOption.App("App 0", mockk(), "package 0", "activity 0"), - AndroidShareOption.App("App 1", mockk(), "package 1", "activity 1") + private val appOptions = mutableListOf( + AppShareOption("App 0", mockk(), "package 0", "activity 0"), + AppShareOption("App 1", mockk(), "package 1", "activity 1") ) - private val appOptionsEmpty = emptyList() + private val appOptionsEmpty = emptyList() private val interactor: ShareInteractor = mockk(relaxed = true) @Test diff --git a/architecture/build.gradle b/architecture/build.gradle index 34b7ff063..8b98edd0d 100644 --- a/architecture/build.gradle +++ b/architecture/build.gradle @@ -36,7 +36,8 @@ dependencies { implementation Deps.kotlin_stdlib implementation Deps.androidx_annotation - implementation Deps.androidx_lifecycle_extensions + implementation Deps.androidx_lifecycle_livedata + implementation Deps.androidx_lifecycle_viewmodel implementation Deps.mozilla_support_base implementation Deps.rxAndroid diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index ce55403f8..9bc49f6f6 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -174,7 +174,7 @@ object Deps { const val androidx_coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:${Versions.androidx_coordinator_layout}" const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}" const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}" - const val androidx_lifecycle_extensions = "androidx.lifecycle:lifecycle-extensions:${Versions.androidx_lifecycle}" + const val androidx_lifecycle_livedata = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.androidx_lifecycle}" const val androidx_lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}" const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle}" const val androidx_paging = "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}"