Use ViewModel for share fragment
parent
333ff8c941
commit
fdd7400ccc
|
@ -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
|
||||
|
|
|
@ -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<Device>)
|
||||
|
@ -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"
|
||||
|
|
|
@ -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<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) {
|
||||
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<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
|
||||
}
|
||||
viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo ->
|
||||
shareToAppsView.setShareTargets(appsToShareTo)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AndroidShareOption>) {
|
||||
fun setShareTargets(targets: List<AppShareOption>) {
|
||||
progressBar.visibility = View.GONE
|
||||
appsList.visibility = View.VISIBLE
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ import org.mozilla.fenix.share.viewholders.AppViewHolder
|
|||
*/
|
||||
class AppShareAdapter(
|
||||
private val interactor: ShareToAppsInteractor
|
||||
) : ListAdapter<AndroidShareOption, AppViewHolder>(DiffCallback) {
|
||||
) : ListAdapter<AppShareOption, AppViewHolder>(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<AndroidShareOption>() {
|
||||
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<AppShareOption>() {
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<Intent>()
|
||||
// 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
|
||||
|
|
|
@ -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<ShareController>(relaxed = true)
|
||||
|
@ -62,7 +62,7 @@ class ShareInteractorTest {
|
|||
|
||||
@Test
|
||||
fun onShareToApp() {
|
||||
val app = mockk<AndroidShareOption.App>()
|
||||
val app = mockk<AppShareOption>()
|
||||
|
||||
interactor.onShareToApp(app)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>(
|
||||
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<AndroidShareOption>()
|
||||
private val appOptionsEmpty = emptyList<AppShareOption>()
|
||||
private val interactor: ShareInteractor = mockk(relaxed = true)
|
||||
|
||||
@Test
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
||||
|
|
Loading…
Reference in New Issue