Make flexiglass window visible when swiping on launcher and status bar

When a touch is going to be proxied from launcher to shade, the flexiglass
window is made visible so touches can slip onto it. Touches on the status
bar are piped into flexiglass through SceneInteractor. Both invocation
methods open the shade scene directly at the moment, awaiting various blockers.

Bug: 290682366
Test: new unit test
Test: manually verified MotionEvents being detected in flexiglass from swipes
on launcher and status bar
Test: manually verified no behavior is changed when the feature flag is disabled

Change-Id: I0feb4b5b38f3bf00ac846e9bba735dbeff145f3c
This commit is contained in:
Shawn Lee
2023-07-18 18:32:38 -07:00
parent cba02ebfd4
commit 02e7d83862
13 changed files with 215 additions and 20 deletions

View File

@@ -92,6 +92,14 @@ private fun Scene(
onSceneChanged: (SceneModel) -> Unit,
modifier: Modifier = Modifier,
) {
val destinationScenes: Map<UserAction, SceneModel> by
scene.destinationScenes(containerName).collectAsState()
val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
val backDestinationScene = destinationScenes[UserAction.Back]
// TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress.
Box(modifier) {
Column(
@@ -103,14 +111,6 @@ private fun Scene(
modifier = Modifier,
)
val destinationScenes: Map<UserAction, SceneModel> by
scene.destinationScenes(containerName).collectAsState()
val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
val backDestinationScene = destinationScenes[UserAction.Back]
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {

View File

@@ -84,6 +84,8 @@ import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -94,6 +96,8 @@ import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.buttons.KeyButtonView;
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeViewController;
@@ -120,6 +124,7 @@ import java.util.concurrent.Executor;
import java.util.function.Supplier;
import javax.inject.Inject;
import javax.inject.Provider;
/**
* Class to send information from overview to launcher with a binder.
@@ -139,6 +144,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
private final Context mContext;
private final FeatureFlags mFeatureFlags;
private final Executor mMainExecutor;
private final ShellInterface mShellInterface;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -147,6 +153,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private final Handler mHandler;
private final Lazy<NavigationBarController> mNavBarControllerLazy;
private final NotificationShadeWindowController mStatusBarWinController;
private final Provider<SceneInteractor> mSceneInteractor;
private final Runnable mConnectionRunnable = () ->
internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
private final ComponentName mRecentsComponentName;
@@ -209,17 +217,28 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mInputFocusTransferStarted = true;
mInputFocusTransferStartY = event.getY();
mInputFocusTransferStartMillis = event.getEventTime();
centralSurfaces.onInputFocusTransfer(
mInputFocusTransferStarted, false /* cancel */,
0 /* velocity */);
// If scene framework is enabled, set the scene container window to
// visible and let the touch "slip" into that window.
if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
mSceneInteractor.get().setVisible(
SceneContainerNames.SYSTEM_UI_DEFAULT, true);
} else {
centralSurfaces.onInputFocusTransfer(
mInputFocusTransferStarted, false /* cancel */,
0 /* velocity */);
}
}
if (action == ACTION_UP || action == ACTION_CANCEL) {
mInputFocusTransferStarted = false;
float velocity = (event.getY() - mInputFocusTransferStartY)
/ (event.getEventTime() - mInputFocusTransferStartMillis);
centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
action == ACTION_CANCEL,
velocity);
if (!mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
float velocity = (event.getY() - mInputFocusTransferStartY)
/ (event.getEventTime() - mInputFocusTransferStartMillis);
centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
action == ACTION_CANCEL,
velocity);
}
}
event.recycle();
});
@@ -552,6 +571,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
NavigationModeController navModeController,
NotificationShadeWindowController statusBarWinController,
SysUiState sysUiState,
Provider<SceneInteractor> sceneInteractor,
UserTracker userTracker,
ScreenLifecycle screenLifecycle,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -559,6 +579,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
DisplayTracker displayTracker,
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
AssistUtils assistUtils,
FeatureFlags featureFlags,
DumpManager dumpManager,
Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder
) {
@@ -568,6 +589,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
mContext = context;
mFeatureFlags = featureFlags;
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
@@ -575,6 +597,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mHandler = new Handler();
mNavBarControllerLazy = navBarControllerLazy;
mStatusBarWinController = statusBarWinController;
mSceneInteractor = sceneInteractor;
mUserTracker = userTracker;
mConnectionBackoffAttempts = 0;
mRecentsComponentName = ComponentName.unflattenFromString(context.getString(

View File

@@ -18,11 +18,14 @@ package com.android.systemui.scene.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.SceneTransitionModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Generic business logic and app state accessors for the scene framework.
@@ -92,4 +95,14 @@ constructor(
fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> {
return repository.sceneTransitions(containerName)
}
private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null)
/** A flow of motion events originating from outside of the scene framework. */
val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow()
/** Handles a remote user input. */
fun onRemoteUserInput(input: RemoteUserInput) {
_remoteUserInput.value = input
}
}

View File

@@ -0,0 +1,35 @@
package com.android.systemui.scene.shared.model
import android.view.MotionEvent
/** A representation of user input that is used by the scene framework. */
data class RemoteUserInput(
val x: Float,
val y: Float,
val action: RemoteUserInputAction,
) {
companion object {
fun translateMotionEvent(event: MotionEvent): RemoteUserInput {
return RemoteUserInput(
x = event.x,
y = event.y,
action =
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> RemoteUserInputAction.DOWN
MotionEvent.ACTION_MOVE -> RemoteUserInputAction.MOVE
MotionEvent.ACTION_UP -> RemoteUserInputAction.UP
MotionEvent.ACTION_CANCEL -> RemoteUserInputAction.CANCEL
else -> RemoteUserInputAction.UNKNOWN
}
)
}
}
}
enum class RemoteUserInputAction {
DOWN,
MOVE,
UP,
CANCEL,
UNKNOWN,
}

View File

@@ -2,6 +2,7 @@ package com.android.systemui.scene.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -16,11 +17,15 @@ class SceneWindowRootView(
context,
attrs,
) {
private lateinit var viewModel: SceneContainerViewModel
fun init(
viewModel: SceneContainerViewModel,
containerConfig: SceneContainerConfig,
scenes: Set<Scene>,
) {
this.viewModel = viewModel
SceneWindowRootViewBinder.bind(
view = this@SceneWindowRootView,
viewModel = viewModel,
@@ -32,6 +37,14 @@ class SceneWindowRootView(
)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return event?.let {
viewModel.onRemoteUserInput(event)
true
}
?: false
}
override fun setVisibility(visibility: Int) {
// Do nothing. We don't want external callers to invoke this. Instead, we drive our own
// visibility from our view-binder.

View File

@@ -16,7 +16,9 @@
package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import kotlinx.coroutines.flow.StateFlow
@@ -26,6 +28,9 @@ class SceneContainerViewModel(
private val interactor: SceneInteractor,
val containerName: String,
) {
/** A flow of motion events originating from outside of the scene framework. */
val remoteUserInput: StateFlow<RemoteUserInput?> = interactor.remoteUserInput
/**
* Keys of all scenes in the container.
*
@@ -49,4 +54,9 @@ class SceneContainerViewModel(
fun setSceneTransitionProgress(progress: Float) {
interactor.setSceneTransitionProgress(containerName, progress)
}
/** Handles a [MotionEvent] representing remote user input. */
fun onRemoteUserInput(event: MotionEvent) {
interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
}
}

View File

@@ -28,6 +28,9 @@ import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -43,6 +46,7 @@ import com.android.systemui.util.view.ViewUtil
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
private const val TAG = "PhoneStatusBarViewController"
@@ -53,10 +57,12 @@ class PhoneStatusBarViewController private constructor(
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
private val shadeViewController: ShadeViewController,
private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
private val userChipViewModel: StatusBarUserChipViewModel,
private val viewUtil: ViewUtil,
private val featureFlags: FeatureFlags,
private val configurationController: ConfigurationController
) : ViewController<PhoneStatusBarView>(view) {
@@ -164,6 +170,16 @@ class PhoneStatusBarViewController private constructor(
return false
}
// If scene framework is enabled, route the touch to it and
// ignore the rest of the gesture.
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
sceneInteractor.get()
.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
// TODO(b/291965119): remove once view is expanded to cover the status bar
sceneInteractor.get().setVisible(SceneContainerNames.SYSTEM_UI_DEFAULT, true)
return false
}
if (event.action == MotionEvent.ACTION_DOWN) {
// If the view that would receive the touch is disabled, just have status
// bar eat the gesture.
@@ -225,6 +241,7 @@ class PhoneStatusBarViewController private constructor(
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
private val shadeViewController: ShadeViewController,
private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController,
@@ -245,10 +262,12 @@ class PhoneStatusBarViewController private constructor(
centralSurfaces,
shadeController,
shadeViewController,
sceneInteractor,
shadeLogger,
statusBarMoveFromCenterAnimationController,
userChipViewModel,
viewUtil,
featureFlags,
configurationController
)
}

View File

@@ -34,15 +34,21 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.ScreenDecorations;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.util.kotlin.JavaAdapter;
import java.io.PrintWriter;
import javax.inject.Inject;
import javax.inject.Provider;
/**
* Manages what parts of the status bar are touchable. Clients are primarily UI that display in the
@@ -78,6 +84,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
ConfigurationController configurationController,
HeadsUpManagerPhone headsUpManager,
ShadeExpansionStateManager shadeExpansionStateManager,
Provider<SceneInteractor> sceneInteractor,
Provider<JavaAdapter> javaAdapter,
FeatureFlags featureFlags,
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
) {
mContext = context;
@@ -115,6 +124,12 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
javaAdapter.get().alwaysCollectFlow(
sceneInteractor.get().isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT),
this::onShadeExpansionFullyChanged);
}
mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
}

View File

@@ -29,6 +29,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -49,11 +50,11 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.sysui.ShellInterface
import com.google.common.util.concurrent.MoreExecutors
import dagger.Lazy
import java.util.Optional
import java.util.concurrent.Executor
import org.junit.After
@@ -82,6 +83,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker)
private val featureFlags = FakeFeatureFlags()
private val screenLifecycle = ScreenLifecycle(dumpManager)
private val wakefulnessLifecycle =
WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
@@ -132,12 +134,13 @@ class OverviewProxyServiceTest : SysuiTestCase() {
executor,
commandQueue,
shellInterface,
Lazy { navBarController },
Lazy { Optional.of(centralSurfaces) },
Lazy { shadeViewController },
{ navBarController },
{ Optional.of(centralSurfaces) },
{ shadeViewController },
navModeController,
statusBarWinController,
sysUiState,
mock(),
userTracker,
screenLifecycle,
wakefulnessLifecycle,
@@ -145,6 +148,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
displayTracker,
sysuiUnlockAnimationController,
assistUtils,
featureFlags,
dumpManager,
unfoldTransitionProgressForwarder
)

View File

@@ -100,4 +100,15 @@ class SceneInteractorTest : SysuiTestCase() {
)
)
}
@Test
fun remoteUserInput() = runTest {
val remoteUserInput by collectLastValue(underTest.remoteUserInput)
assertThat(remoteUserInput).isNull()
for (input in SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE) {
underTest.onRemoteUserInput(input)
assertThat(remoteUserInput).isEqualTo(input)
}
}
}

View File

@@ -18,14 +18,19 @@
package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,4 +73,35 @@ class SceneContainerViewModelTest : SysuiTestCase() {
underTest.setCurrentScene(SceneModel(SceneKey.Shade))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade))
}
@Test
fun onRemoteUserInput() = runTest {
val remoteUserInput by collectLastValue(underTest.remoteUserInput)
assertThat(remoteUserInput).isNull()
val inputs =
SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE.map { remoteUserInputToMotionEvent(it) }
inputs.forEachIndexed { index, input ->
underTest.onRemoteUserInput(input)
assertThat(remoteUserInput).isEqualTo(SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE[index])
}
}
private fun TestScope.remoteUserInputToMotionEvent(input: RemoteUserInput): MotionEvent {
return MotionEvent.obtain(
currentTime,
currentTime,
when (input.action) {
RemoteUserInputAction.DOWN -> MotionEvent.ACTION_DOWN
RemoteUserInputAction.MOVE -> MotionEvent.ACTION_MOVE
RemoteUserInputAction.UP -> MotionEvent.ACTION_UP
RemoteUserInputAction.CANCEL -> MotionEvent.ACTION_CANCEL
RemoteUserInputAction.UNKNOWN -> MotionEvent.ACTION_OUTSIDE
},
input.x,
input.y,
0
)
}
}

View File

@@ -27,6 +27,7 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.shade.ShadeControllerImpl
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -50,6 +51,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.util.Optional
import javax.inject.Provider
@SmallTest
class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@@ -73,6 +75,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Mock
private lateinit var shadeControllerImpl: ShadeControllerImpl
@Mock
private lateinit var sceneInteractor: Provider<SceneInteractor>
@Mock
private lateinit var shadeLogger: ShadeLogger
@Mock
private lateinit var viewUtil: ViewUtil
@@ -197,6 +201,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
centralSurfacesImpl,
shadeControllerImpl,
shadeViewController,
sceneInteractor,
shadeLogger,
viewUtil,
configurationController

View File

@@ -38,6 +38,8 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
@@ -217,5 +219,14 @@ class SceneTestUtils(
companion object {
const val CONTAINER_1 = SceneContainerNames.SYSTEM_UI_DEFAULT
const val CONTAINER_2 = "container2"
val REMOTE_INPUT_DOWN_GESTURE =
listOf(
RemoteUserInput(10f, 10f, RemoteUserInputAction.DOWN),
RemoteUserInput(10f, 20f, RemoteUserInputAction.MOVE),
RemoteUserInput(10f, 30f, RemoteUserInputAction.MOVE),
RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE),
RemoteUserInput(10f, 40f, RemoteUserInputAction.UP),
)
}
}