Loading packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt +19 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,12 @@ package com.android.systemui.util import android.os.Trace import android.os.TraceNameSupplier import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.async /** * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection] Loading Loading @@ -85,5 +91,18 @@ class TraceUtils { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, trackName, cookie) } } /** * Convenience method to avoid one indentation level when we want to add a trace when * launching a coroutine */ fun <T> CoroutineScope.tracedAsync( method: String, context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend () -> T ): Deferred<T> { return async(context, start) { traceAsync(method) { block() } } } } } packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +19 −57 Original line number Diff line number Diff line Loading @@ -53,9 +53,6 @@ import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.media.AudioAttributes; import android.media.AudioSystem; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import android.os.Process; Loading Loading @@ -86,8 +83,6 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.WindowContext; import androidx.concurrent.futures.CallbackToFutureAdapter; import com.android.internal.app.ChooserActivity; import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; Loading @@ -108,7 +103,6 @@ import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; import java.io.File; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; Loading @@ -116,11 +110,11 @@ import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Supplier; import javax.inject.Provider; /** * Controls the state and flow for screenshots. Loading Loading @@ -274,7 +268,8 @@ public class ScreenshotController { private final WindowManager mWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; private final AccessibilityManager mAccessibilityManager; private final ListenableFuture<MediaPlayer> mCameraSound; @Nullable private final ScreenshotSoundController mScreenshotSoundController; private final ScrollCaptureClient mScrollCaptureClient; private final PhoneWindow mWindow; private final DisplayManager mDisplayManager; Loading Loading @@ -339,6 +334,7 @@ public class ScreenshotController { UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, @Assisted int displayId ) { mScreenshotSmartActions = screenshotSmartActions; Loading Loading @@ -387,8 +383,12 @@ public class ScreenshotController { mConfigChanges.applyNewConfig(context.getResources()); reloadAssets(); // Setup the Camera shutter sound mCameraSound = loadCameraSound(); // Sound is only reproduced from the controller of the default display. if (displayId == Display.DEFAULT_DISPLAY) { mScreenshotSoundController = screenshotSoundController.get(); } else { mScreenshotSoundController = null; } mCopyBroadcastReceiver = new BroadcastReceiver() { @Override Loading Loading @@ -573,17 +573,8 @@ public class ScreenshotController { } private void releaseMediaPlayer() { // Note that this may block if the sound is still being loaded (very unlikely) but we can't // reliably release in the background because the service is being destroyed. try { MediaPlayer player = mCameraSound.get(1, TimeUnit.SECONDS); if (player != null) { player.release(); } } catch (InterruptedException | ExecutionException | TimeoutException e) { mCameraSound.cancel(true); Log.w(TAG, "Error releasing shutter sound", e); } if (mScreenshotSoundController == null) return; mScreenshotSoundController.releaseScreenshotSound(); } private void respondToKeyDismissal() { Loading Loading @@ -889,39 +880,10 @@ public class ScreenshotController { } } private ListenableFuture<MediaPlayer> loadCameraSound() { // The media player creation is slow and needs on the background thread. return CallbackToFutureAdapter.getFuture((completer) -> { mBgExecutor.execute(() -> { try { MediaPlayer player = MediaPlayer.create(mContext, Uri.fromFile(new File(mContext.getResources().getString( com.android.internal.R.string.config_cameraShutterSound))), null, new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(), AudioSystem.newAudioSessionId()); completer.set(player); } catch (IllegalStateException e) { Log.w(TAG, "Screenshot sound initialization failed", e); completer.set(null); } }); return "ScreenshotController#loadCameraSound"; }); } private void playCameraSound() { mCameraSound.addListener(() -> { try { MediaPlayer player = mCameraSound.get(); if (player != null) { player.start(); } } catch (InterruptedException | ExecutionException e) { } }, mBgExecutor); private void playCameraSoundIfNeeded() { if (mScreenshotSoundController == null) return; // the controller is not-null only on the default display controller mScreenshotSoundController.playCameraSound(); } /** Loading @@ -930,7 +892,7 @@ public class ScreenshotController { */ private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); playCameraSoundIfNeeded(); saveScreenshotInWorkerThread( owner, Loading Loading @@ -974,7 +936,7 @@ public class ScreenshotController { } // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); playCameraSoundIfNeeded(); if (DEBUG_ANIM) { Log.d(TAG, "starting post-screenshot animation"); Loading packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt 0 → 100644 +79 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot import android.media.MediaPlayer import android.util.Log import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.TraceUtils.Companion.tracedAsync import com.google.errorprone.annotations.CanIgnoreReturnValue import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout /** Controls sound reproduction after a screenshot is taken. */ interface ScreenshotSoundController { /** Reproduces the camera sound. */ @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit> /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */ @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit> } class ScreenshotSoundControllerImpl @Inject constructor( private val soundProvider: ScreenshotSoundProvider, @Application private val coroutineScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher ) : ScreenshotSoundController { val player: Deferred<MediaPlayer?> = coroutineScope.tracedAsync("loadCameraSound", bgDispatcher) { try { soundProvider.getScreenshotSound() } catch (e: IllegalStateException) { Log.w(TAG, "Screenshot sound initialization failed", e) null } } override fun playCameraSound(): Deferred<Unit> { return coroutineScope.tracedAsync("playCameraSound", bgDispatcher) { player.await()?.start() } } override fun releaseScreenshotSound(): Deferred<Unit> { return coroutineScope.tracedAsync("releaseScreenshotSound", bgDispatcher) { try { withTimeout(1.seconds) { player.await()?.release() } } catch (e: TimeoutCancellationException) { player.cancel() Log.w(TAG, "Error releasing shutter sound", e) } } } private companion object { const val TAG = "ScreenshotSoundControllerImpl" } } packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundProvider.kt 0 → 100644 +57 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot import android.content.Context import android.media.AudioAttributes import android.media.AudioSystem import android.media.MediaPlayer import android.net.Uri import com.android.internal.R import com.android.systemui.dagger.SysUISingleton import java.io.File import javax.inject.Inject /** Provides a [MediaPlayer] that reproduces the screenshot sound. */ interface ScreenshotSoundProvider { /** * Creates a new [MediaPlayer] that reproduces the screenshot sound. This should be called from * a background thread, as it might take time. */ fun getScreenshotSound(): MediaPlayer } @SysUISingleton class ScreenshotSoundProviderImpl @Inject constructor( private val context: Context, ) : ScreenshotSoundProvider { override fun getScreenshotSound(): MediaPlayer { return MediaPlayer.create( context, Uri.fromFile(File(context.resources.getString(R.string.config_cameraShutterSound))), /* holder = */ null, AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(), AudioSystem.newAudioSessionId() ) } } packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +12 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,10 @@ import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; import com.android.systemui.screenshot.ScreenshotSoundProviderImpl; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; Loading Loading @@ -69,4 +73,12 @@ public abstract class ScreenshotModule { @Binds abstract ScreenshotRequestProcessor bindScreenshotRequestProcessor( RequestProcessor requestProcessor); @Binds abstract ScreenshotSoundProvider bindScreenshotSoundProvider( ScreenshotSoundProviderImpl screenshotSoundProviderImpl); @Binds abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); } Loading
packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt +19 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,12 @@ package com.android.systemui.util import android.os.Trace import android.os.TraceNameSupplier import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.async /** * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection] Loading Loading @@ -85,5 +91,18 @@ class TraceUtils { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, trackName, cookie) } } /** * Convenience method to avoid one indentation level when we want to add a trace when * launching a coroutine */ fun <T> CoroutineScope.tracedAsync( method: String, context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend () -> T ): Deferred<T> { return async(context, start) { traceAsync(method) { block() } } } } }
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +19 −57 Original line number Diff line number Diff line Loading @@ -53,9 +53,6 @@ import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.media.AudioAttributes; import android.media.AudioSystem; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import android.os.Process; Loading Loading @@ -86,8 +83,6 @@ import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.WindowContext; import androidx.concurrent.futures.CallbackToFutureAdapter; import com.android.internal.app.ChooserActivity; import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; Loading @@ -108,7 +103,6 @@ import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; import java.io.File; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; Loading @@ -116,11 +110,11 @@ import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Supplier; import javax.inject.Provider; /** * Controls the state and flow for screenshots. Loading Loading @@ -274,7 +268,8 @@ public class ScreenshotController { private final WindowManager mWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; private final AccessibilityManager mAccessibilityManager; private final ListenableFuture<MediaPlayer> mCameraSound; @Nullable private final ScreenshotSoundController mScreenshotSoundController; private final ScrollCaptureClient mScrollCaptureClient; private final PhoneWindow mWindow; private final DisplayManager mDisplayManager; Loading Loading @@ -339,6 +334,7 @@ public class ScreenshotController { UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, @Assisted int displayId ) { mScreenshotSmartActions = screenshotSmartActions; Loading Loading @@ -387,8 +383,12 @@ public class ScreenshotController { mConfigChanges.applyNewConfig(context.getResources()); reloadAssets(); // Setup the Camera shutter sound mCameraSound = loadCameraSound(); // Sound is only reproduced from the controller of the default display. if (displayId == Display.DEFAULT_DISPLAY) { mScreenshotSoundController = screenshotSoundController.get(); } else { mScreenshotSoundController = null; } mCopyBroadcastReceiver = new BroadcastReceiver() { @Override Loading Loading @@ -573,17 +573,8 @@ public class ScreenshotController { } private void releaseMediaPlayer() { // Note that this may block if the sound is still being loaded (very unlikely) but we can't // reliably release in the background because the service is being destroyed. try { MediaPlayer player = mCameraSound.get(1, TimeUnit.SECONDS); if (player != null) { player.release(); } } catch (InterruptedException | ExecutionException | TimeoutException e) { mCameraSound.cancel(true); Log.w(TAG, "Error releasing shutter sound", e); } if (mScreenshotSoundController == null) return; mScreenshotSoundController.releaseScreenshotSound(); } private void respondToKeyDismissal() { Loading Loading @@ -889,39 +880,10 @@ public class ScreenshotController { } } private ListenableFuture<MediaPlayer> loadCameraSound() { // The media player creation is slow and needs on the background thread. return CallbackToFutureAdapter.getFuture((completer) -> { mBgExecutor.execute(() -> { try { MediaPlayer player = MediaPlayer.create(mContext, Uri.fromFile(new File(mContext.getResources().getString( com.android.internal.R.string.config_cameraShutterSound))), null, new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(), AudioSystem.newAudioSessionId()); completer.set(player); } catch (IllegalStateException e) { Log.w(TAG, "Screenshot sound initialization failed", e); completer.set(null); } }); return "ScreenshotController#loadCameraSound"; }); } private void playCameraSound() { mCameraSound.addListener(() -> { try { MediaPlayer player = mCameraSound.get(); if (player != null) { player.start(); } } catch (InterruptedException | ExecutionException e) { } }, mBgExecutor); private void playCameraSoundIfNeeded() { if (mScreenshotSoundController == null) return; // the controller is not-null only on the default display controller mScreenshotSoundController.playCameraSound(); } /** Loading @@ -930,7 +892,7 @@ public class ScreenshotController { */ private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); playCameraSoundIfNeeded(); saveScreenshotInWorkerThread( owner, Loading Loading @@ -974,7 +936,7 @@ public class ScreenshotController { } // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); playCameraSoundIfNeeded(); if (DEBUG_ANIM) { Log.d(TAG, "starting post-screenshot animation"); Loading
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt 0 → 100644 +79 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot import android.media.MediaPlayer import android.util.Log import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.TraceUtils.Companion.tracedAsync import com.google.errorprone.annotations.CanIgnoreReturnValue import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout /** Controls sound reproduction after a screenshot is taken. */ interface ScreenshotSoundController { /** Reproduces the camera sound. */ @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit> /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */ @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit> } class ScreenshotSoundControllerImpl @Inject constructor( private val soundProvider: ScreenshotSoundProvider, @Application private val coroutineScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher ) : ScreenshotSoundController { val player: Deferred<MediaPlayer?> = coroutineScope.tracedAsync("loadCameraSound", bgDispatcher) { try { soundProvider.getScreenshotSound() } catch (e: IllegalStateException) { Log.w(TAG, "Screenshot sound initialization failed", e) null } } override fun playCameraSound(): Deferred<Unit> { return coroutineScope.tracedAsync("playCameraSound", bgDispatcher) { player.await()?.start() } } override fun releaseScreenshotSound(): Deferred<Unit> { return coroutineScope.tracedAsync("releaseScreenshotSound", bgDispatcher) { try { withTimeout(1.seconds) { player.await()?.release() } } catch (e: TimeoutCancellationException) { player.cancel() Log.w(TAG, "Error releasing shutter sound", e) } } } private companion object { const val TAG = "ScreenshotSoundControllerImpl" } }
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundProvider.kt 0 → 100644 +57 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot import android.content.Context import android.media.AudioAttributes import android.media.AudioSystem import android.media.MediaPlayer import android.net.Uri import com.android.internal.R import com.android.systemui.dagger.SysUISingleton import java.io.File import javax.inject.Inject /** Provides a [MediaPlayer] that reproduces the screenshot sound. */ interface ScreenshotSoundProvider { /** * Creates a new [MediaPlayer] that reproduces the screenshot sound. This should be called from * a background thread, as it might take time. */ fun getScreenshotSound(): MediaPlayer } @SysUISingleton class ScreenshotSoundProviderImpl @Inject constructor( private val context: Context, ) : ScreenshotSoundProvider { override fun getScreenshotSound(): MediaPlayer { return MediaPlayer.create( context, Uri.fromFile(File(context.resources.getString(R.string.config_cameraShutterSound))), /* holder = */ null, AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(), AudioSystem.newAudioSessionId() ) } }
packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +12 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,10 @@ import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; import com.android.systemui.screenshot.ScreenshotSoundProviderImpl; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; Loading Loading @@ -69,4 +73,12 @@ public abstract class ScreenshotModule { @Binds abstract ScreenshotRequestProcessor bindScreenshotRequestProcessor( RequestProcessor requestProcessor); @Binds abstract ScreenshotSoundProvider bindScreenshotSoundProvider( ScreenshotSoundProviderImpl screenshotSoundProviderImpl); @Binds abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); }