diff --git a/autofill/autofill-impl/src/main/AndroidManifest.xml b/autofill/autofill-impl/src/main/AndroidManifest.xml index a32bb5ece3a3..6f29e9796fbc 100644 --- a/autofill/autofill-impl/src/main/AndroidManifest.xml +++ b/autofill/autofill-impl/src/main/AndroidManifest.xml @@ -3,6 +3,14 @@ package="com.duckduckgo.autofill.impl"> + + launchSharePageChooser(command.link) + is ShowCopiedNotification -> showCopiedNotification() + } + } + + private fun showCopiedNotification() { + Snackbar.make(binding.root, R.string.autofillManagementImportPasswordsGetDesktopBrowserLinkCopied, Toast.LENGTH_SHORT).show() + } + + @SuppressLint("UnspecifiedImmutableFlag") + private fun launchSharePageChooser(link: String) { + val share = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + val message = getString(R.string.autofillManagementImportPasswordsGetDesktopBrowserIntentMessage, link) + putExtra(Intent.EXTRA_TEXT, message) + putExtra(Intent.EXTRA_TITLE, getString(R.string.autofillManagementImportPasswordsGetDesktopBrowserIntentTitle)) + } + + try { + startActivity(Intent.createChooser(share, null)) + } catch (e: ActivityNotFoundException) { + Timber.w(e, "Activity not found") + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt new file mode 100644 index 000000000000..445365127420 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/desktopapp/ImportPasswordsGetDesktopAppViewModel.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.impl.ui.credential.management.AutofillClipboardInteractor +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command.ShareLink +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command.ShowCopiedNotification +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +@ContributesViewModel(AppScope::class) +class ImportPasswordsGetDesktopAppViewModel @Inject constructor( + private val pixel: Pixel, + private val dispatchers: DispatcherProvider, + private val autofillClipboardInteractor: AutofillClipboardInteractor, +) : ViewModel() { + + private val commandChannel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val commands = commandChannel.receiveAsFlow() + + sealed class Command { + data class ShareLink(val link: String) : Command() + data object ShowCopiedNotification : Command() + } + + data class ViewState(val windowsFeatureEnabled: Boolean) + + fun onShareClicked() { + viewModelScope.launch { + commandChannel.send(ShareLink(buildLink())) + } + } + + fun onLinkClicked() { + viewModelScope.launch(dispatchers.io()) { + autofillClipboardInteractor.copyToClipboard(buildLink(), isSensitive = false) + + if (autofillClipboardInteractor.shouldShowCopyNotification()) { + commandChannel.send(ShowCopiedNotification) + } + } + } + + private fun buildLink(): String { + return "$BASE_LINK?$ATTRIBUTION" + } + + companion object { + private const val BASE_LINK = "https://duckduckgo.com/browser" + private const val ATTRIBUTION = "origin=funnel_browser_android_sync" + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt index c51a52d61905..d6f58a163d44 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt @@ -49,8 +49,10 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementR import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter.ContextMenuAction.Edit import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchDeleteAllPasswordsConfirmation +import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchImportPasswords import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchResetNeverSaveListConfirmation import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.PromptUserToAuthenticateMassDeletion +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordActivityParams import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialGrouper import com.duckduckgo.autofill.impl.ui.credential.management.sorting.InitialExtractor import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilder @@ -67,6 +69,7 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.FragmentViewModelFactory import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.mobile.android.R as CommonR +import com.duckduckgo.navigation.api.GlobalActivityStarter import javax.inject.Inject import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -104,6 +107,9 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill @Inject lateinit var stringBuilder: AutofillManagementStringBuilder + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + val viewModel by lazy { ViewModelProvider(requireActivity(), viewModelFactory)[AutofillSettingsViewModel::class.java] } @@ -114,6 +120,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill private var searchMenuItem: MenuItem? = null private var resetNeverSavedSitesMenuItem: MenuItem? = null private var deleteAllPasswordsMenuItem: MenuItem? = null + private var importPasswordsMenuItem: MenuItem? = null private val globalAutofillToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) return@OnCheckedChangeListener @@ -131,6 +138,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill super.onViewCreated(view, savedInstanceState) configureToggle() configureRecyclerView() + configureImportPasswordsButton() observeViewModel() configureToolbar() } @@ -140,6 +148,12 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill hideSearchBar() } + private fun configureImportPasswordsButton() { + binding.emptyStateLayout.importPasswordsButton.setOnClickListener { + viewModel.onImportPasswords() + } + } + private fun configureToolbar() { activity?.addMenuProvider( object : MenuProvider { @@ -151,6 +165,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill searchMenuItem = menu.findItem(R.id.searchLogins) resetNeverSavedSitesMenuItem = menu.findItem(R.id.resetNeverSavedSites) deleteAllPasswordsMenuItem = menu.findItem(R.id.deleteAllPasswords) + importPasswordsMenuItem = menu.findItem(R.id.importPasswords) initializeSearchBar() } @@ -160,6 +175,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill searchMenuItem?.isVisible = loginsSaved deleteAllPasswordsMenuItem?.isVisible = loginsSaved resetNeverSavedSitesMenuItem?.isVisible = viewModel.neverSavedSitesViewState.value.showOptionToReset + importPasswordsMenuItem?.isVisible = loginsSaved } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { @@ -179,6 +195,11 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill true } + R.id.importPasswords -> { + viewModel.onImportPasswords() + true + } + else -> false } } @@ -268,6 +289,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill LaunchResetNeverSaveListConfirmation -> launchResetNeverSavedSitesConfirmation() is LaunchDeleteAllPasswordsConfirmation -> launchDeleteAllLoginsConfirmationDialog(command.numberToDelete) is PromptUserToAuthenticateMassDeletion -> promptUserToAuthenticateMassDeletion(command.authConfiguration) + LaunchImportPasswords -> launchImportPasswordsScreen() } viewModel.commandProcessed(command) } @@ -297,6 +319,12 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill } } + private fun launchImportPasswordsScreen() { + context?.let { + globalActivityStarter.start(it, ImportPasswordActivityParams) + } + } + private suspend fun credentialsListUpdated( credentials: List, credentialSearchQuery: String, @@ -318,6 +346,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill private fun showEmptyCredentialsPlaceholders() { binding.emptyStateLayout.emptyStateContainer.show() + binding.logins.gone() } diff --git a/autofill/autofill-impl/src/main/res/drawable/app_download_128.xml b/autofill/autofill-impl/src/main/res/drawable/app_download_128.xml new file mode 100644 index 000000000000..9fe37ad63e3e --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/app_download_128.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/drawable/autofill_import_instruction_bullet.xml b/autofill/autofill-impl/src/main/res/drawable/autofill_import_instruction_bullet.xml new file mode 100644 index 000000000000..f0254e40862c --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/autofill_import_instruction_bullet.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_passwords_add_96.xml b/autofill/autofill-impl/src/main/res/drawable/ic_passwords_add_96.xml new file mode 100644 index 000000000000..c9d8d7861bf3 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/ic_passwords_add_96.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/drawable/rounded_textview.xml b/autofill/autofill-impl/src/main/res/drawable/rounded_textview.xml new file mode 100644 index 000000000000..1a03d9564345 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/rounded_textview.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/drawable/sync_desktop_new_128.xml b/autofill/autofill-impl/src/main/res/drawable/sync_desktop_new_128.xml new file mode 100644 index 000000000000..2d3c1fd19ebd --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/sync_desktop_new_128.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml b/autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml new file mode 100644 index 000000000000..231362699474 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/activity_get_desktop_app.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/layout/activity_import_passwords.xml b/autofill/autofill-impl/src/main/res/layout/activity_import_passwords.xml new file mode 100644 index 000000000000..e81bce99eb59 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/activity_import_passwords.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml b/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml index c956e6ec36ba..c234aed93668 100644 --- a/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml +++ b/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml @@ -32,7 +32,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:importantForAccessibility="no" - android:src="@drawable/ic_autofill_keychain" + android:src="@drawable/ic_passwords_add_96" app:layout_constraintVertical_bias="0.2" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintEnd_toEndOf="parent" @@ -55,4 +55,31 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/autofillKeyIcon" /> + + + + diff --git a/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml b/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml index 4076fe670226..d8b20f9b45df 100644 --- a/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml +++ b/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml @@ -22,23 +22,33 @@ android:icon="@drawable/ic_find_search_24" android:title="@string/autofillManagementSearchLogins" android:visible="false" + android:orderInCategory="1" app:showAsAction="always" /> + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml index 146be810ce81..156ad2097a71 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -35,4 +35,25 @@ Make sure you still have a way to access your accounts. + Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser. + + Import Passwords + How To Import Passwords + Import passwords in the desktop version of the DuckDuckGo browser, then sync across devices. + Get Desktop Browser + Link copied + Get DuckDuckGo Browser for Mac or Windows + Search privately and block trackers with the DuckDuckGo desktop browser. Visit this link on your computer to download today.\n\n%1$s + Sync With Desktop + Import from the desktop browser: + Open DuckDuckGo on Mac or Windows + "Settings > Passwords]]>" + Select Import Passwords]]>… and follow the steps to import + Once imported on your computer you can set up sync on this device + + Get Desktop App + Get DuckDuckGo for Mac or Windows + On your computer, go to: + duckduckgo.com/browser + Share Download Link \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/styles.xml b/autofill/autofill-impl/src/main/res/values/styles.xml index 3921ffce2e96..31f28f1b94a3 100644 --- a/autofill/autofill-impl/src/main/res/values/styles.xml +++ b/autofill/autofill-impl/src/main/res/values/styles.xml @@ -66,4 +66,25 @@ 0.9 + + + + + \ No newline at end of file