1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.focus
18 
19 import androidx.collection.MutableLongSet
20 import androidx.collection.MutableObjectList
21 import androidx.compose.ui.ComposeUiFlags
22 import androidx.compose.ui.ExperimentalComposeUiApi
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
25 import androidx.compose.ui.focus.CustomDestinationResult.None
26 import androidx.compose.ui.focus.CustomDestinationResult.RedirectCancelled
27 import androidx.compose.ui.focus.CustomDestinationResult.Redirected
28 import androidx.compose.ui.focus.FocusDirection.Companion.Exit
29 import androidx.compose.ui.focus.FocusDirection.Companion.Next
30 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
31 import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
32 import androidx.compose.ui.focus.FocusRequester.Companion.Default
33 import androidx.compose.ui.focus.FocusRequester.Companion.Redirect
34 import androidx.compose.ui.focus.FocusStateImpl.Active
35 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
36 import androidx.compose.ui.focus.FocusStateImpl.Captured
37 import androidx.compose.ui.focus.FocusStateImpl.Inactive
38 import androidx.compose.ui.geometry.Rect
39 import androidx.compose.ui.input.indirect.IndirectTouchEvent
40 import androidx.compose.ui.input.key.KeyEvent
41 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
42 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
43 import androidx.compose.ui.input.key.key
44 import androidx.compose.ui.input.key.type
45 import androidx.compose.ui.input.rotary.RotaryScrollEvent
46 import androidx.compose.ui.internal.requirePrecondition
47 import androidx.compose.ui.node.DelegatableNode
48 import androidx.compose.ui.node.ModifierNodeElement
49 import androidx.compose.ui.node.NodeKind
50 import androidx.compose.ui.node.Nodes
51 import androidx.compose.ui.node.ancestors
52 import androidx.compose.ui.node.dispatchForKind
53 import androidx.compose.ui.node.nearestAncestor
54 import androidx.compose.ui.node.visitAncestors
55 import androidx.compose.ui.node.visitLocalDescendants
56 import androidx.compose.ui.platform.InspectorInfo
57 import androidx.compose.ui.unit.LayoutDirection
58 import androidx.compose.ui.util.fastForEach
59 import androidx.compose.ui.util.fastForEachReversed
60 import androidx.compose.ui.util.trace
61 
62 /**
63  * The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations to
64  * control focus.
65  */
66 internal class FocusOwnerImpl(
67     onRequestApplyChangesListener: (() -> Unit) -> Unit,
68     private val onRequestFocusForOwner:
69         (focusDirection: FocusDirection?, previouslyFocusedRect: Rect?) -> Boolean,
70     private val onMoveFocusInterop: (focusDirection: FocusDirection) -> Boolean,
71     private val onClearFocusForOwner: () -> Unit,
72     private val onFocusRectInterop: () -> Rect?,
73     private val onLayoutDirection: (() -> LayoutDirection)
74 ) : FocusOwner {
75 
76     // The root focus target is not focusable, and acts like a focus group.
77     internal var rootFocusNode = FocusTargetNode(focusability = Focusability.Never)
78 
79     private val focusInvalidationManager =
80         FocusInvalidationManager(
81             onRequestApplyChangesListener,
82             ::invalidateOwnerFocusState,
83             ::rootState,
84             ::activeFocusTargetNode
85         )
86 
87     override val focusTransactionManager: FocusTransactionManager = FocusTransactionManager()
88 
89     /**
90      * A [Modifier] that can be added to the [Owners][androidx.compose.ui.node.Owner] modifier list
91      * that contains the modifiers required by the focus system. (Eg, a root focus modifier).
92      */
93     // TODO(b/168831247): return an empty Modifier when there are no focusable children.
94     override val modifier: Modifier =
95         object : ModifierNodeElement<FocusTargetNode>() {
96             override fun create() = rootFocusNode
97 
98             override fun update(node: FocusTargetNode) {}
99 
100             override fun InspectorInfo.inspectableProperties() {
101                 name = "RootFocusTarget"
102             }
103 
104             override fun hashCode(): Int = rootFocusNode.hashCode()
105 
106             override fun equals(other: Any?) = other === this
107         }
108 
109     /**
110      * This function is called to ask the owner to request focus from the framework. eg. If a
111      * composable calls requestFocus and the root view does not have focus, this function can be
112      * used to request focus for the view.
113      *
114      * @param focusDirection If this focus request was triggered by a call to moveFocus or using the
115      *   keyboard, provide the owner with the direction of focus change.
116      * @param previouslyFocusedRect The bounds of the currently focused item.
117      * @return true if the owner successfully requested focus from the framework. False otherwise.
118      */
119     override fun requestFocusForOwner(
120         focusDirection: FocusDirection?,
121         previouslyFocusedRect: Rect?
122     ): Boolean = onRequestFocusForOwner(focusDirection, previouslyFocusedRect)
123 
124     /**
125      * Keeps track of which keys have received DOWN events without UP events – i.e. which keys are
126      * currently down. This is used to detect UP events for keys that aren't down and ignore them.
127      *
128      * This set is lazily initialized the first time a DOWN event is received for a key.
129      */
130     // TODO(b/307580000) Factor this state out into a class to manage key inputs.
131     private var keysCurrentlyDown: MutableLongSet? = null
132 
133     /**
134      * The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
135      * informs the [focus manager][FocusOwnerImpl] that the [Owner][androidx.compose.ui.node.Owner]
136      * gained focus, and that it should propagate this focus to one of the focus modifiers in the
137      * component hierarchy.
138      *
139      * @param focusDirection the direction to search for the focus target.
140      * @param previouslyFocusedRect the bounds of the currently focused item.
141      * @return true, if a suitable [FocusTargetNode] was found and it took focus, false if no
142      *   [FocusTargetNode] was found or if the focus search was cancelled.
143      */
144     override fun takeFocus(focusDirection: FocusDirection, previouslyFocusedRect: Rect?): Boolean {
145         return focusSearch(focusDirection, previouslyFocusedRect) {
146             it.requestFocus(focusDirection)
147         } ?: false
148     }
149 
150     /**
151      * The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
152      * informs the [focus manager][FocusOwnerImpl] that the [Owner][androidx.compose.ui.node.Owner]
153      * lost focus, and that it should clear focus from all the focus modifiers in the component
154      * hierarchy.
155      */
156     override fun releaseFocus() {
157         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
158             rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
159         } else {
160             focusTransactionManager.withExistingTransaction {
161                 rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
162             }
163         }
164     }
165 
166     /**
167      * Call this function to set the focus to the root focus modifier.
168      *
169      * @param force: Whether we should forcefully clear focus regardless of whether we have any
170      *   components that have captured focus.
171      *
172      * This could be used to clear focus when a user clicks on empty space outside a focusable
173      * component.
174      */
175     override fun clearFocus(force: Boolean) {
176         clearFocus(force, refreshFocusEvents = true, clearOwnerFocus = true, focusDirection = Exit)
177     }
178 
179     override fun clearFocus(
180         force: Boolean,
181         refreshFocusEvents: Boolean,
182         clearOwnerFocus: Boolean,
183         focusDirection: FocusDirection
184     ): Boolean {
185         val clearedFocusSuccessfully =
186             if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
187                 if (!force) {
188                     // Don't clear focus if an item on the focused path has a custom exit specified.
189                     when (rootFocusNode.performCustomClearFocus(focusDirection)) {
190                         Redirected,
191                         Cancelled,
192                         RedirectCancelled -> false
193                         None -> clearFocus(force, refreshFocusEvents)
194                     }
195                 } else {
196                     clearFocus(force, refreshFocusEvents)
197                 }
198             } else {
199                 focusTransactionManager.withNewTransaction(
200                     onCancelled = {
201                         return@withNewTransaction
202                     }
203                 ) {
204                     if (!force) {
205                         // Don't clear focus if an item on the focused path has a custom exit
206                         // specified.
207                         when (rootFocusNode.performCustomClearFocus(focusDirection)) {
208                             Redirected,
209                             Cancelled,
210                             RedirectCancelled -> return@withNewTransaction false
211                             None -> {
212                                 /* Do nothing. */
213                             }
214                         }
215                     }
216                     return@withNewTransaction rootFocusNode.clearFocus(force, refreshFocusEvents)
217                 }
218             }
219 
220         if (clearedFocusSuccessfully && clearOwnerFocus) {
221             onClearFocusForOwner.invoke()
222         }
223         return clearedFocusSuccessfully
224     }
225 
226     private fun clearFocus(forced: Boolean = false, refreshFocusEvents: Boolean): Boolean {
227         if (activeFocusTargetNode == null) return true
228         if (isFocusCaptured && !forced) {
229             return false // Cannot clear focus if it's captured unless forced
230         }
231         val previousActiveFocusTargetNode = activeFocusTargetNode
232         activeFocusTargetNode = null
233         if (refreshFocusEvents && previousActiveFocusTargetNode != null) {
234             previousActiveFocusTargetNode.dispatchFocusCallbacks(
235                 if (isFocusCaptured) Captured else Active,
236                 Inactive
237             )
238             previousActiveFocusTargetNode.visitAncestors(Nodes.FocusTarget) {
239                 it.dispatchFocusCallbacks(ActiveParent, Inactive)
240             }
241         }
242         return true
243     }
244 
245     /**
246      * Moves focus in the specified direction.
247      *
248      * @return true if focus was moved successfully. false if the focused item is unchanged.
249      */
250     override fun moveFocus(focusDirection: FocusDirection): Boolean {
251         // First check to see if the focus should move within child Views
252         @OptIn(ExperimentalComposeUiApi::class)
253         if (ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)) {
254             return true
255         }
256         var requestFocusSuccess: Boolean? = false
257         val generationBefore = focusTransactionManager.generation
258         val activeNodeBefore = activeFocusTargetNode
259         val focusSearchSuccess =
260             focusSearch(focusDirection, onFocusRectInterop()) {
261                 requestFocusSuccess = it.requestFocus(focusDirection)
262                 requestFocusSuccess ?: false
263             }
264         val generationAfter = focusTransactionManager.generation
265         if (
266             focusSearchSuccess == true &&
267                 (generationBefore != generationAfter ||
268                     (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled &&
269                         activeNodeBefore !== activeFocusTargetNode))
270         ) {
271             // There was a successful requestFocus() during the focusSearch
272             return true
273         }
274 
275         // If focus search was cancelled, or if focus search succeeded but request focus was
276         // cancelled, it implies that moveFocus() failed.
277         if (focusSearchSuccess == null || requestFocusSuccess == null) return false
278 
279         // If focus search and request focus succeeded, move focus succeeded.
280         if (focusSearchSuccess == true && requestFocusSuccess == true) return true
281 
282         // To wrap focus around, we clear focus and request initial focus.
283         if (focusDirection.is1dFocusSearch()) {
284             val clearFocus =
285                 clearFocus(
286                     force = false,
287                     refreshFocusEvents = true,
288                     clearOwnerFocus = false,
289                     focusDirection = focusDirection
290                 )
291             return clearFocus && takeFocus(focusDirection, previouslyFocusedRect = null)
292         }
293 
294         // If we couldn't move focus within compose, we attempt to move focus within embedded views.
295         // We don't need this for 1D focus search because the wrap-around logic triggers a
296         // focus exit which will perform a focus search among the subviews.
297         @OptIn(ExperimentalComposeUiApi::class)
298         return !ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)
299     }
300 
301     override fun focusSearch(
302         focusDirection: FocusDirection,
303         focusedRect: Rect?,
304         onFound: (FocusTargetNode) -> Boolean
305     ): Boolean? {
306         val source =
307             findFocusTargetNode()?.also {
308                 // Check if a custom focus traversal order is specified.
309                 when (val customDest = it.customFocusSearch(focusDirection, onLayoutDirection())) {
310                     Cancel -> return null
311                     Redirect -> return findFocusTargetNode()?.let(onFound)
312                     Default -> {
313                         /* Do Nothing */
314                     }
315                     else -> return customDest.findFocusTargetNode(onFound)
316                 }
317             }
318 
319         return rootFocusNode.focusSearch(focusDirection, onLayoutDirection(), focusedRect) {
320             when (it) {
321                 source -> false
322                 rootFocusNode -> error("Focus search landed at the root.")
323                 else -> onFound(it)
324             }
325         }
326     }
327 
328     /** Dispatches a key event through the compose hierarchy. */
329     override fun dispatchKeyEvent(keyEvent: KeyEvent, onFocusedItem: () -> Boolean): Boolean {
330         trace("FocusOwnerImpl:dispatchKeyEvent") {
331             if (focusInvalidationManager.hasPendingInvalidation()) {
332                 // Ignoring this to unblock b/346370327.
333                 println("$FocusWarning: Dispatching key event while focus system is invalidated.")
334                 return false
335             }
336             if (!validateKeyEvent(keyEvent)) return false
337 
338             val activeFocusTarget = findFocusTargetNode()
339             val focusedKeyInputNode =
340                 activeFocusTarget?.lastLocalKeyInputNode()
341                     ?: activeFocusTarget?.nearestAncestorIncludingSelf(Nodes.KeyInput)?.node
342                     ?: rootFocusNode.nearestAncestor(Nodes.KeyInput)?.node
343 
344             focusedKeyInputNode?.traverseAncestorsIncludingSelf(
345                 type = Nodes.KeyInput,
346                 onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
347                 onVisit = { if (onFocusedItem.invoke()) return true },
348                 onPostVisit = { if (it.onKeyEvent(keyEvent)) return true }
349             )
350             return false
351         }
352     }
353 
354     override fun dispatchInterceptedSoftKeyboardEvent(keyEvent: KeyEvent): Boolean {
355         if (focusInvalidationManager.hasPendingInvalidation()) {
356             // Ignoring this to unblock b/346370327.
357             println(
358                 "$FocusWarning: Dispatching intercepted soft keyboard event while the focus system" +
359                     " is invalidated."
360             )
361             return false
362         }
363 
364         val focusedSoftKeyboardInterceptionNode =
365             rootFocusNode
366                 .findActiveFocusNode()
367                 ?.nearestAncestorIncludingSelf(Nodes.SoftKeyboardKeyInput)
368 
369         focusedSoftKeyboardInterceptionNode?.traverseAncestorsIncludingSelf(
370             type = Nodes.SoftKeyboardKeyInput,
371             onPreVisit = { if (it.onPreInterceptKeyBeforeSoftKeyboard(keyEvent)) return true },
372             onVisit = { /* TODO(b/320510084): dispatch soft keyboard events to embedded views. */ },
373             onPostVisit = { if (it.onInterceptKeyBeforeSoftKeyboard(keyEvent)) return true }
374         )
375         return false
376     }
377 
378     /** Dispatches a rotary scroll event through the compose hierarchy. */
379     override fun dispatchRotaryEvent(
380         event: RotaryScrollEvent,
381         onFocusedItem: () -> Boolean
382     ): Boolean {
383         if (focusInvalidationManager.hasPendingInvalidation()) {
384             // Ignoring this to unblock b/379289347.
385             println(
386                 "$FocusWarning: Dispatching rotary event while the focus system is invalidated."
387             )
388             return false
389         }
390 
391         val focusedRotaryInputNode =
392             findFocusTargetNode()?.nearestAncestorIncludingSelf(Nodes.RotaryInput)
393 
394         focusedRotaryInputNode?.traverseAncestorsIncludingSelf(
395             type = Nodes.RotaryInput,
396             onPreVisit = { if (it.onPreRotaryScrollEvent(event)) return true },
397             onVisit = { if (onFocusedItem()) return true },
398             onPostVisit = { if (it.onRotaryScrollEvent(event)) return true }
399         )
400 
401         return false
402     }
403 
404     @OptIn(ExperimentalComposeUiApi::class)
405     override fun dispatchIndirectTouchEvent(
406         event: IndirectTouchEvent,
407         onFocusedItem: () -> Boolean
408     ): Boolean {
409         if (focusInvalidationManager.hasPendingInvalidation()) {
410             // Ignoring this to unblock b/379289347.
411             println(
412                 "$FocusWarning: Dispatching indirect touch event while the focus system is invalidated."
413             )
414             return false
415         }
416 
417         val focusedIndirectTouchInputNode =
418             findFocusTargetNode()?.nearestAncestorIncludingSelf(Nodes.IndirectTouchInput)
419         focusedIndirectTouchInputNode?.traverseAncestorsIncludingSelf(
420             type = Nodes.IndirectTouchInput,
421             onPreVisit = { if (it.onPreIndirectTouchEvent(event)) return true },
422             onVisit = { if (onFocusedItem()) return true },
423             onPostVisit = { if (it.onIndirectTouchEvent(event)) return true }
424         )
425 
426         return false
427     }
428 
429     override fun scheduleInvalidation(node: FocusTargetNode) {
430         focusInvalidationManager.scheduleInvalidation(node)
431     }
432 
433     override fun scheduleInvalidation(node: FocusEventModifierNode) {
434         focusInvalidationManager.scheduleInvalidation(node)
435     }
436 
437     override fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
438         focusInvalidationManager.scheduleInvalidation(node)
439     }
440 
441     override fun scheduleInvalidationForOwner() {
442         focusInvalidationManager.scheduleInvalidationForOwner()
443     }
444 
445     /**
446      * At the end of the invalidations, we need to ensure that the focus system is in a valid state.
447      */
448     private fun invalidateOwnerFocusState() {
449         // If an active item is removed, we currently clear focus from the hierarchy. We don't
450         // clear focus from the root because that could cause initial focus logic to be re-run.
451         // Now that all the invalidations are complete, we run owner.clearFocus() if needed.
452         if (
453             (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled &&
454                 activeFocusTargetNode == null) || rootFocusNode.focusState == Inactive
455         ) {
456             onClearFocusForOwner()
457         }
458     }
459 
460     private inline fun <reified T : DelegatableNode> DelegatableNode.traverseAncestorsIncludingSelf(
461         type: NodeKind<T>,
462         onPreVisit: (T) -> Unit,
463         onVisit: () -> Unit,
464         onPostVisit: (T) -> Unit
465     ) {
466         val ancestors = ancestors(type)
467         ancestors?.fastForEachReversed(onPreVisit)
468         node.dispatchForKind(type, onPreVisit)
469         onVisit.invoke()
470         node.dispatchForKind(type, onPostVisit)
471         ancestors?.fastForEach(onPostVisit)
472     }
473 
474     private inline fun <reified T : Any> DelegatableNode.nearestAncestorIncludingSelf(
475         type: NodeKind<T>
476     ): T? {
477         visitAncestors(type, includeSelf = true) {
478             return it
479         }
480         return null
481     }
482 
483     /** Searches for the currently focused item, and returns its coordinates as a rect. */
484     override fun getFocusRect(): Rect? {
485         return findFocusTargetNode()?.focusRect()
486     }
487 
488     private fun findFocusTargetNode(): FocusTargetNode? {
489         return rootFocusNode.findActiveFocusNode()
490     }
491 
492     override val rootState: FocusState
493         get() = rootFocusNode.focusState
494 
495     override val listeners: MutableObjectList<FocusListener> = MutableObjectList(1)
496 
497     override var activeFocusTargetNode: FocusTargetNode? = null
498         set(value) {
499             val previousValue = field
500             field = value
501             if (value == null || previousValue !== value) isFocusCaptured = false
502             if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
503                 listeners.forEach { it.onFocusChanged(previousValue, value) }
504             }
505         }
506 
507     override var isFocusCaptured: Boolean = false
508         set(value) {
509             requirePrecondition(!value || activeFocusTargetNode != null) {
510                 "Cannot capture focus when the active focus target node is unset"
511             }
512             field = value
513         }
514 
515     private fun DelegatableNode.lastLocalKeyInputNode(): Modifier.Node? {
516         var focusedKeyInputNode: Modifier.Node? = null
517         visitLocalDescendants(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
518             if (modifierNode.isKind(Nodes.FocusTarget)) return focusedKeyInputNode
519 
520             focusedKeyInputNode = modifierNode
521         }
522         return focusedKeyInputNode
523     }
524 
525     // TODO(b/307580000) Factor this out into a class to manage key inputs.
526     private fun validateKeyEvent(keyEvent: KeyEvent): Boolean {
527         val keyCode = keyEvent.key.keyCode
528         when (keyEvent.type) {
529             KeyDown -> {
530                 // It's probably rare for more than 3 hardware keys to be pressed simultaneously.
531                 val keysCurrentlyDown =
532                     keysCurrentlyDown
533                         ?: MutableLongSet(initialCapacity = 3).also { keysCurrentlyDown = it }
534                 keysCurrentlyDown += keyCode
535             }
536             KeyUp -> {
537                 if (keysCurrentlyDown?.contains(keyCode) != true) {
538                     // An UP event for a key that was never DOWN is invalid, ignore it.
539                     return false
540                 }
541                 keysCurrentlyDown?.remove(keyCode)
542             }
543         // Always process Unknown event types.
544         }
545         return true
546     }
547 }
548 
549 /**
550  * focus search in the Android framework wraps around for 1D focus search, but not for 2D focus
551  * search. This is a helper function that can be used to determine whether we should wrap around or
552  * not.
553  */
is1dFocusSearchnull554 internal fun FocusDirection.is1dFocusSearch(): Boolean =
555     when (this) {
556         Next,
557         Previous -> true
558         else -> false
559     }
560