diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 5a7c18a40..7821ac620 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -13,6 +13,115 @@
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
diff --git a/app/src/main/java/mozilla/lockbox/action/AccountAction.kt b/app/src/main/java/mozilla/lockbox/action/AccountAction.kt
index 4c2dae09c..b14178cb6 100644
--- a/app/src/main/java/mozilla/lockbox/action/AccountAction.kt
+++ b/app/src/main/java/mozilla/lockbox/action/AccountAction.kt
@@ -6,10 +6,12 @@
package mozilla.lockbox.action
+import mozilla.components.service.fxa.sharing.ShareableAccount
import mozilla.lockbox.flux.Action
sealed class AccountAction : Action {
object Reset : AccountAction()
object UseTestData : AccountAction()
data class OauthRedirect(val url: String) : AccountAction()
+ data class AutomaticLogin(val account: ShareableAccount) : AccountAction()
}
\ No newline at end of file
diff --git a/app/src/main/java/mozilla/lockbox/action/DialogAction.kt b/app/src/main/java/mozilla/lockbox/action/DialogAction.kt
index c9a12d9e0..9e2598cf6 100644
--- a/app/src/main/java/mozilla/lockbox/action/DialogAction.kt
+++ b/app/src/main/java/mozilla/lockbox/action/DialogAction.kt
@@ -7,6 +7,7 @@
package mozilla.lockbox.action
import mozilla.appservices.logins.ServerPassword
+import mozilla.components.service.fxa.sharing.ShareableAccount
import mozilla.lockbox.R
import mozilla.lockbox.flux.Action
import mozilla.lockbox.model.DialogViewModel
@@ -47,7 +48,7 @@ sealed class DialogAction(
listOf(LifecycleAction.UserReset)
)
- object OnboardingSecurityDialog : DialogAction(
+ data class OnboardingSecurityDialogAutomatic(val account: ShareableAccount) : DialogAction(
DialogViewModel(
R.string.secure_your_device,
R.string.device_security_description,
@@ -56,7 +57,7 @@ sealed class DialogAction(
),
listOf(
SystemSetting(SettingIntent.Security),
- Login
+ AccountAction.AutomaticLogin(account)
),
listOf(Login)
)
@@ -76,4 +77,18 @@ sealed class DialogAction(
ItemList
)
)
-}
\ No newline at end of file
+
+ object OnboardingSecurityDialogManual : DialogAction(
+ DialogViewModel(
+ R.string.secure_your_device,
+ R.string.device_security_description,
+ R.string.set_up_now,
+ R.string.skip_button
+ ),
+ listOf(
+ SystemSetting(SettingIntent.Security),
+ Login
+ ),
+ listOf(Login)
+ )
+}
diff --git a/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt b/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt
index bb2a3a678..5c84fa2fd 100644
--- a/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt
+++ b/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt
@@ -103,6 +103,7 @@ class AppRoutePresenter(
R.id.fragment_null to R.id.fragment_welcome -> R.id.action_init_to_unprepared
R.id.fragment_welcome to R.id.fragment_fxa_login -> R.id.action_welcome_to_fxaLogin
+ R.id.fragment_welcome to R.id.fragment_item_list -> R.id.action_welcome_to_autoLogin
R.id.fragment_fxa_login to R.id.fragment_item_list -> R.id.action_fxaLogin_to_itemList
R.id.fragment_fxa_login to R.id.fragment_fingerprint_onboarding ->
diff --git a/app/src/main/java/mozilla/lockbox/presenter/WelcomePresenter.kt b/app/src/main/java/mozilla/lockbox/presenter/WelcomePresenter.kt
index f23d41670..cbf45a75e 100644
--- a/app/src/main/java/mozilla/lockbox/presenter/WelcomePresenter.kt
+++ b/app/src/main/java/mozilla/lockbox/presenter/WelcomePresenter.kt
@@ -8,31 +8,52 @@ package mozilla.lockbox.presenter
import io.reactivex.Observable
import io.reactivex.rxkotlin.addTo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.fxa.sharing.ShareableAccount
+import mozilla.lockbox.action.AccountAction
import mozilla.lockbox.action.AppWebPageAction
import mozilla.lockbox.action.DialogAction
import mozilla.lockbox.action.RouteAction
import mozilla.lockbox.flux.Dispatcher
import mozilla.lockbox.flux.Presenter
+import mozilla.lockbox.store.AccountStore
import mozilla.lockbox.store.FingerprintStore
+import mozilla.lockbox.support.FeatureFlags
interface WelcomeView {
- val getStartedClicks: Observable
+ val getStartedAutomaticallyClicks: Observable
+ val getStartedManuallyClicks: Observable
val learnMoreClicks: Observable
+ fun showExistingAccount(email: String)
+ fun hideExistingAccount()
}
+@ExperimentalCoroutinesApi
class WelcomePresenter(
private val view: WelcomeView,
private val dispatcher: Dispatcher = Dispatcher.shared,
+ private val accountStore: AccountStore = AccountStore.shared,
private val fingerprintStore: FingerprintStore = FingerprintStore.shared
) : Presenter() {
+
override fun onViewReady() {
- view.getStartedClicks
+ if (FeatureFlags.FXA_LOGIN_WITTH_AUTHPROVIDER) {
+ accountStore.shareableAccount()?.let { account ->
+ view.showExistingAccount(account.email)
+ view.getStartedAutomaticallyClicks
+ .map {
+ routeToExistingAccount(account)
+ }
+ .subscribe(dispatcher::dispatch)
+ .addTo(compositeDisposable)
+ } ?: view.hideExistingAccount()
+ } else {
+ view.hideExistingAccount()
+ }
+
+ view.getStartedManuallyClicks
.map {
- if (fingerprintStore.isKeyguardDeviceSecure)
- RouteAction.Login
- else {
- DialogAction.OnboardingSecurityDialog
- }
+ routeToLoginManually()
}
.subscribe(dispatcher::dispatch)
.addTo(compositeDisposable)
@@ -43,4 +64,18 @@ class WelcomePresenter(
}
.addTo(compositeDisposable)
}
-}
\ No newline at end of file
+
+ private fun routeToExistingAccount(account: ShareableAccount) =
+ if (fingerprintStore.isKeyguardDeviceSecure) {
+ AccountAction.AutomaticLogin(account)
+ } else {
+ DialogAction.OnboardingSecurityDialogAutomatic(account)
+ }
+
+ private fun routeToLoginManually() =
+ if (fingerprintStore.isKeyguardDeviceSecure) {
+ RouteAction.Login
+ } else {
+ DialogAction.OnboardingSecurityDialogManual
+ }
+}
diff --git a/app/src/main/java/mozilla/lockbox/store/AccountStore.kt b/app/src/main/java/mozilla/lockbox/store/AccountStore.kt
index 45d889131..5e89e4700 100644
--- a/app/src/main/java/mozilla/lockbox/store/AccountStore.kt
+++ b/app/src/main/java/mozilla/lockbox/store/AccountStore.kt
@@ -28,6 +28,8 @@ import mozilla.components.concept.sync.Avatar
import mozilla.components.concept.sync.Profile
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.FirefoxAccount
+import mozilla.components.service.fxa.sharing.AccountSharing
+import mozilla.components.service.fxa.sharing.ShareableAccount
import mozilla.lockbox.action.AccountAction
import mozilla.lockbox.action.DataStoreAction
import mozilla.lockbox.action.LifecycleAction
@@ -91,6 +93,7 @@ open class AccountStore(
private lateinit var webView: WebView
private lateinit var logDirectory: File
+ private lateinit var context: Context
init {
val resetObservable = lifecycleStore.lifecycleEvents
@@ -109,6 +112,7 @@ open class AccountStore(
when (it) {
is AccountAction.OauthRedirect -> this.oauthLogin(it.url)
is AccountAction.UseTestData -> this.populateTestAccountInformation(true)
+ is AccountAction.AutomaticLogin -> this.automaticLogin(it.account)
is AccountAction.Reset -> this.clear()
}
}
@@ -126,10 +130,29 @@ open class AccountStore(
override fun injectContext(context: Context) {
detectAccount()
+ this.context = context
webView = WebView(context)
logDirectory = context.getDir("webview", Context.MODE_PRIVATE)
}
+ fun shareableAccount(): ShareableAccount? {
+ return AccountSharing.queryShareableAccounts(context).firstOrNull()
+ }
+
+ private fun automaticLogin(account: ShareableAccount) {
+ fxa?.migrateFromSessionTokenAsync(
+ account.authInfo.sessionToken,
+ account.authInfo.kSync,
+ account.authInfo.kXCS
+ )
+ ?.let {
+ it.asSingle(coroutineContext)
+ .map { true }
+ .subscribe(this::populateAccountInformation, this::pushError)
+ .addTo(compositeDisposable)
+ }
+ }
+
private fun detectAccount() {
storedAccountJSON?.let { accountJSON ->
if (accountJSON == Constant.App.testMarker) {
@@ -202,7 +225,7 @@ open class AccountStore(
private fun generateLoginURL() {
val fxa = fxa ?: return
- fxa.beginOAuthFlowAsync(Constant.FxA.scopes, true)
+ fxa.beginOAuthFlowAsync(Constant.FxA.scopes)
.asMaybe(coroutineContext)
.subscribe((this.loginURL as Subject)::onNext, this::pushError)
.addTo(compositeDisposable)
@@ -243,8 +266,7 @@ open class AccountStore(
private fun removeDeviceFromFxA() {
if (fxa != null) {
- fxa!!.deviceConstellation()
- .destroyCurrentDeviceAsync()
+ fxa!!.disconnectAsync()
.asSingle(coroutineContext)
.subscribe()
.addTo(compositeDisposable)
diff --git a/app/src/main/java/mozilla/lockbox/support/FeatureFlags.kt b/app/src/main/java/mozilla/lockbox/support/FeatureFlags.kt
index 7315f5e48..091e00808 100644
--- a/app/src/main/java/mozilla/lockbox/support/FeatureFlags.kt
+++ b/app/src/main/java/mozilla/lockbox/support/FeatureFlags.kt
@@ -40,4 +40,17 @@ object FeatureFlags {
isRelease -> false
else -> false
}
-}
\ No newline at end of file
+
+ /**
+ * Use existing apps' FxA credentials to login.
+ *
+ * Currently, only Fennec will detect the release version of Lockwise.
+ * To see this work, you need a custom build of Fennec and Android Components.
+ */
+ val FXA_LOGIN_WITTH_AUTHPROVIDER = when {
+ isDebug -> true
+ isTesting -> false
+ isRelease -> false
+ else -> false
+ }
+}
diff --git a/app/src/main/java/mozilla/lockbox/view/WelcomeFragment.kt b/app/src/main/java/mozilla/lockbox/view/WelcomeFragment.kt
index 986511c8d..acc254fa7 100644
--- a/app/src/main/java/mozilla/lockbox/view/WelcomeFragment.kt
+++ b/app/src/main/java/mozilla/lockbox/view/WelcomeFragment.kt
@@ -13,11 +13,13 @@ import android.view.ViewGroup
import io.reactivex.Observable
import com.jakewharton.rxbinding2.view.clicks
import kotlinx.android.synthetic.main.fragment_welcome.view.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.R
import mozilla.lockbox.presenter.WelcomePresenter
import mozilla.lockbox.presenter.WelcomeView
class WelcomeFragment : Fragment(), WelcomeView {
+ @ExperimentalCoroutinesApi
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -28,12 +30,35 @@ class WelcomeFragment : Fragment(), WelcomeView {
val appLabel = getString(R.string.app_label)
view.textViewInstructions.text = getString(R.string.welcome_instructions, appLabel)
view.lockwiseIcon.contentDescription = getString(R.string.app_logo, appLabel)
+
return view
}
- override val getStartedClicks: Observable
+ override fun showExistingAccount(email: String) {
+ view?.apply {
+ buttonGetStarted.text = getString(R.string.welcome_start_automatic_btn, email)
+ buttonGetStarted.visibility = View.VISIBLE
+
+ buttonGetStartedManually.text = getString(R.string.welcome_start_force_manual_btn)
+ buttonGetStartedManually.visibility = View.VISIBLE
+ }
+ }
+
+ override fun hideExistingAccount() {
+ view?.apply {
+ buttonGetStarted.visibility = View.GONE
+
+ buttonGetStartedManually.text = getString(R.string.welcome_start_btn)
+ buttonGetStartedManually.visibility = View.VISIBLE
+ }
+ }
+
+ override val getStartedAutomaticallyClicks: Observable
get() = view!!.buttonGetStarted.clicks()
+ override val getStartedManuallyClicks: Observable
+ get() = view!!.buttonGetStartedManually.clicks()
+
override val learnMoreClicks: Observable
get() = view!!.textViewLearnMore.clicks()
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/layout/fragment_welcome.xml b/app/src/main/res/layout/fragment_welcome.xml
index e003f0320..1aa84167b 100644
--- a/app/src/main/res/layout/fragment_welcome.xml
+++ b/app/src/main/res/layout/fragment_welcome.xml
@@ -73,6 +73,21 @@
+
+
+
Open a link to Learn more
-
+
+ %s
+
+ Different Account
+
Get Started
To use %1$s, you’ll need a Firefox Account with saved logins.
diff --git a/app/src/test/java/mozilla/lockbox/presenter/WelcomePresenterTest.kt b/app/src/test/java/mozilla/lockbox/presenter/WelcomePresenterTest.kt
index 2465c6650..a14e29864 100644
--- a/app/src/test/java/mozilla/lockbox/presenter/WelcomePresenterTest.kt
+++ b/app/src/test/java/mozilla/lockbox/presenter/WelcomePresenterTest.kt
@@ -7,11 +7,13 @@ package mozilla.lockbox.presenter
import io.reactivex.Observable
import io.reactivex.observers.TestObserver
import io.reactivex.subjects.PublishSubject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.lockbox.action.AppWebPageAction
import mozilla.lockbox.action.DialogAction
import mozilla.lockbox.action.RouteAction
import mozilla.lockbox.flux.Action
import mozilla.lockbox.flux.Dispatcher
+import mozilla.lockbox.store.AccountStore
import mozilla.lockbox.store.FingerprintStore
import org.junit.Assert
import org.junit.Before
@@ -19,14 +21,29 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
+@ExperimentalCoroutinesApi
class WelcomePresenterTest {
class FakeWelcomeView : WelcomeView {
+ var existingAccount: Boolean? = null
+
+ override fun showExistingAccount(email: String) {
+ existingAccount = true
+ }
+
+ override fun hideExistingAccount() {
+ existingAccount = false
+ }
+
val learnMoreStub = PublishSubject.create()
override val learnMoreClicks: Observable = learnMoreStub
val getStartedStub: PublishSubject = PublishSubject.create()
- override val getStartedClicks: Observable
+ override val getStartedManuallyClicks: Observable
get() = getStartedStub
+
+ val getStartedExistingAcccountStub: PublishSubject = PublishSubject.create()
+ override val getStartedAutomaticallyClicks: Observable
+ get() = getStartedExistingAcccountStub
}
@Mock
@@ -35,17 +52,24 @@ class WelcomePresenterTest {
val view = FakeWelcomeView()
+ @Mock
+ val accountStore = Mockito.mock(AccountStore::class.java)
+
val dispatcher = Dispatcher()
val dispatcherObserver = TestObserver.create()
- val subject = WelcomePresenter(view, dispatcher, fingerprintStore)
+ val subject = WelcomePresenter(view, dispatcher,
+ accountStore = accountStore,
+ fingerprintStore = fingerprintStore
+ )
@Before
fun setUp() {
dispatcher.register.subscribe(dispatcherObserver)
Mockito.`when`(fingerprintStore.isKeyguardDeviceSecure).thenReturn(isDeviceSecureStub)
-
+ Mockito.`when`(accountStore.shareableAccount()).thenReturn(null)
subject.onViewReady()
+ Assert.assertEquals(false, view.existingAccount)
}
@Test
@@ -54,7 +78,7 @@ class WelcomePresenterTest {
view.getStartedStub.onNext(Unit)
val routeAction = dispatcherObserver.values().first() as RouteAction
- Assert.assertTrue(routeAction is DialogAction.OnboardingSecurityDialog)
+ Assert.assertTrue(routeAction is DialogAction.OnboardingSecurityDialogManual)
}
@Test
@@ -62,4 +86,4 @@ class WelcomePresenterTest {
view.learnMoreStub.onNext(Unit)
dispatcherObserver.assertValue(AppWebPageAction.FaqWelcome)
}
-}
\ No newline at end of file
+}
diff --git a/build.gradle b/build.gradle
index a6a79451a..3f6454513 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,7 @@
buildscript {
ext.android_support_version = '28.0.0'
ext.kotlin_version = '1.3.0'
- ext.android_components_version = '7.0.0'
+ ext.android_components_version = '9.0.0'
// Determined from
// https://github.com/mozilla-mobile/android-components/blob/v0.51.0/buildSrc/src/main/java/Dependencies.kt,
// where the version in the URL is the tag corresponding to `android_components_version`.