Create a long-press menu for sending screenshots to bubbles.
Test: Manual. Enable the long-press bubble menu with "adb shell settings set secure allow_bubble_menu 1". Then, long-press on a bubble to show a menu and tap the screenshot button to send a screenshot to the selected bubble. This won't work when the bubble is expanded.
Change-Id: I35380a8a4775e568699cf4527c6071bf932eb715
diff --git a/packages/SystemUI/res/layout/bubble_menu_view.xml b/packages/SystemUI/res/layout/bubble_menu_view.xml
new file mode 100644
index 0000000..24608d3
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_menu_view.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 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.
+ -->
+<com.android.systemui.bubbles.BubbleMenuView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:background="#66000000"
+ android:visibility="gone"
+ android:id="@+id/bubble_menu_container">
+
+ <FrameLayout
+ android:layout_height="@dimen/individual_bubble_size"
+ android:layout_width="wrap_content"
+ android:background="#FFFFFF"
+ android:id="@+id/bubble_menu_view">
+
+ <ImageView
+ android:id="@*android:id/icon"
+ android:layout_width="@dimen/global_actions_grid_item_icon_width"
+ android:layout_height="@dimen/global_actions_grid_item_icon_height"
+ android:layout_marginTop="@dimen/global_actions_grid_item_icon_top_margin"
+ android:layout_marginBottom="@dimen/global_actions_grid_item_icon_bottom_margin"
+ android:layout_marginLeft="@dimen/global_actions_grid_item_icon_side_margin"
+ android:layout_marginRight="@dimen/global_actions_grid_item_icon_side_margin"
+ android:scaleType="centerInside"
+ android:tint="@color/global_actions_text"
+ />
+ </FrameLayout>
+</com.android.systemui.bubbles.BubbleMenuView>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index dbb1936..1938194 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -44,13 +44,19 @@
import android.annotation.UserIdInt;
import android.app.ActivityManager.RunningTaskInfo;
+import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.RemoteInput;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ShortcutManager;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.service.notification.NotificationListenerService.RankingMap;
@@ -69,6 +75,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.util.ScreenshotHelper;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -86,6 +93,7 @@
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.statusbar.policy.ZenModeController;
import java.io.FileDescriptor;
@@ -93,8 +101,10 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -138,6 +148,8 @@
@Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
private final NotificationGroupManager mNotificationGroupManager;
private final Lazy<ShadeController> mShadeController;
+ private final RemoteInputUriController mRemoteInputUriController;
+ private Handler mHandler = new Handler() {};
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
@@ -155,6 +167,8 @@
private final StatusBarWindowController mStatusBarWindowController;
private final ZenModeController mZenModeController;
private StatusBarStateListener mStatusBarStateListener;
+ private final ScreenshotHelper mScreenshotHelper;
+
private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
private IStatusBarService mBarService;
@@ -192,6 +206,16 @@
}
/**
+ * Listener for handling bubble screenshot events.
+ */
+ public interface BubbleScreenshotListener {
+ /**
+ * Called to trigger taking a screenshot and sending the result to a bubble.
+ */
+ void onBubbleScreenshot(Bubble bubble);
+ }
+
+ /**
* Listens for the current state of the status bar and updates the visibility state
* of bubbles as needed.
*/
@@ -226,10 +250,12 @@
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager,
NotificationGroupManager groupManager,
- NotificationEntryManager entryManager) {
+ NotificationEntryManager entryManager,
+ RemoteInputUriController remoteInputUriController) {
this(context, statusBarWindowController, statusBarStateController, shadeController,
data, null /* synchronizer */, configurationController, interruptionStateProvider,
- zenModeController, notifUserManager, groupManager, entryManager);
+ zenModeController, notifUserManager, groupManager, entryManager,
+ remoteInputUriController);
}
public BubbleController(Context context,
@@ -243,11 +269,13 @@
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager,
NotificationGroupManager groupManager,
- NotificationEntryManager entryManager) {
+ NotificationEntryManager entryManager,
+ RemoteInputUriController remoteInputUriController) {
mContext = context;
mNotificationInterruptionStateProvider = interruptionStateProvider;
mNotifUserManager = notifUserManager;
mZenModeController = zenModeController;
+ mRemoteInputUriController = remoteInputUriController;
mZenModeController.addCallback(new ZenModeController.Callback() {
@Override
public void onZenChanged(int zen) {
@@ -320,6 +348,8 @@
});
mUserCreatedBubbles = new HashSet<>();
+
+ mScreenshotHelper = new ScreenshotHelper(context);
}
/**
@@ -337,6 +367,9 @@
if (mExpandListener != null) {
mStackView.setExpandListener(mExpandListener);
}
+ if (mBubbleScreenshotListener != null) {
+ mStackView.setBubbleScreenshotListener(mBubbleScreenshotListener);
+ }
}
}
@@ -1058,4 +1091,71 @@
}
}
}
+
+ // TODO: Copied from RemoteInputView. Consolidate RemoteInput intent logic.
+ private Intent prepareRemoteInputFromData(String contentType, Uri data,
+ RemoteInput remoteInput, NotificationEntry entry) {
+ HashMap<String, Uri> results = new HashMap<>();
+ results.put(contentType, data);
+ mRemoteInputUriController.grantInlineReplyUriPermission(entry.getSbn(), data);
+ Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ RemoteInput.addDataResultToIntent(remoteInput, fillInIntent, results);
+
+ return fillInIntent;
+ }
+
+ // TODO: Copied from RemoteInputView. Consolidate RemoteInput intent logic.
+ private void sendRemoteInput(Intent intent, NotificationEntry entry,
+ PendingIntent pendingIntent) {
+ // Tell ShortcutManager that this package has been "activated". ShortcutManager
+ // will reset the throttling for this package.
+ // Strictly speaking, the intent receiver may be different from the notification publisher,
+ // but that's an edge case, and also because we can't always know which package will receive
+ // an intent, so we just reset for the publisher.
+ mContext.getSystemService(ShortcutManager.class).onApplicationActive(
+ entry.getSbn().getPackageName(),
+ entry.getSbn().getUser().getIdentifier());
+
+ try {
+ pendingIntent.send(mContext, 0, intent);
+ } catch (PendingIntent.CanceledException e) {
+ Log.i(TAG, "Unable to send remote input result", e);
+ }
+ }
+
+ private void sendScreenshotToBubble(Bubble bubble) {
+ // delay allows the bubble menu to disappear before the screenshot
+ // done here because we already have a Handler to delay with.
+ // TODO: Hide bubble + menu UI from screenshots entirely instead of just delaying.
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mScreenshotHelper.takeScreenshot(
+ android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ true /* hasStatus */,
+ true /* hasNav */,
+ mHandler,
+ new Consumer<Uri>() {
+ @Override
+ public void accept(Uri uri) {
+ if (uri != null) {
+ NotificationEntry entry = bubble.getEntry();
+ Pair<RemoteInput, Notification.Action> pair = entry.getSbn()
+ .getNotification().findRemoteInputActionPair(false);
+ RemoteInput remoteInput = pair.first;
+ Notification.Action action = pair.second;
+ Intent dataIntent = prepareRemoteInputFromData("image/png", uri,
+ remoteInput, entry);
+ sendRemoteInput(dataIntent, entry, action.actionIntent);
+ mBubbleData.setSelectedBubble(bubble);
+ mBubbleData.setExpanded(true);
+ }
+ }
+ });
+ }
+ }, 200);
+ }
+
+ private final BubbleScreenshotListener mBubbleScreenshotListener =
+ bubble -> sendScreenshotToBubble(bubble);
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index e138d93..8299f22 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -68,6 +68,9 @@
private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps";
+ private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu";
+ private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false;
+
/**
* When true, if a notification has the information necessary to bubble (i.e. valid
* contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
@@ -123,6 +126,16 @@
}
/**
+ * When true, show a menu when a bubble is long-pressed, which will allow the user to take
+ * actions on that bubble.
+ */
+ static boolean allowBubbleScreenshotMenu(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ ALLOW_BUBBLE_MENU,
+ ALLOW_BUBBLE_MENU_DEFAULT ? 1 : 0) != 0;
+ }
+
+ /**
* If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
* {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
* the notification has necessary info for BubbleMetadata.
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java
new file mode 100644
index 0000000..e8eb72e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 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.bubbles;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+/**
+ * Menu which allows users to take actions on bubbles, ex. screenshots.
+ */
+public class BubbleMenuView extends FrameLayout {
+ private FrameLayout mMenu;
+ private boolean mShowing = false;
+
+ public BubbleMenuView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BubbleMenuView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMenu = findViewById(R.id.bubble_menu_view);
+ ImageView icon = findViewById(com.android.internal.R.id.icon);
+ icon.setImageDrawable(mContext.getDrawable(com.android.internal.R.drawable.ic_screenshot));
+ }
+
+ /**
+ * Get the bubble menu view.
+ */
+ public View getMenuView() {
+ return mMenu;
+ }
+
+ /**
+ * Checks whether the bubble menu is currently displayed.
+ */
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ /**
+ * Show the bubble menu at the specified position on the screen.
+ */
+ public void show(float x, float y) {
+ mShowing = true;
+ this.setVisibility(VISIBLE);
+ mMenu.setTranslationX(x);
+ mMenu.setTranslationY(y);
+ }
+
+ /**
+ * Hide the bubble menu.
+ */
+ public void hide() {
+ mShowing = false;
+ this.setVisibility(GONE);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 29de2f0..29a4bb1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -110,6 +110,7 @@
/** How long to wait, in milliseconds, before hiding the flyout. */
@VisibleForTesting
static final int FLYOUT_HIDE_AFTER = 5000;
+ private BubbleController.BubbleScreenshotListener mBubbleScreenshotListener;
/**
* Interface to synchronize {@link View} state and the screen.
@@ -163,6 +164,7 @@
private ExpandedAnimationController mExpandedAnimationController;
private FrameLayout mExpandedViewContainer;
+ @Nullable private BubbleMenuView mBubbleMenuView;
private BubbleFlyoutView mFlyout;
/** Runnable that fades out the flyout and then sets it to GONE. */
@@ -194,6 +196,7 @@
private int mPointerHeight;
private int mStatusBarHeight;
private int mImeOffset;
+ private int mBubbleMenuOffset = 252;
private BubbleIconFactory mBubbleIconFactory;
private Bubble mExpandedBubble;
private boolean mIsExpanded;
@@ -492,6 +495,9 @@
mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
});
+
+ mInflater.inflate(R.layout.bubble_menu_view, this);
+ mBubbleMenuView = findViewById(R.id.bubble_menu_container);
}
private void setUpFlyout() {
@@ -683,6 +689,13 @@
}
/**
+ * Sets the screenshot listener.
+ */
+ public void setBubbleScreenshotListener(BubbleController.BubbleScreenshotListener listener) {
+ mBubbleScreenshotListener = listener;
+ }
+
+ /**
* Whether the stack of bubbles is expanded or not.
*/
public boolean isExpanded() {
@@ -870,6 +883,12 @@
public View getTargetView(MotionEvent event) {
float x = event.getRawX();
float y = event.getRawY();
+ if (mBubbleMenuView.isShowing()) {
+ if (isIntersecting(mBubbleMenuView.getMenuView(), x, y)) {
+ return mBubbleMenuView;
+ }
+ return null;
+ }
if (mIsExpanded) {
if (isIntersecting(mBubbleContainer, x, y)) {
// Could be tapping or dragging a bubble while expanded
@@ -1074,6 +1093,7 @@
return;
}
+ hideBubbleMenu();
mStackAnimationController.cancelStackPositionAnimations();
mBubbleContainer.setActiveController(mStackAnimationController);
hideFlyoutImmediate();
@@ -1473,6 +1493,11 @@
@Override
public void getBoundsOnScreen(Rect outRect) {
+ // If the bubble menu is open, the entire screen should capture touch events.
+ if (mBubbleMenuView.isShowing()) {
+ outRect.set(0, 0, getWidth(), getHeight());
+ return;
+ }
if (!mIsExpanded) {
if (mBubbleContainer.getChildCount() > 0) {
mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
@@ -1700,4 +1725,43 @@
}
return bubbles;
}
+
+ /**
+ * Show the bubble menu, positioned relative to the stack.
+ */
+ public void showBubbleMenu() {
+ PointF currentPos = mStackAnimationController.getStackPosition();
+ float yPos = currentPos.y;
+ float xPos = currentPos.x;
+ if (mStackAnimationController.isStackOnLeftSide()) {
+ xPos += mBubbleSize;
+ } else {
+ //TODO: Use the width of the menu instead of this fixed offset. Offset used for now
+ // because menu width isn't correct the first time the menu is shown.
+ xPos -= mBubbleMenuOffset;
+ }
+
+ mBubbleMenuView.show(xPos, yPos);
+ }
+
+ /**
+ * Hide the bubble menu.
+ */
+ public void hideBubbleMenu() {
+ mBubbleMenuView.hide();
+ }
+
+ /**
+ * Determines whether the bubble menu is currently showing.
+ */
+ public boolean isShowingBubbleMenu() {
+ return mBubbleMenuView.isShowing();
+ }
+
+ /**
+ * Take a screenshot and send it to the specified bubble.
+ */
+ public void sendScreenshotToBubble(Bubble bubble) {
+ mBubbleScreenshotListener.onBubbleScreenshot(bubble);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 44e013a..b1d205c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -57,6 +57,7 @@
private final PointF mViewPositionOnTouchDown = new PointF();
private final BubbleStackView mStack;
private final BubbleData mBubbleData;
+ private final Context mContext;
private BubbleController mController = Dependency.get(BubbleController.class);
@@ -75,6 +76,7 @@
mTouchSlopSquared = touchSlop * touchSlop;
mBubbleData = bubbleData;
mStack = stackView;
+ mContext = context;
}
@Override
@@ -91,15 +93,24 @@
// anything, collapse the stack.
if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
mBubbleData.setExpanded(false);
+ mStack.hideBubbleMenu();
resetForNextGesture();
return false;
}
+ if (mTouchedView instanceof BubbleMenuView) {
+ mStack.hideBubbleMenu();
+ resetForNextGesture();
+ mStack.sendScreenshotToBubble(mBubbleData.getSelectedBubble());
+ return false;
+ }
+
if (!(mTouchedView instanceof BadgedImageView)
&& !(mTouchedView instanceof BubbleStackView)
&& !(mTouchedView instanceof BubbleFlyoutView)) {
// Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
// of expanded view).
+ mStack.hideBubbleMenu();
resetForNextGesture();
return false;
}
@@ -132,6 +143,10 @@
break;
case MotionEvent.ACTION_MOVE:
+ // block all further touch inputs once the menu is open
+ if (mStack.isShowingBubbleMenu()) {
+ return true;
+ }
trackMovement(event);
final float deltaX = rawX - mTouchDown.x;
final float deltaY = rawY - mTouchDown.y;
@@ -148,6 +163,13 @@
} else {
mStack.onBubbleDragged(mTouchedView, viewX, viewY);
}
+ } else {
+ float touchTime = event.getEventTime() - event.getDownTime();
+ if (touchTime > ViewConfiguration.getLongPressTimeout() && !mStack.isExpanded()
+ && BubbleExperimentConfig.allowBubbleScreenshotMenu(mContext)) {
+ mStack.showBubbleMenu();
+ return true;
+ }
}
final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
@@ -171,6 +193,10 @@
break;
case MotionEvent.ACTION_UP:
+ if (mStack.isShowingBubbleMenu()) {
+ resetForNextGesture();
+ return true;
+ }
trackMovement(event);
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
final float velX = mVelocityTracker.getXVelocity();
@@ -261,7 +287,6 @@
mVelocityTracker.recycle();
mVelocityTracker = null;
}
-
mTouchedView = null;
mMovedEnough = false;
mInDismissTarget = false;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 4c707f4..ae43aa2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -82,6 +82,7 @@
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.util.InjectionInflationController;
@@ -153,6 +154,8 @@
private Resources mResources;
@Mock
private Lazy<ShadeController> mShadeController;
+ @Mock
+ private RemoteInputUriController mRemoteInputUriController;
private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
private BubbleData mBubbleData;
@@ -212,7 +215,8 @@
mZenModeController,
mLockscreenUserManager,
mNotificationGroupManager,
- mNotificationEntryManager);
+ mNotificationEntryManager,
+ mRemoteInputUriController);
mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener);
mBubbleController.setExpandListener(mBubbleExpandListener);
@@ -708,11 +712,13 @@
ZenModeController zenModeController,
NotificationLockscreenUserManager lockscreenUserManager,
NotificationGroupManager groupManager,
- NotificationEntryManager entryManager) {
+ NotificationEntryManager entryManager,
+ RemoteInputUriController remoteInputUriController) {
super(context,
statusBarWindowController, statusBarStateController, shadeController,
data, Runnable::run, configurationController, interruptionStateProvider,
- zenModeController, lockscreenUserManager, groupManager, entryManager);
+ zenModeController, lockscreenUserManager, groupManager, entryManager,
+ remoteInputUriController);
}
}