/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* 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.example.google.tv.anymotelibrary.client;
import com.google.anymote.Key.Action;
import com.google.anymote.Key.Code;
import com.google.anymote.Messages.DataItem;
import com.google.anymote.Messages.DataList;
import com.google.anymote.Messages.FlingResult;
import com.google.anymote.common.AnymoteFactory;
import com.google.anymote.common.ConnectInfo;
import com.google.anymote.common.ErrorListener;
import com.google.anymote.device.DeviceAdapter;
import com.google.anymote.device.MessageReceiver;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.example.google.tv.anymotelibrary.connection.AckManager;
import com.example.google.tv.anymotelibrary.connection.AckManager.Listener;
import com.example.google.tv.anymotelibrary.connection.ConnectingTask;
import com.example.google.tv.anymotelibrary.util.KeyEventTranslator;
import java.io.IOException;
import javax.net.ssl.SSLSocket;
/**
* A proxy class that sends messages to the Anymote server using Anymote
* protocol.
*/
public final class AnymoteSender implements MessageReceiver {
private static final String LOG_TAG = AnymoteSender.class.getSimpleName();
/** Data type used to send a string in a data message. */
private static final String DATA_TYPE_STRING = "com.google.tv.string";
/** Device name used upon connection. */
private static final String DEVICE_NAME = "android";
/** Manages connection to the server */
private final ConnectingTask connectingTask;
/** Error listener for Anymote protocol */
private final ErrorListener errorListener;
/** Sender for Anymote protocol */
private DeviceAdapter deviceAdapter;
/** ACK manager (ping etc) */
private AckManager ackManager;
/** Remote device protocol version number */
private int deviceVersion;
private MessageSenderThread mMessageSenderThread;
private static final int KEY = 1;
private static final int KEYPRESS = 2;
private static final int SCROLL = 3;
private static final int DATA = 4;
private static final int URL = 5;
private static final int CLICK = 6;
private static final int MOUSEMOVE = 7;
private static final int CONNECT = 8;
private static final int PING = 9;
private class AnymoteKeyEvent {
Code code;
Action action;
AnymoteKeyEvent(Code code, Action action) {
this.code = code;
this.action = action;
}
}
/**
* Constructor
*
* @param task The Thread that handles connectivity to the TV device.
*/
public AnymoteSender(ConnectingTask task) {
connectingTask = task;
errorListener = new ErrorListener() {
public void onIoError(String message, Throwable exception) {
Log.d(LOG_TAG, "IoError: " + message, exception);
onConnectionError();
}
};
ackManager = new AckManager(new Listener() {
public void onTimeout() {
ackManager.stop();
onConnectionError();
}
}, this);
mMessageSenderThread = new MessageSenderThread();
mMessageSenderThread.start();
}
/**
* Attempts to establish connection with the Anymote service socket.
*
* @param sslSocket TV device socket.
* @return boolean indicating if connection to socket was successful.
*/
public boolean attemptToConnect(final SSLSocket sslSocket) {
if (sslSocket == null) {
throw new NullPointerException("null socket");
}
return instantiateProtocol(sslSocket);
}
private boolean instantiateProtocol(SSLSocket sslSocket) {
disconnect();
try {
deviceAdapter = AnymoteFactory.getDeviceAdapter(
this, sslSocket.getInputStream(), sslSocket.getOutputStream(), errorListener);
} catch (IOException e) {
Log.d(LOG_TAG, "Unable to create sender", e);
deviceAdapter = null;
return false;
}
sendConnect();
ackManager.start();
return true;
}
/**
* Disconnects from Anymote service.
*
* @return boolean indicating if the device was successfully disconnected.
*/
public synchronized boolean disconnect() {
ackManager.stop();
if (deviceAdapter != null) {
deviceAdapter.stop();
deviceAdapter = null;
return true;
}
return false;
}
/**
* Destroys the connection to anymote service.
*/
public void destroy() {
disconnect();
ackManager.quit();
}
private void onConnectionError() {
if (disconnect()) {
connectingTask.onConnectionDisconnected();
}
}
/**
* Sends click event to Anymote service.
*
* @param action
*/
public void sendClick(final Action action) {
final Message msg = Message.obtain();
msg.obj = action;
msg.what = CLICK;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Sends Url to Anymote service. This is url is a serialzed representation
* of Intent.
*
* @param url
*/
public void sendUrl(final String url) {
final Message msg = Message.obtain();
msg.obj = url;
msg.what = URL;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Sends a sequence of keystrokes in String format to Anymote service.
* Example input: "AHDFSDF".
*
* @param url
*/
public void sendData(final String data) {
final Message msg = Message.obtain();
msg.obj = data;
msg.what = URL;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Converts Intent to String and sends it to deviceAdapter.
*
* @param intent The Intent to be sent to Anymote service.
*/
public void sendIntent(Intent intent) {
sendUrl(intent.toUri(Intent.URI_INTENT_SCHEME));
}
/**
* Converts Android KeyEvent.KeyCode to Anymote Key.Code and sends it to
* deviceAdapter.
*
* @param keyEvent The key event to be sent to Anymote service.
*/
public void sendKeyPress(int keyEvent) {
sendKeyPress(KeyEventTranslator.fromKeyEvent(keyEvent));
}
/**
* Sends key to Anymote service.
*
* @param keycode The keycode of the key to be sent.
* @param action The key up/down action.
*/
public void sendKey(final Code keycode, final Action action) {
final Message msg = Message.obtain();
msg.obj = new AnymoteKeyEvent(keycode, action);
msg.what = KEY;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Sends key press event to Anymote service.
*
* @param key code of the key that was pressed.
*/
public void sendKeyPress(final Code key) {
final Message msg = Message.obtain();
msg.obj = key;
msg.what = KEYPRESS;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Sends relative mouse move event to Anymote service.
*
* @param deltaX the delta between intial and final x positions of the the
* mouse movement.
* @param deltaY the delta between intial and final y positions of the the
* mouse movement.
*/
public void sendMoveRelative(final int deltaX, final int deltaY) {
final Message msg = Message.obtain();
msg.arg1 = deltaX;
msg.arg2 = deltaY;
msg.what = MOUSEMOVE;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Sends scroll event to Anymote service.
*
* @param deltaX the delta between intial and final x positions of the the
* scroll movement.
* @param deltaY the delta between intial and final y positions of the the
* scroll movement.
*/
public void sendScroll(final int deltaX, final int deltaY) {
final Message msg = Message.obtain();
msg.arg1 = deltaX;
msg.arg2 = deltaY;
msg.what = SCROLL;
mMessageSenderThread.mHandler.sendMessage(msg);
}
/**
* Sends ping to Anymote service to monitor connection state.
*/
public void sendPing() {
final Message msg = Message.obtain();
msg.what = PING;
mMessageSenderThread.mHandler.sendMessage(msg);
}
private void sendConnect() {
final Message msg = Message.obtain();
msg.what = CONNECT;
msg.obj = new ConnectInfo(DEVICE_NAME, connectingTask.getVersionCode());
mMessageSenderThread.mHandler.sendMessage(msg);
}
private class MessageSenderThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
if (deviceAdapter == null)
return;
switch (msg.what) {
case KEYPRESS:
deviceAdapter.sendKeyEvent((Code) msg.obj, Action.DOWN);
deviceAdapter.sendKeyEvent((Code) msg.obj, Action.UP);
break;
case MOUSEMOVE:
deviceAdapter.sendMouseMove(msg.arg1, msg.arg2);
break;
case CLICK:
deviceAdapter.sendKeyEvent(Code.BTN_MOUSE, (Action) msg.obj);
break;
case URL:
deviceAdapter.sendFling((String)msg.obj, 0);
break;
case DATA:
deviceAdapter.sendData(DATA_TYPE_STRING, (String)msg.obj);
break;
case KEY:
final AnymoteKeyEvent keyEvent = (AnymoteKeyEvent)msg.obj;
deviceAdapter.sendKeyEvent(keyEvent.code, keyEvent.action);
break;
case SCROLL:
deviceAdapter.sendMouseWheel(msg.arg1, msg.arg2);
break;
case PING:
deviceAdapter.sendPing();
break;
case CONNECT:
deviceAdapter.sendConnect((ConnectInfo)msg.obj);
}
}
};
Looper.loop();
}
}
public void onAck() {
ackManager.onAck();
}
public void onData(String type, String data) {
Log.d(LOG_TAG, "onData: " + type + " / " + data);
}
public void onDataList(DataList dataList) {
Log.d(LOG_TAG, "onDataList: " + dataList.getType());
for (DataItem dataItem : dataList.getItemList()) {
for (String string : dataItem.getStringFieldList()) {
Log.d(LOG_TAG, " data item [string]: " + string);
}
for (int i : dataItem.getIntFieldList()) {
Log.d(LOG_TAG, " data item [int]: " + i);
}
}
}
/**
* Called when connection to Anymote service is established.
*
* @param connectInfo The Anymote connection info.
*/
public void onConnect(final ConnectInfo connectInfo) {
Log.d(LOG_TAG, "onConnect: " + connectInfo.toString() + " "
+ connectInfo.getVersionNumber());
deviceVersion = connectInfo.getVersionNumber();
ackManager.start();
}
public void onFlingResult(FlingResult flingResult, Integer sequenceNumber) {
Log.d(LOG_TAG, "onFlingResult: " + sequenceNumber);
}
}