1
0
Fork 0

Use ViewModel for share fragment

master
Tiger Oakes 2019-11-05 17:30:04 -08:00 committed by Emily Kager
parent 333ff8c941
commit fdd7400ccc
15 changed files with 353 additions and 209 deletions

View File

@ -10,11 +10,12 @@ apply from: "$project.rootDir/automation/gradle/versionCode.gradle"
apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.android.gms.oss-licenses-plugin' 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.StyledTextOutput.Style
import org.gradle.internal.logging.text.StyledTextOutputFactory import org.gradle.internal.logging.text.StyledTextOutputFactory
import static org.gradle.api.tasks.testing.TestResult.ResultType import static org.gradle.api.tasks.testing.TestResult.ResultType
import com.android.build.OutputFile
android { android {
compileSdkVersion 28 compileSdkVersion 28
@ -413,6 +414,7 @@ dependencies {
implementation Deps.androidx_navigation_fragment implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_livedata
implementation Deps.androidx_lifecycle_runtime implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_viewmodel implementation Deps.androidx_lifecycle_viewmodel
implementation Deps.androidx_core implementation Deps.androidx_core

View File

@ -26,7 +26,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.share.listadapters.AndroidShareOption import org.mozilla.fenix.share.listadapters.AppShareOption
/** /**
* [ShareFragment] controller. * [ShareFragment] controller.
@ -36,7 +36,7 @@ import org.mozilla.fenix.share.listadapters.AndroidShareOption
interface ShareController { interface ShareController {
fun handleReauth() fun handleReauth()
fun handleShareClosed() fun handleShareClosed()
fun handleShareToApp(app: AndroidShareOption.App) fun handleShareToApp(app: AppShareOption)
fun handleAddNewDevice() fun handleAddNewDevice()
fun handleShareToDevice(device: Device) fun handleShareToDevice(device: Device)
fun handleShareToAllDevices(devices: List<Device>) fun handleShareToAllDevices(devices: List<Device>)
@ -72,7 +72,7 @@ class DefaultShareController(
dismiss() dismiss()
} }
override fun handleShareToApp(app: AndroidShareOption.App) { override fun handleShareToApp(app: AppShareOption) {
val intent = Intent(ACTION_SEND).apply { val intent = Intent(ACTION_SEND).apply {
putExtra(EXTRA_TEXT, getShareText()) putExtra(EXTRA_TEXT, getShareText())
type = "text/plain" type = "text/plain"

View File

@ -5,97 +5,38 @@
package org.mozilla.fenix.share package org.mozilla.fenix.share
import android.content.Context 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.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.content.getSystemService import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
import androidx.lifecycle.observe
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_share.view.* 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.feature.sendtab.SendTabUseCases
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbarPresenter import org.mozilla.fenix.components.FenixSnackbarPresenter
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents 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() { class ShareFragment : AppCompatDialogFragment() {
private val viewModel: ShareViewModel by viewModels {
AndroidViewModelFactory(requireActivity().application)
}
private lateinit var shareInteractor: ShareInteractor private lateinit var shareInteractor: ShareInteractor
private lateinit var shareCloseView: ShareCloseView private lateinit var shareCloseView: ShareCloseView
private lateinit var shareToAccountDevicesView: ShareToAccountDevicesView private lateinit var shareToAccountDevicesView: ShareToAccountDevicesView
private lateinit var shareToAppsView: ShareToAppsView private lateinit var shareToAppsView: ShareToAppsView
private lateinit var appsListDeferred: Deferred<List<AndroidShareOption>>
private lateinit var devicesListDeferred: Deferred<List<SyncShareOption>>
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) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
viewModel.loadDevicesAndApps()
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()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -149,84 +90,11 @@ class ShareFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.devicesList.observe(viewLifecycleOwner) { devicesShareOptions ->
// 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()
shareToAccountDevicesView.setShareTargets(devicesShareOptions) shareToAccountDevicesView.setShareTargets(devicesShareOptions)
val appsToShareTo = appsListDeferred.await()
shareToAppsView.setShareTargets(appsToShareTo)
} }
} viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setShareTargets(appsToShareTo)
@WorkerThread
private fun getIntentActivities(shareIntent: Intent, context: Context): List<ResolveInfo>? {
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<ResolveInfo>?,
context: Context
): List<AndroidShareOption> {
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<SyncShareOption> {
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<SyncShareOption>()
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
}
} }
} }

View File

@ -5,7 +5,7 @@
package org.mozilla.fenix.share package org.mozilla.fenix.share
import mozilla.components.concept.sync.Device 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. * Interactor for the share screen.
@ -37,7 +37,7 @@ class ShareInteractor(
controller.handleShareToAllDevices(devices) controller.handleShareToAllDevices(devices)
} }
override fun onShareToApp(appToShareTo: AndroidShareOption.App) { override fun onShareToApp(appToShareTo: AppShareOption) {
controller.handleShareToApp(appToShareTo) controller.handleShareToApp(appToShareTo)
} }
} }

View File

@ -10,14 +10,14 @@ import android.view.ViewGroup
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.share_to_apps.* import kotlinx.android.synthetic.main.share_to_apps.*
import org.mozilla.fenix.R 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.AppShareAdapter
import org.mozilla.fenix.share.listadapters.AppShareOption
/** /**
* Callbacks for possible user interactions on the [ShareCloseView] * Callbacks for possible user interactions on the [ShareCloseView]
*/ */
interface ShareToAppsInteractor { interface ShareToAppsInteractor {
fun onShareToApp(appToShareTo: AndroidShareOption.App) fun onShareToApp(appToShareTo: AppShareOption)
} }
class ShareToAppsView( class ShareToAppsView(
@ -34,7 +34,7 @@ class ShareToAppsView(
appsList.adapter = adapter appsList.adapter = adapter
} }
fun setShareTargets(targets: List<AndroidShareOption>) { fun setShareTargets(targets: List<AppShareOption>) {
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
appsList.visibility = View.VISIBLE appsList.visibility = View.VISIBLE

View File

@ -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<ConnectivityManager>() }
private val fxaAccountManager = application.components.backgroundServices.accountManager
private val devicesListLiveData = MutableLiveData<List<SyncShareOption>>(emptyList())
private val appsListLiveData = MutableLiveData<List<AppShareOption>>(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<List<SyncShareOption>> get() = devicesListLiveData
/**
* List of applications that can be shared to.
*/
val appsList: LiveData<List<AppShareOption>> 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<ResolveInfo>? {
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<ResolveInfo>?,
context: Context
): List<AppShareOption> {
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<SyncShareOption> {
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<SyncShareOption>()
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
}
}
}
}

View File

@ -17,7 +17,7 @@ import org.mozilla.fenix.share.viewholders.AppViewHolder
*/ */
class AppShareAdapter( class AppShareAdapter(
private val interactor: ShareToAppsInteractor private val interactor: ShareToAppsInteractor
) : ListAdapter<AndroidShareOption, AppViewHolder>(DiffCallback) { ) : ListAdapter<AppShareOption, AppViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
@ -30,37 +30,26 @@ class AppShareAdapter(
holder.bind(getItem(position)) holder.bind(getItem(position))
} }
private object DiffCallback : DiffUtil.ItemCallback<AndroidShareOption>() { private object DiffCallback : DiffUtil.ItemCallback<AppShareOption>() {
override fun areItemsTheSame(oldItem: AndroidShareOption, newItem: AndroidShareOption) = override fun areItemsTheSame(oldItem: AppShareOption, newItem: AppShareOption) =
when (oldItem) { oldItem.packageName == newItem.packageName
AndroidShareOption.Invisible -> oldItem === newItem
is AndroidShareOption.App ->
newItem is AndroidShareOption.App && oldItem.packageName == newItem.packageName
}
@Suppress("DiffUtilEquals") override fun areContentsTheSame(oldItem: AppShareOption, newItem: AppShareOption) =
override fun areContentsTheSame(oldItem: AndroidShareOption, newItem: AndroidShareOption) =
oldItem == newItem oldItem == newItem
} }
} }
/** /**
* Represents an app that can be shared to. * 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 { data class AppShareOption(
object Invisible : AndroidShareOption() val name: String,
/** val icon: Drawable,
* Represents an app that can be shared to. val packageName: String,
* val activityName: String
* @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()
}

View File

@ -6,43 +6,32 @@ package org.mozilla.fenix.share.viewholders
import android.view.View import android.view.View
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.app_share_list_item.view.* import kotlinx.android.synthetic.main.app_share_list_item.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.share.ShareToAppsInteractor import org.mozilla.fenix.share.ShareToAppsInteractor
import org.mozilla.fenix.share.listadapters.AndroidShareOption import org.mozilla.fenix.share.listadapters.AppShareOption
class AppViewHolder( class AppViewHolder(
itemView: View, itemView: View,
@VisibleForTesting val interactor: ShareToAppsInteractor @VisibleForTesting val interactor: ShareToAppsInteractor
) : RecyclerView.ViewHolder(itemView) { ) : RecyclerView.ViewHolder(itemView) {
private var application: AndroidShareOption? = null private var application: AppShareOption? = null
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
Do exhaustive when (val app = application) { application?.let { app ->
AndroidShareOption.Invisible, null -> { /* no-op */ } interactor.onShareToApp(app)
is AndroidShareOption.App -> interactor.onShareToApp(app)
} }
} }
} }
fun bind(item: AndroidShareOption) { fun bind(item: AppShareOption) {
application = item application = item
when (item) { itemView.appName.text = item.name
AndroidShareOption.Invisible -> { itemView.appIcon.setImageDrawable(item.icon)
itemView.isInvisible = true
}
is AndroidShareOption.App -> {
itemView.isInvisible = false
itemView.appName.text = item.name
itemView.appIcon.setImageDrawable(item.icon)
}
}
} }
companion object { companion object {

View File

@ -39,6 +39,7 @@
android:id="@+id/appsList" android:id="@+id/appsList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="160dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="horizontal" android:orientation="horizontal"

View File

@ -40,7 +40,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav 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.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ -84,7 +84,7 @@ class ShareControllerTest {
fun `handleShareToApp should start a new sharing activity and close this`() { fun `handleShareToApp should start a new sharing activity and close this`() {
val appPackageName = "package" val appPackageName = "package"
val appClassName = "activity" val appClassName = "activity"
val appShareOption = AndroidShareOption.App("app", mockk(), appPackageName, appClassName) val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName)
val shareIntent = slot<Intent>() val shareIntent = slot<Intent>()
// Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call // 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 // needed for capturing the actual Intent used the `slot` one doesn't have this flag so we

View File

@ -8,7 +8,7 @@ import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import mozilla.components.concept.sync.Device import mozilla.components.concept.sync.Device
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.share.listadapters.AndroidShareOption import org.mozilla.fenix.share.listadapters.AppShareOption
class ShareInteractorTest { class ShareInteractorTest {
private val controller = mockk<ShareController>(relaxed = true) private val controller = mockk<ShareController>(relaxed = true)
@ -62,7 +62,7 @@ class ShareInteractorTest {
@Test @Test
fun onShareToApp() { fun onShareToApp() {
val app = mockk<AndroidShareOption.App>() val app = mockk<AppShareOption>()
interactor.onShareToApp(app) interactor.onShareToApp(app)

View File

@ -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<ConnectivityManager>() } returns connectivityManager
every { application.components.backgroundServices.accountManager } returns fxaAccountManager
viewModel = ShareViewModel(application)
}
@Test
fun `liveData should be initialized as empty list`() {
assertEquals(emptyList<SyncShareOption>(), viewModel.devicesList.value)
assertEquals(emptyList<AppShareOption>(), 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<AppShareOption>(), 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
}
}

View File

@ -14,7 +14,6 @@ import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import io.mockk.verifyOrder import io.mockk.verifyOrder
import kotlinx.android.synthetic.main.app_share_list_item.view.*
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -28,11 +27,11 @@ import org.robolectric.annotation.Config
@Config(application = TestApplication::class) @Config(application = TestApplication::class)
class AppShareAdapterTest { class AppShareAdapterTest {
private val appOptions = mutableListOf<AndroidShareOption>( private val appOptions = mutableListOf(
AndroidShareOption.App("App 0", mockk(), "package 0", "activity 0"), AppShareOption("App 0", mockk(), "package 0", "activity 0"),
AndroidShareOption.App("App 1", mockk(), "package 1", "activity 1") AppShareOption("App 1", mockk(), "package 1", "activity 1")
) )
private val appOptionsEmpty = emptyList<AndroidShareOption>() private val appOptionsEmpty = emptyList<AppShareOption>()
private val interactor: ShareInteractor = mockk(relaxed = true) private val interactor: ShareInteractor = mockk(relaxed = true)
@Test @Test

View File

@ -36,7 +36,8 @@ dependencies {
implementation Deps.kotlin_stdlib implementation Deps.kotlin_stdlib
implementation Deps.androidx_annotation 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.mozilla_support_base
implementation Deps.rxAndroid implementation Deps.rxAndroid

View File

@ -174,7 +174,7 @@ object Deps {
const val androidx_coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:${Versions.androidx_coordinator_layout}" 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_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}"
const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}" 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_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_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle}"
const val androidx_paging = "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}" const val androidx_paging = "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}"