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 com.android.systemui.communal.domain.interactor
18 
19 import android.app.smartspace.SmartspaceTarget
20 import android.content.ComponentName
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.os.UserHandle
24 import android.os.UserManager
25 import android.provider.Settings
26 import com.android.compose.animation.scene.ObservableTransitionState
27 import com.android.compose.animation.scene.SceneKey
28 import com.android.compose.animation.scene.TransitionKey
29 import com.android.systemui.broadcast.BroadcastDispatcher
30 import com.android.systemui.communal.data.repository.CommunalMediaRepository
31 import com.android.systemui.communal.data.repository.CommunalPrefsRepository
32 import com.android.systemui.communal.data.repository.CommunalWidgetRepository
33 import com.android.systemui.communal.domain.model.CommunalContentModel
34 import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent
35 import com.android.systemui.communal.shared.model.CommunalContentSize
36 import com.android.systemui.communal.shared.model.CommunalContentSize.FULL
37 import com.android.systemui.communal.shared.model.CommunalContentSize.HALF
38 import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD
39 import com.android.systemui.communal.shared.model.CommunalScenes
40 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
41 import com.android.systemui.communal.shared.model.EditModeState
42 import com.android.systemui.communal.widgets.CommunalAppWidgetHost
43 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
44 import com.android.systemui.communal.widgets.WidgetConfigurator
45 import com.android.systemui.dagger.SysUISingleton
46 import com.android.systemui.dagger.qualifiers.Application
47 import com.android.systemui.dagger.qualifiers.Background
48 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
49 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
50 import com.android.systemui.keyguard.shared.model.Edge
51 import com.android.systemui.keyguard.shared.model.KeyguardState
52 import com.android.systemui.log.LogBuffer
53 import com.android.systemui.log.core.Logger
54 import com.android.systemui.log.dagger.CommunalLog
55 import com.android.systemui.log.dagger.CommunalTableLog
56 import com.android.systemui.log.table.TableLogBuffer
57 import com.android.systemui.log.table.logDiffsForTable
58 import com.android.systemui.plugins.ActivityStarter
59 import com.android.systemui.scene.domain.interactor.SceneInteractor
60 import com.android.systemui.scene.shared.flag.SceneContainerFlag
61 import com.android.systemui.scene.shared.model.Scenes
62 import com.android.systemui.settings.UserTracker
63 import com.android.systemui.smartspace.data.repository.SmartspaceRepository
64 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
65 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
66 import com.android.systemui.util.kotlin.emitOnStart
67 import javax.inject.Inject
68 import kotlinx.coroutines.CoroutineDispatcher
69 import kotlinx.coroutines.CoroutineScope
70 import kotlinx.coroutines.ExperimentalCoroutinesApi
71 import kotlinx.coroutines.channels.BufferOverflow
72 import kotlinx.coroutines.flow.Flow
73 import kotlinx.coroutines.flow.MutableSharedFlow
74 import kotlinx.coroutines.flow.MutableStateFlow
75 import kotlinx.coroutines.flow.SharingStarted
76 import kotlinx.coroutines.flow.StateFlow
77 import kotlinx.coroutines.flow.asSharedFlow
78 import kotlinx.coroutines.flow.asStateFlow
79 import kotlinx.coroutines.flow.combine
80 import kotlinx.coroutines.flow.distinctUntilChanged
81 import kotlinx.coroutines.flow.emptyFlow
82 import kotlinx.coroutines.flow.filter
83 import kotlinx.coroutines.flow.flatMapLatest
84 import kotlinx.coroutines.flow.flow
85 import kotlinx.coroutines.flow.flowOf
86 import kotlinx.coroutines.flow.flowOn
87 import kotlinx.coroutines.flow.map
88 import kotlinx.coroutines.flow.onEach
89 import kotlinx.coroutines.flow.shareIn
90 import kotlinx.coroutines.flow.stateIn
91 
92 /** Encapsulates business-logic related to communal mode. */
93 @OptIn(ExperimentalCoroutinesApi::class)
94 @SysUISingleton
95 class CommunalInteractor
96 @Inject
97 constructor(
98     @Application val applicationScope: CoroutineScope,
99     @Background val bgDispatcher: CoroutineDispatcher,
100     broadcastDispatcher: BroadcastDispatcher,
101     private val widgetRepository: CommunalWidgetRepository,
102     private val communalPrefsRepository: CommunalPrefsRepository,
103     private val mediaRepository: CommunalMediaRepository,
104     smartspaceRepository: SmartspaceRepository,
105     keyguardInteractor: KeyguardInteractor,
106     keyguardTransitionInteractor: KeyguardTransitionInteractor,
107     communalSettingsInteractor: CommunalSettingsInteractor,
108     private val appWidgetHost: CommunalAppWidgetHost,
109     private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
110     private val userTracker: UserTracker,
111     private val activityStarter: ActivityStarter,
112     private val userManager: UserManager,
113     private val communalSceneInteractor: CommunalSceneInteractor,
114     sceneInteractor: SceneInteractor,
115     @CommunalLog logBuffer: LogBuffer,
116     @CommunalTableLog tableLogBuffer: TableLogBuffer,
117 ) {
118     private val logger = Logger(logBuffer, "CommunalInteractor")
119 
120     private val _editModeOpen = MutableStateFlow(false)
121 
122     /** Whether edit mode is currently open. */
123     val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow()
124 
125     /** Whether communal features are enabled. */
126     val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled
127 
128     /** Whether communal features are enabled and available. */
129     val isCommunalAvailable: Flow<Boolean> =
130         allOf(
131                 communalSettingsInteractor.isCommunalEnabled,
132                 not(keyguardInteractor.isEncryptedOrLockdown),
133                 keyguardInteractor.isKeyguardShowing
134             )
135             .distinctUntilChanged()
136             .onEach { available ->
137                 logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) {
138                     bool1 = available
139                 }
140             }
141             .logDiffsForTable(
142                 tableLogBuffer = tableLogBuffer,
143                 columnPrefix = "",
144                 columnName = "isCommunalAvailable",
145                 initialValue = false,
146             )
147             .shareIn(
148                 scope = applicationScope,
149                 started = SharingStarted.WhileSubscribed(),
150                 replay = 1,
151             )
152 
153     /** Whether to show communal when exiting the occluded state. */
154     val showCommunalFromOccluded: Flow<Boolean> =
155         keyguardTransitionInteractor.startedKeyguardTransitionStep
156             .filter { step -> step.to == KeyguardState.OCCLUDED }
157             .combine(isCommunalAvailable, ::Pair)
158             .map { (step, available) -> available && step.from == KeyguardState.GLANCEABLE_HUB }
159             .flowOn(bgDispatcher)
160             .stateIn(
161                 scope = applicationScope,
162                 started = SharingStarted.WhileSubscribed(),
163                 initialValue = false,
164             )
165 
166     /** Whether to start dreaming when returning from occluded */
167     val dreamFromOccluded: Flow<Boolean> =
168         keyguardTransitionInteractor
169             .transition(Edge.create(to = KeyguardState.OCCLUDED))
170             .map { it.from == KeyguardState.DREAMING }
171             .stateIn(scope = applicationScope, SharingStarted.Eagerly, false)
172 
173     /**
174      * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
175      *
176      * If [isCommunalAvailable] is false, will return [CommunalScenes.Blank]
177      */
178     @Deprecated(
179         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
180     )
181     val desiredScene: Flow<SceneKey> = communalSceneInteractor.currentScene
182 
183     /** Transition state of the hub mode. */
184     @Deprecated(
185         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
186     )
187     val transitionState: StateFlow<ObservableTransitionState> =
188         communalSceneInteractor.transitionState
189 
190     private val _userActivity: MutableSharedFlow<Unit> =
191         MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
192     val userActivity: Flow<Unit> = _userActivity.asSharedFlow()
193 
194     fun signalUserInteraction() {
195         _userActivity.tryEmit(Unit)
196     }
197 
198     /**
199      * Repopulates the communal widgets database by first reading a backed-up state from disk and
200      * updating the widget ids indicated by [oldToNewWidgetIdMap]. The backed-up state is removed
201      * from disk afterwards.
202      */
203     fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {
204         widgetRepository.restoreWidgets(oldToNewWidgetIdMap)
205     }
206 
207     /**
208      * Aborts the task of restoring widgets from a backup. The backed up state stored on disk is
209      * removed.
210      */
211     fun abortRestoreWidgets() {
212         widgetRepository.abortRestoreWidgets()
213     }
214 
215     /**
216      * Updates the transition state of the hub [SceneTransitionLayout].
217      *
218      * Note that you must call is with `null` when the UI is done or risk a memory leak.
219      */
220     @Deprecated(
221         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
222     )
223     fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) =
224         communalSceneInteractor.setTransitionState(transitionState)
225 
226     /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */
227     @Deprecated(
228         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
229     )
230     fun transitionProgressToScene(targetScene: SceneKey) =
231         communalSceneInteractor.transitionProgressToScene(targetScene)
232 
233     /**
234      * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is
235      * the [CommunalScenes.Communal].
236      *
237      * This will be true as soon as the desired scene is set programmatically or at whatever point
238      * during a fling that SceneTransitionLayout determines that the end state will be the communal
239      * scene. The value also does not change while flinging away until the target scene is no longer
240      * communal.
241      *
242      * If you need a flow that is only true when communal is fully showing and not in transition,
243      * use [isIdleOnCommunal].
244      */
245     // TODO(b/323215860): rename to something more appropriate after cleaning up usages
246     val isCommunalShowing: Flow<Boolean> =
247         flow { emit(SceneContainerFlag.isEnabled) }
248             .flatMapLatest { sceneContainerEnabled ->
249                 if (sceneContainerEnabled) {
250                     sceneInteractor.currentScene.map { it == Scenes.Communal }
251                 } else {
252                     desiredScene.map { it == CommunalScenes.Communal }
253                 }
254             }
255             .distinctUntilChanged()
256             .onEach { showing ->
257                 logger.i({ "Communal is ${if (bool1) "showing" else "gone"}" }) { bool1 = showing }
258             }
259             .logDiffsForTable(
260                 tableLogBuffer = tableLogBuffer,
261                 columnPrefix = "",
262                 columnName = "isCommunalShowing",
263                 initialValue = false,
264             )
265             .shareIn(
266                 scope = applicationScope,
267                 started = SharingStarted.WhileSubscribed(),
268                 replay = 1,
269             )
270 
271     /**
272      * Flow that emits a boolean if the communal UI is fully visible and not in transition.
273      *
274      * This will not be true while transitioning to the hub and will turn false immediately when a
275      * swipe to exit the hub starts.
276      */
277     @Deprecated(
278         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
279     )
280     val isIdleOnCommunal: StateFlow<Boolean> = communalSceneInteractor.isIdleOnCommunal
281 
282     /**
283      * Flow that emits a boolean if any portion of the communal UI is visible at all.
284      *
285      * This flow will be true during any transition and when idle on the communal scene.
286      */
287     @Deprecated(
288         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
289     )
290     val isCommunalVisible: Flow<Boolean> = communalSceneInteractor.isCommunalVisible
291 
292     /**
293      * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
294      * installed transition or the one specified by [transitionKey], if provided.
295      */
296     @Deprecated(
297         "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
298     )
299     fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) =
300         communalSceneInteractor.changeScene(newScene, transitionKey)
301 
302     fun setEditModeOpen(isOpen: Boolean) {
303         _editModeOpen.value = isOpen
304     }
305 
306     /** Show the widget editor Activity. */
307     fun showWidgetEditor(
308         preselectedKey: String? = null,
309         shouldOpenWidgetPickerOnStart: Boolean = false,
310     ) {
311         communalSceneInteractor.setEditModeState(EditModeState.STARTING)
312         editWidgetsActivityStarter.startActivity(preselectedKey, shouldOpenWidgetPickerOnStart)
313     }
314 
315     /**
316      * Navigates to communal widget setting after user has unlocked the device. Currently, this
317      * setting resides within the Hub Mode settings screen.
318      */
319     fun navigateToCommunalWidgetSettings() {
320         activityStarter.postStartActivityDismissingKeyguard(
321             Intent(Settings.ACTION_COMMUNAL_SETTING)
322                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP),
323             /* delay= */ 0,
324         )
325     }
326 
327     /** Dismiss the CTA tile from the hub in view mode. */
328     suspend fun dismissCtaTile() = communalPrefsRepository.setCtaDismissedForCurrentUser()
329 
330     /** Add a widget at the specified position. */
331     fun addWidget(
332         componentName: ComponentName,
333         user: UserHandle,
334         priority: Int,
335         configurator: WidgetConfigurator?,
336     ) = widgetRepository.addWidget(componentName, user, priority, configurator)
337 
338     /**
339      * Delete a widget by id. Called when user deletes a widget from the hub or a widget is
340      * uninstalled from App widget host.
341      */
342     fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
343 
344     /**
345      * Reorder the widgets.
346      *
347      * @param widgetIdToPriorityMap mapping of the widget ids to their new priorities.
348      */
349     fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) =
350         widgetRepository.updateWidgetOrder(widgetIdToPriorityMap)
351 
352     /** Request to unpause work profile that is currently in quiet mode. */
353     fun unpauseWorkProfile() {
354         userTracker.userProfiles
355             .find { it.isManagedProfile }
356             ?.userHandle
357             ?.let { userHandle ->
358                 userManager.requestQuietModeEnabled(/* enableQuietMode */ false, userHandle)
359             }
360     }
361 
362     /** Returns true if work profile is in quiet mode (disabled) for user handle. */
363     private fun isQuietModeEnabled(userHandle: UserHandle): Boolean =
364         userManager.isManagedProfile(userHandle.identifier) &&
365             userManager.isQuietModeEnabled(userHandle)
366 
367     /** Emits whenever a work profile pause or unpause broadcast is received. */
368     private val updateOnWorkProfileBroadcastReceived: Flow<Unit> =
369         broadcastDispatcher
370             .broadcastFlow(
371                 filter =
372                     IntentFilter().apply {
373                         addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
374                         addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
375                     },
376             )
377             .emitOnStart()
378 
379     /** All widgets present in db. */
380     val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
381         isCommunalAvailable.flatMapLatest { available ->
382             if (!available) emptyFlow() else widgetRepository.communalWidgets
383         }
384 
385     /** A list of widget content to be displayed in the communal hub. */
386     val widgetContent: Flow<List<WidgetContent>> =
387         combine(
388             widgetRepository.communalWidgets
389                 .map { filterWidgetsByExistingUsers(it) }
390                 .combine(communalSettingsInteractor.allowedByDevicePolicyForWorkProfile) {
391                     // exclude widgets under work profile if not allowed by device policy
392                     widgets,
393                     allowedForWorkProfile ->
394                     filterWidgetsAllowedByDevicePolicy(widgets, allowedForWorkProfile)
395                 },
396             communalSettingsInteractor.communalWidgetCategories,
397             updateOnWorkProfileBroadcastReceived,
398         ) { widgets, allowedCategories, _ ->
399             widgets.map { widget ->
400                 when (widget) {
401                     is CommunalWidgetContentModel.Available -> {
402                         if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
403                             // At least one category this widget specified is allowed, so show it
404                             WidgetContent.Widget(
405                                 appWidgetId = widget.appWidgetId,
406                                 providerInfo = widget.providerInfo,
407                                 appWidgetHost = appWidgetHost,
408                                 inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
409                             )
410                         } else {
411                             WidgetContent.DisabledWidget(
412                                 appWidgetId = widget.appWidgetId,
413                                 providerInfo = widget.providerInfo,
414                             )
415                         }
416                     }
417                     is CommunalWidgetContentModel.Pending -> {
418                         WidgetContent.PendingWidget(
419                             appWidgetId = widget.appWidgetId,
420                             packageName = widget.packageName,
421                             icon = widget.icon,
422                         )
423                     }
424                 }
425             }
426         }
427 
428     /** Filter widgets based on whether their associated profile is allowed by device policy. */
429     private fun filterWidgetsAllowedByDevicePolicy(
430         list: List<CommunalWidgetContentModel>,
431         allowedByDevicePolicyForWorkProfile: Boolean
432     ): List<CommunalWidgetContentModel> =
433         if (allowedByDevicePolicyForWorkProfile) {
434             list
435         } else {
436             // Get associated work profile for the currently selected user.
437             val workProfile = userTracker.userProfiles.find { it.isManagedProfile }
438             list.filter { model ->
439                 val uid =
440                     when (model) {
441                         is CommunalWidgetContentModel.Available ->
442                             model.providerInfo.profile.identifier
443                         is CommunalWidgetContentModel.Pending -> model.user.identifier
444                     }
445                 uid != workProfile?.id
446             }
447         }
448 
449     /** A flow of available smartspace targets. Currently only showing timers. */
450     private val smartspaceTargets: Flow<List<SmartspaceTarget>> =
451         if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) {
452             flowOf(emptyList())
453         } else {
454             smartspaceRepository.communalSmartspaceTargets.map { targets ->
455                 targets.filter { target ->
456                     target.featureType == SmartspaceTarget.FEATURE_TIMER &&
457                         target.remoteViews != null
458                 }
459             }
460         }
461 
462     /** CTA tile to be displayed in the glanceable hub (view mode). */
463     val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> =
464         communalPrefsRepository.isCtaDismissed.map { isDismissed ->
465             if (isDismissed) emptyList() else listOf(CommunalContentModel.CtaTileInViewMode())
466         }
467 
468     /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */
469     val tutorialContent: List<CommunalContentModel.Tutorial> =
470         listOf(
471             CommunalContentModel.Tutorial(id = 0, FULL),
472             CommunalContentModel.Tutorial(id = 1, THIRD),
473             CommunalContentModel.Tutorial(id = 2, THIRD),
474             CommunalContentModel.Tutorial(id = 3, THIRD),
475             CommunalContentModel.Tutorial(id = 4, HALF),
476             CommunalContentModel.Tutorial(id = 5, HALF),
477             CommunalContentModel.Tutorial(id = 6, HALF),
478             CommunalContentModel.Tutorial(id = 7, HALF),
479         )
480 
481     /**
482      * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and
483      * sized dynamically.
484      */
485     fun getOngoingContent(mediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> =
486         combine(smartspaceTargets, mediaRepository.mediaModel) { smartspace, media ->
487                 val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>()
488 
489                 // Add smartspace
490                 ongoingContent.addAll(
491                     smartspace.map { target ->
492                         CommunalContentModel.Smartspace(
493                             smartspaceTargetId = target.smartspaceTargetId,
494                             remoteViews = target.remoteViews!!,
495                             createdTimestampMillis = target.creationTimeMillis,
496                         )
497                     }
498                 )
499 
500                 // Add UMO
501                 if (mediaHostVisible && media.hasActiveMediaOrRecommendation) {
502                     ongoingContent.add(
503                         CommunalContentModel.Umo(
504                             createdTimestampMillis = media.createdTimestampMillis,
505                         )
506                     )
507                 }
508 
509                 // Order by creation time descending
510                 ongoingContent.sortByDescending { it.createdTimestampMillis }
511 
512                 // Dynamic sizing
513                 ongoingContent.forEachIndexed { index, model ->
514                     model.size = dynamicContentSize(ongoingContent.size, index)
515                 }
516 
517                 ongoingContent
518             }
519             .flowOn(bgDispatcher)
520 
521     /**
522      * Filter and retain widgets associated with an existing user, safeguarding against displaying
523      * stale data following user deletion.
524      */
525     private fun filterWidgetsByExistingUsers(
526         list: List<CommunalWidgetContentModel>,
527     ): List<CommunalWidgetContentModel> {
528         val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
529         return list.filter { widget ->
530             when (widget) {
531                 is CommunalWidgetContentModel.Available ->
532                     currentUserIds.contains(widget.providerInfo.profile?.identifier)
533                 is CommunalWidgetContentModel.Pending -> true
534             }
535         }
536     }
537 
538     companion object {
539         /**
540          * The user activity timeout which should be used when the communal hub is opened. A value
541          * of -1 means that the user's chosen screen timeout will be used instead.
542          */
543         const val AWAKE_INTERVAL_MS = -1
544 
545         /**
546          * Calculates the content size dynamically based on the total number of contents of that
547          * type.
548          *
549          * Contents with the same type are expected to fill each column evenly. Currently there are
550          * three possible sizes. When the total number is 1, size for that content is [FULL], when
551          * the total number is 2, size for each is [HALF], and 3, size for each is [THIRD].
552          *
553          * When dynamic contents fill in multiple columns, the first column follows the algorithm
554          * above, and the remaining contents are packed in [THIRD]s. For example, when the total
555          * number if 4, the first one is [FULL], filling the column, and the remaining 3 are
556          * [THIRD].
557          *
558          * @param size The total number of contents of this type.
559          * @param index The index of the current content of this type.
560          */
561         private fun dynamicContentSize(size: Int, index: Int): CommunalContentSize {
562             val remainder = size % CommunalContentSize.entries.size
563             return CommunalContentSize.toSize(
564                 span =
565                     FULL.span /
566                         if (index > remainder - 1) CommunalContentSize.entries.size else remainder
567             )
568         }
569     }
570 }
571