Skip to content
Merged
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ docs/validation/touch-search/** -text -whitespace

# Preserve empty-feedback evidence and raw Android hierarchy CRLF.
docs/validation/empty-feedback/** -text -whitespace

# Preserve keyboard visibility receipts and native XML CRLF.
docs/validation/keyboard-toggle/** -text -whitespace

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions app/src/main/java/dev/tinycast/android/EmojiUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ fun EmojiPicker(store: EmojiStore, searchEpoch: Int, actionCurrent: () -> Boolea
copiedName = null
queryEditing = false
}
if (wide) IconButton(onClick = { keyboardRequested = true; searchFocus.requestFocus(); keyboard?.show() }, modifier = Modifier.size(48.dp).semantics { contentDescription = "Show on-screen keyboard" }) {
Glyph("keyboard", PaletteTheme.tertiary, 16)
}
if (wide) SearchKeyboardButton(imeVisible, Triple(store, searchEpoch, windowFocused), "emoji-keyboard", searchFocus,
current = { actionCurrent() && !menu && !actionsMenu && paletteMenus.current == null },
requested = { keyboardRequested = it })
Box {
// Retain natural scaled text and a 48 dp target without extra
// vertical padding shifting this editor below the launcher.
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/dev/tinycast/android/PaletteUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,10 @@ fun TinycastApp(store: PaletteStore, globalEnabled: Boolean, run: (Command) -> U
searchValue = TextFieldValue("")
store.search("")
}
if(wide) IconButton(onClick={keyboardRequested=true;focus.requestFocus();keyboard?.show()},modifier=Modifier.size(48.dp).semantics{contentDescription="Show on-screen keyboard"}){Glyph("keyboard",PaletteTheme.tertiary,16)}
if (wide) SearchKeyboardButton(imeVisible, Triple(store, searchEpoch, windowFocused), "search-keyboard", focus,
current = { store.focusEpoch == searchEpoch && store.dialog == null && store.textReview == null &&
!store.backupBusy && ShortcutRuntime.recordingOwner == null && !menu && paletteMenus.current == null },
requested = { keyboardRequested = it })
}
if (store.category == "Files") FileSearchToolbar(store, run)
LaunchedEffect(store.folderSearch.includeInLauncher) { store.syncFileSearch() }
Expand Down
68 changes: 68 additions & 0 deletions app/src/main/java/dev/tinycast/android/SearchKeyboardButton.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package dev.tinycast.android

import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.unit.dp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

private class SearchKeyboardLifetime { var active = true }

/** Set an explicit visibility state without replacing the editor's text or focus. */
@Composable
internal fun SearchKeyboardButton(
imeVisible: Boolean,
owner: Any,
tag: String,
focus: FocusRequester,
current: () -> Boolean,
requested: (Boolean) -> Unit
) {
val view = LocalView.current
val keyboard = LocalSoftwareKeyboardController.current
// A callback from an earlier mode stays obsolete even after visible/hidden
// returns to its former value within the same search owner.
val lifetime = remember(owner, imeVisible) { SearchKeyboardLifetime() }
DisposableEffect(lifetime) { onDispose { lifetime.active = false } }
val activate: () -> Boolean = {
val insets = ViewCompat.getRootWindowInsets(view)
if (lifetime.active && view.hasWindowFocus() && current() &&
insets != null && insets.isVisible(WindowInsetsCompat.Type.ime()) == imeVisible) {
val show = !imeVisible
requested(show)
val controller = ViewCompat.getWindowInsetsController(view)
if (show) {
focus.requestFocus()
keyboard?.show()
controller?.show(WindowInsetsCompat.Type.ime())
} else {
keyboard?.hide()
controller?.hide(WindowInsetsCompat.Type.ime())
}
true
} else false
}
// Publish the same owned action for accessibility. Foundation's default
// click semantics forwards through a mutable node to its latest callback;
// a retained action must instead keep this visibility/owner snapshot.
IconButton(onClick = { activate() }, modifier = Modifier.size(48.dp).testTag(tag).clearAndSetSemantics {
role = Role.Button
contentDescription = if (imeVisible) "Hide on-screen keyboard" else "Show on-screen keyboard"
onClick { activate() }
}) {
Glyph(if (imeVisible) "keyboard-hide" else "keyboard", PaletteTheme.tertiary, 20)
}
}
25 changes: 25 additions & 0 deletions docs/PARITY-TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,28 @@ The [#111 empty-feedback repair](EMPTY-FEEDBACK-READABILITY.md) now shows one co
The frozen `6c5825bf` app/test pair records **25 positive executions / 12 whole cohorts / 186 successful restoration checks**, plus **eight intended old/intermediate failures / zero passes / 68 checks** with the same final instrumentation. Modern landscape/phone/tablet run at both font scales; API26 adds normal-viewport native feedback smoke, with existing Clear/ownership and controlled-input regressions. All **14 separate final-preservation checks** pass, including both original modern APKs, all 573 existing app/evidence files, original legacy package absence and the current four-field IME baselines. Builds pass; JVM343 executed with no failure/error/skip; lint records zero errors / 56 warnings per variant. Python135 per mode retains the exact unchanged Python source from `15eb95c`.

The checkpoint binds **478 indexed public files plus itself, 84 actual screenshot sets, 1,121 retained capture hashes and 254 source inputs**. Earlier primary/secondary clipping failures, intermediate passing criteria and the restored whole-cohort orchestration stop remain separate. The complete retained accounting is 59 executions / 39 passes / 20 failures / 471 successful cohort checks. Exact-head hosted CI, both independent sol/high audits and an expected-head merge remain required. Nonempty states are functional smoke, not full result typography; composition through every system-IME transition, Files-toolbar variants, #69/#71/#89, broader parity/accessibility/OEM, original pre-probe IME-history identity and signing/migration remain open or uncertified. No project rename or migration is performed.


PR #113 merged reviewed `b74893cd3cafe424558d8ee60f58713abbfe9a70` as `adb41de4281aaae328e2d227064060f8f7495d71`. Both independent gpt-6.1-sol/high full-change audits cleared; root verified exact-head pull_request Android run42078762341 and local/android58176983369 success, recorded COMMENT review5789633960, and used an expected-head merge. Both parents and merged/reviewed tree `c4169d764a10d2b2f8301d5ff874bb39543c8c49` match. #111 is closed within native Apps/Emoji empty-feedback scope; the PR watch is stopped.

The active [#114](https://github.com/moikdev/droidcast/issues/114) slice adds an app-level Hide/Show action in the existing wide-screen keyboard slot. Actual retained short-landscape nonempty frames show the header while result content is below the viewport; the existing button always requests Show. Matched pointer/actual-IME controls and exact query/range/focus/geometry contracts are under preparation. No implementation or runtime credit is claimed yet. System dismissal/scrolling remain available; full row/grid typography, every system-IME transition, Files/OEM/TalkBack/Store and signing/migration remain outside this slice.


The first #114 harness compile failed for a missing explicit experimental-selection API opt-in; the corrected harness built successfully before native execution. Its old-app landscape100 cohort has two intended actual-Hide failures with 17 successful restoration checks. The landscape200 cohort remains a mixed failure: one intended launcher-Hide assertion and an Emoji initial-Show timeout before that assertion, also with 17 checks. The registry remains incomplete and earns no complete 200% control credit. A stronger protocol now captures initial Show as an actual pointer action after settled geometry, then separately checks Hide. This protocol change explains no production cause or repair for the earlier timeout.


The stronger #114 baseline protocol completes four intended old-app Hide failures across 100%/200%, with 34 successful restoration checks. Its actual pointer Show succeeds before each Hide attempt; the earlier mixed timeout cohort remains failed and earns no production-cause or repair credit. The implementation reuses the existing fixed 48 dp slot and keyboard/keyboard-hide glyphs, validates actual visibility/foreground/owner state, and invalidates retained callbacks across visibility modes, epochs and disposal. Native candidate and callback regression evidence are pending.


Candidate `1355f84c` passed both 200% landscape pointer-toggle cases, then failed both retained-mode callback cases; all 32 cohort restoration checks passed and the rest of that pipeline was not run. Pinned AndroidX 1.7.6 source and cached AAR bytecode establish that the default click semantics stores a node proxy which calls its current `onClick`. Thus the retained semantics reference reached the new current Hide action after the mode cycle, not the original guarded application closure. The next implementation publishes an explicit owned accessibility action, with Button role and the same action used by pointer clicks. It binds the published visibility/owner snapshot and reports rejection accurately. Earlier failures remain bound to their original binaries; no keyboard-Show timeout cause is inferred.


Candidate `411b7f73` publishes the owned action and passes the launcher callback case, while Emoji passes the obsolete-mode, epoch and obstructing-menu checks before `UiDevice.pressBack()` clears its query. The whole cohort remains failed 1/2 with 15 successful restoration checks; its later planned cohorts were unrun. [#115](https://github.com/moikdev/droidcast/issues/115) retains the actual Back observation, with cause and historical-app attribution unestablished. The next #114 callback protocol dismisses the menu through a geometry-derived actual pointer on its backdrop and retains the same exact-query assertion. This changes no production sources after `411b7f73` and earns no Back cause/repair credit.


The `f30bebd6` driver passed both callback cases and all four wide pointer profiles, with its matched proxy and old Show-only controls failing as intended. It then failed both 100% landscape feedback regressions because their geometry selector still demanded the old constant Show description after the actual IME was shown; the retained primary-line and compact-status checks had passed. The corrected selector requires the expected Hide/Show description for that phase and keeps every text/48 dp geometry assertion. The new driver also adds a supported current accessibility-action positive and a separate injected-system-Back diagnostic on both historical/current apps. No production/build/JVM sources change after `f30bebd6`; all intermediate cohorts remain separate.


The [#114 touch keyboard control](TOUCH-KEYBOARD-VISIBILITY.md) now has **24 final positives / 11 whole cohorts / 176 successful restoration checks**, with **six matched intended failures / zero passes / 49 checks**. The executed app is exact `f30bebd6/98da1466…aa5de3`; final `69bd578d/dc22049a…ccdf3d` instrumentation/controller changes only the fixture/allowlist after that app source. All 255 current source hashes, unchanged production/build/JVM inputs, actual app byte equality and all 11 original assets/notices are verified. Current pointer and accepted-accessibility actions preserve query/range/focus/geometry, while obsolete mode/epoch/menu/replacement/recreation actions reject. Tablet/short-landscape run 100%/200%; selected targets, empty feedback, enlarged narrow-phone Clear and modern/API26 controlled input remain scoped whole cohorts.

All 14 separate final-preservation checks pass, including original modern APKs/all 573 files, legacy absence and both current four-field IME baselines. Fresh fixture builds/lint (0 errors/56 warnings each) and Python 135 per mode pass; JVM 343 is retained from exact unchanged production/build/JVM inputs. The checkpoint binds 536 indexed public files plus itself, 89 actual screenshot sets and 1150 retained capture hashes. Complete retained accounting stays 64 executions / 37 passes / 27 failures / 515 cohort checks, with all earlier incomplete pipelines, initialization/proxy/Back/selector failures separate. The isolated injected-system-Back probe fails on both old/current apps with 30 successful checks; #115 remains open for actual key/dispatcher tracing and earns no cause/repair credit. The raw pressBack return is false and is not key-delivery proof. Exact-head hosted CI, both independent sol / high audits and expected-head merge remain gates. #69/#71/#89, broader row/grid/footer/OEM/TalkBack/Store and signing/migration limits remain open.
Loading
Loading