diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 791b832775715..c6f03271f9316 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -255,6 +255,9 @@
+
+
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index 07985ab5a43c5..02ae1f8afcdf6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -27,6 +27,7 @@ import android.os.UserHandle;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.SystemUI;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -66,7 +67,8 @@ public class TvStatusBar extends SystemUI implements CommandQueue.Callbacks {
// If the system process isn't there we're doomed anyway.
}
- new AudioRecordingDisclosureBar(mContext).start();
+ // Creating AudioRecordingDisclosureBar and just letting it run
+ new AudioRecordingDisclosureBar(mContext);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioActivityObserver.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioActivityObserver.java
new file mode 100644
index 0000000000000..87b3956060f3b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioActivityObserver.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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.statusbar.tv.micdisclosure;
+
+import android.content.Context;
+
+import java.util.Set;
+
+/**
+ * A base class for implementing observers for different kinds of activities related to audio
+ * recording. These observers are to be initialized by {@link AudioRecordingDisclosureBar} and to
+ * report back to it.
+ */
+abstract class AudioActivityObserver {
+
+ interface OnAudioActivityStateChangeListener {
+ void onAudioActivityStateChange(boolean active, String packageName);
+ }
+
+ final Context mContext;
+
+ final OnAudioActivityStateChangeListener mListener;
+
+ AudioActivityObserver(Context context, OnAudioActivityStateChangeListener listener) {
+ mContext = context;
+ mListener = listener;
+ }
+
+ abstract Set getActivePackages();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/AudioRecordingDisclosureBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/statusbar/tv/AudioRecordingDisclosureBar.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
index e70e30a5ab573..914868301d728 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/AudioRecordingDisclosureBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.tv;
+package com.android.systemui.statusbar.tv.micdisclosure;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -25,7 +25,6 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.IntDef;
import android.annotation.UiThread;
-import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -40,6 +39,7 @@ import android.view.WindowManager;
import android.widget.TextView;
import com.android.systemui.R;
+import com.android.systemui.statusbar.tv.TvStatusBar;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -54,9 +54,10 @@ import java.util.Set;
*
* @see TvStatusBar
*/
-class AudioRecordingDisclosureBar {
- private static final String TAG = "AudioRecordingDisclosureBar";
- private static final boolean DEBUG = false;
+public class AudioRecordingDisclosureBar implements
+ AudioActivityObserver.OnAudioActivityStateChangeListener {
+ private static final String TAG = "AudioRecordingDisclosure";
+ static final boolean DEBUG = false;
// This title is used to test the microphone disclosure indicator in
// CtsSystemUiHostTestCases:TvMicrophoneCaptureIndicatorTest
@@ -98,10 +99,12 @@ class AudioRecordingDisclosureBar {
private TextView mTextView;
@State private int mState = STATE_NOT_SHOWN;
+
/**
- * Set of the applications that currently are conducting audio recording.
+ * Array of the observers that monitor different aspects of the system, such as AppOps and
+ * microphone foreground services
*/
- private final Set mActiveAudioRecordingPackages = new ArraySet<>();
+ private final AudioActivityObserver[] mAudioActivityObservers;
/**
* Set of applications that we've notified the user about since the indicator came up. Meaning
* that if an application is in this list then at some point since the indicator came up, it
@@ -119,29 +122,52 @@ class AudioRecordingDisclosureBar {
* one of the two aforementioned states.
*/
private final Queue mPendingNotificationPackages = new LinkedList<>();
+ /**
+ * Set of applications for which we make an exception and do not show the indicator. This gets
+ * populated once - in {@link #AudioRecordingDisclosureBar(Context)}.
+ */
+ private final Set mExemptPackages;
- AudioRecordingDisclosureBar(Context context) {
+ public AudioRecordingDisclosureBar(Context context) {
mContext = context;
- }
- void start() {
- // Register AppOpsManager callback
- final AppOpsManager appOpsManager = (AppOpsManager) mContext.getSystemService(
- Context.APP_OPS_SERVICE);
- appOpsManager.startWatchingActive(
- new String[]{AppOpsManager.OPSTR_RECORD_AUDIO},
- mContext.getMainExecutor(),
- new OnActiveRecordingListener());
+ mExemptPackages = new ArraySet<>(
+ Arrays.asList(mContext.getResources().getStringArray(
+ R.array.audio_recording_disclosure_exempt_apps)));
+
+ mAudioActivityObservers = new AudioActivityObserver[]{
+ new RecordAudioAppOpObserver(mContext, this),
+ new MicrophoneForegroundServicesObserver(mContext, this),
+ };
}
@UiThread
- private void onStartedRecording(String packageName) {
- if (!mActiveAudioRecordingPackages.add(packageName)) {
- // This app is already known to perform recording
+ @Override
+ public void onAudioActivityStateChange(boolean active, String packageName) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "onAudioActivityStateChange, packageName=" + packageName + ", active="
+ + active);
+ }
+
+ if (mExemptPackages.contains(packageName)) {
+ if (DEBUG) Log.d(TAG, " - exempt package: ignoring");
return;
}
+
+ if (active) {
+ showIndicatorForPackageIfNeeded(packageName);
+ } else {
+ hideIndicatorIfNeeded();
+ }
+ }
+
+ @UiThread
+ private void showIndicatorForPackageIfNeeded(String packageName) {
+ if (DEBUG) Log.d(TAG, "showIndicatorForPackageIfNeeded, packageName=" + packageName);
if (!mSessionNotifiedPackages.add(packageName)) {
// We've already notified user about this app, no need to do it again.
+ if (DEBUG) Log.d(TAG, " - already notified");
return;
}
@@ -167,23 +193,33 @@ class AudioRecordingDisclosureBar {
}
@UiThread
- private void onDoneRecording(String packageName) {
- if (!mActiveAudioRecordingPackages.remove(packageName)) {
- // Was not marked as an active recorder, do nothing
- return;
+ private void hideIndicatorIfNeeded() {
+ if (DEBUG) Log.d(TAG, "hideIndicatorIfNeeded");
+ // If not MINIMIZED, will check whether the indicator should be hidden when the indicator
+ // comes to the STATE_MINIMIZED eventually.
+ if (mState != STATE_MINIMIZED) return;
+
+ // If is in the STATE_MINIMIZED, but there are other active recorders - simply ignore.
+ for (int index = mAudioActivityObservers.length - 1; index >= 0; index--) {
+ for (String activePackage : mAudioActivityObservers[index].getActivePackages()) {
+ if (mExemptPackages.contains(activePackage)) continue;
+ if (DEBUG) Log.d(TAG, " - there are still ongoing activities");
+ return;
+ }
}
- // If not MINIMIZED, will check whether the indicator should be hidden when the indicator
- // comes to the STATE_MINIMIZED eventually. If is in the STATE_MINIMIZED, but there are
- // other active recorders - simply ignore.
- if (mState == STATE_MINIMIZED && mActiveAudioRecordingPackages.isEmpty()) {
- mSessionNotifiedPackages.clear();
- hide();
- }
+ // Clear the state and hide the indicator.
+ mSessionNotifiedPackages.clear();
+ hide();
}
@UiThread
private void show(String packageName) {
+ final String label = getApplicationLabel(packageName);
+ if (DEBUG) {
+ Log.d(TAG, "Showing indicator for " + packageName + " (" + label + ")...");
+ }
+
// Inflate the indicator view
mIndicatorView = LayoutInflater.from(mContext).inflate(
R.layout.tv_audio_recording_indicator,
@@ -196,7 +232,6 @@ class AudioRecordingDisclosureBar {
mBgRight = mIndicatorView.findViewById(R.id.bg_right);
// Set up the notification text
- final String label = getApplicationLabel(packageName);
mTextView.setText(mContext.getString(R.string.app_accessed_mic, label));
// Initially change the visibility to INVISIBLE, wait until and receives the size and
@@ -260,6 +295,9 @@ class AudioRecordingDisclosureBar {
@UiThread
private void expand(String packageName) {
final String label = getApplicationLabel(packageName);
+ if (DEBUG) {
+ Log.d(TAG, "Expanding for " + packageName + " (" + label + ")...");
+ }
mTextView.setText(mContext.getString(R.string.app_accessed_mic, label));
final AnimatorSet set = new AnimatorSet();
@@ -283,6 +321,7 @@ class AudioRecordingDisclosureBar {
@UiThread
private void minimize() {
+ if (DEBUG) Log.d(TAG, "Minimizing...");
final int targetOffset = mTextsContainers.getWidth();
final AnimatorSet set = new AnimatorSet();
set.playTogether(
@@ -305,6 +344,7 @@ class AudioRecordingDisclosureBar {
@UiThread
private void hide() {
+ if (DEBUG) Log.d(TAG, "Hiding...");
final int targetOffset =
mIndicatorView.getWidth() - (int) mIconTextsContainer.getTranslationX();
final AnimatorSet set = new AnimatorSet();
@@ -326,6 +366,7 @@ class AudioRecordingDisclosureBar {
@UiThread
private void onExpanded() {
+ if (DEBUG) Log.d(TAG, "Expanded");
mState = STATE_SHOWN;
mIndicatorView.postDelayed(this::minimize, MAXIMIZED_DURATION);
@@ -333,20 +374,21 @@ class AudioRecordingDisclosureBar {
@UiThread
private void onMinimized() {
+ if (DEBUG) Log.d(TAG, "Minimized");
mState = STATE_MINIMIZED;
if (!mPendingNotificationPackages.isEmpty()) {
// There is a new application that started recording, tell the user about it.
expand(mPendingNotificationPackages.poll());
- } else if (mActiveAudioRecordingPackages.isEmpty()) {
- // Nobody is recording anymore, clear state and remove the indicator.
- mSessionNotifiedPackages.clear();
- hide();
+ } else {
+ hideIndicatorIfNeeded();
}
}
@UiThread
private void onHidden() {
+ if (DEBUG) Log.d(TAG, "Hidden");
+
final WindowManager windowManager = (WindowManager) mContext.getSystemService(
Context.WINDOW_SERVICE);
windowManager.removeView(mIndicatorView);
@@ -392,35 +434,4 @@ class AudioRecordingDisclosureBar {
}
return pm.getApplicationLabel(appInfo).toString();
}
-
- private class OnActiveRecordingListener implements AppOpsManager.OnOpActiveChangedListener {
- private final Set mExemptApps;
-
- private OnActiveRecordingListener() {
- mExemptApps = new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(
- R.array.audio_recording_disclosure_exempt_apps)));
- }
-
- @Override
- public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
- if (DEBUG) {
- Log.d(TAG,
- "OP_RECORD_AUDIO active change, active=" + active + ", app="
- + packageName);
- }
-
- if (mExemptApps.contains(packageName)) {
- if (DEBUG) {
- Log.d(TAG, "\t- exempt app");
- }
- return;
- }
-
- if (active) {
- onStartedRecording(packageName);
- } else {
- onDoneRecording(packageName);
- }
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/MicrophoneForegroundServicesObserver.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/MicrophoneForegroundServicesObserver.java
new file mode 100644
index 0000000000000..1ede88a260207
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/MicrophoneForegroundServicesObserver.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 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.statusbar.tv.micdisclosure;
+
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+
+import static com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar.DEBUG;
+
+import android.annotation.UiThread;
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.app.IProcessObserver;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The purpose of these class is to detect packages that are running foreground services of type
+ * 'microphone' and to report back to {@link AudioRecordingDisclosureBar}.
+ */
+class MicrophoneForegroundServicesObserver extends AudioActivityObserver {
+ private static final String TAG = "MicrophoneForegroundServicesObserver";
+ private static final boolean ENABLED = true;
+
+ private final IActivityManager mActivityManager;
+ /**
+ * A dictionary that maps PIDs to the package names. We only keep track of the PIDs that are
+ * "active" (those that are running FGS with FOREGROUND_SERVICE_TYPE_MICROPHONE flag).
+ */
+ private final SparseArray mPidToPackages = new SparseArray<>();
+ /**
+ * A dictionary that maps "active" packages to the number of the "active" processes associated
+ * with those packages. We really only need this in case when one application is running in
+ * multiple processes, so that we don't lose track of the package when one of its "active"
+ * processes ceases, while others remain "active".
+ */
+ private final Map mPackageToProcessCount = new ArrayMap<>();
+
+ MicrophoneForegroundServicesObserver(Context context,
+ OnAudioActivityStateChangeListener listener) {
+ super(context, listener);
+
+ mActivityManager = ActivityManager.getService();
+ try {
+ mActivityManager.registerProcessObserver(mProcessObserver);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't register process observer", e);
+ }
+ }
+
+ @Override
+ Set getActivePackages() {
+ return ENABLED ? mPackageToProcessCount.keySet() : Collections.emptySet();
+ }
+
+ @UiThread
+ private void onProcessForegroundServicesChanged(int pid, boolean hasMicFgs) {
+ final String[] changedPackages;
+ if (hasMicFgs) {
+ if (mPidToPackages.contains(pid)) {
+ // We are already tracking this pid - ignore.
+ changedPackages = null;
+ } else {
+ changedPackages = getPackageNames(pid);
+ mPidToPackages.append(pid, changedPackages);
+ }
+ } else {
+ changedPackages = mPidToPackages.removeReturnOld(pid);
+ }
+
+ if (changedPackages == null) {
+ return;
+ }
+
+ for (int index = changedPackages.length - 1; index >= 0; index--) {
+ final String packageName = changedPackages[index];
+ int processCount = mPackageToProcessCount.getOrDefault(packageName, 0);
+ final boolean shouldNotify;
+ if (hasMicFgs) {
+ processCount++;
+ shouldNotify = processCount == 1;
+ } else {
+ processCount--;
+ shouldNotify = processCount == 0;
+ }
+ if (processCount > 0) {
+ mPackageToProcessCount.put(packageName, processCount);
+ } else {
+ mPackageToProcessCount.remove(packageName);
+ }
+ if (shouldNotify) notifyPackageStateChanged(packageName, hasMicFgs);
+ }
+ }
+
+ @UiThread
+ private void onProcessDied(int pid) {
+ final String[] packages = mPidToPackages.removeReturnOld(pid);
+ if (packages == null) {
+ // This PID was not active - ignore.
+ return;
+ }
+
+ for (int index = packages.length - 1; index >= 0; index--) {
+ final String packageName = packages[index];
+ int processCount = mPackageToProcessCount.getOrDefault(packageName, 0);
+ if (processCount <= 0) {
+ Log.e(TAG, "Bookkeeping error, process count for " + packageName + " is "
+ + processCount);
+ continue;
+ }
+ processCount--;
+ if (processCount > 0) {
+ mPackageToProcessCount.put(packageName, processCount);
+ } else {
+ mPackageToProcessCount.remove(packageName);
+ notifyPackageStateChanged(packageName, false);
+ }
+ }
+ }
+
+ @UiThread
+ private void notifyPackageStateChanged(String packageName, boolean active) {
+ if (active) {
+ if (DEBUG) Log.d(TAG, "New microphone fgs detected, package=" + packageName);
+ } else {
+ if (DEBUG) Log.d(TAG, "Microphone fgs is gone, package=" + packageName);
+ }
+
+ if (ENABLED) mListener.onAudioActivityStateChange(active, packageName);
+ }
+
+ @UiThread
+ private String[] getPackageNames(int pid) {
+ final List runningApps;
+ try {
+ runningApps = mActivityManager.getRunningAppProcesses();
+ } catch (RemoteException e) {
+ Log.d(TAG, "Couldn't get package name for pid=" + pid);
+ return null;
+ }
+ if (runningApps == null) {
+ Log.wtf(TAG, "No running apps reported");
+ }
+ for (ActivityManager.RunningAppProcessInfo app : runningApps) {
+ if (app.pid == pid) {
+ return app.pkgList;
+ }
+ }
+ return null;
+ }
+
+ private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
+ @Override
+ public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {}
+
+ @Override
+ public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
+ mContext.getMainExecutor().execute(() -> onProcessForegroundServicesChanged(pid,
+ (serviceTypes & FOREGROUND_SERVICE_TYPE_MICROPHONE) != 0));
+ }
+
+ @Override
+ public void onProcessDied(int pid, int uid) {
+ mContext.getMainExecutor().execute(
+ () -> MicrophoneForegroundServicesObserver.this.onProcessDied(pid));
+ }
+ };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/RecordAudioAppOpObserver.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/RecordAudioAppOpObserver.java
new file mode 100644
index 0000000000000..b5b1c2b3018a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/RecordAudioAppOpObserver.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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.statusbar.tv.micdisclosure;
+
+import static com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar.DEBUG;
+
+import android.annotation.UiThread;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.Set;
+
+/**
+ * The purpose of these class is to detect packages that are conducting audio recording (according
+ * to {@link AppOpsManager}) and report this to {@link AudioRecordingDisclosureBar}.
+ */
+class RecordAudioAppOpObserver extends AudioActivityObserver implements
+ AppOpsManager.OnOpActiveChangedListener {
+ private static final String TAG = "RecordAudioAppOpObserver";
+
+ /**
+ * Set of the applications that currently are conducting audio recording according to {@link
+ * AppOpsManager}.
+ */
+ private final Set mActiveAudioRecordingPackages = new ArraySet<>();
+
+ RecordAudioAppOpObserver(Context context, OnAudioActivityStateChangeListener listener) {
+ super(context, listener);
+
+ // Register AppOpsManager callback
+ final AppOpsManager appOpsManager = (AppOpsManager) mContext.getSystemService(
+ Context.APP_OPS_SERVICE);
+ appOpsManager.startWatchingActive(
+ new String[]{AppOpsManager.OPSTR_RECORD_AUDIO},
+ mContext.getMainExecutor(),
+ this);
+ }
+
+ @UiThread
+ @Override
+ Set getActivePackages() {
+ return mActiveAudioRecordingPackages;
+ }
+
+ @UiThread
+ @Override
+ public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "OP_RECORD_AUDIO active change, active=" + active + ", package="
+ + packageName);
+ }
+
+ if (active) {
+ if (mActiveAudioRecordingPackages.add(packageName)) {
+ mListener.onAudioActivityStateChange(true, packageName);
+ }
+ } else {
+ if (mActiveAudioRecordingPackages.remove(packageName)) {
+ mListener.onAudioActivityStateChange(false, packageName);
+ }
+ }
+ }
+}