Skip to content

Commit

Permalink
Add new screens with instructions for importing passwords via sync
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed Jun 25, 2024
1 parent d41511f commit f97cee5
Show file tree
Hide file tree
Showing 18 changed files with 1,037 additions and 3 deletions.
8 changes: 8 additions & 0 deletions autofill/autofill-impl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
package="com.duckduckgo.autofill.impl">

<application>
<activity
android:name=".ui.credential.management.importpassword.ImportPasswordsActivity"
android:label="@string/autofillManagementImportPasswords"
android:exported="false" />
<activity
android:name=".ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppActivity"
android:label="@string/autofillManagementImportPasswordsGetDesktopAppTitle"
android:exported="false" />
<activity
android:name=".email.incontext.EmailProtectionInContextSignupActivity"
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build.VERSION_CODES
import android.os.PersistableBundle
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject

interface AutofillClipboardInteractor {
fun copyToClipboard(toCopy: String, isSensitive: Boolean)
fun shouldShowCopyNotification(): Boolean
}

@ContributesBinding(ActivityScope::class)
@ContributesBinding(AppScope::class)
class RealAutofillClipboardInteractor @Inject constructor(
context: Context,
private val appBuildConfig: AppBuildConfig,
) : AutofillClipboardInteractor {
private val clipboardManager by lazy { context.getSystemService(ClipboardManager::class.java) }

Expand All @@ -45,4 +49,14 @@ class RealAutofillClipboardInteractor @Inject constructor(
}
clipboardManager.setPrimaryClip(clipData)
}

override fun shouldShowCopyNotification(): Boolean {
// Samsung on Android 12 shows its own toast when copying text, so we don't want to show our own
if (appBuildConfig.manufacturer == "samsung" && (appBuildConfig.sdkInt == VERSION_CODES.S || appBuildConfig.sdkInt == VERSION_CODES.S_V2)) {
return false
}

// From Android 13, the system shows its own toast when copying text, so we don't want to show our own
return appBuildConfig.sdkInt <= VERSION_CODES.S_V2
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsVie
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.DuckAddressStatus.NotManageable
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.DuckAddressStatus.SettingActivationStatus
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.neversaved.NeverSavedSitesViewState
Expand Down Expand Up @@ -683,6 +684,10 @@ class AutofillSettingsViewModel @Inject constructor(
}
}

fun onImportPasswords() {
addCommand(LaunchImportPasswords)
}

data class ViewState(
val autofillEnabled: Boolean = true,
val showAutofillEnabledToggle: Boolean = true,
Expand Down Expand Up @@ -758,6 +763,7 @@ class AutofillSettingsViewModel @Inject constructor(
data object LaunchResetNeverSaveListConfirmation : ListModeCommand()
data class LaunchDeleteAllPasswordsConfirmation(val numberToDelete: Int) : ListModeCommand()
data class PromptUserToAuthenticateMassDeletion(val authConfiguration: AuthConfiguration) : ListModeCommand()
data object LaunchImportPasswords : ListModeCommand()
}

sealed class DuckAddressStatus {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2024 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

import android.os.Bundle
import androidx.annotation.StringRes
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ActivityImportPasswordsBinding
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.GetDesktopAppParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.view.text.DaxTextView
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.extensions.html
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import com.duckduckgo.sync.api.SyncActivityWithEmptyParams
import javax.inject.Inject

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(ImportPasswordActivityParams::class)
class ImportPasswordsActivity : DuckDuckGoActivity() {

val binding: ActivityImportPasswordsBinding by viewBinding()

@Inject
lateinit var globalActivityStarter: GlobalActivityStarter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(binding.root)
setupToolbar(binding.includeToolbar.toolbar)
configureEventHandlers()
configureNumberedInstructions()
}

private fun configureEventHandlers() {
binding.getDesktopBrowserButton.setOnClickListener {
globalActivityStarter.start(this, GetDesktopAppParams)
}
binding.syncWithDesktopButton.setOnClickListener {
globalActivityStarter.start(this, SyncActivityWithEmptyParams)
}
}

private fun configureNumberedInstructions() {
with(binding) {
importFromDesktopInstructions1.applyHtml(R.string.autofillManagementImportPasswordsImportFromDesktopInstructionOne)
importFromDesktopInstructions2.applyHtml(R.string.autofillManagementImportPasswordsImportFromDesktopInstructionTwo)
importFromDesktopInstructions3.applyHtml(R.string.autofillManagementImportPasswordsImportFromDesktopInstructionThree)
importFromDesktopInstructions4.applyHtml(R.string.autofillManagementImportPasswordsImportFromDesktopInstructionFour)
}
}

private fun DaxTextView.applyHtml(@StringRes resId: Int) {
text = getString(resId).html(this@ImportPasswordsActivity)
}
}

data object ImportPasswordActivityParams : ActivityParams {
private fun readResolve(): Any = ImportPasswordActivityParams
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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 android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.autofill.impl.R
import com.duckduckgo.autofill.impl.databinding.ActivityGetDesktopAppBinding
import com.duckduckgo.autofill.impl.ui.credential.management.AutofillClipboardInteractor
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordsGetDesktopAppViewModel.Command
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.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
import com.google.android.material.snackbar.Snackbar
import javax.inject.Inject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber

data object GetDesktopAppParams : ActivityParams {
private fun readResolve(): Any = GetDesktopAppParams
}

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(GetDesktopAppParams::class)
class ImportPasswordsGetDesktopAppActivity : DuckDuckGoActivity() {

private val viewModel: ImportPasswordsGetDesktopAppViewModel by bindViewModel()
private val binding: ActivityGetDesktopAppBinding by viewBinding()

@Inject
lateinit var globalActivityStarter: GlobalActivityStarter

@Inject
lateinit var clipboardInteractor: AutofillClipboardInteractor

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

viewModel.commands.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { executeCommand(it) }
.launchIn(lifecycleScope)

setContentView(binding.root)
setupToolbar(binding.includeToolbar.toolbar)
configureUiEventHandlers()
}

private fun configureUiEventHandlers() {
binding.shareLinkButton.setOnClickListener {
viewModel.onShareClicked()
}
binding.downloadLinkText.setOnClickListener {
viewModel.onLinkClicked()
}
}

private fun executeCommand(command: Command) {
when (command) {
is ShareLink -> 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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Command>(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"
}
}
Loading

0 comments on commit f97cee5

Please sign in to comment.