/**
* Copyright (C) 2013- Iordan Iordanov
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
package com.undatech.opaque;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import android.R.integer;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import org.freedesktop.gstreamer.GStreamer;
import com.undatech.opaque.input.RemoteKeyboard;
import com.undatech.opaque.input.RemoteSpicePointer;
public class SpiceCommunicator {
private HashMap<String, Integer> deviceToFdMap = new HashMap<String, Integer>();
UsbManager mUsbManager = null;
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Constants.ACTION_USB_PERMISSION.equals(action)) {
UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device != null) {
int vid = device.getVendorId();
int pid = device.getProductId();
String mapKey = Integer.toString(vid)+":"+Integer.toString(pid);
synchronized (deviceToFdMap.get(mapKey)) {
deviceToFdMap.get(mapKey).notify();
}
}
}
}
};
private final static String TAG = "SpiceCommunicator";
public native int FetchVmNames(String URI, String user, String password, String sslCaFile, boolean sslStrict);
public native int CreateOvirtSession(String uri,
String user,
String password,
String sslCaFile,
boolean sound, boolean sslStrict);
public native int StartSessionFromVvFile(String fileName, boolean sound);
public native int SpiceClientConnect(String ip,
String port,
String tport,
String password,
String ca_file,
String cert_subj,
boolean sound);
public native void SpiceClientDisconnect();
public native void SpiceButtonEvent(int x, int y, int metaState, int pointerMask);
public native void SpiceKeyEvent(boolean keyDown, int virtualKeyCode);
public native void UpdateBitmap(Bitmap bitmap, int x, int y, int w, int h);
public native void SpiceRequestResolution(int x, int y);
static {
System.loadLibrary("gstreamer_android");
System.loadLibrary("spice");
}
final static int LCONTROL = 29;
final static int RCONTROL = 285;
final static int LALT = 56;
final static int RALT = 312;
final static int LSHIFT = 42;
final static int RSHIFT = 54;
final static int LWIN = 347;
final static int RWIN = 348;
int remoteMetaState = 0;
private int width = 0;
private int height = 0;
boolean isInNormalProtocol = false;
private Thread thread = null;
public SpiceCommunicator (Context context, RemoteCanvas canvas, boolean res, boolean usb) {
this.context = context;
this.canvas = canvas;
this.isRequestingNewDisplayResolution = res;
this.usbEnabled = usb;
myself = this;
mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
try {
GStreamer.init(context);
} catch (Exception e) {
e.printStackTrace();
canvas.displayShortToastMessage(e.getMessage());
}
}
private static SpiceCommunicator myself = null;
private RemoteCanvas canvas = null;
private CanvasDrawableContainer canvasDrawable = null;
private Handler handler = null;
private ArrayList<String> vmNames = null;
private boolean isRequestingNewDisplayResolution = false;
private boolean usbEnabled = false;
private Context context = null;
public void setHandler(Handler handler) {
this.handler = handler;
}
public Handler getHandler() {
return handler;
}
public ArrayList<String> getVmNames() {
return vmNames;
}
/**
* Launches a new thread which performs a plain SPICE connection.
*/
public void connectSpice(String ip, String port, String tport, String password, String cf, String cs, boolean sound) {
android.util.Log.e(TAG, "connectSpice: " + ip + ", " + port + ", " + tport + ", " + cf + ", " + cs);
thread = new SpiceThread(ip, port, tport, password, cf, cs, sound);
thread.start();
}
/**
* Launches a new thread which performs an oVirt/RHEV session connection
*/
public void connectOvirt(String ip, String vmname,
String user, String password,
String sslCaFile,
boolean sound, boolean sslStrict) {
android.util.Log.e(TAG, "connectOvirt: " + ip + ", " + vmname + ", " + user);
thread = new OvirtThread(ip, vmname, user, password, sslCaFile, sound, sslStrict);
thread.start();
}
/**
* Connects to an oVirt/RHEV server to fetch the names of all VMs available to the specified user.
*/
public int startSessionFromVvFile (String vvFileName, boolean sound) {
return StartSessionFromVvFile(vvFileName, sound);
}
/**
* Connects to an oVirt/RHEV server to fetch the names of all VMs available to the specified user.
*/
public int fetchOvirtVmNames (String ip, String user, String password, String sslCaFile, boolean sslStrict) {
vmNames = new ArrayList<String>();
return FetchVmNames("https://" + ip + "//api", user, password, sslCaFile, sslStrict);
}
public void disconnect() {
if (isInNormalProtocol) {
SpiceClientDisconnect();
}
if (thread != null && thread.isAlive()) {
try {thread.join(3000);} catch (InterruptedException e) {}
}
}
class SpiceThread extends Thread {
private String ip, port, tport, password, cf, cs;
boolean sound;
public SpiceThread(String ip, String port, String tport, String password, String cf, String cs, boolean sound) {
this.ip = ip;
this.port = port;
this.tport = tport;
this.password = password;
this.cf = cf;
this.cs = cs;
this.sound = sound;
}
public void run() {
SpiceClientConnect (ip, port, tport, password, cf, cs, sound);
android.util.Log.e(TAG, "SpiceClientConnect returned.");
// If we've exited SpiceClientConnect, the connection is certainly
// interrupted or was never established.
if (handler != null) {
handler.sendEmptyMessage(Constants.SPICE_CONNECT_FAILURE);
}
}
}
class OvirtThread extends Thread {
private String ip, vmname, user, password, sslCaFile;
boolean sound, sslStrict;
public OvirtThread(String ip, String vmname,
String user, String password,
String sslCaFile,
boolean sound, boolean sslStrict) {
this.ip = ip;
this.vmname = vmname;
this.user = user;
this.password = password;
this.sslCaFile = sslCaFile;
this.sound = sound;
this.sslStrict = sslStrict;
}
public void run() {
CreateOvirtSession ("ovirt://" + ip + "/" + vmname, user, password, sslCaFile, sound, sslStrict);
android.util.Log.e(TAG, "CreateOvirtSession returned.");
// If we've exited CreateOvirtSession, the connection is certainly
// interrupted or was never established.
if (handler != null) {
handler.sendEmptyMessage(Constants.SPICE_CONNECT_FAILURE);
}
}
}
public void sendMouseEvent (int x, int y, int metaState, int pointerMask) {
SpiceButtonEvent(x, y, metaState, pointerMask);
}
public void sendSpiceKeyEvent (boolean keyDown, int virtualKeyCode) {
SpiceKeyEvent(keyDown, virtualKeyCode);
}
public int framebufferWidth() {
return width;
}
public int framebufferHeight() {
return height;
}
public void setFramebufferWidth(int w) {
width = w;
}
public void setFramebufferHeight(int h) {
height = h;
}
public String desktopName() {
// TODO Auto-generated method stub
return "";
}
public void requestUpdate(boolean incremental) {
// TODO Auto-generated method stub
}
public void writeClientCutText(String text) {
// TODO Auto-generated method stub
}
public void setIsInNormalProtocol(boolean state) {
isInNormalProtocol = state;
}
public boolean isInNormalProtocol() {
return isInNormalProtocol;
}
public String getEncoding() {
// TODO Auto-generated method stub
return "";
}
public void sendPointerEvent(int x, int y, int metaState, int pointerMask) {
remoteMetaState = metaState;
if ((pointerMask & RemoteSpicePointer.POINTER_DOWN_MASK) != 0)
sendModifierKeys(true);
sendMouseEvent(x, y, metaState, pointerMask);
if ((pointerMask & RemoteSpicePointer.POINTER_DOWN_MASK) == 0)
sendModifierKeys(false);
}
private void sendModifierKeys(boolean keyDown) {
if ((remoteMetaState & RemoteKeyboard.CTRL_ON_MASK) != 0) {
android.util.Log.e("SpiceCommunicator", "Sending CTRL: " + LCONTROL + " down: " + keyDown);
sendSpiceKeyEvent(keyDown, LCONTROL);
}
if ((remoteMetaState & RemoteKeyboard.ALT_ON_MASK) != 0) {
android.util.Log.e("SpiceCommunicator", "Sending ALT: " + LALT + " down: " + keyDown);
sendSpiceKeyEvent(keyDown, LALT);
}
if ((remoteMetaState & RemoteKeyboard.ALTGR_ON_MASK) != 0) {
android.util.Log.e("SpiceCommunicator", "Sending ALTGR: " + RALT + " down: " + keyDown);
sendSpiceKeyEvent(keyDown, RALT);
}
if ((remoteMetaState & RemoteKeyboard.SUPER_ON_MASK) != 0) {
android.util.Log.e("SpiceCommunicator", "Sending SUPER: " + LWIN + " down: " + keyDown);
sendSpiceKeyEvent(keyDown, LWIN);
}
if ((remoteMetaState & RemoteKeyboard.SHIFT_ON_MASK) != 0) {
android.util.Log.e("SpiceCommunicator", "Sending SHIFT: " + LSHIFT + " down: " + keyDown);
sendSpiceKeyEvent(keyDown, LSHIFT);
}
}
public void writeKeyEvent(int key, int metaState, boolean keyDown) {
if (keyDown) {
remoteMetaState = metaState;
sendModifierKeys (true);
}
android.util.Log.e("SpiceCommunicator", "Sending scanCode: " + key + ". Is it down: " + keyDown);
sendSpiceKeyEvent(keyDown, key);
if (!keyDown) {
sendModifierKeys (false);
remoteMetaState = 0;
}
}
public void writeSetPixelFormat(int bitsPerPixel, int depth,
boolean bigEndian, boolean trueColour, int redMax, int greenMax,
int blueMax, int redShift, int greenShift, int blueShift,
boolean fGreyScale) {
// TODO Auto-generated method stub
}
public void writeFramebufferUpdateRequest(int x, int y, int w, int h, boolean b) {
// TODO Auto-generated method stub
}
public void close() {
disconnect();
}
public void requestNewResolutionIfNeeded() {
if (isInNormalProtocol) {
int currentWidth = this.width;
int currentHeight = this.height;
if (isRequestingNewDisplayResolution) {
canvas.waitUntilInflated();
int desiredWidth = canvas.getDesiredWidth();
int desiredHeight = canvas.getDesiredHeight();
if (currentWidth != desiredWidth || currentHeight != desiredHeight) {
android.util.Log.e(TAG, "Requesting new res: " + desiredWidth + "x" + desiredHeight);
SpiceRequestResolution (desiredWidth, desiredHeight);
}
}
}
}
/* Callbacks from jni and corresponding non-static methods */
public static int openUsbDevice(int vid, int pid) throws InterruptedException {
Log.i(TAG, "Attempting to open a USB device and return a file descriptor.");
if (!myself.usbEnabled || android.os.Build.VERSION.SDK_INT < 12) {
return -1;
}
String mapKey = Integer.toString(vid)+":"+Integer.toString(pid);
myself.deviceToFdMap.put(mapKey, 0);
boolean deviceFound = false;
UsbDevice device = null;
HashMap<String, UsbDevice> stringDeviceMap = null;
int timeout = Constants.usbDeviceTimeout;
while (!deviceFound && timeout > 0) {
stringDeviceMap = myself.mUsbManager.getDeviceList();
Collection<UsbDevice> usbDevices = stringDeviceMap.values();
Iterator<UsbDevice> usbDeviceIter = usbDevices.iterator();
while (usbDeviceIter.hasNext()) {
UsbDevice ud = usbDeviceIter.next();
Log.i(TAG, "DEVICE: " + ud.toString());
if (ud.getVendorId() == vid && ud.getProductId() == pid) {
Log.i(TAG, "USB device successfully matched.");
deviceFound = true;
device = ud;
break;
}
}
timeout -= 100;
SystemClock.sleep(100);
}
int fd = -1;
// If the device was located in the Java layer, we try to open it, and failing that
// we request permission and wait for it to be granted or denied, or for a timeout to occur.
if (device != null) {
UsbDeviceConnection deviceConnection = myself.mUsbManager.openDevice(device);
if (deviceConnection != null) {
fd = deviceConnection.getFileDescriptor();
} else {
// Request permission to access the device.
synchronized (myself.deviceToFdMap.get(mapKey)) {
PendingIntent mPermissionIntent = PendingIntent.getBroadcast(myself.context, 0, new Intent(Constants.ACTION_USB_PERMISSION), 0);
// TODO: Try putting this intent filter into the activity in the manifest file.
IntentFilter filter = new IntentFilter(Constants.ACTION_USB_PERMISSION);
myself.context.registerReceiver(myself.mUsbReceiver, filter);
myself.mUsbManager.requestPermission(device, mPermissionIntent);
// Wait for permission with a timeout.
myself.deviceToFdMap.get(mapKey).wait(Constants.usbDevicePermissionTimeout);
deviceConnection = myself.mUsbManager.openDevice(device);
if (deviceConnection != null) {
fd = deviceConnection.getFileDescriptor();
}
}
}
}
return fd;
}
public static void sendMessage (int message) {
android.util.Log.d(TAG, "sendMessage called with message: " + message);
myself.handler.sendEmptyMessage(message);
}
public static void sendMessageWithText (int message, String messageText) {
android.util.Log.d(TAG, "sendMessage called with message: " + messageText);
Bundle b = new Bundle();
b.putString("message", messageText);
Message m = new Message();
m.what = message;
m.setData(b);
myself.handler.sendMessage(m);
}
public static void LaunchVncViewer (String address, String port, String password) {
android.util.Log.d(TAG, "LaunchVncViewer called");
myself.handler.sendEmptyMessage(Constants.LAUNCH_VNC_VIEWER);
}
private static void AddVm (String vmname) {
android.util.Log.d(TAG, "Adding VM: " + vmname + "to list of VMs");
myself.vmNames.add(vmname);
}
public void onSettingsChanged(int width, int height, int bpp) {
android.util.Log.e(TAG, "onSettingsChanged called, wxh: " + width + "x" + height);
setFramebufferWidth(width);
setFramebufferHeight(height);
canvasDrawable = canvas.reallocateDrawable(width, height);
setIsInNormalProtocol(true);
handler.sendEmptyMessage(Constants.SPICE_CONNECT_SUCCESS);
if (isRequestingNewDisplayResolution) {
requestNewResolutionIfNeeded();
}
}
private static void OnSettingsChanged(int inst, int width, int height, int bpp) {
myself.onSettingsChanged(width, height, bpp);
}
/*
public void onGraphicsUpdate(int x, int y, int width, int height) {
//android.util.Log.e(TAG, "onGraphicsUpdate called: " + x +", " + y + " + " + width + "x" + height );
synchronized (canvasDrawable) {
UpdateBitmap(canvasDrawable.bitmap, x, y, width, height);
}
canvas.reDraw(x, y, width, height);
}
*/
private static void OnGraphicsUpdate(int inst, int x, int y, int width, int height) {
//android.util.Log.e(TAG, "onGraphicsUpdate called: " + x +", " + y + " + " + width + "x" + height );
synchronized (myself.canvasDrawable) {
myself.UpdateBitmap(myself.canvasDrawable.bitmap, x, y, width, height);
}
myself.canvas.reDraw(x, y, width, height);
//myself.onGraphicsUpdate(x, y, width, height);
}
/* END Callbacks from jni and corresponding non-static methods */
}