/**
* 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.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Timer;
import javax.crypto.NullCipher;
import javax.security.auth.login.LoginException;
import org.apache.http.HttpException;
import org.json.JSONException;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.text.ClipboardManager;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.KeyEvent;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.ImageView;
import android.widget.Toast;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap;
import android.graphics.RectF;
import com.undatech.opaque.R;
import com.undatech.opaque.SpiceCommunicator;
import com.undatech.opaque.input.RemoteKeyboard;
import com.undatech.opaque.input.RemotePointer;
import com.undatech.opaque.input.RemoteSpiceKeyboard;
import com.undatech.opaque.input.RemoteSpicePointer;
import com.undatech.opaque.proxmox.ProxmoxClient;
import com.undatech.opaque.proxmox.pojo.PveResource;
import com.undatech.opaque.proxmox.pojo.SpiceDisplay;
import com.undatech.opaque.proxmox.pojo.VmStatus;
import com.undatech.opaque.dialogs.*;
public class RemoteCanvas extends ImageView {
private final static String TAG = "RemoteCanvas";
public Handler handler;
// Current connection parameters
private ConnectionSettings settings;
// Indicates whether we intend to maintain the connection.
boolean stayConnected = true;
// Variable indicating that we are currently moving the cursor in one of the input modes.
public boolean cursorBeingMoved = false;
// The drawable of this ImageView.
public CanvasDrawableContainer myDrawable = null;
// The class that provides zooming functions to the canvas.
public CanvasZoomer canvasZoomer;
// The remote pointer and keyboard
private RemotePointer pointer;
private RemoteKeyboard keyboard;
// The class that abstracts communication with the SPICE backend.
SpiceCommunicator spicecomm;
// Map of VM names to IDs
Map<String, String> vmNameToId;
// Used to set the contents of the clipboard.
ClipboardManager clipboard;
Timer clipboardMonitorTimer;
ClipboardMonitor clipboardMonitor;
public boolean serverJustCutText = false;
// Indicates that an update from the SPICE server was received.
boolean spiceUpdateReceived = false;
/*
* These variables indicate how far the top left corner of the visible screen
* has scrolled down and to the right of the top corner of the remote desktop.
*/
int absX = 0;
int absY = 0;
/*
* How much to shift coordinates over when converting from full to view coordinates.
*/
float shiftX = 0;
float shiftY = 0;
/*
* This variable holds the height of the visible rectangle of the screen. It is used to keep track
* of how much of the screen is hidden by the soft keyboard if any.
*/
int visibleHeight = -1;
/*
* These variables contain the width and height of the display in pixels
*/
int displayWidth = 0;
int displayHeight = 0;
float displayDensity = 0;
/*
* Variable used for BB workarounds.
*/
boolean bb = false;
ProgressDialog progressDialog = null;
public RemoteCanvas(final Context context, AttributeSet attrSet) {
super(context, attrSet);
clipboard = (ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE);
final Display display = ((Activity)context).getWindow().getWindowManager().getDefaultDisplay();
displayWidth = display.getWidth();
displayHeight = display.getHeight();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
displayDensity = metrics.density;
canvasZoomer = new CanvasZoomer (this);
setScaleType(ImageView.ScaleType.MATRIX);
if (android.os.Build.MODEL.contains("BlackBerry") ||
android.os.Build.BRAND.contains("BlackBerry") ||
android.os.Build.MANUFACTURER.contains("BlackBerry")) {
bb = true;
}
progressDialog = new ProgressDialog(context);
progressDialog.setMessage(context.getString(R.string.message_please_wait));
progressDialog.setCancelable(false);
vmNameToId = new HashMap<String, String>();
}
/**
* Checks whether the device has networking and quits with an error if it doesn't.
*/
private void checkNetworkConnectivity() {
ConnectivityManager cm = (ConnectivityManager)getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork == null || !activeNetwork.isAvailable() || !activeNetwork.isConnected()) {
disconnectAndShowMessage(R.string.error_not_connected_to_network, R.string.error_dialog_title);
}
}
/**
* Initializes the clipboard monitor.
*/
private void initializeClipboardMonitor() {
clipboardMonitor = new ClipboardMonitor(getContext(), this);
if (clipboardMonitor != null) {
clipboardMonitorTimer = new Timer ();
if (clipboardMonitorTimer != null) {
clipboardMonitorTimer.schedule(clipboardMonitor, 0, 500);
}
}
}
/**
* Initialize the canvas to show the remote desktop
*/
void initialize(final String vvFileName, final ConnectionSettings settings, final RemoteCanvasActivityHandler handler) {
this.settings = settings;
this.handler = handler;
checkNetworkConnectivity();
initializeClipboardMonitor();
Thread cThread = new Thread () {
@Override
public void run() {
try {
spicecomm = new SpiceCommunicator (getContext(), RemoteCanvas.this, settings.isRequestingNewDisplayResolution(), settings.isUsbEnabled());
pointer = new RemoteSpicePointer (spicecomm, RemoteCanvas.this, handler);
keyboard = new RemoteSpiceKeyboard (getResources(), spicecomm, RemoteCanvas.this, handler, settings.getLayoutMap());
spicecomm.setHandler(handler);
spicecomm.startSessionFromVvFile(vvFileName, settings.isAudioPlaybackEnabled());
} catch (Throwable e) {
if (stayConnected) {
e.printStackTrace();
android.util.Log.e(TAG, e.toString());
if (e instanceof OutOfMemoryError) {
disposeDrawable ();
disconnectAndShowMessage(R.string.error_out_of_memory, R.string.error_dialog_title);
}
}
}
}
};
cThread.start();
}
private void deleteMyFile (String path) {
new File(path).delete();
}
/**
* Initialize the canvas to show the remote desktop
* @return
*/
// TODO: Switch away from writing out a file to initiating a connection directly.
String retrieveVvFileFromPve(final ConnectionSettings settings) {
android.util.Log.i(TAG, String.format("Trying to connect to PVE host: " + settings.getHostname()));
final String tempVvFile = getContext().getFilesDir() + "/tempfile.vv";
deleteMyFile(tempVvFile);
// TODO: Improve error handling.
Thread cThread = new Thread () {
@Override
public void run() {
try {
String user = settings.getUser();
String realm = Constants.PVE_DEFAULT_REALM;
// Try to parse credentials.
int indexOfAt = settings.getUser().indexOf('@');
if (indexOfAt != -1) {
realm = user.substring(indexOfAt+1);
user = user.substring(0, indexOfAt);
}
// Login with provided credentials
// TODO: Obtain cert from vv file and pass it into the API client
ProxmoxClient api = new ProxmoxClient(settings.getHostname(), user, realm, settings.getPassword(), settings, handler);
// Parse out node, virtualization type and VM ID
String node = Constants.PVE_DEFAULT_NODE;
String virt = Constants.PVE_DEFAULT_VIRTUALIZATION;
String vmname = settings.getVmname();
int indexOfFirstSlash = settings.getVmname().indexOf('/');
if (indexOfFirstSlash != -1) {
// If we find at least one slash, then we need to parse out node for sure.
node = vmname.substring(0, indexOfFirstSlash);
vmname = vmname.substring(indexOfFirstSlash+1);
int indexOfSecondSlash = vmname.indexOf('/');
if (indexOfSecondSlash != -1) {
// If we find a second slash, we need to parse out virtualization type and vmname after node.
virt = vmname.substring(0, indexOfSecondSlash);
vmname = vmname.substring(indexOfSecondSlash+1);
}
}
VmStatus status = api.getCurrentStatus(node, virt, Integer.parseInt(vmname));
if (status.getStatus().equals(VmStatus.STOPPED)) {
api.startVm(node, virt, Integer.parseInt(vmname));
while (!status.getStatus().equals(VmStatus.RUNNING)) {
status = api.getCurrentStatus(node, virt, Integer.parseInt(vmname));
SystemClock.sleep(500);
}
}
SpiceDisplay spiceData = api.spiceVm(node, virt, Integer.parseInt(vmname));
if (spiceData != null) {
spiceData.outputToFile(tempVvFile, settings.getHostname());
} else {
android.util.Log.e(TAG, "PVE returned null data for display.");
handler.sendEmptyMessage(Constants.PVE_NULL_DATA);
}
} catch (LoginException e) {
android.util.Log.e(TAG, "Failed to login to PVE.");
handler.sendEmptyMessage(Constants.PVE_FAILED_TO_AUTHENTICATE);
} catch (JSONException e) {
android.util.Log.e(TAG, "Failed to parse json from PVE.");
handler.sendEmptyMessage(Constants.PVE_FAILED_TO_PARSE_JSON);
} catch (NumberFormatException e) {
android.util.Log.e(TAG, "Error converting PVE ID to integer.");
handler.sendEmptyMessage(Constants.PVE_VMID_NOT_NUMERIC);
} catch (IOException e) {
android.util.Log.e(TAG, "IO Error communicating with PVE API: " + e.getMessage());
handler.sendMessage(RemoteCanvasActivityHandler.getMessageString(Constants.PVE_API_IO_ERROR,
"error", e.getMessage()));
e.printStackTrace();
} catch (HttpException e) {
android.util.Log.e(TAG, "PVE API returned error code: " + e.getMessage());
handler.sendMessage(RemoteCanvasActivityHandler.getMessageString(Constants.PVE_API_UNEXPECTED_CODE,
"error", e.getMessage()));
}
// At this stage we have either retrieved display data or failed, so permit the UI thread to continue.
synchronized(tempVvFile) {
tempVvFile.notify();
}
}
};
cThread.start();
// Wait until a timeout or until we are notified the worker thread trying to retrieve display data is done.
synchronized (tempVvFile) {
try {
tempVvFile.wait();
} catch (InterruptedException e) {
handler.sendEmptyMessage(Constants.PVE_TIMEOUT_COMMUNICATING);
e.printStackTrace();
}
}
File checkFile = new File(tempVvFile);
if (!checkFile.exists() || checkFile.length() == 0) {
return null;
}
return tempVvFile;
}
/**
* Initialize the canvas to show the remote desktop
*/
void initializePve(final ConnectionSettings settings, final RemoteCanvasActivityHandler handler) {
this.settings = settings;
if (!progressDialog.isShowing())
progressDialog.show();
this.handler = handler;
checkNetworkConnectivity();
initializeClipboardMonitor();
Thread cThread = new Thread () {
@Override
public void run() {
try {
spicecomm = new SpiceCommunicator (getContext(), RemoteCanvas.this, settings.isRequestingNewDisplayResolution(), settings.isUsbEnabled());
pointer = new RemoteSpicePointer (spicecomm, RemoteCanvas.this, handler);
keyboard = new RemoteSpiceKeyboard (getResources(), spicecomm, RemoteCanvas.this, handler, settings.getLayoutMap());
spicecomm.setHandler(handler);
// Obtain user's password if necessary.
if (settings.getPassword().equals("")) {
android.util.Log.i (TAG, "Displaying a dialog to obtain user's password.");
handler.sendEmptyMessage(Constants.GET_PASSWORD);
synchronized(spicecomm) {
spicecomm.wait();
}
}
// If not VM name is specified, then get a list of VMs and let the user pick one.
if (settings.getVmname().isEmpty()) {
try {
String user = settings.getUser();
String realm = Constants.PVE_DEFAULT_REALM;
// Try to parse credentials.
int indexOfAt = settings.getUser().indexOf('@');
if (indexOfAt != -1) {
realm = user.substring(indexOfAt+1);
user = user.substring(0, indexOfAt);
}
// Login with provided credentials
ProxmoxClient api = new ProxmoxClient(settings.getHostname(), user, realm, settings.getPassword(), settings, handler);
// Get map of user parseable names to resources
Map<String, PveResource> nameToResources = api.getResources();
if (nameToResources.isEmpty()) {
android.util.Log.e(TAG, "No available suitable resources in PVE cluster");
disconnectAndShowMessage(R.string.error_no_vm_found_for_user, R.string.error_dialog_title);
}
// If there is just one VM, pick it and skip the dialog.
if (nameToResources.size() == 1) {
PveResource a = nameToResources.get(0);
settings.setVmname(a.getNode() + "/" + a.getType() + "/" + a.getVmid());
settings.saveToSharedPreferences(getContext());
} else {
while (settings.getVmname().equals("")) {
android.util.Log.i (TAG, "PVE: Displaying a dialog with VMs to the user.");
// Populate the data structure that is used to convert VM names to IDs.
for (String s : nameToResources.keySet()) {
vmNameToId.put(nameToResources.get(s).getName() + " (" + s + ")", s);
}
// Get the user parseable names and display them
ArrayList<String> vms = new ArrayList<String>(vmNameToId.keySet());
handler.sendMessage(RemoteCanvasActivityHandler.getMessageStringList(Constants.DIALOG_DISPLAY_VMS,
"vms", vms));
synchronized(spicecomm) {
spicecomm.wait();
}
}
}
} catch (LoginException e) {
android.util.Log.e(TAG, "Failed to login to PVE.");
handler.sendEmptyMessage(Constants.PVE_FAILED_TO_AUTHENTICATE);
} catch (JSONException e) {
android.util.Log.e(TAG, "Failed to parse json from PVE.");
handler.sendEmptyMessage(Constants.PVE_FAILED_TO_PARSE_JSON);
} catch (IOException e) {
android.util.Log.e(TAG, "IO Error communicating with PVE API: " + e.getMessage());
handler.sendMessage(RemoteCanvasActivityHandler.getMessageString(Constants.PVE_API_IO_ERROR,
"error", e.getMessage()));
e.printStackTrace();
} catch (HttpException e) {
android.util.Log.e(TAG, "PVE API returned error code: " + e.getMessage());
handler.sendMessage(RemoteCanvasActivityHandler.getMessageString(Constants.PVE_API_UNEXPECTED_CODE,
"error", e.getMessage()));
}
}
// Only if we managed to obtain a VM name we try to get a .vv file for the display.
if (!settings.getVmname().isEmpty()) {
String vvFileName = retrieveVvFileFromPve(settings);
if (vvFileName != null) {
initialize(vvFileName, settings, handler);
}
}
} catch (Throwable e) {
if (stayConnected) {
e.printStackTrace();
android.util.Log.e(TAG, e.toString());
if (e instanceof OutOfMemoryError) {
disposeDrawable ();
disconnectAndShowMessage(R.string.error_out_of_memory, R.string.error_dialog_title);
}
}
}
}
};
cThread.start();
}
/**
* Initialize the canvas to show the remote desktop
*/
void initialize(final ConnectionSettings settings, final RemoteCanvasActivityHandler handler) {
this.settings = settings;
if (!progressDialog.isShowing())
progressDialog.show();
this.handler = handler;
checkNetworkConnectivity();
initializeClipboardMonitor();
Thread cThread = new Thread () {
@Override
public void run() {
try {
spicecomm = new SpiceCommunicator (getContext(), RemoteCanvas.this, settings.isRequestingNewDisplayResolution(), settings.isUsbEnabled());
pointer = new RemoteSpicePointer (spicecomm, RemoteCanvas.this, handler);
keyboard = new RemoteSpiceKeyboard (getResources(), spicecomm, RemoteCanvas.this, handler, settings.getLayoutMap());
spicecomm.setHandler(handler);
// Obtain user's password if necessary.
if (settings.getPassword().equals("")) {
android.util.Log.i (TAG, "Displaying a dialog to obtain user's password.");
handler.sendEmptyMessage(Constants.GET_PASSWORD);
synchronized(spicecomm) {
spicecomm.wait();
}
}
String ovirtCaFile = null;
if (settings.isUsingCustomOvirtCa()) {
ovirtCaFile = settings.getOvirtCaFile();
} else {
String caBundleFileName = getContext().getFilesDir() + "/ca-bundle.crt";
ovirtCaFile = caBundleFileName;
}
// If not VM name is specified, then get a list of VMs and let the user pick one.
if (settings.getVmname().equals("")) {
int success = spicecomm.fetchOvirtVmNames(settings.getHostname(), settings.getUser(),
settings.getPassword(), ovirtCaFile,
settings.isSslStrict());
// VM retrieval was unsuccessful we do not continue.
ArrayList<String> vmNames = spicecomm.getVmNames();
if (success != 0 || vmNames.isEmpty()) {
return;
} else {
// If there is just one VM, pick it and skip the dialog.
if (vmNames.size() == 1) {
settings.setVmname(vmNames.get(0));
settings.saveToSharedPreferences(getContext());
} else {
while (settings.getVmname().equals("")) {
android.util.Log.i (TAG, "Displaying a dialog with VMs to the user.");
// Populate the data structure that is used to convert VM names to IDs.
for (String s : vmNames) {
vmNameToId.put(s, s);
}
handler.sendMessage(RemoteCanvasActivityHandler.getMessageStringList(Constants.DIALOG_DISPLAY_VMS,
"vms", vmNames));
synchronized(spicecomm) {
spicecomm.wait();
}
}
}
}
}
spicecomm.connectOvirt(settings.getHostname(),
settings.getVmname(),
settings.getUser(),
settings.getPassword(),
ovirtCaFile,
settings.isAudioPlaybackEnabled(), settings.isSslStrict());
try {
synchronized(spicecomm) {
spicecomm.wait(35000);
}
} catch (InterruptedException e) {}
if (!spiceUpdateReceived && stayConnected) {
handler.sendEmptyMessage(Constants.OVIRT_TIMEOUT);
}
} catch (Throwable e) {
if (stayConnected) {
e.printStackTrace();
android.util.Log.e(TAG, e.toString());
if (e instanceof OutOfMemoryError) {
disposeDrawable ();
disconnectAndShowMessage(R.string.error_out_of_memory, R.string.error_dialog_title);
}
}
}
}
};
cThread.start();
}
/**
* Retreives the requested remote width.
*/
int getDesiredWidth () {
int w = getWidth();
android.util.Log.e(TAG, "Width requested: " + w);
return w;
}
/**
* Retreives the requested remote height.
*/
int getDesiredHeight () {
int h = getHeight();
android.util.Log.e(TAG, "Height requested: " + h);
return h;
}
public boolean getMouseFollowPan() {
// TODO: Fix
return true; //connection.getFollowPan();
}
public void displayShortToastMessage (final CharSequence message) {
screenMessage = message;
handler.removeCallbacks(showMessage);
handler.post(showMessage);
}
public void displayShortToastMessage (final int messageID) {
screenMessage = getResources().getText(messageID);
handler.removeCallbacks(showMessage);
handler.post(showMessage);
}
void disconnectAndShowMessage (final int messageId, final int titleId) {
disconnectAndCleanUp();
handler.post(new Runnable() {
public void run() {
MessageDialogs.displayMessageAndFinish(getContext(), messageId, titleId);
}
});
}
void disconnectAndShowMessage (final int messageId, final int titleId, final String textToAppend) {
disconnectAndCleanUp();
handler.post(new Runnable() {
public void run() {
MessageDialogs.displayMessageAndFinish(getContext(), messageId, titleId, textToAppend);
}
});
}
/**
* Set the device clipboard text with the string parameter.
* @param readServerCutText set the device clipboard to the text in this parameter.
*/
public void setClipboardText(String s) {
if (s != null && s.length() > 0) {
clipboard.setText(s);
}
}
void disposeDrawable() {
if (myDrawable != null)
myDrawable.destroy();
myDrawable = null;
System.gc();
}
CanvasDrawableContainer reallocateDrawable(int width, int height) {
disposeDrawable();
try {
myDrawable = new CanvasDrawableContainer(width, height);
} catch (Throwable e) {
disconnectAndShowMessage (R.string.error_out_of_memory, R.string.error_dialog_title);
}
// TODO: Implement cursor integration.
initializeSoftCursor();
// Set the drawable for the canvas, now that we have it (re)initialized.
handler.post(drawableSetter);
computeShiftFromFullToView ();
return myDrawable;
}
public void disconnectAndCleanUp() {
stayConnected = false;
if (keyboard != null) {
// Tell the server to release any meta keys.
keyboard.clearOnScreenMetaState();
keyboard.keyEvent(0, new KeyEvent(KeyEvent.ACTION_UP, 0));
}
if (spicecomm != null)
spicecomm.close();
if (handler != null) {
handler.removeCallbacksAndMessages(null);
}
if (clipboardMonitorTimer != null) {
clipboardMonitorTimer.cancel();
// Occasionally causes a NullPointerException
//clipboardMonitorTimer.purge();
clipboardMonitorTimer = null;
}
clipboardMonitor = null;
clipboard = null;
try {
if (myDrawable != null && myDrawable.bitmap != null) {
String location = settings.getFilename();
FileOutputStream out = new FileOutputStream(getContext().getFilesDir() + "/" + location + ".png");
Bitmap tmp = Bitmap.createScaledBitmap(myDrawable.bitmap, 360, 300, true);
myDrawable.bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
tmp.recycle();
}
} catch (Exception e) {
e.printStackTrace();
}
disposeDrawable ();
}
/**
* Make sure the remote pointer is visible
*/
public void movePanToMakePointerVisible() {
if (spicecomm != null) {
int x = pointer.getX();
int y = pointer.getY();
int newAbsX = absX;
int newAbsY = absY;
int wthresh = 30;
int hthresh = 30;
int visibleWidth = getVisibleDesktopWidth();
int visibleHeight = getVisibleDesktopHeight();
int desktopWidth = getDesktopWidth();
int desktopHeight = getDesktopHeight();
if (x - absX >= visibleWidth - wthresh) {
newAbsX = x - (visibleWidth - wthresh);
} else if (x < absX + wthresh) {
newAbsX = x - wthresh;
}
if (y - absY >= visibleHeight - hthresh) {
newAbsY = y - (visibleHeight - hthresh);
} else if (y < absY + hthresh) {
newAbsY = y - hthresh;
}
if (newAbsX < 0) {
newAbsX = 0;
}
if (newAbsX + visibleWidth > desktopWidth) {
newAbsX = desktopWidth - visibleWidth;
}
if (newAbsY < 0) {
newAbsY = 0;
}
if (newAbsY + visibleHeight > desktopHeight) {
newAbsY = desktopHeight - visibleHeight;
}
absolutePan(newAbsX, newAbsY);
}
}
/**
* Relative pan.
* @param dX
* @param dY
*/
public void relativePan(int dX, int dY) {
double zoomFactor = getZoomFactor();
absolutePan((int)(absX + dX/zoomFactor), (int)(absY + dY/zoomFactor));
}
/**
* Absolute pan.
* @param x
* @param y
*/
public void absolutePan(int x, int y) {
if (canvasZoomer != null) {
int vW = getVisibleDesktopWidth();
int vH = getVisibleDesktopHeight();
int w = getDesktopWidth();
int h = getDesktopHeight();
if (x + vW > w) x = w - vW;
if (y + vH > h) y = h - vH;
if (x < 0) x = 0;
if (y < 0) y = 0;
absX = x;
absY = y;
resetScroll();
}
}
/**
* Reset the canvas's scroll position.
*/
void resetScroll() {
float scale = getZoomFactor();
scrollTo((int)((absX - shiftX) * scale),
(int)((absY - shiftY) * scale));
}
/**
* Computes the X and Y offset for converting coordinates from full-frame coordinates to view coordinates.
*/
public void computeShiftFromFullToView () {
shiftX = (spicecomm.framebufferWidth() - getWidth()) / 2;
shiftY = (spicecomm.framebufferHeight() - getHeight()) / 2;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (myDrawable != null) {
pointer.movePointerToMakeVisible();
}
}
/**
* This runnable displays a message on the screen.
*/
CharSequence screenMessage;
private Runnable showMessage = new Runnable() {
public void run() { Toast.makeText( getContext(), screenMessage, Toast.LENGTH_SHORT).show(); }
};
/**
* This runnable sets the drawable for this ImageView.
*/
private Runnable drawableSetter = new Runnable() {
public void run() {
if (myDrawable != null)
setImageDrawable(myDrawable);
canvasZoomer.resetScaling();
}
};
/**
* Causes a redraw of the bitmapData to happen at the indicated coordinates.
*/
public void reDraw(int x, int y, int w, int h) {
float scale = getZoomFactor();
float shiftedX = x-shiftX;
float shiftedY = y-shiftY;
// Make the box slightly larger to avoid artifacts due to truncation errors.
postInvalidate ((int)((shiftedX-1)*scale), (int)((shiftedY-1)*scale),
(int)((shiftedX+w+1)*scale), (int)((shiftedY+h+1)*scale));
}
/**
* This is a float-accepting version of reDraw().
* Causes a redraw of the bitmapData to happen at the indicated coordinates.
*/
public void reDraw(float x, float y, float w, float h) {
float scale = getZoomFactor();
float shiftedX = x-shiftX;
float shiftedY = y-shiftY;
// Make the box slightly larger to avoid artifacts due to truncation errors.
postInvalidate ((int)((shiftedX-1.f)*scale), (int)((shiftedY-1.f)*scale),
(int)((shiftedX+w+1.f)*scale), (int)((shiftedY+h+1.f)*scale));
}
/**
* Redraws the location of the remote pointer.
*/
public void reDrawRemotePointer() {
if (myDrawable != null) {
myDrawable.moveCursorRect(pointer.getX(), pointer.getY());
RectF r = myDrawable.getCursorRect();
reDraw(r.left, r.top, r.width(), r.height());
}
}
/**
* Moves soft cursor into a particular location.
* @param x
* @param y
*/
synchronized void softCursorMove(int x, int y) {
if (myDrawable.isNotInitSoftCursor()) {
initializeSoftCursor();
}
if (!cursorBeingMoved) {
pointer.setX(x);
pointer.setY(y);
RectF prevR = new RectF(myDrawable.getCursorRect());
// Move the cursor.
myDrawable.moveCursorRect(x, y);
// Show the cursor.
RectF r = myDrawable.getCursorRect();
reDraw(r.left, r.top, r.width(), r.height());
reDraw(prevR.left, prevR.top, prevR.width(), prevR.height());
}
}
void initializeSoftCursor () {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.cursor);
int w = bm.getWidth();
int h = bm.getHeight();
int [] tempPixels = new int[w*h];
bm.getPixels(tempPixels, 0, w, 0, 0, w, h);
// Set cursor rectangle as well.
myDrawable.setCursorRect(pointer.getX(), pointer.getY(), w, h, 0, 0);
// Set softCursor to whatever the resource is.
myDrawable.setSoftCursor (tempPixels);
bm.recycle();
}
public RemotePointer getPointer() {
return pointer;
}
public RemoteKeyboard getKeyboard() {
return keyboard;
}
public float getZoomFactor() {
if (canvasZoomer == null)
return 1;
return canvasZoomer.getZoomFactor();
}
public int getVisibleDesktopWidth() {
return (int)((double)getWidth() / getZoomFactor() + 0.5);
}
public int getVisibleDesktopHeight() {
if (visibleHeight > 0)
return (int)((double)visibleHeight / getZoomFactor() + 0.5);
else
return (int)((double)getHeight() / getZoomFactor() + 0.5);
}
public void setVisibleDesktopHeight(int newHeight) {
visibleHeight = newHeight;
}
public int getDesktopWidth() {
return spicecomm.framebufferWidth();
}
public int getDesktopHeight() {
return spicecomm.framebufferHeight();
}
public float getMinimumScale() {
if (myDrawable != null) {
return myDrawable.getMinimumScale(getWidth(), getHeight());
} else
return 1.f;
}
public int getAbsX() {
return absX;
}
public int getAbsY() {
return absY;
}
public float getShiftX() {
return shiftX;
}
public float getShiftY() {
return shiftY;
}
public float getDisplayDensity() {
return displayDensity;
}
public float getDisplayWidth() {
return displayWidth;
}
public float getDisplayHeight() {
return displayHeight;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
android.util.Log.d(TAG, "onCreateInputConnection called");
int version = android.os.Build.VERSION.SDK_INT;
BaseInputConnection bic = null;
if (!bb && version >= Build.VERSION_CODES.JELLY_BEAN) {
bic = new BaseInputConnection(this, false) {
final static String junk_unit = "%%%%%%%%%%";
final static int multiple = 1000;
Editable e;
@Override
public Editable getEditable() {
if (e == null) {
int numTotalChars = junk_unit.length()*multiple;
String junk = new String();
for (int i = 0; i < multiple ; i++) {
junk += junk_unit;
}
e = Editable.Factory.getInstance().newEditable(junk);
Selection.setSelection(e, numTotalChars);
if (RemoteCanvas.this.keyboard != null) {
RemoteCanvas.this.keyboard.skippedJunkChars = false;
}
}
return e;
}
};
} else {
bic = new BaseInputConnection(this, false);
}
outAttrs.actionLabel = null;
outAttrs.inputType = InputType.TYPE_NULL;
// Workaround for IME's that don't support InputType.TYPE_NULL.
if (version >= 21) {
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
}
return bic;
}
/**
* Used to wait until getWidth and getHeight return sane values.
*/
public void waitUntilInflated() {
synchronized (this) {
while (getWidth() == 0 || getHeight() == 0) {
try {
this.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
/**
* Used to detect when the view is inflated to a sane size other than 0x0.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w > 0 && h > 0) {
synchronized (this) {
this.notify();
}
}
}
}