Fix Android local open flow

This commit is contained in:
Joe Julian
2026-04-01 21:42:19 -07:00
parent b030b69c0d
commit 5b6f7bb1de
3 changed files with 109 additions and 8 deletions
+19 -2
View File
@@ -577,7 +577,6 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
apiPolicyGroupScope: true,
autofillNoticePreference: autofillNoticeAll,
backgroundResults: make(chan backgroundActionResult, 8),
requestMasterPassFocus: true,
}
u.apiPolicyAllow.Value = true
u.apiPolicyGroupScopeW.Value = true
@@ -598,10 +597,12 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.loadRecentVaults()
u.loadRecentRemotes()
u.restoreStartupLifecycleTarget()
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
u.loadUIPreferences()
u.loadSettings()
u.loadSettingsFormFromPreferences()
u.loadSettingsDraft()
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
u.filter()
u.syncAutofillCache()
return u
@@ -663,6 +664,10 @@ func shouldUsePreviewWindowSize(mode, goos string) bool {
return !strings.EqualFold(strings.TrimSpace(goos), "android")
}
func supportsDesktopFilePicker(goos string) bool {
return !strings.EqualFold(strings.TrimSpace(goos), "android")
}
func (u *ui) selectedAttachmentItems() []attachmentItem {
item, ok := u.selectedEntry()
if !ok || len(item.Attachments) == 0 {
@@ -1594,6 +1599,15 @@ func (u *ui) restoreStartupLifecycleTarget() {
}
}
func (u *ui) hasSelectedLifecycleTarget() bool {
switch strings.TrimSpace(u.lifecycleMode) {
case "remote":
return strings.TrimSpace(u.remoteBaseURL.Text()) != "" && strings.TrimSpace(u.remotePath.Text()) != ""
default:
return strings.TrimSpace(u.vaultPath.Text()) != ""
}
}
func (u *ui) latestRecentVault() (string, time.Time) {
for _, path := range u.recentVaults {
if strings.TrimSpace(path) == "" {
@@ -3699,7 +3713,10 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
}),
)
}
return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx)
if supportsDesktopFilePicker(runtime.GOOS) {
return selectorEditorHelp(u.theme, "Local Vault Path", "Choose the other local .kdbx file to synchronize with.", &u.syncLocalPath, &u.pickSyncLocalPath, "Choose File", false)(gtx)
}
return labeledEditorHelp(u.theme, "Local Vault Path", "Enter the shared-storage path to the other local .kdbx file to synchronize with.", &u.syncLocalPath, false)(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+55
View File
@@ -3519,6 +3519,50 @@ func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) {
}
}
func TestUIStartupDoesNotRequestMasterPasswordFocusWithoutSelectedTarget(t *testing.T) {
t.Parallel()
dir := t.TempDir()
u := newUIWithSession("phone", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
SettingsPath: filepath.Join(dir, "settings.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
AutofillCachePath: filepath.Join(dir, "autofill-cache.json"),
})
if u.requestMasterPassFocus {
t.Fatal("requestMasterPassFocus = true without a selected startup target, want false")
}
}
func TestUIStartupRequestsMasterPasswordFocusForSelectedRecentVault(t *testing.T) {
t.Parallel()
dir := t.TempDir()
paths := statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
SettingsPath: filepath.Join(dir, "settings.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
AutofillCachePath: filepath.Join(dir, "autofill-cache.json"),
}
first := newUIWithSession("phone", &session.Manager{}, paths)
first.noteRecentVault("/tmp/demo.kdbx")
reopened := newUIWithSession("phone", &session.Manager{}, paths)
if got := reopened.vaultPath.Text(); got != "/tmp/demo.kdbx" {
t.Fatalf("vaultPath = %q, want /tmp/demo.kdbx", got)
}
if !reopened.requestMasterPassFocus {
t.Fatal("requestMasterPassFocus = false with a selected startup vault, want true")
}
}
func TestUIGroupToolsDisclosureStatePersists(t *testing.T) {
t.Parallel()
@@ -4311,6 +4355,17 @@ func TestShouldUsePreviewWindowSizeSkipsAndroid(t *testing.T) {
}
}
func TestSupportsDesktopFilePicker(t *testing.T) {
t.Parallel()
if got := supportsDesktopFilePicker("android"); got {
t.Fatal("supportsDesktopFilePicker(android) = true, want false")
}
if got := supportsDesktopFilePicker("linux"); !got {
t.Fatal("supportsDesktopFilePicker(linux) = false, want true")
}
}
func TestEnterOnLocalLifecycleScreenDefaultsToOpenVault(t *testing.T) {
t.Parallel()
+35 -6
View File
@@ -6,6 +6,7 @@ import (
"image/color"
"net/url"
"path/filepath"
"runtime"
"strings"
"gioui.org/layout"
@@ -27,7 +28,7 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
message := "Choose a recent vault or pick a `.kdbx` file, then unlock it."
message := "Choose a recent vault or enter a .kdbx path, then unlock it."
if u.lifecycleMode == "remote" {
message = "Connect to a remote vault, then unlock it with the KeePass master key."
}
@@ -182,9 +183,9 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
selectedPath := strings.TrimSpace(u.vaultPath.Text())
switch {
case busy:
return labeledEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, false)(gtx)
return labeledEditorHelp(u.theme, "Vault Path", localVaultPathHelp(), &u.vaultPath, false)(gtx)
case selectedPath == "":
return selectorEditorHelp(u.theme, "Vault Path", "Choose the existing .kdbx file to open.", &u.vaultPath, &u.pickVaultPath, "Choose File", false)(gtx)
return localPathSelector(u.theme, &u.vaultPath, &u.pickVaultPath)(gtx)
default:
lastGroup := u.recentVaultGroup(selectedPath)
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
@@ -242,9 +243,9 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
return labeledEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, false)(gtx)
return labeledEditorHelp(u.theme, "Key File", keyFileHelp(), &u.keyFilePath, false)(gtx)
}
return selectorEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, &u.pickKeyFile, "Choose File", false)(gtx)
return keyFileSelector(u.theme, &u.keyFilePath, &u.pickKeyFile)(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if busy {
@@ -1149,6 +1150,34 @@ func labeledEditorHelp(th *material.Theme, label, help string, editor *widget.Ed
return labeledEditorHelpFocus(th, defaultAccessibilityPreferences(), label, help, editor, sensitive, false)
}
func localVaultPathHelp() string {
if supportsDesktopFilePicker(runtime.GOOS) {
return "Choose the existing .kdbx file to open."
}
return "Enter the shared-storage path to the existing .kdbx file, for example /sdcard/Download/vault.kdbx."
}
func keyFileHelp() string {
if supportsDesktopFilePicker(runtime.GOOS) {
return "Optional path to a KeePass-compatible key file."
}
return "Optional shared-storage path to a KeePass-compatible key file."
}
func localPathSelector(th *material.Theme, editor *widget.Editor, click *widget.Clickable) layout.Widget {
if supportsDesktopFilePicker(runtime.GOOS) {
return selectorEditorHelp(th, "Vault Path", localVaultPathHelp(), editor, click, "Choose File", false)
}
return labeledEditorHelp(th, "Vault Path", localVaultPathHelp(), editor, false)
}
func keyFileSelector(th *material.Theme, editor *widget.Editor, click *widget.Clickable) layout.Widget {
if supportsDesktopFilePicker(runtime.GOOS) {
return selectorEditorHelp(th, "Key File", keyFileHelp(), editor, click, "Choose File", false)
}
return labeledEditorHelp(th, "Key File", keyFileHelp(), editor, false)
}
func labeledEditorHelpFocus(th *material.Theme, prefs accessibilityPreferences, label, help string, editor *widget.Editor, sensitive bool, focused bool) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
@@ -1252,7 +1281,7 @@ func (u *ui) unlockPanel(gtx layout.Context) layout.Dimensions {
return u.masterPasswordField(gtx, "Used alone or together with a key file to unlock the vault.")
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(selectorEditorHelp(u.theme, "Key File", "Optional path to a KeePass-compatible key file.", &u.keyFilePath, &u.pickKeyFile, "Choose File", false)),
layout.Rigid(keyFileSelector(u.theme, &u.keyFilePath, &u.pickKeyFile)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock")