Merge "Dispatch input events to the TV input"
This commit is contained in:
@@ -28163,12 +28163,19 @@ package android.tv {
|
||||
field public static final java.lang.String SERVICE_INTERFACE = "android.tv.TvInputService";
|
||||
}
|
||||
|
||||
public abstract class TvInputService.TvInputSessionImpl {
|
||||
public abstract class TvInputService.TvInputSessionImpl implements android.view.KeyEvent.Callback {
|
||||
ctor public TvInputService.TvInputSessionImpl();
|
||||
method public android.view.View onCreateOverlayView();
|
||||
method public boolean onGenericMotionEvent(android.view.MotionEvent);
|
||||
method public boolean onKeyDown(int, android.view.KeyEvent);
|
||||
method public boolean onKeyLongPress(int, android.view.KeyEvent);
|
||||
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
|
||||
method public boolean onKeyUp(int, android.view.KeyEvent);
|
||||
method public abstract void onRelease();
|
||||
method public abstract boolean onSetSurface(android.view.Surface);
|
||||
method public abstract void onSetVolume(float);
|
||||
method public boolean onTouchEvent(android.view.MotionEvent);
|
||||
method public boolean onTrackballEvent(android.view.MotionEvent);
|
||||
method public abstract boolean onTune(android.net.Uri);
|
||||
method public void setOverlayViewEnabled(boolean);
|
||||
}
|
||||
@@ -28178,9 +28185,16 @@ package android.tv {
|
||||
ctor public TvView(android.content.Context, android.util.AttributeSet);
|
||||
ctor public TvView(android.content.Context, android.util.AttributeSet, int);
|
||||
method public void bindTvInput(android.content.ComponentName, android.tv.TvInputManager.SessionCreateCallback);
|
||||
method public boolean dispatchUnhandledInputEvent(android.view.InputEvent);
|
||||
method public boolean onUnhandledInputEvent(android.view.InputEvent);
|
||||
method public void setOnUnhandledInputEventListener(android.tv.TvView.OnUnhandledInputEventListener);
|
||||
method public void unbindTvInput();
|
||||
}
|
||||
|
||||
public static abstract interface TvView.OnUnhandledInputEventListener {
|
||||
method public abstract boolean onUnhandledInputEvent(android.view.InputEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package android.util {
|
||||
|
||||
@@ -18,6 +18,7 @@ package android.tv;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.tv.ITvInputSession;
|
||||
import android.view.InputChannel;
|
||||
|
||||
/**
|
||||
* Interface a client of the ITvInputManager implements, to identify itself and receive information
|
||||
@@ -25,6 +26,6 @@ import android.tv.ITvInputSession;
|
||||
* @hide
|
||||
*/
|
||||
oneway interface ITvInputClient {
|
||||
void onSessionCreated(in ComponentName name, IBinder token, int seq);
|
||||
void onSessionCreated(in ComponentName name, IBinder token, in InputChannel channel, int seq);
|
||||
void onAvailabilityChanged(in ComponentName name, boolean isAvailable);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package android.tv;
|
||||
|
||||
import android.tv.ITvInputServiceCallback;
|
||||
import android.tv.ITvInputSessionCallback;
|
||||
import android.view.InputChannel;
|
||||
|
||||
/**
|
||||
* Top-level interface to a TV input component (implemented in a Service).
|
||||
@@ -26,5 +27,5 @@ import android.tv.ITvInputSessionCallback;
|
||||
oneway interface ITvInputService {
|
||||
void registerCallback(ITvInputServiceCallback callback);
|
||||
void unregisterCallback(in ITvInputServiceCallback callback);
|
||||
void createSession(ITvInputSessionCallback callback);
|
||||
void createSession(in InputChannel channel, ITvInputSessionCallback callback);
|
||||
}
|
||||
|
||||
@@ -20,9 +20,16 @@ import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.tv.TvInputManager.Session;
|
||||
import android.tv.TvInputService.TvInputSessionImpl;
|
||||
import android.util.Log;
|
||||
import android.view.InputChannel;
|
||||
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.HandlerCaller;
|
||||
@@ -45,50 +52,66 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand
|
||||
private static final int DO_RELAYOUT_OVERLAY_VIEW = 6;
|
||||
private static final int DO_REMOVE_OVERLAY_VIEW = 7;
|
||||
|
||||
private TvInputSessionImpl mTvInputSession;
|
||||
private final HandlerCaller mCaller;
|
||||
|
||||
public ITvInputSessionWrapper(Context context, TvInputSessionImpl session) {
|
||||
private TvInputSessionImpl mTvInputSessionImpl;
|
||||
private InputChannel mChannel;
|
||||
private TvInputEventReceiver mReceiver;
|
||||
|
||||
public ITvInputSessionWrapper(Context context, TvInputSessionImpl sessionImpl,
|
||||
InputChannel channel) {
|
||||
mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */);
|
||||
mTvInputSession = session;
|
||||
mTvInputSessionImpl = sessionImpl;
|
||||
mChannel = channel;
|
||||
if (channel != null) {
|
||||
mReceiver = new TvInputEventReceiver(channel, context.getMainLooper());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeMessage(Message msg) {
|
||||
if (mTvInputSession == null) {
|
||||
if (mTvInputSessionImpl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.what) {
|
||||
case DO_RELEASE: {
|
||||
mTvInputSession.release();
|
||||
mTvInputSession = null;
|
||||
mTvInputSessionImpl.release();
|
||||
mTvInputSessionImpl = null;
|
||||
if (mReceiver != null) {
|
||||
mReceiver.dispose();
|
||||
mReceiver = null;
|
||||
}
|
||||
if (mChannel != null) {
|
||||
mChannel.dispose();
|
||||
mChannel = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case DO_SET_SURFACE: {
|
||||
mTvInputSession.setSurface((Surface) msg.obj);
|
||||
mTvInputSessionImpl.setSurface((Surface) msg.obj);
|
||||
return;
|
||||
}
|
||||
case DO_SET_VOLUME: {
|
||||
mTvInputSession.setVolume((Float) msg.obj);
|
||||
mTvInputSessionImpl.setVolume((Float) msg.obj);
|
||||
return;
|
||||
}
|
||||
case DO_TUNE: {
|
||||
mTvInputSession.tune((Uri) msg.obj);
|
||||
mTvInputSessionImpl.tune((Uri) msg.obj);
|
||||
return;
|
||||
}
|
||||
case DO_CREATE_OVERLAY_VIEW: {
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
mTvInputSession.createOverlayView((IBinder) args.arg1, (Rect) args.arg2);
|
||||
mTvInputSessionImpl.createOverlayView((IBinder) args.arg1, (Rect) args.arg2);
|
||||
args.recycle();
|
||||
return;
|
||||
}
|
||||
case DO_RELAYOUT_OVERLAY_VIEW: {
|
||||
mTvInputSession.relayoutOverlayView((Rect) msg.obj);
|
||||
mTvInputSessionImpl.relayoutOverlayView((Rect) msg.obj);
|
||||
return;
|
||||
}
|
||||
case DO_REMOVE_OVERLAY_VIEW: {
|
||||
mTvInputSession.removeOverlayView(true);
|
||||
mTvInputSessionImpl.removeOverlayView(true);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
@@ -133,4 +156,24 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand
|
||||
public void removeOverlayView() {
|
||||
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_OVERLAY_VIEW));
|
||||
}
|
||||
|
||||
private final class TvInputEventReceiver extends InputEventReceiver {
|
||||
public TvInputEventReceiver(InputChannel inputChannel, Looper looper) {
|
||||
super(inputChannel, looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputEvent(InputEvent event) {
|
||||
if (mTvInputSessionImpl == null) {
|
||||
// The session has been finished.
|
||||
finishInputEvent(event, false);
|
||||
return;
|
||||
}
|
||||
|
||||
int handled = mTvInputSessionImpl.dispatchInputEvent(event, this);
|
||||
if (handled != Session.DISPATCH_IN_PROGRESS) {
|
||||
finishInputEvent(event, handled == Session.DISPATCH_HANDLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,16 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
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.Pool;
|
||||
import android.util.Pools.SimplePool;
|
||||
import android.util.SparseArray;
|
||||
import android.view.InputChannel;
|
||||
import android.view.InputEvent;
|
||||
import android.view.InputEventSender;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
|
||||
@@ -138,7 +145,8 @@ public final class TvInputManager {
|
||||
mUserId = userId;
|
||||
mClient = new ITvInputClient.Stub() {
|
||||
@Override
|
||||
public void onSessionCreated(ComponentName name, IBinder token, int seq) {
|
||||
public void onSessionCreated(ComponentName name, IBinder token, InputChannel channel,
|
||||
int seq) {
|
||||
synchronized (mSessionCreateCallbackRecordMap) {
|
||||
SessionCreateCallbackRecord record = mSessionCreateCallbackRecordMap.get(seq);
|
||||
mSessionCreateCallbackRecordMap.delete(seq);
|
||||
@@ -148,7 +156,7 @@ public final class TvInputManager {
|
||||
}
|
||||
Session session = null;
|
||||
if (token != null) {
|
||||
session = new Session(name, token, mService, mUserId);
|
||||
session = new Session(token, channel, mService, mUserId);
|
||||
}
|
||||
record.postSessionCreated(session);
|
||||
}
|
||||
@@ -321,13 +329,30 @@ public final class TvInputManager {
|
||||
|
||||
/** The Session provides the per-session functionality of TV inputs. */
|
||||
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 ITvInputManager mService;
|
||||
private final int mUserId;
|
||||
|
||||
// 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 final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20);
|
||||
private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20);
|
||||
|
||||
private IBinder mToken;
|
||||
private TvInputEventSender mSender;
|
||||
private InputChannel mChannel;
|
||||
|
||||
/** @hide */
|
||||
private Session(ComponentName name, IBinder token, ITvInputManager service, int userId) {
|
||||
private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId) {
|
||||
mToken = token;
|
||||
mChannel = channel;
|
||||
mService = service;
|
||||
mUserId = userId;
|
||||
}
|
||||
@@ -347,6 +372,18 @@ public final class TvInputManager {
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
synchronized (mHandler) {
|
||||
if (mChannel != null) {
|
||||
if (mSender != null) {
|
||||
flushPendingEventsLocked();
|
||||
mSender.dispose();
|
||||
mSender = null;
|
||||
}
|
||||
mChannel.dispose();
|
||||
mChannel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,5 +515,228 @@ public final class TvInputManager {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an input event to this session.
|
||||
*
|
||||
* @param event {@link InputEvent} to dispatch.
|
||||
* @param token A token used to identify the input event later in the callback.
|
||||
* @param callback A callback used to receive the dispatch result.
|
||||
* @param handler {@link Handler} that the dispatch result will be delivered to.
|
||||
* @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.
|
||||
* @throws IllegalArgumentException if any of the necessary arguments is {@code null}.
|
||||
* @hide
|
||||
*/
|
||||
public int dispatchInputEvent(InputEvent event, Object token,
|
||||
FinishedInputEventCallback callback, Handler handler) {
|
||||
if (event == null) {
|
||||
throw new IllegalArgumentException("event cannot be null");
|
||||
}
|
||||
if (callback != null && handler == null) {
|
||||
throw new IllegalArgumentException("handler cannot be null");
|
||||
}
|
||||
synchronized (mHandler) {
|
||||
if (mChannel == 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void onFinishedInputEvent(Object token, boolean handled);
|
||||
}
|
||||
|
||||
// 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 (mChannel != null) {
|
||||
if (mSender == null) {
|
||||
mSender = new TvInputEventSender(mChannel, 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 seesion to handle input event after "
|
||||
+ INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
|
||||
} else {
|
||||
mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
|
||||
}
|
||||
}
|
||||
|
||||
invokeFinishedInputEventCallback(p, handled);
|
||||
}
|
||||
|
||||
// Assumes the event has already been removed from the queue.
|
||||
void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
|
||||
p.mHandled = handled;
|
||||
if (p.mHandler.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.mHandler, p);
|
||||
msg.setAsynchronous(true);
|
||||
msg.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
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 PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
|
||||
FinishedInputEventCallback callback, Handler handler) {
|
||||
PendingEvent p = mPendingEventPool.acquire();
|
||||
if (p == null) {
|
||||
p = new PendingEvent();
|
||||
}
|
||||
p.mEvent = event;
|
||||
p.mToken = token;
|
||||
p.mCallback = callback;
|
||||
p.mHandler = handler;
|
||||
return p;
|
||||
}
|
||||
|
||||
private void recyclePendingEventLocked(PendingEvent p) {
|
||||
p.recycle();
|
||||
mPendingEventPool.release(p);
|
||||
}
|
||||
|
||||
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 {
|
||||
public 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 mToken;
|
||||
public FinishedInputEventCallback mCallback;
|
||||
public Handler mHandler;
|
||||
public boolean mHandled;
|
||||
|
||||
public void recycle() {
|
||||
mEvent = null;
|
||||
mToken = null;
|
||||
mCallback = null;
|
||||
mHandler = null;
|
||||
mHandled = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mCallback.onFinishedInputEvent(mToken, mHandled);
|
||||
|
||||
synchronized (mHandler) {
|
||||
recyclePendingEventLocked(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,21 @@ import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.RemoteCallbackList;
|
||||
import android.os.RemoteException;
|
||||
import android.tv.TvInputManager.Session;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
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 android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.os.SomeArgs;
|
||||
|
||||
/**
|
||||
* A base class for implementing television input service.
|
||||
@@ -89,10 +97,17 @@ public abstract class TvInputService extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createSession(ITvInputSessionCallback cb) {
|
||||
if (cb != null) {
|
||||
mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, cb).sendToTarget();
|
||||
public void createSession(InputChannel channel, ITvInputSessionCallback cb) {
|
||||
if (channel == null) {
|
||||
Log.w(TAG, "Creating session without input channel");
|
||||
}
|
||||
if (cb == null) {
|
||||
return;
|
||||
}
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.arg1 = channel;
|
||||
args.arg2 = cb;
|
||||
mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -131,7 +146,8 @@ public abstract class TvInputService extends Service {
|
||||
/**
|
||||
* Base class for derived classes to implement to provide {@link TvInputManager.Session}.
|
||||
*/
|
||||
public abstract class TvInputSessionImpl {
|
||||
public abstract class TvInputSessionImpl implements KeyEvent.Callback {
|
||||
private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
|
||||
private final WindowManager mWindowManager;
|
||||
private WindowManager.LayoutParams mWindowParams;
|
||||
private View mOverlayView;
|
||||
@@ -143,6 +159,13 @@ public abstract class TvInputService extends Service {
|
||||
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the overlay view. By default, the overlay view is disabled. Must be
|
||||
* called explicitly after the session is created to enable the overlay view.
|
||||
*
|
||||
* @param enable {@code true} if you want to enable the overlay view. {@code false}
|
||||
* otherwise.
|
||||
*/
|
||||
public void setOverlayViewEnabled(final boolean enable) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
@@ -202,6 +225,121 @@ public abstract class TvInputService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent)
|
||||
* KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event).
|
||||
* <p>
|
||||
* Override this to intercept key down events before they are processed by the application.
|
||||
* If you return true, the application will not process the event itself. If you return
|
||||
* false, the normal application processing will occur as if the TV input had not seen the
|
||||
* event at all.
|
||||
*
|
||||
* @param keyCode The value in event.getKeyCode().
|
||||
* @param event Description of the key event.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of
|
||||
* {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
|
||||
* KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event).
|
||||
* <p>
|
||||
* Override this to intercept key long press events before they are processed by the
|
||||
* application. If you return true, the application will not process the event itself. If
|
||||
* you return false, the normal application processing will occur as if the TV input had not
|
||||
* seen the event at all.
|
||||
*
|
||||
* @param keyCode The value in event.getKeyCode().
|
||||
* @param event Description of the key event.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of
|
||||
* {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
|
||||
* KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event).
|
||||
* <p>
|
||||
* Override this to intercept special key multiple events before they are processed by the
|
||||
* application. If you return true, the application will not itself process the event. If
|
||||
* you return false, the normal application processing will occur as if the TV input had not
|
||||
* seen the event at all.
|
||||
*
|
||||
* @param keyCode The value in event.getKeyCode().
|
||||
* @param count The number of times the action was made.
|
||||
* @param event Description of the key event.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent)
|
||||
* KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event).
|
||||
* <p>
|
||||
* Override this to intercept key up events before they are processed by the application. If
|
||||
* you return true, the application will not itself process the event. If you return false,
|
||||
* the normal application processing will occur as if the TV input had not seen the event at
|
||||
* all.
|
||||
*
|
||||
* @param keyCode The value in event.getKeyCode().
|
||||
* @param event Description of the key event.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this method to handle touch screen motion events on the current input session.
|
||||
*
|
||||
* @param event The motion event being received.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
* @see View#onTouchEvent
|
||||
*/
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this method to handle trackball events on the current input session.
|
||||
*
|
||||
* @param event The motion event being received.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
* @see View#onTrackballEvent
|
||||
*/
|
||||
public boolean onTrackballEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this method to handle generic motion events on the current input session.
|
||||
*
|
||||
* @param event The motion event being received.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
* @see View#onGenericMotionEvent
|
||||
*/
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the application would like to stop using the current input
|
||||
* session.
|
||||
@@ -212,7 +350,7 @@ public abstract class TvInputService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link onSetSurface}.
|
||||
* Calls {@link #onSetSurface}.
|
||||
*/
|
||||
void setSurface(Surface surface) {
|
||||
onSetSurface(surface);
|
||||
@@ -220,14 +358,14 @@ public abstract class TvInputService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link onSetVolume}.
|
||||
* Calls {@link #onSetVolume}.
|
||||
*/
|
||||
void setVolume(float volume) {
|
||||
onSetVolume(volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link onTune}.
|
||||
* Calls {@link #onTune}.
|
||||
*/
|
||||
void tune(Uri channelUri) {
|
||||
onTune(channelUri);
|
||||
@@ -235,8 +373,8 @@ public abstract class TvInputService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an overlay view. This calls {@link onCreateOverlayView} to get
|
||||
* a view to attach to the overlay window.
|
||||
* Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach
|
||||
* to the overlay window.
|
||||
*
|
||||
* @param windowToken A window token of an application.
|
||||
* @param frame A position of the overlay view.
|
||||
@@ -314,6 +452,42 @@ public abstract class TvInputService extends Service {
|
||||
mWindowParams = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (((KeyEvent) event).dispatch(this, mDispatcherState, this)) {
|
||||
return Session.DISPATCH_HANDLED;
|
||||
}
|
||||
} else if (event instanceof MotionEvent) {
|
||||
MotionEvent motionEvent = (MotionEvent) event;
|
||||
final int source = motionEvent.getSource();
|
||||
if (motionEvent.isTouchEvent()) {
|
||||
if (onTouchEvent(motionEvent)) {
|
||||
return Session.DISPATCH_HANDLED;
|
||||
}
|
||||
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
|
||||
if (onTrackballEvent(motionEvent)) {
|
||||
return Session.DISPATCH_HANDLED;
|
||||
}
|
||||
} else {
|
||||
if (onGenericMotionEvent(motionEvent)) {
|
||||
return Session.DISPATCH_HANDLED;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mOverlayView == null) {
|
||||
return Session.DISPATCH_NOT_HANDLED;
|
||||
}
|
||||
if (!mOverlayView.hasWindowFocus()) {
|
||||
mOverlayView.getViewRootImpl().windowFocusChanged(true, true);
|
||||
}
|
||||
mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver);
|
||||
return Session.DISPATCH_IN_PROGRESS;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ServiceHandler extends Handler {
|
||||
@@ -324,20 +498,23 @@ public abstract class TvInputService extends Service {
|
||||
public final void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case DO_CREATE_SESSION: {
|
||||
ITvInputSessionCallback cb = (ITvInputSessionCallback) msg.obj;
|
||||
SomeArgs args = (SomeArgs) msg.obj;
|
||||
InputChannel channel = (InputChannel) args.arg1;
|
||||
ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2;
|
||||
try {
|
||||
TvInputSessionImpl sessionImpl = onCreateSession();
|
||||
if (sessionImpl == null) {
|
||||
// Failed to create a session.
|
||||
cb.onSessionCreated(null);
|
||||
return;
|
||||
} else {
|
||||
ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this,
|
||||
sessionImpl, channel);
|
||||
cb.onSessionCreated(stub);
|
||||
}
|
||||
ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this,
|
||||
sessionImpl);
|
||||
cb.onSessionCreated(stub);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "error in onSessionCreated");
|
||||
}
|
||||
args.recycle();
|
||||
return;
|
||||
}
|
||||
case DO_BROADCAST_AVAILABILITY_CHANGE: {
|
||||
|
||||
@@ -20,20 +20,24 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.tv.TvInputManager;
|
||||
import android.tv.TvInputManager.Session;
|
||||
import android.tv.TvInputManager.Session.FinishedInputEventCallback;
|
||||
import android.tv.TvInputManager.SessionCreateCallback;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
/**
|
||||
* View playing TV
|
||||
*/
|
||||
public class TvView extends SurfaceView {
|
||||
// STOPSHIP: Turn debugging off.
|
||||
private static final boolean DEBUG = true;
|
||||
private static final String TAG = "TvView";
|
||||
|
||||
private final Handler mHandler = new Handler();
|
||||
@@ -41,11 +45,11 @@ public class TvView extends SurfaceView {
|
||||
private Surface mSurface;
|
||||
private boolean mOverlayViewCreated;
|
||||
private Rect mOverlayViewFrame;
|
||||
private boolean mGlobalListenersAdded;
|
||||
private TvInputManager mTvInputManager;
|
||||
private final TvInputManager mTvInputManager;
|
||||
private SessionCreateCallback mSessionCreateCallback;
|
||||
private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
|
||||
|
||||
private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
|
||||
private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width
|
||||
@@ -70,6 +74,25 @@ public class TvView extends SurfaceView {
|
||||
}
|
||||
};
|
||||
|
||||
private final FinishedInputEventCallback mFinishedInputEventCallback =
|
||||
new FinishedInputEventCallback() {
|
||||
@Override
|
||||
public void onFinishedInputEvent(Object token, boolean handled) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")");
|
||||
}
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
// TODO: Re-order unhandled events.
|
||||
InputEvent event = (InputEvent) token;
|
||||
if (dispatchUnhandledInputEvent(event)) {
|
||||
return;
|
||||
}
|
||||
getViewRootImpl().dispatchUnhandledInputEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
public TvView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
@@ -124,6 +147,98 @@ public class TvView extends SurfaceView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an unhandled input event to the next receiver.
|
||||
* <p>
|
||||
* Except system keys, TvView always consumes input events in the normal flow. This is called
|
||||
* asynchronously from where the event is dispatched. It gives the host application a chance to
|
||||
* dispatch the unhandled input events.
|
||||
*
|
||||
* @param event The input event.
|
||||
* @return {@code true} if the event was handled by the view, {@code false} otherwise.
|
||||
*/
|
||||
public boolean dispatchUnhandledInputEvent(InputEvent event) {
|
||||
if (mOnUnhandledInputEventListener != null) {
|
||||
if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return onUnhandledInputEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an unhandled input event was also not handled by the user provided callback. This
|
||||
* is the last chance to handle the unhandled input event in the TvView.
|
||||
*
|
||||
* @param event The input event.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to be
|
||||
* handled by the next receiver, return {@code false}.
|
||||
*/
|
||||
public boolean onUnhandledInputEvent(InputEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be invoked when an input event was not handled by the bound TV input.
|
||||
*
|
||||
* @param listener The callback to invoke when the unhandled input event was received.
|
||||
*/
|
||||
public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
|
||||
mOnUnhandledInputEventListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (super.dispatchKeyEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
|
||||
if (mSession == null) {
|
||||
return false;
|
||||
}
|
||||
int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler);
|
||||
return ret != Session.DISPATCH_NOT_HANDLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||
if (super.dispatchTouchEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
|
||||
if (mSession == null) {
|
||||
return false;
|
||||
}
|
||||
int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler);
|
||||
return ret != Session.DISPATCH_NOT_HANDLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTrackballEvent(MotionEvent event) {
|
||||
if (super.dispatchTrackballEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
|
||||
if (mSession == null) {
|
||||
return false;
|
||||
}
|
||||
int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler);
|
||||
return ret != Session.DISPATCH_NOT_HANDLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent event) {
|
||||
if (super.dispatchGenericMotionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
|
||||
if (mSession == null) {
|
||||
return false;
|
||||
}
|
||||
int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler);
|
||||
return ret != Session.DISPATCH_NOT_HANDLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
@@ -196,6 +311,23 @@ public class TvView extends SurfaceView {
|
||||
location[0] + getWidth(), location[1] + getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when the unhandled input event is received.
|
||||
*/
|
||||
public interface OnUnhandledInputEventListener {
|
||||
/**
|
||||
* Called when an input event was not handled by the bound TV input.
|
||||
* <p>
|
||||
* This is called asynchronously from where the event is dispatched. It gives the host
|
||||
* application a chance to handle the unhandled input events.
|
||||
*
|
||||
* @param event The input event.
|
||||
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
||||
* be handled by the next receiver, return {@code false}.
|
||||
*/
|
||||
boolean onUnhandledInputEvent(InputEvent event);
|
||||
}
|
||||
|
||||
private class MySessionCreateCallback implements SessionCreateCallback {
|
||||
final SessionCreateCallback mExternalCallback;
|
||||
|
||||
|
||||
@@ -3212,8 +3212,11 @@ public final class ViewRootImpl implements ViewParent,
|
||||
doDie();
|
||||
break;
|
||||
case MSG_DISPATCH_INPUT_EVENT: {
|
||||
InputEvent event = (InputEvent)msg.obj;
|
||||
enqueueInputEvent(event, null, 0, true);
|
||||
SomeArgs args = (SomeArgs)msg.obj;
|
||||
InputEvent event = (InputEvent)args.arg1;
|
||||
InputEventReceiver receiver = (InputEventReceiver)args.arg2;
|
||||
enqueueInputEvent(event, receiver, 0, true);
|
||||
args.recycle();
|
||||
} break;
|
||||
case MSG_DISPATCH_KEY_FROM_IME: {
|
||||
if (LOCAL_LOGV) Log.v(
|
||||
@@ -5795,7 +5798,14 @@ public final class ViewRootImpl implements ViewParent,
|
||||
}
|
||||
|
||||
public void dispatchInputEvent(InputEvent event) {
|
||||
Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, event);
|
||||
dispatchInputEvent(event, null);
|
||||
}
|
||||
|
||||
public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
|
||||
SomeArgs args = SomeArgs.obtain();
|
||||
args.arg1 = event;
|
||||
args.arg2 = receiver;
|
||||
Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
|
||||
msg.setAsynchronous(true);
|
||||
mHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ import android.tv.ITvInputSession;
|
||||
import android.tv.ITvInputSessionCallback;
|
||||
import android.tv.TvInputInfo;
|
||||
import android.tv.TvInputService;
|
||||
import android.util.Log;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArray;
|
||||
import android.view.InputChannel;
|
||||
import android.view.Surface;
|
||||
|
||||
import com.android.internal.content.PackageMonitor;
|
||||
@@ -292,6 +292,9 @@ public final class TvInputManagerService extends SystemService {
|
||||
Slog.d(TAG, "createSessionInternalLocked(name=" + sessionState.name.getClassName()
|
||||
+ ")");
|
||||
}
|
||||
|
||||
final InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
|
||||
|
||||
// Set up a callback to send the session token.
|
||||
ITvInputSessionCallback callback = new ITvInputSessionCallback.Stub() {
|
||||
@Override
|
||||
@@ -304,30 +307,32 @@ public final class TvInputManagerService extends SystemService {
|
||||
if (session == null) {
|
||||
removeSessionStateLocked(sessionToken, userId);
|
||||
sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null,
|
||||
sessionState.seq, userId);
|
||||
null, sessionState.seq, userId);
|
||||
} else {
|
||||
sendSessionTokenToClientLocked(sessionState.client, sessionState.name,
|
||||
sessionToken, sessionState.seq, userId);
|
||||
sessionToken, channels[0], sessionState.seq, userId);
|
||||
}
|
||||
channels[0].dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a session. When failed, send a null token immediately.
|
||||
try {
|
||||
service.createSession(callback);
|
||||
service.createSession(channels[1], callback);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "error in createSession", e);
|
||||
removeSessionStateLocked(sessionToken, userId);
|
||||
sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null,
|
||||
sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null, null,
|
||||
sessionState.seq, userId);
|
||||
}
|
||||
channels[1].dispose();
|
||||
}
|
||||
|
||||
private void sendSessionTokenToClientLocked(ITvInputClient client, ComponentName name,
|
||||
IBinder sessionToken, int seq, int userId) {
|
||||
IBinder sessionToken, InputChannel channel, int seq, int userId) {
|
||||
try {
|
||||
client.onSessionCreated(name, sessionToken, seq);
|
||||
client.onSessionCreated(name, sessionToken, channel, seq);
|
||||
} catch (RemoteException exception) {
|
||||
Slog.e(TAG, "error in onSessionCreated", exception);
|
||||
}
|
||||
@@ -834,7 +839,7 @@ public final class TvInputManagerService extends SystemService {
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
Log.w(TAG, "Unhandled message code: " + msg.what);
|
||||
Slog.w(TAG, "Unhandled message code: " + msg.what);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user