Close #14313: Always add copy to clipboard action in share actions

upstream-sync
Roger Yang 2 years ago committed by mergify[bot]
parent df4d7a9004
commit d45543ec40

@ -5,6 +5,8 @@
package org.mozilla.fenix.share
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_SEND
@ -91,6 +93,13 @@ class DefaultShareController(
}
override fun handleShareToApp(app: AppShareOption) {
if (app.packageName == ACTION_COPY_LINK_TO_CLIPBOARD) {
copyClipboard()
dismiss(ShareController.Result.SUCCESS)
return
}
viewLifecycleScope.launch(dispatcher) {
recentAppsStorage.updateRecentApp(app.activityName)
}
@ -111,6 +120,7 @@ class DefaultShareController(
when (e) {
is SecurityException, is ActivityNotFoundException -> {
snackbar.setText(context.getString(R.string.share_error_snackbar))
snackbar.setLength(FenixSnackbar.LENGTH_LONG)
snackbar.show()
ShareController.Result.SHARE_ERROR
}
@ -217,4 +227,18 @@ class DefaultShareController(
private fun String.toDataUri(): String {
return "data:,${Uri.encode(this)}"
}
private fun copyClipboard() {
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(getShareSubject(), getShareText())
clipboardManager.setPrimaryClip(clipData)
snackbar.setText(context.getString(R.string.toast_copy_link_to_clipboard))
snackbar.setLength(FenixSnackbar.LENGTH_SHORT)
snackbar.show()
}
companion object {
const val ACTION_COPY_LINK_TO_CLIPBOARD = "org.mozilla.fenix.COPY_LINK_TO_CLIPBOARD"
}
}

@ -40,7 +40,7 @@ class ShareFragment : AppCompatDialogFragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
viewModel.loadDevicesAndApps()
viewModel.loadDevicesAndApps(requireContext())
}
override fun onCreate(savedInstanceState: Bundle?) {

@ -13,6 +13,7 @@ import android.net.Network
import android.net.NetworkRequest
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
@ -23,8 +24,10 @@ import kotlinx.coroutines.launch
import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.feature.share.RecentAppsStorage
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline
import org.mozilla.fenix.share.DefaultShareController.Companion.ACTION_COPY_LINK_TO_CLIPBOARD
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.SyncShareOption
@ -79,7 +82,7 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
* Load a list of devices and apps into [devicesList] and [appsList].
* Should be called when the fragment is attached so the data can be fetched early.
*/
fun loadDevicesAndApps() {
fun loadDevicesAndApps(context: Context) {
val networkRequest = NetworkRequest.Builder().build()
connectivityManager?.registerNetworkCallback(networkRequest, networkCallback)
@ -89,12 +92,18 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
type = "text/plain"
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val shareAppsActivities = getIntentActivities(shareIntent, getApplication())
var apps = buildAppsList(shareAppsActivities, getApplication())
val shareAppsActivities = getIntentActivities(shareIntent, context)
var apps = buildAppsList(shareAppsActivities, context)
recentAppsStorage.updateDatabaseWithNewApps(apps.map { app -> app.activityName })
val recentApps = buildRecentAppsList(apps)
apps = filterOutRecentApps(apps, recentApps)
// if copy app is available, prepend to the list of actions
getCopyApp(context)?.let {
apps = listOf(it) + apps
}
recentAppsListLiveData.postValue(recentApps)
appsListLiveData.postValue(apps)
}
@ -105,6 +114,19 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
}
}
private fun getCopyApp(context: Context): AppShareOption? {
val copyIcon = AppCompatResources.getDrawable(context, R.drawable.ic_share_clipboard)
return copyIcon?.let {
AppShareOption(
context.getString(R.string.share_copy_link_to_clipboard),
copyIcon,
ACTION_COPY_LINK_TO_CLIPBOARD,
""
)
}
}
private fun filterOutRecentApps(
apps: List<AppShareOption>,
recentApps: List<AppShareOption>

@ -0,0 +1,25 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="oval">
<solid android:color="@color/default_share_background" />
<padding
android:left="10dp"
android:top="8dp"
android:right="6dp"
android:bottom="8dp" />
</shape>
</item>
<item>
<vector
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@color/device_foreground"
android:fillType="nonZero"
android:pathData="M10.69,3A3.313,3.313 0,0 1,14 6.31v10.38A3.313,3.313 0,0 1,10.69 20L3.31,20A3.313,3.313 0,0 1,0 16.69L0,6.31A3.313,3.313 0,0 1,3.31 3h7.38zM10.69,5L3.31,5C2.587,5 2,5.587 2,6.31v10.38c0,0.723 0.587,1.31 1.31,1.31h7.38c0.723,0 1.31,-0.587 1.31,-1.31L12,6.31C12,5.587 11.413,5 10.69,5zM10.995,0A6.003,6.003 0,0 1,17 6v0.016l-0.024,8.987a2,2 0,0 1,-2.005 1.994h-0.002l0.03,-10.986L14.999,6c0,-2.21 -1.793,-4 -4.004,-4L3,2a2,2 0,0 1,2 -2h5.995z" />
</vector>
</item>
</layer-list>

@ -954,6 +954,10 @@
<string name="share_link_all_apps_subheader">All actions</string>
<!-- Sub-header in the dialog to share a link to an app from the most-recent sorted list -->
<string name="share_link_recent_apps_subheader">Recently used</string>
<!-- Text for the copy link action in the share screen. -->
<string name="share_copy_link_to_clipboard">Copy to clipboard</string>
<!-- Toast shown after copying link to clipboard -->
<string name="toast_copy_link_to_clipboard">Copied to clipboard</string>
<!-- An option from the three dot menu to into sync -->
<string name="sync_menu_sign_in">Sign in to sync</string>
<!-- An option from the share dialog to sign into sync -->

@ -35,6 +35,7 @@ import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.share.DefaultShareController.Companion.ACTION_COPY_LINK_TO_CLIPBOARD
import org.mozilla.fenix.share.ShareViewModel.Companion.RECENT_APPS_LIMIT
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.SyncShareOption
@ -100,7 +101,7 @@ class ShareViewModelTest {
every { viewModel.buildAppsList(any(), any()) } returns appOptions
viewModel.recentAppsStorage = storage
viewModel.loadDevicesAndApps()
viewModel.loadDevicesAndApps(testContext)
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
verify {
@ -111,7 +112,7 @@ class ShareViewModelTest {
}
assertEquals(1, viewModel.recentAppsList.asFlow().first().size)
assertEquals(0, viewModel.appsList.asFlow().first().size)
assertEquals(1, viewModel.appsList.asFlow().first().size)
}
@Test
@ -162,6 +163,45 @@ class ShareViewModelTest {
)
}
@Test
fun `GIVEN only one app THEN show copy to clipboard before the app`() = runBlockingTest {
val appOptions = listOf(
AppShareOption("Label", mockk(), "Package", "Activity")
)
val appEntity = mockk<RecentApp>()
every { appEntity.activityName } returns "Activity"
every { storage.updateDatabaseWithNewApps(appOptions.map { app -> app.packageName }) } just Runs
every { storage.getRecentAppsUpTo(RECENT_APPS_LIMIT) } returns emptyList()
every { viewModel.buildAppsList(any(), any()) } returns appOptions
viewModel.recentAppsStorage = storage
viewModel.loadDevicesAndApps(testContext)
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
assertEquals(0, viewModel.recentAppsList.asFlow().first().size)
assertEquals(2, viewModel.appsList.asFlow().first().size)
assertEquals(ACTION_COPY_LINK_TO_CLIPBOARD, viewModel.appsList.asFlow().first()[0].packageName)
}
@Test
fun `WHEN no app THEN at least have copy to clipboard as app`() = runBlockingTest {
val appEntity = mockk<RecentApp>()
every { appEntity.activityName } returns "Activity"
every { storage.getRecentAppsUpTo(RECENT_APPS_LIMIT) } returns emptyList()
every { viewModel.buildAppsList(any(), any()) } returns emptyList()
viewModel.recentAppsStorage = storage
viewModel.loadDevicesAndApps(testContext)
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
assertEquals(0, viewModel.recentAppsList.asFlow().first().size)
assertEquals(1, viewModel.appsList.asFlow().first().size)
assertEquals(ACTION_COPY_LINK_TO_CLIPBOARD, viewModel.appsList.asFlow().first()[0].packageName)
}
private fun createResolveInfo(
label: String,
icon: Drawable,

Loading…
Cancel
Save