summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--media/java/android/media/tv/interactive/ITvIAppClient.aidl1
-rw-r--r--media/java/android/media/tv/interactive/ITvIAppManager.aidl4
-rw-r--r--media/java/android/media/tv/interactive/ITvIAppSession.aidl4
-rw-r--r--media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl1
-rw-r--r--media/java/android/media/tv/interactive/TvIAppManager.java74
-rw-r--r--media/java/android/media/tv/interactive/TvIAppService.java129
-rw-r--r--media/java/android/media/tv/interactive/TvIAppView.java172
-rw-r--r--services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java79
8 files changed, 460 insertions, 4 deletions
diff --git a/media/java/android/media/tv/interactive/ITvIAppClient.aidl b/media/java/android/media/tv/interactive/ITvIAppClient.aidl
index 419793413e39..0dd64b83df5f 100644
--- a/media/java/android/media/tv/interactive/ITvIAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvIAppClient.aidl
@@ -24,4 +24,5 @@ package android.media.tv.interactive;
oneway interface ITvIAppClient {
void onSessionCreated(in String iAppServiceId, IBinder token, int seq);
void onSessionReleased(int seq);
+ void onLayoutSurface(int left, int top, int right, int bottom, int seq);
} \ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/ITvIAppManager.aidl b/media/java/android/media/tv/interactive/ITvIAppManager.aidl
index a435e201030b..c7b08d50824d 100644
--- a/media/java/android/media/tv/interactive/ITvIAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvIAppManager.aidl
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.media.tv.interactive.ITvIAppClient;
+import android.view.Surface;
/**
* Interface to the TV interactive app service.
@@ -27,4 +28,7 @@ interface ITvIAppManager {
void createSession(
in ITvIAppClient client, in String iAppServiceId, int type, int seq, int userId);
void releaseSession(in IBinder sessionToken, int userId);
+ void setSurface(in IBinder sessionToken, in Surface surface, int userId);
+ void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
+ int userId);
} \ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/ITvIAppSession.aidl b/media/java/android/media/tv/interactive/ITvIAppSession.aidl
index 0cbdc8eed6e5..0afa9716a783 100644
--- a/media/java/android/media/tv/interactive/ITvIAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvIAppSession.aidl
@@ -16,6 +16,8 @@
package android.media.tv.interactive;
+import android.view.Surface;
+
/**
* Sub-interface of ITvIAppService.aidl which is created per session and has its own context.
* @hide
@@ -23,4 +25,6 @@ package android.media.tv.interactive;
oneway interface ITvIAppSession {
void startIApp();
void release();
+ void setSurface(in Surface surface);
+ void dispatchSurfaceChanged(int format, int width, int height);
} \ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl
index b3b317e808ac..0873aad8f5c6 100644
--- a/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl
@@ -25,4 +25,5 @@ import android.media.tv.interactive.ITvIAppSession;
*/
oneway interface ITvIAppSessionCallback {
void onSessionCreated(in ITvIAppSession session);
+ void onLayoutSurface(int left, int top, int right, int bottom);
} \ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/TvIAppManager.java b/media/java/android/media/tv/interactive/TvIAppManager.java
index f6565340abd4..16e19e782761 100644
--- a/media/java/android/media/tv/interactive/TvIAppManager.java
+++ b/media/java/android/media/tv/interactive/TvIAppManager.java
@@ -25,6 +25,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;
+import android.view.Surface;
import com.android.internal.util.Preconditions;
@@ -88,6 +89,18 @@ public final class TvIAppManager {
record.postSessionReleased();
}
}
+
+ @Override
+ public void onLayoutSurface(int left, int top, int right, int bottom, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postLayoutSurface(left, top, right, bottom);
+ }
+ }
};
}
@@ -159,6 +172,44 @@ public final class TvIAppManager {
}
/**
+ * Sets the {@link android.view.Surface} for this session.
+ *
+ * @param surface A {@link android.view.Surface} used to render video.
+ */
+ public void setSurface(Surface surface) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ // surface can be null.
+ try {
+ mService.setSurface(mToken, surface, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Notifies of any structural changes (format or size) of the surface passed in
+ * {@link #setSurface}.
+ *
+ * @param format The new PixelFormat of the surface.
+ * @param width The new width of the surface.
+ * @param height The new height of the surface.
+ */
+ public void dispatchSurfaceChanged(int format, int width, int height) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Releases this session.
*/
public void release() {
@@ -211,6 +262,16 @@ public final class TvIAppManager {
}
});
}
+
+ void postLayoutSurface(final int left, final int top, final int right,
+ final int bottom) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom);
+ }
+ });
+ }
}
/**
@@ -235,5 +296,18 @@ public final class TvIAppManager {
*/
public void onSessionReleased(@NonNull Session session) {
}
+
+ /**
+ * This is called when {@link TvIAppService.Session#layoutSurface} is called to change the
+ * layout of surface.
+ *
+ * @param session A {@link TvIAppManager.Session} associated with this callback.
+ * @param left Left position.
+ * @param top Top position.
+ * @param right Right position.
+ * @param bottom Bottom position.
+ */
+ public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
+ }
}
}
diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java
index f363728181c6..b385d9c7c11a 100644
--- a/media/java/android/media/tv/interactive/TvIAppService.java
+++ b/media/java/android/media/tv/interactive/TvIAppService.java
@@ -16,10 +16,12 @@
package android.media.tv.interactive;
+import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
@@ -27,6 +29,7 @@ import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.KeyEvent;
+import android.view.Surface;
import com.android.internal.os.SomeArgs;
@@ -94,6 +97,20 @@ public abstract class TvIAppService extends Service {
// @GuardedBy("mLock")
private final List<Runnable> mPendingActions = new ArrayList<>();
+ private final Context mContext;
+ private final Handler mHandler;
+ private Surface mSurface;
+
+ /**
+ * Creates a new Session.
+ *
+ * @param context The context of the application
+ */
+ public Session(Context context) {
+ mContext = context;
+ mHandler = new Handler(context.getMainLooper());
+ }
+
/**
* Starts TvIAppService session.
*/
@@ -101,16 +118,79 @@ public abstract class TvIAppService extends Service {
}
/**
+ * Called when the application sets the surface.
+ *
+ * <p>The TV IApp service should render interactive app UI onto the given surface. When
+ * called with {@code null}, the input service should immediately free any references to the
+ * currently set surface and stop using it.
+ *
+ * @param surface The surface to be used for interactive app UI rendering. Can be
+ * {@code null}.
+ * @return {@code true} if the surface was set successfully, {@code false} otherwise.
+ */
+ public abstract boolean onSetSurface(@Nullable Surface surface);
+
+ /**
+ * Called after any structural changes (format or size) have been made to the surface passed
+ * in {@link #onSetSurface}. This method is always called at least once, after
+ * {@link #onSetSurface} is called with non-null surface.
+ *
+ * @param format The new PixelFormat of the surface.
+ * @param width The new width of the surface.
+ * @param height The new height of the surface.
+ */
+ public void onSurfaceChanged(int format, int width, int height) {
+ }
+
+ /**
* Releases TvIAppService session.
*/
public void onRelease() {
}
+ /**
+ * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
+ * is relative to the overlay view that sits on top of this surface.
+ *
+ * @param left Left position in pixels, relative to the overlay view.
+ * @param top Top position in pixels, relative to the overlay view.
+ * @param right Right position in pixels, relative to the overlay view.
+ * @param bottom Bottom position in pixels, relative to the overlay view.
+ */
+ public void layoutSurface(final int left, final int top, final int right,
+ final int bottom) {
+ if (left > right || top > bottom) {
+ throw new IllegalArgumentException("Invalid parameter");
+ }
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top
+ + ", r=" + right + ", b=" + bottom + ",)");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onLayoutSurface(left, top, right, bottom);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in layoutSurface", e);
+ }
+ }
+ });
+ }
+
void startIApp() {
onStartIApp();
}
+
void release() {
onRelease();
+ if (mSurface != null) {
+ mSurface.release();
+ mSurface = null;
+ }
}
private void initialize(ITvIAppSessionCallback callback) {
@@ -122,6 +202,45 @@ public abstract class TvIAppService extends Service {
mPendingActions.clear();
}
}
+
+ /**
+ * Calls {@link #onSetSurface}.
+ */
+ void setSurface(Surface surface) {
+ onSetSurface(surface);
+ if (mSurface != null) {
+ mSurface.release();
+ }
+ mSurface = surface;
+ // TODO: Handle failure.
+ }
+
+ /**
+ * Calls {@link #onSurfaceChanged}.
+ */
+ void dispatchSurfaceChanged(int format, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width
+ + ", height=" + height + ")");
+ }
+ onSurfaceChanged(format, width, height);
+ }
+
+ private void executeOrPostRunnableOnMainThread(Runnable action) {
+ synchronized (mLock) {
+ if (mSessionCallback == null) {
+ // The session is not initialized yet.
+ mPendingActions.add(action);
+ } else {
+ if (mHandler.getLooper().isCurrentThread()) {
+ action.run();
+ } else {
+ // Posts the runnable if this is not called from the main thread
+ mHandler.post(action);
+ }
+ }
+ }
+ }
}
/**
@@ -143,6 +262,16 @@ public abstract class TvIAppService extends Service {
public void release() {
mSessionImpl.release();
}
+
+ @Override
+ public void setSurface(Surface surface) {
+ mSessionImpl.setSurface(surface);
+ }
+
+ @Override
+ public void dispatchSurfaceChanged(int format, int width, int height) {
+ mSessionImpl.dispatchSurfaceChanged(format, width, height);
+ }
}
@SuppressLint("HandlerLeak")
diff --git a/media/java/android/media/tv/interactive/TvIAppView.java b/media/java/android/media/tv/interactive/TvIAppView.java
index f56ea0a87439..adaaab0905c5 100644
--- a/media/java/android/media/tv/interactive/TvIAppView.java
+++ b/media/java/android/media/tv/interactive/TvIAppView.java
@@ -17,10 +17,18 @@
package android.media.tv.interactive;
import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
import android.media.tv.interactive.TvIAppManager.Session;
import android.media.tv.interactive.TvIAppManager.SessionCallback;
import android.os.Handler;
+import android.util.AttributeSet;
import android.util.Log;
+import android.util.Xml;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
import android.view.ViewGroup;
/**
@@ -35,20 +43,139 @@ public class TvIAppView extends ViewGroup {
private final Handler mHandler = new Handler();
private Session mSession;
private MySessionCallback mSessionCallback;
+ private SurfaceView mSurfaceView;
+ private Surface mSurface;
+
+ private boolean mSurfaceChanged;
+ private int mSurfaceFormat;
+ private int mSurfaceWidth;
+ private int mSurfaceHeight;
+
+ private boolean mUseRequestedSurfaceLayout;
+ private int mSurfaceViewLeft;
+ private int mSurfaceViewRight;
+ private int mSurfaceViewTop;
+ private int mSurfaceViewBottom;
+
+ private final AttributeSet mAttrs;
+ private final int mDefStyleAttr;
+ private final XmlResourceParser mParser;
+
+ private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format
+ + ", width=" + width + ", height=" + height + ")");
+ }
+ mSurfaceFormat = format;
+ mSurfaceWidth = width;
+ mSurfaceHeight = height;
+ mSurfaceChanged = true;
+ dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurface = holder.getSurface();
+ setSessionSurface(mSurface);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurface = null;
+ mSurfaceChanged = false;
+ setSessionSurface(null);
+ }
+ };
public TvIAppView(Context context) {
+ this(context, null, 0);
+ }
+
+ public TvIAppView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, /* attrs = */null, /* defStyleAttr = */0);
+ int sourceResId = Resources.getAttributeSetSourceResId(attrs);
+ if (sourceResId != Resources.ID_NULL) {
+ Log.d(TAG, "Build local AttributeSet");
+ mParser = context.getResources().getXml(sourceResId);
+ mAttrs = Xml.asAttributeSet(mParser);
+ } else {
+ Log.d(TAG, "Use passed in AttributeSet");
+ mParser = null;
+ mAttrs = attrs;
+ }
+ mDefStyleAttr = defStyleAttr;
+ resetSurfaceView();
mTvIAppManager = (TvIAppManager) getContext().getSystemService("tv_interactive_app");
}
@Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (DEBUG) {
- Log.d(TAG,
- "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)");
+ Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
+ + ", bottom=" + bottom + ",)");
+ }
+ if (mUseRequestedSurfaceLayout) {
+ mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
+ mSurfaceViewBottom);
+ } else {
+ mSurfaceView.layout(0, 0, right - left, bottom - top);
}
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
+ int width = mSurfaceView.getMeasuredWidth();
+ int height = mSurfaceView.getMeasuredHeight();
+ int childState = mSurfaceView.getMeasuredState();
+ setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
+ resolveSizeAndState(height, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mSurfaceView.setVisibility(visibility);
+ }
+
+ private void resetSurfaceView() {
+ if (mSurfaceView != null) {
+ mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
+ removeView(mSurfaceView);
+ }
+ mSurface = null;
+ mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr);
+ // The surface view's content should be treated as secure all the time.
+ mSurfaceView.setSecure(true);
+ mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
+ addView(mSurfaceView);
+ }
+
+ /**
+ * Resets this TvIAppView.
+ */
+ public void reset() {
+ if (DEBUG) Log.d(TAG, "reset()");
+ resetInternal();
+ }
+
+ private void setSessionSurface(Surface surface) {
+ if (mSession == null) {
+ return;
+ }
+ mSession.setSurface(surface);
+ }
+
+ private void dispatchSurfaceChanged(int format, int width, int height) {
+ if (mSession == null) {
+ return;
+ }
+ mSession.dispatchSurfaceChanged(format, width, height);
+ }
+
/**
* Prepares the interactive application.
*/
@@ -75,6 +202,17 @@ public class TvIAppView extends ViewGroup {
}
}
+ private void resetInternal() {
+ mSessionCallback = null;
+ if (mSession != null) {
+ setSessionSurface(null);
+ mUseRequestedSurfaceLayout = false;
+ mSession.release();
+ mSession = null;
+ resetSurfaceView();
+ }
+ }
+
private class MySessionCallback extends SessionCallback {
final String mIAppServiceId;
int mType;
@@ -99,7 +237,15 @@ public class TvIAppView extends ViewGroup {
}
mSession = session;
if (session != null) {
- // TODO: handle SurfaceView and InputChannel.
+ // mSurface may not be ready yet as soon as starting an application.
+ // In the case, we don't send Session.setSurface(null) unnecessarily.
+ // setSessionSurface will be called in surfaceCreated.
+ if (mSurface != null) {
+ setSessionSurface(mSurface);
+ if (mSurfaceChanged) {
+ dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
+ }
+ }
} else {
// Failed to create
// Todo: forward error to Tv App
@@ -119,5 +265,23 @@ public class TvIAppView extends ViewGroup {
mSessionCallback = null;
mSession = null;
}
+
+ @Override
+ public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
+ if (DEBUG) {
+ Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
+ + right + ", bottom=" + bottom + ",)");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onLayoutSurface - session not created");
+ return;
+ }
+ mSurfaceViewLeft = left;
+ mSurfaceViewTop = top;
+ mSurfaceViewRight = right;
+ mSurfaceViewBottom = bottom;
+ mUseRequestedSurfaceLayout = true;
+ requestLayout();
+ }
}
}
diff --git a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
index d09ecebfcad1..cf212dffb373 100644
--- a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
@@ -36,6 +36,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.SparseArray;
+import android.view.Surface;
import com.android.internal.annotations.GuardedBy;
import com.android.server.SystemService;
@@ -136,6 +137,16 @@ public class TvIAppManagerService extends SystemService {
return sessionState;
}
+ @GuardedBy("mLock")
+ private ITvIAppSession getSessionLocked(SessionState sessionState) {
+ ITvIAppSession session = sessionState.mSession;
+ if (session == null) {
+ throw new IllegalStateException("Session not yet created for token "
+ + sessionState.mSessionToken);
+ }
+ return session;
+ }
+
private final class BinderService extends ITvIAppManager.Stub {
@Override
@@ -234,6 +245,55 @@ public class TvIAppManagerService extends SystemService {
Slogf.e(TAG, "error in start", e);
}
}
+
+ @Override
+ public void setSurface(IBinder sessionToken, Surface surface, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "setSurface");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).setSurface(surface);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in setSurface", e);
+ }
+ }
+ } finally {
+ if (surface != null) {
+ // surface is not used in TvIAppManagerService.
+ surface.release();
+ }
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width,
+ int height, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "dispatchSurfaceChanged");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).dispatchSurfaceChanged(format, width,
+ height);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in dispatchSurfaceChanged", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
@GuardedBy("mLock")
@@ -616,6 +676,25 @@ public class TvIAppManagerService extends SystemService {
}
}
+ @Override
+ public void onLayoutSurface(int left, int top, int right, int bottom) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top
+ + ", right=" + right + ", bottom=" + bottom + ",)");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onLayoutSurface(left, top, right, bottom,
+ mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onLayoutSurface", e);
+ }
+ }
+ }
+
@GuardedBy("mLock")
private boolean addSessionTokenToClientStateLocked(ITvIAppSession session) {
try {