1
0
Fork 0
Added kapt plugin + dependencies in order to be able to use Room
Added recent apps to share fragment (top 6)
Extracted dimens of share_to_apps.xml in the dimens file
master
Mihai Branescu 2019-11-26 17:39:54 +02:00 committed by Emily Kager
parent 3486f30b6a
commit fdbf63fb97
12 changed files with 162 additions and 31 deletions

View File

@ -5,6 +5,7 @@ plugins {
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco' apply plugin: 'jacoco'
apply from: "$project.rootDir/automation/gradle/versionCode.gradle" apply from: "$project.rootDir/automation/gradle/versionCode.gradle"
apply plugin: 'androidx.navigation.safeargs.kotlin' apply plugin: 'androidx.navigation.safeargs.kotlin'
@ -436,6 +437,7 @@ dependencies {
implementation Deps.mozilla_feature_readerview implementation Deps.mozilla_feature_readerview
implementation Deps.mozilla_feature_tab_collections implementation Deps.mozilla_feature_tab_collections
implementation Deps.mozilla_feature_top_sites implementation Deps.mozilla_feature_top_sites
implementation Deps.mozilla_feature_share
implementation Deps.mozilla_feature_accounts_push implementation Deps.mozilla_feature_accounts_push
implementation Deps.mozilla_feature_webcompat implementation Deps.mozilla_feature_webcompat
implementation Deps.mozilla_feature_webnotifications implementation Deps.mozilla_feature_webnotifications

View File

@ -293,7 +293,7 @@ private fun assertSendToDeviceTitle() = SendToDeviceTitle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun ShareALinkTitle() = private fun ShareALinkTitle() =
onView(allOf(withText("SHARE A LINK"), withResourceName("link_header"))) onView(allOf(withText(R.string.share_link_all_apps_subheader), withResourceName("apps_link_header")))
private fun assertShareALinkTitle() = ShareALinkTitle() private fun assertShareALinkTitle() = ShareALinkTitle()

View File

@ -14,6 +14,7 @@ import android.net.Uri
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController import androidx.navigation.NavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -22,6 +23,7 @@ import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.sync.Device import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.TabData import mozilla.components.concept.sync.TabData
import mozilla.components.feature.accounts.push.SendTabUseCases import mozilla.components.feature.accounts.push.SendTabUseCases
import mozilla.components.feature.share.RecentAppsStorage
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -65,6 +67,8 @@ class DefaultShareController(
private val sendTabUseCases: SendTabUseCases, private val sendTabUseCases: SendTabUseCases,
private val snackbar: FenixSnackbar, private val snackbar: FenixSnackbar,
private val navController: NavController, private val navController: NavController,
private val recentAppsStorage: RecentAppsStorage,
private val lifecycleScope: CoroutineScope,
private val dismiss: (ShareController.Result) -> Unit private val dismiss: (ShareController.Result) -> Unit
) : ShareController { ) : ShareController {
@ -79,6 +83,10 @@ class DefaultShareController(
} }
override fun handleShareToApp(app: AppShareOption) { override fun handleShareToApp(app: AppShareOption) {
lifecycleScope.launch(Dispatchers.IO) {
recentAppsStorage.updateRecentApp(app.packageName)
}
val intent = Intent(ACTION_SEND).apply { val intent = Intent(ACTION_SEND).apply {
putExtra(EXTRA_TEXT, getShareText()) putExtra(EXTRA_TEXT, getShareText())
putExtra(EXTRA_SUBJECT, shareData.map { it.title }.joinToString(", ")) putExtra(EXTRA_SUBJECT, shareData.map { it.title }.joinToString(", "))

View File

@ -12,14 +12,20 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe 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.synthetic.main.fragment_share.view.* import kotlinx.android.synthetic.main.fragment_share.view.appsShareLayout
import kotlinx.android.synthetic.main.fragment_share.view.closeSharingContent
import kotlinx.android.synthetic.main.fragment_share.view.closeSharingScrim
import kotlinx.android.synthetic.main.fragment_share.view.devicesShareLayout
import kotlinx.android.synthetic.main.fragment_share.view.shareWrapper
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.feature.accounts.push.SendTabUseCases import mozilla.components.feature.accounts.push.SendTabUseCases
import mozilla.components.feature.share.RecentAppsStorage
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
@ -67,7 +73,9 @@ class ShareFragment : AppCompatDialogFragment() {
shareData = shareData, shareData = shareData,
snackbar = FenixSnackbar.makeWithToolbarPadding(requireActivity().getRootView()!!), snackbar = FenixSnackbar.makeWithToolbarPadding(requireActivity().getRootView()!!),
navController = findNavController(), navController = findNavController(),
sendTabUseCases = SendTabUseCases(accountManager) sendTabUseCases = SendTabUseCases(accountManager),
recentAppsStorage = RecentAppsStorage(requireContext()),
lifecycleScope = lifecycleScope
) { result -> ) { result ->
consumePrompt { consumePrompt {
when (result) { when (result) {
@ -108,6 +116,9 @@ class ShareFragment : AppCompatDialogFragment() {
viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo -> viewModel.appsList.observe(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setShareTargets(appsToShareTo) shareToAppsView.setShareTargets(appsToShareTo)
} }
viewModel.recentAppsList.observe(viewLifecycleOwner) { appsToShareTo ->
shareToAppsView.setRecentShareTargets(appsToShareTo)
}
} }
/** /**

View File

@ -8,7 +8,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.appsList
import kotlinx.android.synthetic.main.share_to_apps.progressBar
import kotlinx.android.synthetic.main.share_to_apps.recentAppsContainer
import kotlinx.android.synthetic.main.share_to_apps.recentAppsList
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.share.listadapters.AppShareAdapter import org.mozilla.fenix.share.listadapters.AppShareAdapter
import org.mozilla.fenix.share.listadapters.AppShareOption import org.mozilla.fenix.share.listadapters.AppShareOption
@ -26,18 +29,30 @@ class ShareToAppsView(
) : LayoutContainer { ) : LayoutContainer {
private val adapter = AppShareAdapter(interactor) private val adapter = AppShareAdapter(interactor)
private val recentAdapter = AppShareAdapter(interactor)
init { init {
LayoutInflater.from(containerView.context) LayoutInflater.from(containerView.context)
.inflate(R.layout.share_to_apps, containerView, true) .inflate(R.layout.share_to_apps, containerView, true)
appsList.adapter = adapter appsList.adapter = adapter
recentAppsList.adapter = recentAdapter
} }
fun setShareTargets(targets: List<AppShareOption>) { fun setShareTargets(targets: List<AppShareOption>) {
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
appsList.visibility = View.VISIBLE
appsList.visibility = View.VISIBLE
adapter.submitList(targets) adapter.submitList(targets)
} }
fun setRecentShareTargets(recentTargets: List<AppShareOption>) {
if (recentTargets.isEmpty()) {
return
}
progressBar.visibility = View.GONE
recentAppsContainer.visibility = View.VISIBLE
recentAdapter.submitList(recentTargets)
}
} }

View File

@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.concept.sync.DeviceCapability import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.feature.share.RecentAppsStorage
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline import org.mozilla.fenix.ext.isOnline
@ -29,11 +30,17 @@ import org.mozilla.fenix.share.listadapters.SyncShareOption
class ShareViewModel(application: Application) : AndroidViewModel(application) { class ShareViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private const val RECENT_APPS_LIMIT = 6
}
private val connectivityManager by lazy { application.getSystemService<ConnectivityManager>() } private val connectivityManager by lazy { application.getSystemService<ConnectivityManager>() }
private val fxaAccountManager = application.components.backgroundServices.accountManager private val fxaAccountManager = application.components.backgroundServices.accountManager
private val recentAppsStorage = RecentAppsStorage(application.applicationContext)
private val devicesListLiveData = MutableLiveData<List<SyncShareOption>>(emptyList()) private val devicesListLiveData = MutableLiveData<List<SyncShareOption>>(emptyList())
private val appsListLiveData = MutableLiveData<List<AppShareOption>>(emptyList()) private val appsListLiveData = MutableLiveData<List<AppShareOption>>(emptyList())
private val recentAppsListLiveData = MutableLiveData<List<AppShareOption>>(emptyList())
@VisibleForTesting @VisibleForTesting
internal val networkCallback = object : ConnectivityManager.NetworkCallback() { internal val networkCallback = object : ConnectivityManager.NetworkCallback() {
@ -61,6 +68,10 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
* List of applications that can be shared to. * List of applications that can be shared to.
*/ */
val appsList: LiveData<List<AppShareOption>> get() = appsListLiveData val appsList: LiveData<List<AppShareOption>> get() = appsListLiveData
/**
* List of recent applications that can be shared to.
*/
val recentAppsList: LiveData<List<AppShareOption>> get() = recentAppsListLiveData
/** /**
* Load a list of devices and apps into [devicesList] and [appsList]. * Load a list of devices and apps into [devicesList] and [appsList].
@ -77,7 +88,12 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
val shareAppsActivities = getIntentActivities(shareIntent, getApplication()) val shareAppsActivities = getIntentActivities(shareIntent, getApplication())
val apps = buildAppsList(shareAppsActivities, getApplication()) var apps = buildAppsList(shareAppsActivities, getApplication())
recentAppsStorage.updateDatabaseWithNewApps(apps.map { app -> app.packageName })
val recentApps = buildRecentAppsList(apps)
apps = filterOutRecentApps(apps, recentApps)
recentAppsListLiveData.postValue(recentApps)
appsListLiveData.postValue(apps) appsListLiveData.postValue(apps)
} }
@ -87,6 +103,27 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
private fun filterOutRecentApps(
apps: List<AppShareOption>,
recentApps: List<AppShareOption>
): List<AppShareOption> {
return apps.filter { app -> !recentApps.contains(app) }
}
@WorkerThread
internal fun buildRecentAppsList(apps: List<AppShareOption>): List<AppShareOption> {
val recentAppsDatabase = recentAppsStorage.getRecentAppsUpTo(RECENT_APPS_LIMIT)
val result: MutableList<AppShareOption> = ArrayList()
for (recentApp in recentAppsDatabase) {
for (app in apps) {
if (recentApp.packageName == app.packageName) {
result.add(app)
}
}
}
return result
}
/** /**
* Unregisters the network callback and cleans up. * Unregisters the network callback and cleans up.
*/ */

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?inset"/>
<corners android:radius="@dimen/share_recent_apps_background_radius"/>
</shape>

View File

@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
<!-- 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 - 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/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -13,43 +11,75 @@
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="76dp" android:layout_width="76dp"
android:layout_height="37dp" android:layout_height="37dp"
android:layout_marginTop="16dp" android:layout_marginTop="@dimen/share_progress_bar_margin"
android:layout_marginBottom="16dp" android:layout_marginBottom="@dimen/share_progress_bar_margin"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/link_header" /> app:layout_constraintTop_toBottomOf="@id/apps_link_header" />
<TextView <LinearLayout
android:id="@+id/link_header" android:id="@+id/recentAppsContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="@dimen/share_recent_apps_margin"
android:layout_marginTop="8dp" android:layout_marginTop="@dimen/share_recent_apps_margin"
android:singleLine="true" android:layout_marginBottom="@dimen/share_recent_apps_margin"
android:text="@string/share_link_subheader" android:background="@drawable/recent_apps_background"
android:textAllCaps="true" android:orientation="vertical"
android:textColor="?secondaryText" android:paddingStart="@dimen/share_recent_apps_padding"
android:textSize="12sp" android:paddingTop="@dimen/share_recent_apps_padding"
android:textStyle="bold" android:paddingEnd="@dimen/share_recent_apps_padding"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/recent_apps_link_header"
style="@style/ShareHeaderTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/share_link_recent_apps_subheader" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recentAppsList"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clipToPadding="false"
android:minHeight="@dimen/share_list_min_height"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
</LinearLayout>
<TextView
android:id="@+id/apps_link_header"
style="@style/ShareHeaderTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/share_all_apps_header_margin"
android:layout_marginTop="@dimen/share_all_apps_header_margin"
android:text="@string/share_link_all_apps_subheader"
app:layout_constraintStart_toEndOf="@+id/recentAppsContainer"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/appsList" android:id="@+id/appsList"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="160dp" android:layout_marginBottom="@dimen/share_all_apps_list_margin"
android:layout_marginBottom="8dp"
android:clipToPadding="false" android:clipToPadding="false"
android:minHeight="@dimen/share_list_min_height"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="16dp" android:paddingStart="@dimen/share_all_apps_list_padding_start"
android:paddingEnd="8dp" android:paddingEnd="@dimen/share_all_apps_list_padding_end"
android:visibility="gone" android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@+id/recentAppsContainer"
app:layout_constraintTop_toBottomOf="@id/link_header" app:layout_constraintTop_toBottomOf="@id/apps_link_header"
app:spanCount="2" /> app:spanCount="2" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -115,4 +115,16 @@
<dimen name="migration_progress_margin_vertical">8dp</dimen> <dimen name="migration_progress_margin_vertical">8dp</dimen>
<dimen name="migration_progress_margin_start">4dp</dimen> <dimen name="migration_progress_margin_start">4dp</dimen>
<!-- Share Fragment -->
<dimen name="share_recent_apps_background_radius">10dp</dimen>
<dimen name="share_recent_apps_padding">8dp</dimen>
<dimen name="share_recent_apps_margin">8dp</dimen>
<dimen name="share_all_apps_header_margin">16dp</dimen>
<dimen name="share_list_min_height">160dp</dimen>
<dimen name="share_all_apps_list_margin">8dp</dimen>
<dimen name="share_all_apps_list_padding_start">16dp</dimen>
<dimen name="share_all_apps_list_padding_end">8dp</dimen>
<dimen name="share_header_text_size">12sp</dimen>
<dimen name="share_progress_bar_margin">16dp</dimen>
</resources> </resources>

View File

@ -361,7 +361,14 @@
<item name="android:minHeight">40dp</item> <item name="android:minHeight">40dp</item>
<item name="android:layout_marginTop">32dp</item> <item name="android:layout_marginTop">32dp</item>
<item name="android:textColor">?accentUsedOnDarkBackground</item> <item name="android:textColor">?accentUsedOnDarkBackground</item>
</style>
<style name="ShareHeaderTextStyle">
<item name="android:singleLine">true</item>
<item name="android:textAllCaps">true</item>
<item name="android:textColor">?secondaryText</item>
<item name="android:textSize">@dimen/share_header_text_size</item>
<item name="android:textStyle">bold</item>
</style> </style>
<style name="FirefoxAccountsDialogStyle" parent="DialogStyleBase"> <style name="FirefoxAccountsDialogStyle" parent="DialogStyleBase">

View File

@ -13,9 +13,9 @@ import android.net.ConnectivityManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import io.mockk.mockkStatic
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext

View File

@ -126,6 +126,7 @@ object Deps {
const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}" const val mozilla_feature_tab_collections = "org.mozilla.components:feature-tab-collections:${Versions.mozilla_android_components}"
const val mozilla_feature_accounts_push = "org.mozilla.components:feature-accounts-push:${Versions.mozilla_android_components}" const val mozilla_feature_accounts_push = "org.mozilla.components:feature-accounts-push:${Versions.mozilla_android_components}"
const val mozilla_feature_top_sites = "org.mozilla.components:feature-top-sites:${Versions.mozilla_android_components}" const val mozilla_feature_top_sites = "org.mozilla.components:feature-top-sites:${Versions.mozilla_android_components}"
const val mozilla_feature_share = "org.mozilla.components:feature-share:${Versions.mozilla_android_components}"
const val mozilla_feature_webcompat = "org.mozilla.components:feature-webcompat:${Versions.mozilla_android_components}" const val mozilla_feature_webcompat = "org.mozilla.components:feature-webcompat:${Versions.mozilla_android_components}"
const val mozilla_feature_webnotifications = "org.mozilla.components:feature-webnotifications:${Versions.mozilla_android_components}" const val mozilla_feature_webnotifications = "org.mozilla.components:feature-webnotifications:${Versions.mozilla_android_components}"