{% set rfcid = “RFC-0177” %} {% include “docs/contribute/governance/rfcs/_common/_rfc_header.md” %}
{# Fuchsia RFCs use templates to display various fields from _rfcs.yaml. View the #} {# fully rendered RFCs at https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs #}
This RFC proposes an API design for view focus that is safe to use out of tree, by ordinary UI clients, and clarifies the security constraints around focus observability. The emphasis is on minimality of information exposure and an elegant developer experience.
In order to create a user experience (graphics, input, etc) from multiple components, it is a common pattern for UI clients to delegate content production to other UI clients by setting up a view tree, where a parent view manages one or more child views. The Ermine system shell is one such example; Google's Smart Display is another. One key responsibility of a parent view is to monitor view focus state:
Focus may change without the parent view's involvement (user touch, view detach, etc). The parent view must be kept informed of how view focus moves around, but in a way that respects the information limits set up by the global view tree.
This RFC proposes a “focus observer” design which (1) allows a parent view to correctly respond to view focus changes, (2) is safe to use out of tree, and (3) improves the security posture of the Fuchsia View system, with minimal information exposure.
Facilitator:
Reviewers: sanjayc@google.com (Workstation), quiche@google.com (HCI), neelsa@google.com (HCI), akbiggs@google.com (Flutter)
Consulted: shiveshganju@google.com, fmil@google.com, emircan@google.com, jsankey@google.com
Socialization:
This RFC went through socialization with leads of affected teams.
The core proposal for this focus observer is the following FIDL protocol.
library fuchsia.ui.observation.focus; using zx; protocol ScopedProvider { Watch() -> (ScopedResponse); }; type ScopedResponse = table { 1: observation_end zx.time; 2: focused zx.koid; };
The “Scoped” in the name indicates that the protocol provides focus information that is scoped to, or constrained within, the focus.ScopedProvider client‘s view tree. The focus.ScopedProvider client’s view is the root of this observable view tree.
The observation_end
time marks the end of the Watch period, so that the client knows when the returned focus was accurate. For example, it allows the client to distinguish between distinct returns of the same focus value, if a series of focus changes happened to return back to a previous focus in a single Watch period.
The focused
KOID is either a view ref KOID, or the special sentinel value ZX_KOID_INVALID
that indicates view focus is outside the focus.ScopedProvider client's view tree. The possible values and semantics are discussed in more detail below.
Consider the following view topology, where each circle represents a View, and view “U” is a client of focus.ScopedProvider.
A client of focus.ScopedProvider has limited visibility into the global view tree (see Security Considerations for details). It can learn that view focus is either in its view tree (rooted at the focus.ScopedProvider client's view) or outside its view tree, but the specifics are intentionally elided.
When focus is outside the focus.ScopedProvider client's view tree, the client is informed of only of that very general fact, with the ZX_KOID_INVALID
sentinel value. The client does not learn the identity of the new view focus.
Wen focus is held within the focus.ScopedProvider client's view tree, the client is informed of only the following information:
The parent view needs to know when it has the power to move focus amongst its children. It has this power when view focus is in its view tree. Otherwise, a call to fuchsia.ui.views.Focuser.RequestFocus() will always fail.
It‘s worth noting that the focus.ScopedProvider’s information is a snapshot propagated over a channel, so a request to change focus may race with the next snapshot update. For example, one snapshot might indicate that focus is in the focus.ScopedProvider client's view tree, and a request to change focus to a direct child may get denied if an ancestor view successfully requested a focus change to outside this view tree.
In this sequence diagram, U is notified when focus moved to U, and again when focus moved entirely out of U's view tree.
focused
is one of three value classes, which includes the ZX_KOID_INVALID
sentinel value. If focused
is valid (i.e., not the sentinel), then the view has the power to move focus arbitrarily between itself and its child views.
Specifically:
focused
is ZX_KOID_INVALID
, then focus has left this view tree. This situation can arise for multiple reasons. For example, the view tree at U might be connected to the global view tree, but an ancestor view may have moved focus out, to U's sibling view. Or, U might have become disconnected from the global view tree, meaning U is no longer eligible to hold focus. Or, an ancestor of U may itself be disconnected, in which case all descendants of that ancestor cannot hold focus. See focus policy.In this example, focused moved to X, a child of V under U. The focus observer reports the direct child of U, which is V.
If there were multiple focus changes during the past Watch period, this API will return only the final focus. A client typically cannot act on past focus changes, hence the API was simplified to return just a “summary”.
Typically, if a hanging-get client parks a callback via Watch, a focus change will result in an immediate return to the client. However, it‘s possible for a client to get delayed parking the next hanging-get, so the server may see multiple focus changes to summarize on the next return. It’s also possible for the server to receive a flurry of focus changes, so depending on thread or task scheduling, a parked hanging-get may get serviced after a number of focus changes.
In these examples, U gets the same notification, regardless of specific Watch() call timing.
The Watch call is driven by state changes on a per-client basis.
With level changes, the server notifies the client only when a change happens after a Watch call was received, and ignores changes prior to receiving a Watch call. The client would miss a focus change summary during that period, which is not appropriate for the intended use case.
Making it based on state change creates a larger burden on the server implementation, since it needs to track the last issued state for each observer channel. However, it leads to a more intuitive developer experience, since state change is robust against ordering swaps between the client‘s Watch call and any focus changes. For example, after a Watch call is parked on the server, several focus changes can happen in the interim before the callback is processed, depending on the server’s threadedness and implementation details.
View focus is closely tied to view lifecycle and maintenance of view topology. Scenic is the view manager component, so the implementation of this protocol belongs in Scenic.
Focus changes can be frequent, but realistically move at “human scale”. Thus FIDL call frequency is not perceived to be an issue. The FIDL payload is also very light, and the flow control pattern avoids channel stuffing.
This API strives to improve DX over its predecessors. Simplified error handling, lossy summary semantics, and the absence of container data types should mean easier adoption.
This API is intended to be consumed in OOT repositories, and the server implementation resides in a platform component, Scenic. The API will evolve safely, and retain backward compatibility, by adding newer hanging-get methods. When all repositories that use a deprecated method get updated to a newer method, the deprecated method can be marked as deleted.
This API hooks into the fuchsia.ui.composition.Flatland.ViewBoundProtocols table, which tightly associates this API's server endpoint to the specific ViewRef associated with the parent view, at view creation time.
The API client cannot ask for more detailed information deep in its own view tree, or outside its view tree. The view ref KOID information received is scoped to itself and its direct children, which improves the view's security posture.
In a more permissive system, a malicious view could “steal” focus from any other view merely by asking for it. The Fuchsia View system's focus policy mitigates this possibility by defining the circumstance and scope of focus movement: a view can move focus only if it was granted focus by an ancestor view, and it can only move focus within its view subtree, not outside of it.
This focus observer design follows the scoped approach of this focus policy, by limiting observability to the observed view and its direct children.
Another small improvement is that the focus observer protocol hands out the child view ref‘s KOID, instead of a copy of the view ref itself. Some UI protocols act on a view ref, so returning a KOID reduces the possibility of misuse. For example, if Ermine’s focus.ScopedProvider channel endpoint was delegated to another component ‘C’, it is a safe delegation, because ‘C’ cannot impersonate Ermine or any child view of Ermine, to the KeyboardListener protocol.
The typical usage is to identify which view has gained focus, for which the view‘s view ref KOID is sufficient. Note that requesting focus requires a live view ref, not just a view ref’s KOID. Clients are expected to maintain their own list of child view refs (i.e., obtained over the Flatland protocol), and these view refs can be used to request focus.
The FocusChainListener protocol gave full visibility of the view tree, right up to the root view. This focus observer protocol intentionally limits the scope of visibility, where the visible view tree is rooted at the client view itself.
The ViewRefFocused is already scoped to the client‘s view. This focus observer protocol extends the client’s visibility to the client view's direct child views only.
Because of these mitigations, we expect privacy impact to be minimal.
The implementation will have unit tests and platform-side integration tests. Additionally, as with any other SDK-visible FIDL, it will have a CTF test.
There will be a usage documentation guide in fuchsia.dev.
This API does not fit all known usages for observing focus. However, a previous socialization effort reinforced the need to create separate APIs for separate needs. A follow-on RFC will tackle other “focus observer” APIs.
Today's only option for observing view focus movement across a view tree is the fuchsia.ui.focus.FocusChainListener protocol. It is deprecated, due to these problems: