1 /*
<lambda>null2  * Copyright (C) 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 android.tools.flicker.subject.wm
18 
19 import android.tools.Rotation
20 import android.tools.flicker.subject.FlickerTraceSubject
21 import android.tools.flicker.subject.region.RegionTraceSubject
22 import android.tools.function.AssertionPredicate
23 import android.tools.io.Reader
24 import android.tools.traces.component.ComponentNameMatcher
25 import android.tools.traces.component.IComponentMatcher
26 import android.tools.traces.region.RegionTrace
27 import android.tools.traces.wm.WindowManagerTrace
28 import android.tools.traces.wm.WindowState
29 import java.util.function.Predicate
30 
31 /**
32  * Subject for [WindowManagerTrace] objects, used to make assertions over behaviors that occur
33  * throughout a whole trace.
34  *
35  * To make assertions over a trace it is recommended to create a subject using
36  * [WindowManagerTraceSubject](myTrace).
37  *
38  * Example:
39  * ```
40  *    val trace = WindowManagerTraceParser().parse(myTraceFile)
41  *    val subject = WindowManagerTraceSubject(trace)
42  *        .contains("ValidWindow")
43  *        .notContains("ImaginaryWindow")
44  *        .showsAboveAppWindow("NavigationBar")
45  *        .forAllEntries()
46  * ```
47  *
48  * Example2:
49  * ```
50  *    val trace = WindowManagerTraceParser().parse(myTraceFile)
51  *    val subject = WindowManagerTraceSubject(trace) {
52  *        check(myCustomAssertion(this)) { "My assertion lazy message" }
53  *    }
54  * ```
55  */
56 class WindowManagerTraceSubject
57 @JvmOverloads
58 constructor(val trace: WindowManagerTrace, override val reader: Reader? = null) :
59     FlickerTraceSubject<WindowManagerStateSubject>(),
60     IWindowManagerSubject<WindowManagerTraceSubject, RegionTraceSubject> {
61 
62     override val subjects by lazy {
63         trace.entries.map { WindowManagerStateSubject(it, reader, this) }
64     }
65 
66     /** {@inheritDoc} */
67     override fun then(): WindowManagerTraceSubject = apply { super.then() }
68 
69     /** {@inheritDoc} */
70     override fun skipUntilFirstAssertion(): WindowManagerTraceSubject = apply {
71         super.skipUntilFirstAssertion()
72     }
73 
74     /** {@inheritDoc} */
75     override fun isEmpty(): WindowManagerTraceSubject = apply {
76         check { "Trace is empty" }.that(trace.entries.isEmpty()).isEqual(true)
77     }
78 
79     /** {@inheritDoc} */
80     override fun isNotEmpty(): WindowManagerTraceSubject = apply {
81         check { "Trace is not empty" }.that(trace.entries.isEmpty()).isEqual(false)
82     }
83 
84     /**
85      * @return List of [WindowStateSubject]s matching [componentMatcher] in the order they
86      *
87      * ```
88      *      appear on the trace
89      *
90      * @param componentMatcher
91      * ```
92      *
93      * Components to search
94      */
95     fun windowStates(componentMatcher: IComponentMatcher): List<WindowStateSubject> = windowStates {
96         componentMatcher.windowMatchesAnyOf(it)
97     }
98 
99     /**
100      * @return List of [WindowStateSubject]s matching [predicate] in the order they
101      *
102      * ```
103      *      appear on the trace
104      *
105      * @param predicate
106      * ```
107      *
108      * To search
109      */
110     fun windowStates(predicate: Predicate<WindowState>): List<WindowStateSubject> {
111         return subjects.mapNotNull { it.windowState { window -> predicate.test(window) } }
112     }
113 
114     /** {@inheritDoc} */
115     override fun notContains(componentMatcher: IComponentMatcher): WindowManagerTraceSubject =
116         notContains(componentMatcher, isOptional = false)
117 
118     /** See [notContains] */
119     fun notContains(
120         componentMatcher: IComponentMatcher,
121         isOptional: Boolean,
122     ): WindowManagerTraceSubject = apply {
123         addAssertion("notContains(${componentMatcher.toWindowIdentifier()})", isOptional) {
124             it.notContains(componentMatcher)
125         }
126     }
127 
128     /** {@inheritDoc} */
129     override fun isAboveAppWindowVisible(
130         componentMatcher: IComponentMatcher
131     ): WindowManagerTraceSubject = isAboveAppWindowVisible(componentMatcher, isOptional = false)
132 
133     /** See [isAboveAppWindowVisible] */
134     fun isAboveAppWindowVisible(
135         componentMatcher: IComponentMatcher,
136         isOptional: Boolean,
137     ): WindowManagerTraceSubject = apply {
138         addAssertion(
139             "isAboveAppWindowVisible(${componentMatcher.toWindowIdentifier()})",
140             isOptional,
141         ) {
142             it.isAboveAppWindowVisible(componentMatcher)
143         }
144     }
145 
146     /** {@inheritDoc} */
147     override fun isAboveAppWindowInvisible(
148         componentMatcher: IComponentMatcher
149     ): WindowManagerTraceSubject = isAboveAppWindowInvisible(componentMatcher, isOptional = false)
150 
151     /** See [isAboveAppWindowInvisible] */
152     fun isAboveAppWindowInvisible(
153         componentMatcher: IComponentMatcher,
154         isOptional: Boolean,
155     ): WindowManagerTraceSubject = apply {
156         addAssertion(
157             "isAboveAppWindowInvisible(${componentMatcher.toWindowIdentifier()})",
158             isOptional,
159         ) {
160             it.isAboveAppWindowInvisible(componentMatcher)
161         }
162     }
163 
164     /** {@inheritDoc} */
165     override fun isBelowAppWindowVisible(
166         componentMatcher: IComponentMatcher
167     ): WindowManagerTraceSubject = isBelowAppWindowVisible(componentMatcher, isOptional = false)
168 
169     /** See [isBelowAppWindowVisible] */
170     fun isBelowAppWindowVisible(
171         componentMatcher: IComponentMatcher,
172         isOptional: Boolean,
173     ): WindowManagerTraceSubject = apply {
174         addAssertion(
175             "isBelowAppWindowVisible(${componentMatcher.toWindowIdentifier()})",
176             isOptional,
177         ) {
178             it.isBelowAppWindowVisible(componentMatcher)
179         }
180     }
181 
182     /** {@inheritDoc} */
183     override fun isBelowAppWindowInvisible(
184         componentMatcher: IComponentMatcher
185     ): WindowManagerTraceSubject = isBelowAppWindowInvisible(componentMatcher, isOptional = false)
186 
187     /** See [isBelowAppWindowInvisible] */
188     fun isBelowAppWindowInvisible(
189         componentMatcher: IComponentMatcher,
190         isOptional: Boolean,
191     ): WindowManagerTraceSubject = apply {
192         addAssertion(
193             "isBelowAppWindowInvisible(${componentMatcher.toWindowIdentifier()})",
194             isOptional,
195         ) {
196             it.isBelowAppWindowInvisible(componentMatcher)
197         }
198     }
199 
200     /** {@inheritDoc} */
201     override fun isNonAppWindowVisible(
202         componentMatcher: IComponentMatcher
203     ): WindowManagerTraceSubject = isNonAppWindowVisible(componentMatcher, isOptional = false)
204 
205     /** See [isNonAppWindowVisible] */
206     fun isNonAppWindowVisible(
207         componentMatcher: IComponentMatcher,
208         isOptional: Boolean,
209     ): WindowManagerTraceSubject = apply {
210         addAssertion(
211             "isNonAppWindowVisible(${componentMatcher.toWindowIdentifier()})",
212             isOptional,
213         ) {
214             it.isNonAppWindowVisible(componentMatcher)
215         }
216     }
217 
218     /** {@inheritDoc} */
219     override fun isNonAppWindowInvisible(
220         componentMatcher: IComponentMatcher
221     ): WindowManagerTraceSubject = isNonAppWindowInvisible(componentMatcher, isOptional = false)
222 
223     /** See [isNonAppWindowInvisible] */
224     fun isNonAppWindowInvisible(
225         componentMatcher: IComponentMatcher,
226         isOptional: Boolean,
227     ): WindowManagerTraceSubject = apply {
228         addAssertion(
229             "isNonAppWindowInvisible(${componentMatcher.toWindowIdentifier()})",
230             isOptional,
231         ) {
232             it.isNonAppWindowInvisible(componentMatcher)
233         }
234     }
235 
236     /** {@inheritDoc} */
237     override fun isAppWindowOnTop(componentMatcher: IComponentMatcher): WindowManagerTraceSubject =
238         isAppWindowOnTop(componentMatcher, isOptional = false)
239 
240     /** See [isAppWindowOnTop] */
241     fun isAppWindowOnTop(
242         componentMatcher: IComponentMatcher,
243         isOptional: Boolean,
244     ): WindowManagerTraceSubject = apply {
245         addAssertion("isAppWindowOnTop(${componentMatcher.toWindowIdentifier()})", isOptional) {
246             it.isAppWindowOnTop(componentMatcher)
247         }
248     }
249 
250     /** {@inheritDoc} */
251     override fun isAppWindowNotOnTop(
252         componentMatcher: IComponentMatcher
253     ): WindowManagerTraceSubject = isAppWindowNotOnTop(componentMatcher, isOptional = false)
254 
255     /** See [isAppWindowNotOnTop] */
256     fun isAppWindowNotOnTop(
257         componentMatcher: IComponentMatcher,
258         isOptional: Boolean,
259     ): WindowManagerTraceSubject = apply {
260         addAssertion("appWindowNotOnTop(${componentMatcher.toWindowIdentifier()})", isOptional) {
261             it.isAppWindowNotOnTop(componentMatcher)
262         }
263     }
264 
265     /** {@inheritDoc} */
266     override fun isAppWindowVisible(
267         componentMatcher: IComponentMatcher
268     ): WindowManagerTraceSubject = isAppWindowVisible(componentMatcher, isOptional = false)
269 
270     /** See [isAppWindowVisible] */
271     fun isAppWindowVisible(
272         componentMatcher: IComponentMatcher,
273         isOptional: Boolean,
274     ): WindowManagerTraceSubject = apply {
275         addAssertion("isAppWindowVisible(${componentMatcher.toWindowIdentifier()})", isOptional) {
276             it.isAppWindowVisible(componentMatcher)
277         }
278     }
279 
280     /** {@inheritDoc} */
281     override fun hasNoVisibleAppWindow(): WindowManagerTraceSubject =
282         hasNoVisibleAppWindow(isOptional = false)
283 
284     /** See [hasNoVisibleAppWindow] */
285     fun hasNoVisibleAppWindow(isOptional: Boolean): WindowManagerTraceSubject = apply {
286         addAssertion("hasNoVisibleAppWindow()", isOptional) { it.hasNoVisibleAppWindow() }
287     }
288 
289     /** {@inheritDoc} */
290     override fun isKeyguardShowing(): WindowManagerTraceSubject =
291         isKeyguardShowing(isOptional = false)
292 
293     /** See [isKeyguardShowing] */
294     fun isKeyguardShowing(isOptional: Boolean): WindowManagerTraceSubject = apply {
295         addAssertion("isKeyguardShowing()", isOptional) { it.isKeyguardShowing() }
296     }
297 
298     /** {@inheritDoc} */
299     override fun isAppSnapshotStartingWindowVisibleFor(
300         componentMatcher: IComponentMatcher
301     ): WindowManagerTraceSubject =
302         isAppSnapshotStartingWindowVisibleFor(componentMatcher, isOptional = false)
303 
304     /** See [isAppSnapshotStartingWindowVisibleFor] */
305     fun isAppSnapshotStartingWindowVisibleFor(
306         componentMatcher: IComponentMatcher,
307         isOptional: Boolean,
308     ): WindowManagerTraceSubject = apply {
309         addAssertion(
310             "isAppSnapshotStartingWindowVisibleFor(${componentMatcher.toWindowIdentifier()})",
311             isOptional,
312         ) {
313             it.isAppSnapshotStartingWindowVisibleFor(componentMatcher)
314         }
315     }
316 
317     /** {@inheritDoc} */
318     override fun isAppWindowInvisible(
319         componentMatcher: IComponentMatcher
320     ): WindowManagerTraceSubject = isAppWindowInvisible(componentMatcher, isOptional = false)
321 
322     /** See [isAppWindowInvisible] */
323     fun isAppWindowInvisible(
324         componentMatcher: IComponentMatcher,
325         isOptional: Boolean,
326     ): WindowManagerTraceSubject = apply {
327         addAssertion("isAppWindowInvisible(${componentMatcher.toWindowIdentifier()})", isOptional) {
328             it.isAppWindowInvisible(componentMatcher)
329         }
330     }
331 
332     /** {@inheritDoc} */
333     override fun doNotOverlap(
334         vararg componentMatcher: IComponentMatcher
335     ): WindowManagerTraceSubject = apply {
336         val repr = componentMatcher.joinToString(", ") { it.toWindowIdentifier() }
337         addAssertion("noWindowsOverlap($repr)") { it.doNotOverlap(*componentMatcher) }
338     }
339 
340     /** {@inheritDoc} */
341     override fun isAboveWindow(
342         aboveWindowComponentMatcher: IComponentMatcher,
343         belowWindowComponentMatcher: IComponentMatcher,
344     ): WindowManagerTraceSubject = apply {
345         val aboveWindowTitle = aboveWindowComponentMatcher.toWindowIdentifier()
346         val belowWindowTitle = belowWindowComponentMatcher.toWindowIdentifier()
347         addAssertion("$aboveWindowTitle is above $belowWindowTitle") {
348             it.isAboveWindow(aboveWindowComponentMatcher, belowWindowComponentMatcher)
349         }
350     }
351 
352     /** See [isAppWindowInvisible] */
353     override fun visibleRegion(componentMatcher: IComponentMatcher?): RegionTraceSubject {
354         val regionTrace =
355             RegionTrace(
356                 componentMatcher,
357                 subjects.map { it.visibleRegion(componentMatcher).regionEntry },
358             )
359 
360         return RegionTraceSubject(regionTrace, reader)
361     }
362 
363     /** {@inheritDoc} */
364     override fun contains(componentMatcher: IComponentMatcher): WindowManagerTraceSubject =
365         contains(componentMatcher, isOptional = false)
366 
367     /** See [contains] */
368     fun contains(
369         componentMatcher: IComponentMatcher,
370         isOptional: Boolean,
371     ): WindowManagerTraceSubject = apply {
372         addAssertion("contains(${componentMatcher.toWindowIdentifier()})", isOptional) {
373             it.contains(componentMatcher)
374         }
375     }
376 
377     /** {@inheritDoc} */
378     override fun containsAboveAppWindow(
379         componentMatcher: IComponentMatcher
380     ): WindowManagerTraceSubject = containsAboveAppWindow(componentMatcher, isOptional = false)
381 
382     /** See [containsAboveAppWindow] */
383     fun containsAboveAppWindow(
384         componentMatcher: IComponentMatcher,
385         isOptional: Boolean,
386     ): WindowManagerTraceSubject = apply {
387         addAssertion(
388             "containsAboveAppWindow(${componentMatcher.toWindowIdentifier()})",
389             isOptional,
390         ) {
391             it.containsAboveAppWindow(componentMatcher)
392         }
393     }
394 
395     /** {@inheritDoc} */
396     override fun containsAppWindow(componentMatcher: IComponentMatcher): WindowManagerTraceSubject =
397         containsAppWindow(componentMatcher, isOptional = false)
398 
399     /** See [containsAppWindow] */
400     fun containsAppWindow(
401         componentMatcher: IComponentMatcher,
402         isOptional: Boolean,
403     ): WindowManagerTraceSubject = apply {
404         addAssertion("containsAppWindow(${componentMatcher.toWindowIdentifier()})", isOptional) {
405             it.containsAboveAppWindow(componentMatcher)
406         }
407     }
408 
409     /** {@inheritDoc} */
410     override fun containsBelowAppWindow(
411         componentMatcher: IComponentMatcher
412     ): WindowManagerTraceSubject = containsBelowAppWindow(componentMatcher, isOptional = false)
413 
414     /** See [containsBelowAppWindow] */
415     fun containsBelowAppWindow(
416         componentMatcher: IComponentMatcher,
417         isOptional: Boolean,
418     ): WindowManagerTraceSubject = apply {
419         addAssertion(
420             "containsBelowAppWindows(${componentMatcher.toWindowIdentifier()})",
421             isOptional,
422         ) {
423             it.containsBelowAppWindow(componentMatcher)
424         }
425     }
426 
427     /** {@inheritDoc} */
428     override fun containsNonAppWindow(
429         componentMatcher: IComponentMatcher
430     ): WindowManagerTraceSubject = containsNonAppWindow(componentMatcher, isOptional = false)
431 
432     /** See [containsNonAppWindow] */
433     fun containsNonAppWindow(
434         componentMatcher: IComponentMatcher,
435         isOptional: Boolean,
436     ): WindowManagerTraceSubject = apply {
437         addAssertion("containsNonAppWindow(${componentMatcher.toWindowIdentifier()})", isOptional) {
438             it.containsNonAppWindow(componentMatcher)
439         }
440     }
441 
442     /** {@inheritDoc} */
443     override fun isHomeActivityInvisible(): WindowManagerTraceSubject =
444         isHomeActivityInvisible(isOptional = false)
445 
446     /** See [isHomeActivityInvisible] */
447     fun isHomeActivityInvisible(isOptional: Boolean): WindowManagerTraceSubject = apply {
448         addAssertion("isHomeActivityInvisible", isOptional) { it.isHomeActivityInvisible() }
449     }
450 
451     /** {@inheritDoc} */
452     override fun isHomeActivityVisible(): WindowManagerTraceSubject =
453         isHomeActivityVisible(isOptional = false)
454 
455     /** See [isHomeActivityVisible] */
456     fun isHomeActivityVisible(isOptional: Boolean): WindowManagerTraceSubject = apply {
457         addAssertion("isHomeActivityVisible", isOptional) { it.isHomeActivityVisible() }
458     }
459 
460     /** {@inheritDoc} */
461     override fun hasRotation(rotation: Rotation, displayId: Int): WindowManagerTraceSubject =
462         hasRotation(rotation, displayId, isOptional = false)
463 
464     /** See [hasRotation] */
465     fun hasRotation(
466         rotation: Rotation,
467         displayId: Int,
468         isOptional: Boolean,
469     ): WindowManagerTraceSubject = apply {
470         addAssertion("hasRotation($rotation, display=$displayId)", isOptional) {
471             it.hasRotation(rotation, displayId)
472         }
473     }
474 
475     /** {@inheritDoc} */
476     override fun isNotPinned(componentMatcher: IComponentMatcher): WindowManagerTraceSubject =
477         isNotPinned(componentMatcher, isOptional = false)
478 
479     /** See [isNotPinned] */
480     fun isNotPinned(
481         componentMatcher: IComponentMatcher,
482         isOptional: Boolean,
483     ): WindowManagerTraceSubject = apply {
484         addAssertion("isNotPinned(${componentMatcher.toWindowIdentifier()})", isOptional) {
485             it.isNotPinned(componentMatcher)
486         }
487     }
488 
489     /** {@inheritDoc} */
490     override fun isFocusedApp(app: String): WindowManagerTraceSubject =
491         isFocusedApp(app, isOptional = false)
492 
493     /** See [isFocusedApp] */
494     fun isFocusedApp(app: String, isOptional: Boolean): WindowManagerTraceSubject = apply {
495         addAssertion("isFocusedApp($app)", isOptional) { it.isFocusedApp(app) }
496     }
497 
498     /** {@inheritDoc} */
499     override fun isNotFocusedApp(app: String): WindowManagerTraceSubject =
500         isNotFocusedApp(app, isOptional = false)
501 
502     /** See [isNotFocusedApp] */
503     fun isNotFocusedApp(app: String, isOptional: Boolean): WindowManagerTraceSubject = apply {
504         addAssertion("isNotFocusedApp($app)", isOptional) { it.isNotFocusedApp(app) }
505     }
506 
507     /** {@inheritDoc} */
508     override fun isPinned(componentMatcher: IComponentMatcher): WindowManagerTraceSubject =
509         isPinned(componentMatcher, isOptional = false)
510 
511     /** See [isPinned] */
512     fun isPinned(
513         componentMatcher: IComponentMatcher,
514         isOptional: Boolean,
515     ): WindowManagerTraceSubject = apply {
516         addAssertion("isPinned(${componentMatcher.toWindowIdentifier()})", isOptional) {
517             it.isPinned(componentMatcher)
518         }
519     }
520 
521     /** {@inheritDoc} */
522     override fun isRecentsActivityInvisible(): WindowManagerTraceSubject =
523         isRecentsActivityInvisible(isOptional = false)
524 
525     /** See [isRecentsActivityInvisible] */
526     fun isRecentsActivityInvisible(isOptional: Boolean): WindowManagerTraceSubject = apply {
527         addAssertion("isRecentsActivityInvisible", isOptional) { it.isRecentsActivityInvisible() }
528     }
529 
530     /** {@inheritDoc} */
531     override fun isRecentsActivityVisible(): WindowManagerTraceSubject =
532         isRecentsActivityVisible(isOptional = false)
533 
534     /** See [isRecentsActivityVisible] */
535     fun isRecentsActivityVisible(isOptional: Boolean): WindowManagerTraceSubject = apply {
536         addAssertion("isRecentsActivityVisible", isOptional) { it.isRecentsActivityVisible() }
537     }
538 
539     override fun isValid(): WindowManagerTraceSubject = apply {
540         addAssertion("isValid") { it.isValid() }
541     }
542 
543     /** {@inheritDoc} */
544     override fun notContainsAppWindow(
545         componentMatcher: IComponentMatcher
546     ): WindowManagerTraceSubject = notContainsAppWindow(componentMatcher, isOptional = false)
547 
548     /** See [notContainsAppWindow] */
549     fun notContainsAppWindow(
550         componentMatcher: IComponentMatcher,
551         isOptional: Boolean,
552     ): WindowManagerTraceSubject = apply {
553         addAssertion("notContainsAppWindow(${componentMatcher.toWindowIdentifier()})", isOptional) {
554             it.notContainsAppWindow(componentMatcher)
555         }
556     }
557 
558     /** {@inheritDoc} */
559     override fun containsAtLeastOneDisplay(): WindowManagerTraceSubject = apply {
560         addAssertion("containAtLeastOneDisplay", isOptional = false) {
561             it.containsAtLeastOneDisplay()
562         }
563     }
564 
565     /** Checks that all visible layers are shown for more than one consecutive entry */
566     fun visibleWindowsShownMoreThanOneConsecutiveEntry(
567         ignoreWindows: List<ComponentNameMatcher> =
568             listOf(
569                 ComponentNameMatcher.SPLASH_SCREEN,
570                 ComponentNameMatcher.SNAPSHOT,
571                 ComponentNameMatcher.SECONDARY_HOME_HANDLE,
572                 ComponentNameMatcher.EDGE_BACK_GESTURE_HANDLER,
573             )
574     ): WindowManagerTraceSubject = apply {
575         visibleEntriesShownMoreThanOneConsecutiveTime { subject ->
576             subject.wmState.windowStates
577                 .filter { it.isVisible }
578                 .filter { window ->
579                     ignoreWindows.none { matcher -> matcher.windowMatchesAnyOf(window) }
580                 }
581                 .map { it.name }
582                 .toSet()
583         }
584     }
585 
586     /** Executes a custom [assertion] on the current subject */
587     operator fun invoke(
588         name: String,
589         isOptional: Boolean = false,
590         assertion: AssertionPredicate<WindowManagerStateSubject>,
591     ): WindowManagerTraceSubject = apply { addAssertion(name, isOptional, assertion) }
592 
593     /** Run the assertions for all trace entries within the specified time range */
594     fun forElapsedTimeRange(startTime: Long, endTime: Long) {
595         val subjectsInRange =
596             subjects.filter { it.wmState.timestamp.elapsedNanos in startTime..endTime }
597         assertionsChecker.test(subjectsInRange)
598     }
599 
600     /**
601      * User-defined entry point for the trace entry with [timestamp]
602      *
603      * @param timestamp of the entry
604      */
605     fun getEntryByElapsedTimestamp(timestamp: Long): WindowManagerStateSubject =
606         subjects.first { it.wmState.timestamp.elapsedNanos == timestamp }
607 }
608