diff options
| author | 2021-11-24 11:08:50 +0000 | |
|---|---|---|
| committer | 2021-11-24 11:08:50 +0000 | |
| commit | 04e056e16af84e1afa2131dd5b6227ee88858d5d (patch) | |
| tree | 84dd34a9d6685cc4ace1d6ea05c491f9b1a1b680 | |
| parent | 08d2e4f5df87a14c2d65200c1a8d1a34852af56c (diff) | |
| parent | 44142891097e49b1feed15bb514b82e33b34590b (diff) | |
Merge "TIAF: handle Input Channel"
5 files changed, 420 insertions, 28 deletions
diff --git a/media/java/android/media/tv/interactive/ITvIAppClient.aidl b/media/java/android/media/tv/interactive/ITvIAppClient.aidl index 0dd64b83df5f..dabea304591b 100644 --- a/media/java/android/media/tv/interactive/ITvIAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppClient.aidl @@ -16,13 +16,15 @@ package android.media.tv.interactive; +import android.view.InputChannel; + /** * Interface a client of the ITvIAppManager implements, to identify itself and receive information * about changes to the state of each TV interactive application service. * @hide */ oneway interface ITvIAppClient { - void onSessionCreated(in String iAppServiceId, IBinder token, int seq); + void onSessionCreated(in String iAppServiceId, IBinder token, in InputChannel channel, 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/ITvIAppService.aidl b/media/java/android/media/tv/interactive/ITvIAppService.aidl index 2f165f0de7e5..1dee9cc4ed28 100644 --- a/media/java/android/media/tv/interactive/ITvIAppService.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppService.aidl @@ -18,6 +18,7 @@ package android.media.tv.interactive; import android.media.tv.interactive.ITvIAppServiceCallback; import android.media.tv.interactive.ITvIAppSessionCallback; +import android.view.InputChannel; /** * Top-level interface to a TV IApp component (implemented in a Service). It's used for @@ -27,5 +28,6 @@ import android.media.tv.interactive.ITvIAppSessionCallback; oneway interface ITvIAppService { void registerCallback(in ITvIAppServiceCallback callback); void unregisterCallback(in ITvIAppServiceCallback callback); - void createSession(in ITvIAppSessionCallback callback, in String iAppServiceId, int type); + void createSession(in InputChannel channel, in ITvIAppSessionCallback callback, + in String iAppServiceId, int type); }
\ 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 093e1be6ca5e..7479b2bd9f20 100644 --- a/media/java/android/media/tv/interactive/TvIAppManager.java +++ b/media/java/android/media/tv/interactive/TvIAppManager.java @@ -23,9 +23,15 @@ import android.content.Context; import android.media.tv.TvInputManager; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.util.Pools; import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; import android.view.Surface; import com.android.internal.util.Preconditions; @@ -67,8 +73,8 @@ public final class TvIAppManager { mUserId = userId; mClient = new ITvIAppClient.Stub() { @Override - public void onSessionCreated(String iAppServiceId, IBinder token, int seq) { - // TODO: use InputChannel for input events + public void onSessionCreated(String iAppServiceId, IBinder token, InputChannel channel, + int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { @@ -77,7 +83,7 @@ public final class TvIAppManager { } Session session = null; if (token != null) { - session = new Session(token, mService, mUserId, seq, + session = new Session(token, channel, mService, mUserId, seq, mSessionCallbackRecordMap); } else { mSessionCallbackRecordMap.delete(seq); @@ -351,18 +357,33 @@ public final class TvIAppManager { * @hide */ public static final class Session { + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; + private final ITvIAppManager mService; private final int mUserId; private final int mSeq; private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; - private IBinder mToken; + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); private TvInputManager.Session mInputSession; + private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20); - private Session(IBinder token, ITvIAppManager service, int userId, int seq, - SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { + private IBinder mToken; + private TvInputEventSender mSender; + private InputChannel mInputChannel; + + private Session(IBinder token, InputChannel channel, ITvIAppManager service, int userId, + int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { mToken = token; + mInputChannel = channel; mService = service; mUserId = userId; mSeq = seq; @@ -428,6 +449,43 @@ public final class TvIAppManager { } /** + * Dispatches an input event to this session. + * + * @param event An {@link InputEvent} to dispatch. Cannot be {@code null}. + * @param token A token used to identify the input event later in the callback. + * @param callback A callback used to receive the dispatch result. Cannot be {@code null}. + * @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be + * {@code null}. + * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns + * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns + * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will + * be invoked later. + * @hide + */ + public int dispatchInputEvent(@NonNull InputEvent event, Object token, + @NonNull FinishedInputEventCallback callback, @NonNull Handler handler) { + Preconditions.checkNotNull(event); + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(handler); + synchronized (mHandler) { + if (mInputChannel == null) { + return DISPATCH_NOT_HANDLED; + } + PendingEvent p = obtainPendingEventLocked(event, token, callback, handler); + if (Looper.myLooper() == Looper.getMainLooper()) { + // Already running on the main thread so we can send the event immediately. + return sendInputEventOnMainLooperLocked(p); + } + + // Post the event to the main thread. + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessage(msg); + return DISPATCH_IN_PROGRESS; + } + } + + /** * Releases this session. */ public void release() { @@ -444,12 +502,208 @@ public final class TvIAppManager { releaseInternal(); } + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + private void releaseInternal() { mToken = null; + synchronized (mHandler) { + if (mInputChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mInputChannel.dispose(); + mInputChannel = null; + } + } synchronized (mSessionCallbackRecordMap) { mSessionCallbackRecordMap.delete(mSeq); } } + + private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + PendingEvent p = mPendingEventPool.acquire(); + if (p == null) { + p = new PendingEvent(); + } + p.mEvent = event; + p.mEventToken = token; + p.mCallback = callback; + p.mEventHandler = handler; + return p; + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mEventHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mEventHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mInputChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for session to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token A token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + void onFinishedInputEvent(Object token, boolean handled); + } + + private final class InputEventHandler extends Handler { + public static final int MSG_SEND_INPUT_EVENT = 1; + public static final int MSG_TIMEOUT_INPUT_EVENT = 2; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + private final class TvInputEventSender extends InputEventSender { + TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mEventToken; + public FinishedInputEventCallback mCallback; + public Handler mEventHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mEventToken = null; + mCallback = null; + mEventHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mEventToken, mHandled); + + synchronized (mEventHandler) { + recyclePendingEventLocked(this); + } + } + } } private static final class SessionCallbackRecord { diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java index 78b8173e0af7..25dec62a2a2e 100644 --- a/media/java/android/media/tv/interactive/TvIAppService.java +++ b/media/java/android/media/tv/interactive/TvIAppService.java @@ -25,11 +25,17 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.Surface; import com.android.internal.os.SomeArgs; @@ -85,14 +91,16 @@ public abstract class TvIAppService extends Service { } @Override - public void createSession(ITvIAppSessionCallback cb, String iAppServiceId, int type) { + public void createSession(InputChannel channel, ITvIAppSessionCallback cb, + String iAppServiceId, int type) { if (cb == null) { return; } SomeArgs args = SomeArgs.obtain(); - args.arg1 = cb; - args.arg2 = iAppServiceId; - args.arg3 = type; + args.arg1 = channel; + args.arg2 = cb; + args.arg3 = iAppServiceId; + args.arg4 = type; mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args) .sendToTarget(); } @@ -122,6 +130,8 @@ public abstract class TvIAppService extends Service { * @hide */ public abstract static class Session implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); + private final Object mLock = new Object(); // @GuardedBy("mLock") private ITvIAppSessionCallback mSessionCallback; @@ -182,6 +192,60 @@ public abstract class TvIAppService extends Service { } /** + * TODO: JavaDoc of APIs related to input events. + * @hide + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + /** + * @hide + */ + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + /** + * @hide + */ + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + + /** + * @hide + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + /** + * @hide + */ + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + /** + * @hide + */ + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + /** + * @hide + */ + public boolean onGenericMotionEvent(MotionEvent event) { + return false; + } + + /** * 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. * @@ -226,6 +290,39 @@ public abstract class TvIAppService extends Service { } } + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.dispatch(this, mDispatcherState, this)) { + return TvIAppManager.Session.DISPATCH_HANDLED; + } + + // TODO: special handlings of navigation keys and media keys + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return TvIAppManager.Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return TvIAppManager.Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return TvIAppManager.Session.DISPATCH_HANDLED; + } + } + } + // TODO: handle overlay view + return TvIAppManager.Session.DISPATCH_NOT_HANDLED; + } + private void initialize(ITvIAppSessionCallback callback) { synchronized (mLock) { mSessionCallback = callback; @@ -281,10 +378,17 @@ public abstract class TvIAppService extends Service { * @hide */ public static class ITvIAppSessionWrapper extends ITvIAppSession.Stub { + // TODO: put ITvIAppSessionWrapper in a separate Java file private final Session mSessionImpl; + private InputChannel mChannel; + private TvIAppEventReceiver mReceiver; - public ITvIAppSessionWrapper(Session mSessionImpl) { + public ITvIAppSessionWrapper(Context context, Session mSessionImpl, InputChannel channel) { this.mSessionImpl = mSessionImpl; + mChannel = channel; + if (channel != null) { + mReceiver = new TvIAppEventReceiver(channel, context.getMainLooper()); + } } @Override @@ -306,6 +410,26 @@ public abstract class TvIAppService extends Service { public void dispatchSurfaceChanged(int format, int width, int height) { mSessionImpl.dispatchSurfaceChanged(format, width, height); } + + private final class TvIAppEventReceiver extends InputEventReceiver { + TvIAppEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mSessionImpl.dispatchInputEvent(event, this); + if (handled != TvIAppManager.Session.DISPATCH_IN_PROGRESS) { + finishInputEvent(event, handled == TvIAppManager.Session.DISPATCH_HANDLED); + } + } + } } @SuppressLint("HandlerLeak") @@ -318,9 +442,10 @@ public abstract class TvIAppService extends Service { switch (msg.what) { case DO_CREATE_SESSION: { SomeArgs args = (SomeArgs) msg.obj; - ITvIAppSessionCallback cb = (ITvIAppSessionCallback) args.arg1; - String iAppServiceId = (String) args.arg2; - int type = (int) args.arg3; + InputChannel channel = (InputChannel) args.arg1; + ITvIAppSessionCallback cb = (ITvIAppSessionCallback) args.arg2; + String iAppServiceId = (String) args.arg3; + int type = (int) args.arg4; args.recycle(); Session sessionImpl = onCreateSession(iAppServiceId, type); if (sessionImpl == null) { @@ -332,7 +457,8 @@ public abstract class TvIAppService extends Service { } return; } - ITvIAppSession stub = new ITvIAppSessionWrapper(sessionImpl); + ITvIAppSession stub = new ITvIAppSessionWrapper( + TvIAppService.this, sessionImpl, channel); SomeArgs someArgs = SomeArgs.obtain(); someArgs.arg1 = sessionImpl; 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 dfb07523d830..d0c6d137f5ef 100644 --- a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java @@ -47,6 +47,7 @@ import android.os.UserManager; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseArray; +import android.view.InputChannel; import android.view.Surface; import com.android.internal.annotations.GuardedBy; @@ -611,14 +612,14 @@ public class TvIAppManagerService extends SystemService { if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) { // Only current user and its running profiles can create sessions. // Let the client get onConnectionFailed callback for this case. - sendSessionTokenToClientLocked(client, iAppServiceId, null, seq); + sendSessionTokenToClientLocked(client, iAppServiceId, null, null, seq); return; } UserState userState = getOrCreateUserStateLocked(resolvedUserId); TvIAppState iAppState = userState.mIAppMap.get(iAppServiceId); if (iAppState == null) { Slogf.w(TAG, "Failed to find state for iAppServiceId=" + iAppServiceId); - sendSessionTokenToClientLocked(client, iAppServiceId, null, seq); + sendSessionTokenToClientLocked(client, iAppServiceId, null, null, seq); return; } ServiceState serviceState = @@ -631,7 +632,7 @@ public class TvIAppManagerService extends SystemService { } // Send a null token immediately while reconnecting. if (serviceState.mReconnecting) { - sendSessionTokenToClientLocked(client, iAppServiceId, null, seq); + sendSessionTokenToClientLocked(client, iAppServiceId, null, null, seq); return; } @@ -780,9 +781,9 @@ public class TvIAppManagerService extends SystemService { @GuardedBy("mLock") private void sendSessionTokenToClientLocked(ITvIAppClient client, String iAppServiceId, - IBinder sessionToken, int seq) { + IBinder sessionToken, InputChannel channel, int seq) { try { - client.onSessionCreated(iAppServiceId, sessionToken, seq); + client.onSessionCreated(iAppServiceId, sessionToken, channel, seq); } catch (RemoteException e) { Slogf.e(TAG, "error in onSessionCreated", e); } @@ -797,20 +798,23 @@ public class TvIAppManagerService extends SystemService { Slogf.d(TAG, "createSessionInternalLocked(iAppServiceId=" + sessionState.mIAppServiceId + ")"); } + InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString()); // Set up a callback to send the session token. - ITvIAppSessionCallback callback = new SessionCallback(sessionState); + ITvIAppSessionCallback callback = new SessionCallback(sessionState, channels); boolean created = true; // Create a session. When failed, send a null token immediately. try { - service.createSession(callback, sessionState.mIAppServiceId, sessionState.mType); + service.createSession( + channels[1], callback, sessionState.mIAppServiceId, sessionState.mType); } catch (RemoteException e) { Slogf.e(TAG, "error in createSession", e); sendSessionTokenToClientLocked(sessionState.mClient, sessionState.mIAppServiceId, null, - sessionState.mSeq); + null, sessionState.mSeq); created = false; } + channels[1].dispose(); return created; } @@ -883,7 +887,7 @@ public class TvIAppManagerService extends SystemService { for (SessionState sessionState : sessionsToAbort) { removeSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId); sendSessionTokenToClientLocked(sessionState.mClient, - sessionState.mIAppServiceId, null, sessionState.mSeq); + sessionState.mIAppServiceId, null, null, sessionState.mSeq); } updateServiceConnectionLocked(serviceState.mComponent, userId); } @@ -1136,9 +1140,11 @@ public class TvIAppManagerService extends SystemService { private final class SessionCallback extends ITvIAppSessionCallback.Stub { private final SessionState mSessionState; + private final InputChannel[] mInputChannels; - SessionCallback(SessionState sessionState) { + SessionCallback(SessionState sessionState, InputChannel[] channels) { mSessionState = sessionState; + mInputChannels = channels; } @Override @@ -1154,12 +1160,14 @@ public class TvIAppManagerService extends SystemService { mSessionState.mClient, mSessionState.mIAppServiceId, mSessionState.mSessionToken, + mInputChannels[0], mSessionState.mSeq); } else { removeSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId); sendSessionTokenToClientLocked(mSessionState.mClient, - mSessionState.mIAppServiceId, null, mSessionState.mSeq); + mSessionState.mIAppServiceId, null, null, mSessionState.mSeq); } + mInputChannels[0].dispose(); } } |