Commit 8e0b3010 authored by ekager's avatar ekager
Browse files

For #7134 - Adds login selection to prompt feature

parent 0ecf302c
......@@ -9,8 +9,8 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginStorageDelegate
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.Autocomplete
import org.mozilla.geckoview.GeckoResult
/**
* This class exists only to convert incoming [LoginEntry] arguments into [Login]s, then forward
......
......@@ -13,9 +13,7 @@ import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
......@@ -69,7 +67,6 @@ import mozilla.components.support.base.feature.OnNeedToRequestPermissions
import mozilla.components.support.base.feature.PermissionsFeature
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import java.lang.ref.WeakReference
import java.security.InvalidParameterException
import java.util.Date
......@@ -77,8 +74,6 @@ import java.util.Date
@VisibleForTesting(otherwise = PRIVATE)
internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
private const val PROGRESS_ALMOST_COMPLETE = 90
/**
* Feature for displaying native dialogs for html elements like: input type
* date, file, time, color, option, menu, authentication, confirmation and alerts.
......@@ -132,7 +127,6 @@ class PromptFeature private constructor(
// These three scopes have identical lifetimes. We do not yet have a way of combining scopes
private var handlePromptScope: CoroutineScope? = null
private var dismissPromptScope: CoroutineScope? = null
private var sessionPromptScope: CoroutineScope? = null
private var activePromptRequest: PromptRequest? = null
internal val promptAbuserDetector = PromptAbuserDetector()
......@@ -243,6 +237,9 @@ class PromptFeature private constructor(
.collect { state ->
state?.content?.let {
if (it.promptRequest != activePromptRequest) {
if (activePromptRequest is SelectLoginPrompt) {
loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt)
}
onPromptRequested(state)
} else if (!it.loading) {
promptAbuserDetector.resetJSAlertAbuseState()
......@@ -252,33 +249,23 @@ class PromptFeature private constructor(
}
}
// Dismiss all prompts when page loads are nearly finished. This prevents prompts from the
// previous page from covering content. See Fenix#5326
// Dismiss all prompts when page URL or session id changes. See Fenix#5326
dismissPromptScope = store.flowScoped { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabId) }
.ifChanged { it.content.progress }
.filter { it.content.progress >= PROGRESS_ALMOST_COMPLETE }
.collect {
val prompt = activePrompt?.get()
if (prompt?.shouldDismissOnLoad() == true) {
prompt.dismiss()
}
activePrompt?.clear()
loginPicker?.dismissCurrentLoginSelect()
flow.ifAnyChanged { state ->
arrayOf(
state.selectedTabId,
state.findTabOrCustomTabOrSelectedTab(customTabId)?.content?.url
)
}.collect {
if (activePromptRequest is SelectLoginPrompt) {
loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt)
}
}
// Dismiss prompts when a new tab is selected.
sessionPromptScope = store.flowScoped { flow ->
flow.ifChanged { browserState -> browserState.selectedTabId }
.collect {
val prompt = activePrompt?.get()
if (prompt?.shouldDismissOnLoad() == true) {
prompt.dismiss()
}
activePrompt?.clear()
loginPicker?.dismissCurrentLoginSelect()
val prompt = activePrompt?.get()
if (prompt?.shouldDismissOnLoad() == true) {
prompt.dismiss()
}
activePrompt?.clear()
}
}
fragmentManager.findFragmentByTag(FRAGMENT_TAG)?.let { fragment ->
......@@ -295,7 +282,6 @@ class PromptFeature private constructor(
override fun stop() {
handlePromptScope?.cancel()
dismissPromptScope?.cancel()
sessionPromptScope?.cancel()
}
/**
......
......@@ -4,11 +4,11 @@
package mozilla.components.feature.prompts.login
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.storage.Login
import mozilla.components.feature.prompts.consumePromptFrom
import mozilla.components.support.base.log.logger.Logger
/**
* The [LoginPicker] displays a list of possible logins in a [LoginPickerView] for a site after
......@@ -34,21 +34,13 @@ internal class LoginPicker(
}
internal fun handleSelectLoginRequest(request: PromptRequest.SelectLoginPrompt) {
if (currentRequest != null) {
store.consumePromptFrom(sessionId) {
(it as PromptRequest.SelectLoginPrompt).onDismiss()
}
}
currentRequest = request
loginSelectBar.showPicker(request.logins)
}
@VisibleForTesting
internal var currentRequest: PromptRequest.SelectLoginPrompt? = null
override fun onLoginSelected(login: Login) {
(currentRequest as PromptRequest.SelectLoginPrompt).onConfirm(login)
currentRequest = null
store.consumePromptFrom(sessionId) {
if (it is PromptRequest.SelectLoginPrompt) it.onConfirm(login)
}
loginSelectBar.hidePicker()
}
......@@ -57,13 +49,15 @@ internal class LoginPicker(
dismissCurrentLoginSelect()
}
fun dismissCurrentLoginSelect() {
if (currentRequest != null) {
store.consumePromptFrom(sessionId) {
(it as PromptRequest.SelectLoginPrompt).onDismiss()
@Suppress("TooGenericExceptionCaught")
fun dismissCurrentLoginSelect(promptRequest: PromptRequest.SelectLoginPrompt? = null) {
try {
promptRequest?.let { it.onDismiss() } ?: store.consumePromptFrom(sessionId) {
if (it is PromptRequest.SelectLoginPrompt) it.onDismiss()
}
} catch (e: RuntimeException) {
Logger.error("Can't dismiss this login select prompt", e)
}
currentRequest = null
loginSelectBar.hidePicker()
}
}
......@@ -5,9 +5,12 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/login_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingStart="63dp"
android:paddingTop="8dp"
......@@ -18,7 +21,11 @@
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:importantForAutofill="no"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textIsSelectable="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
......@@ -28,12 +35,16 @@
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:importantForAutofill="no"
android:inputType="textPassword"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textIsSelectable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/username"
tools:text="password"
tools:ignore="TextViewEdits" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
tools:ignore="TextViewEdits"
tools:text="password" />
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -9,66 +9,78 @@
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/saved_logins_header"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:contentDescription="@string/mozac_feature_prompts_expand_logins_content_description"
android:drawablePadding="24dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="56dp"
android:text="@string/mozac_feature_prompts_saved_logins"
android:textColor="?android:colorEdgeEffect"
android:textSize="16sp"
app:drawableStartCompat="@drawable/mozac_ic_login"
app:drawableTint="?android:colorEdgeEffect"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/login_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/mozac_feature_login_multiselect_expand"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:padding="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_arrowhead_down"
app:tint="?android:colorEdgeEffect" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/scroll_child"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/logins_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/mozac_feature_login_multiselect_expand"
tools:listitem="@layout/login_selection_list_item" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/saved_logins_header"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:contentDescription="@string/mozac_feature_prompts_expand_logins_content_description"
android:drawablePadding="24dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="56dp"
android:text="@string/mozac_feature_prompts_saved_logins"
android:textColor="?android:colorEdgeEffect"
android:textSize="16sp"
app:drawableStartCompat="@drawable/mozac_ic_login"
app:drawableTint="?android:colorEdgeEffect"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/mozac_feature_login_multiselect_expand"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:padding="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mozac_ic_arrowhead_down"
app:tint="?android:colorEdgeEffect" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/logins_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/mozac_feature_login_multiselect_expand"
tools:listitem="@layout/login_selection_list_item" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/manage_logins"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:drawablePadding="24dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="0dp"
android:text="@string/mozac_feature_prompts_manage_logins"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
android:visibility="gone"
app:drawableStartCompat="@drawable/mozac_ic_settings"
app:drawableTint="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/logins_list" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/manage_logins"
android:layout_width="0dp"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:drawablePadding="24dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="0dp"
android:text="@string/mozac_feature_prompts_manage_logins"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
android:visibility="gone"
app:drawableStartCompat="@drawable/mozac_ic_settings"
app:drawableTint="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/logins_list" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</merge>
......@@ -23,6 +23,7 @@ import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.createTab
......@@ -1025,7 +1026,7 @@ class PromptFeatureTest {
}
@Test
fun `dialog will be dismissed if progress reaches 90%`() {
fun `dialog will be dismissed if tab ID changes`() {
val feature = spy(
PromptFeature(
activity = mock(),
......@@ -1047,15 +1048,15 @@ class PromptFeatureTest {
whenever(fragment.shouldDismissOnLoad()).thenReturn(true)
feature.activePrompt = WeakReference(fragment)
store.dispatch(ContentAction.UpdateProgressAction(tabId, 0)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 10)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 11)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 28)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 32)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 49)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 60)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 89)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 90)).joinBlocking()
val secondTabId = "second-test-tab"
store.dispatch(
TabListAction.AddTabAction(
TabSessionState(
id = secondTabId,
content = ContentState(url = "mozilla.org")
), select = true
)
).joinBlocking()
verify(fragment, times(1)).dismiss()
}
......@@ -1091,7 +1092,7 @@ class PromptFeatureTest {
}
@Test
fun `dialog will be dismissed if new page load progress skips past 90%`() {
fun `dialog will be dismissed if tab URL changes`() {
val feature = spy(
PromptFeature(
activity = mock(),
......@@ -1113,10 +1114,7 @@ class PromptFeatureTest {
whenever(fragment.shouldDismissOnLoad()).thenReturn(true)
feature.activePrompt = WeakReference(fragment)
store.dispatch(ContentAction.UpdateProgressAction(tabId, 0)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 10)).joinBlocking()
store.dispatch(ContentAction.UpdateProgressAction(tabId, 100)).joinBlocking()
store.dispatch(ContentAction.UpdateUrlAction(tabId, "mozilla.org")).joinBlocking()
verify(fragment, times(1)).dismiss()
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment