diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt index 15558c7f8..80cca4471 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -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" + } } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt index 51f9fadf7..64f3bd892 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt @@ -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?) { diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt b/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt index 29cd7c769..1d4ab4940 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt @@ -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, recentApps: List diff --git a/app/src/main/res/drawable/ic_share_clipboard.xml b/app/src/main/res/drawable/ic_share_clipboard.xml new file mode 100644 index 000000000..bc31de208 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_clipboard.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17c982952..b881a137d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -954,6 +954,10 @@ All actions Recently used + + Copy to clipboard + + Copied to clipboard Sign in to sync diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt index 9ad7d78cc..0c34e2634 100644 --- a/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt +++ b/app/src/test/java/org/mozilla/fenix/share/ShareViewModelTest.kt @@ -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() + 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() + 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,