Implement Rust-backed Android name search (#101)
This commit is contained in:
1 parent
a06f39af23
commit
c7703d8efd
21 files changed
+910
-27
No files matched your search
@@ -255,6 +255,9 @@ class EntryViewerTest {
|
||||
)
|
||||
)
|
||||
)
|
||||
override fun openSearch(id: String, revision: String) {
|
||||
error("Search is not used by this fixture")
|
||||
}
|
||||
override fun restore(path: String, selected: String?, revision: String?) = Unit
|
||||
override fun refresh() = Unit
|
||||
override fun activate(id: String, revision: String) = Unit
|
||||
|
||||
@@ -112,6 +112,18 @@ class PasswordScreenTest {
|
||||
compose.onNodeWithText("仕事").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun resumingDoesNotReplaceAPendingSearchSelection() {
|
||||
val repository = Fixture()
|
||||
repository.state.value = repository.state.value.copy(loading = true)
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.setProtectedTestContent {
|
||||
MaterialTheme { PasswordScreen(repository, repository.authentication) }
|
||||
}
|
||||
}
|
||||
compose.waitForIdle()
|
||||
assertEquals(0, repository.refreshes)
|
||||
}
|
||||
|
||||
@Test fun expandedPanesAndLargeFontsKeepTheSelectedEntryAndControlsUsable() {
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
@@ -184,6 +196,7 @@ class PasswordScreenTest {
|
||||
}
|
||||
var lastId: String? = null
|
||||
var replaceOnRefresh = false
|
||||
var refreshes = 0
|
||||
override fun reveal(id: ULong) = Unit
|
||||
override fun hide(id: ULong) = Unit
|
||||
override fun copy(id: ULong) = Unit
|
||||
@@ -194,8 +207,12 @@ class PasswordScreenTest {
|
||||
override fun prepareMutation(action: de.rfc1437.ironstorage.core.PasswordMutation) = Unit
|
||||
override fun dismissMutation() = Unit
|
||||
override fun mutate(destination: String?, overwrite: Boolean) = Unit
|
||||
override fun openSearch(id: String, revision: String) {
|
||||
error("Search is not used by this fixture")
|
||||
}
|
||||
override fun restore(path: String, selected: String?, revision: String?) = Unit
|
||||
override fun refresh() {
|
||||
refreshes++
|
||||
if (!replaceOnRefresh) return
|
||||
state.value =
|
||||
state.value.copy(selected = null, sections = null, stale = true)
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package de.rfc1437.ironstorage
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performImeAction
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import de.rfc1437.ironstorage.core.PasswordKind
|
||||
import de.rfc1437.ironstorage.core.PasswordRow
|
||||
import de.rfc1437.ironstorage.core.SearchRepository
|
||||
import de.rfc1437.ironstorage.core.SearchState
|
||||
import de.rfc1437.ironstorage.core.ShellIcon
|
||||
import de.rfc1437.ironstorage.features.SearchScreen
|
||||
import de.rfc1437.ironstorage.features.ShellViewModel
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SearchScreenTest {
|
||||
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
|
||||
|
||||
@Test fun inspectSearchWithComputerUse() {
|
||||
assumeTrue(InstrumentationRegistry.getArguments().getString("searchInspect") == "true")
|
||||
val repository = Fixture()
|
||||
val done = CountDownLatch(1)
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.setProtectedTestContent {
|
||||
MaterialTheme {
|
||||
Column {
|
||||
Text("Public name search fixture")
|
||||
TextButton(onClick = {
|
||||
repository.complete()
|
||||
}) { Text("Load public matches") }
|
||||
TextButton(onClick = {
|
||||
done.countDown()
|
||||
}) { Text("Finish search inspection") }
|
||||
SearchScreen(repository, { opened ->
|
||||
assertEquals("entry:仕事/銀行/login", opened.id)
|
||||
}, androidx.compose.ui.Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val start = android.os.SystemClock.elapsedRealtime()
|
||||
while (!done.await(100, TimeUnit.MILLISECONDS)) {
|
||||
compose.mainClock.advanceTimeBy(100)
|
||||
compose.waitForIdle()
|
||||
org.junit.Assert.assertTrue(android.os.SystemClock.elapsedRealtime() - start < 600000)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun queryKeyboardActionsCanonicalLocationsAndRecoveryRemainAccessible() {
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
compose.waitUntil(10000) {
|
||||
compose.activity.resources.configuration.orientation ==
|
||||
android.content.res.Configuration.ORIENTATION_PORTRAIT
|
||||
}
|
||||
val repository = Fixture()
|
||||
var opened: PasswordRow? = null
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.setProtectedTestContent {
|
||||
MaterialTheme { SearchScreen(repository, { opened = it }) }
|
||||
}
|
||||
}
|
||||
compose.onNodeWithText("Enter an entry name, folder, or path.").assertIsDisplayed()
|
||||
compose.onNodeWithText("Entry name or path").performTextInput("login")
|
||||
compose.onNodeWithText("Searching names…").assertIsDisplayed()
|
||||
compose.onNodeWithText("Cancel search").performClick()
|
||||
assertEquals("login", repository.state.value.query)
|
||||
compose.onNodeWithText("Search cancelled. Refresh to try again.").assertIsDisplayed()
|
||||
compose.onNodeWithText("Entry name or path").performImeAction()
|
||||
compose.runOnIdle { repository.complete() }
|
||||
compose.onNodeWithText("仕事/銀行").assertIsDisplayed()
|
||||
compose.onNodeWithText("仕事/銀行").performSemanticsAction(SemanticsActions.OnClick) { it() }
|
||||
assertEquals("entry:仕事/銀行/login", opened?.id)
|
||||
assertEquals("public-revision", opened?.revision)
|
||||
compose.runOnIdle { repository.state.value = SearchState("none") }
|
||||
compose.onNodeWithText("No matching entries.").assertIsDisplayed()
|
||||
compose.runOnIdle {
|
||||
repository.state.value =
|
||||
SearchState("none", error = "Public typed error")
|
||||
}
|
||||
compose.onNodeWithText("Public typed error").assertIsDisplayed()
|
||||
compose.onNodeWithText("Refresh results").performClick()
|
||||
compose.onNodeWithText("Searching names…").assertIsDisplayed()
|
||||
compose.onNodeWithText("Clear search").performClick()
|
||||
compose.onNodeWithText("Enter an entry name, folder, or path.").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun nativeQuerySurvivesRotationWithoutEnteringSavedStateAndClearsOnManualLock() {
|
||||
val marker = "public-name-query-marker-101"
|
||||
val repository = ViewModelProvider(compose.activity)[ShellViewModel::class.java].search
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.setProtectedTestContent {
|
||||
MaterialTheme { SearchScreen(repository, {}) }
|
||||
}
|
||||
}
|
||||
compose.onNodeWithText("Entry name or path").performTextInput(marker)
|
||||
compose.runOnIdle {
|
||||
val saved = Bundle()
|
||||
InstrumentationRegistry.getInstrumentation().callActivityOnSaveInstanceState(
|
||||
compose.activity,
|
||||
saved
|
||||
)
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.writeBundle(saved)
|
||||
val bytes = parcel.marshall()
|
||||
assertFalse(bytes.toString(Charsets.UTF_8).contains(marker))
|
||||
assertFalse(bytes.toString(Charsets.UTF_16LE).contains(marker))
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
compose.activityRule.scenario.recreate()
|
||||
compose.runOnIdle {
|
||||
val current = ViewModelProvider(compose.activity)[ShellViewModel::class.java].search
|
||||
assertEquals(marker, current.state.value.query)
|
||||
compose.activity.authentication.lock()
|
||||
assertEquals("", current.state.value.query)
|
||||
assertEquals(emptyList<PasswordRow>(), current.state.value.results)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun expandedWindowKeepsQueryAndCanonicalResultUsable() {
|
||||
val repository = Fixture()
|
||||
repository.complete()
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
compose.waitUntil(10000) {
|
||||
compose.activity.resources.configuration.orientation ==
|
||||
android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
}
|
||||
compose.activity.runOnUiThread {
|
||||
compose.activity.setProtectedTestContent {
|
||||
MaterialTheme { SearchScreen(repository, {}) }
|
||||
}
|
||||
}
|
||||
compose.runOnIdle { repository.complete() }
|
||||
compose.onNodeWithText("Entry name or path").assertIsDisplayed()
|
||||
compose.onNodeWithText("仕事/銀行").assertIsDisplayed()
|
||||
compose.onNodeWithText("Clear search").assertIsDisplayed()
|
||||
}
|
||||
private class Fixture : SearchRepository {
|
||||
override val state = MutableStateFlow(SearchState())
|
||||
override fun query(value: String) {
|
||||
state.value = SearchState(value, loading = true)
|
||||
}
|
||||
override fun refresh() {
|
||||
if (state.value.query.isNotBlank()) {
|
||||
state.value =
|
||||
SearchState(state.value.query, loading = true)
|
||||
}
|
||||
}
|
||||
override fun cancel() {
|
||||
state.value = state.value.copy(loading = false, cancelled = state.value.loading)
|
||||
}
|
||||
override fun clear() {
|
||||
state.value = SearchState()
|
||||
}
|
||||
fun complete() {
|
||||
state.value =
|
||||
SearchState(
|
||||
"login",
|
||||
results = listOf(
|
||||
PasswordRow(
|
||||
"entry:仕事/銀行/login",
|
||||
"仕事/銀行/login",
|
||||
"login",
|
||||
"仕事/銀行",
|
||||
PasswordKind.Entry,
|
||||
"public-revision",
|
||||
ShellIcon.Key
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,10 @@ import de.rfc1437.ironstorage.core.NativeHomeRepository
|
||||
import de.rfc1437.ironstorage.core.NativeOnboardingRepository
|
||||
import de.rfc1437.ironstorage.core.NativePasswordRepository
|
||||
import de.rfc1437.ironstorage.core.NativePrivateSession
|
||||
import de.rfc1437.ironstorage.core.NativeSearchRepository
|
||||
import de.rfc1437.ironstorage.core.OnboardingRepository
|
||||
import de.rfc1437.ironstorage.core.PasswordRepository
|
||||
import de.rfc1437.ironstorage.core.SearchRepository
|
||||
import de.rfc1437.ironstorage.core.ShellRepository
|
||||
import de.rfc1437.ironstorage.platform.AndroidKeystoreAdapter
|
||||
import de.rfc1437.ironstorage.platform.AndroidSecretClipboard
|
||||
@@ -28,6 +30,18 @@ import kotlinx.coroutines.SupervisorJob
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ShellModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun searchRepository(
|
||||
session: NativePrivateSession,
|
||||
authentication: NativeAuthenticationRepository
|
||||
): SearchRepository = NativeSearchRepository(
|
||||
session,
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
|
||||
Dispatchers.IO,
|
||||
authentication.presentation
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun homeRepository(
|
||||
|
||||
Reference in new issue
Block a user