/**
* Copyright (C) 2012 Iordan Iordanov
* Copyright (C) 2010 Michael A. MacDonald
* Copyright (C) 2004 Horizon Wimba. All Rights Reserved.
* Copyright (C) 2001-2003 HorizonLive.com, Inc. All Rights Reserved.
* Copyright (C) 2001,2002 Constantin Kaplinsky. All Rights Reserved.
* Copyright (C) 2000 Tridia Corporation. All Rights Reserved.
* Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
*
* 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 2 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.
*/
//
// RemoteCanvas is a subclass of android.view.SurfaceView which draws a VNC
// desktop on it.
//
package com.iiordanov.bVNC;
import java.io.IOException;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Locale;
import java.util.Timer;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
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.Base64;
import android.util.DisplayMetrics;
import android.util.Log;
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 com.freerdp.freerdpcore.application.GlobalApp;
import com.freerdp.freerdpcore.application.SessionState;
import com.freerdp.freerdpcore.domain.BookmarkBase;
import com.freerdp.freerdpcore.domain.ManualBookmark;
import com.freerdp.freerdpcore.services.LibFreeRDP;
import com.iiordanov.android.bc.BCFactory;
import com.iiordanov.bVNC.input.RemoteKeyboard;
import com.iiordanov.bVNC.input.RemotePointer;
import com.iiordanov.bVNC.input.RemoteRdpKeyboard;
import com.iiordanov.bVNC.input.RemoteRdpPointer;
import com.iiordanov.bVNC.input.RemoteSpiceKeyboard;
import com.iiordanov.bVNC.input.RemoteSpicePointer;
import com.iiordanov.bVNC.input.RemoteVncKeyboard;
import com.iiordanov.bVNC.input.RemoteVncPointer;
import com.iiordanov.tigervnc.vncviewer.CConn;
import com.iiordanov.bVNC.dialogs.GetTextFragment;
import com.iiordanov.bVNC.exceptions.AnonCipherUnsupportedException;
import com.iiordanov.bVNC.*;
import com.iiordanov.freebVNC.*;
import com.iiordanov.aRDP.*;
import com.iiordanov.freeaRDP.*;
import com.iiordanov.aSPICE.*;
import com.iiordanov.freeaSPICE.*;
public class RemoteCanvas extends ImageView implements LibFreeRDP.UIEventListener, LibFreeRDP.EventListener {
private final static String TAG = "RemoteCanvas";
public AbstractScaling scaling;
// Variable indicating that we are currently scrolling in simulated touchpad mode.
public boolean inScrolling = false;
// Connection parameters
ConnectionBean connection;
Database database;
private SSHConnection sshConnection = null;
// VNC protocol connection
public RfbConnectable rfbconn = null;
private RfbProto rfb = null;
private CConn cc = null;
private RdpCommunicator rdpcomm = null;
private SpiceCommunicator spicecomm = null;
private Socket sock = null;
boolean maintainConnection = true;
// RFB Decoder
Decoder decoder = null;
// The remote pointer and keyboard
RemotePointer pointer;
RemoteKeyboard keyboard;
// Internal bitmap data
private int capacity;
public AbstractBitmapData bitmapData;
boolean useFull = false;
boolean compact = false;
// Keeps track of libFreeRDP instance.
GlobalApp freeRdpApp = null;
SessionState session = null;
// Progress dialog shown at connection time.
ProgressDialog pd;
// Used to set the contents of the clipboard.
ClipboardManager clipboard;
Timer clipboardMonitorTimer;
ClipboardMonitor clipboardMonitor;
public boolean serverJustCutText = false;
private Runnable setModes;
// This variable indicates whether or not the user has accepted an untrusted
// security certificate. Used to control progress while the dialog asking the user
// to confirm the authenticity of a certificate is displayed.
private boolean certificateAccepted = false;
/*
* Position of the top left portion of the <i>visible</i> part of the screen, in
* full-frame coordinates
*/
int absoluteXPosition = 0, absoluteYPosition = 0;
/*
* How much to shift coordinates over when converting from full to view coordinates.
*/
float shiftX = 0, 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;
/*
* This flag indicates whether this is the RDP 'version' or not.
*/
boolean isRdp = false;
/*
* This flag indicates whether this is the SPICE 'version' or not.
*/
boolean isSpice = false;
boolean spiceUpdateReceived = false;
/*
* Variable used for BB workarounds.
*/
boolean bb = false;
/**
* Constructor used by the inflation apparatus
*
* @param context
*/
public RemoteCanvas(final Context context, AttributeSet attrs) {
super(context, attrs);
clipboard = (ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE);
decoder = new Decoder (this);
isRdp = getContext().getPackageName().contains("RDP");
isSpice = getContext().getPackageName().contains("SPICE");
final Display display = ((Activity)context).getWindow().getWindowManager().getDefaultDisplay();
displayWidth = display.getWidth();
displayHeight = display.getHeight();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
displayDensity = metrics.density;
if (android.os.Build.MODEL.contains("BlackBerry") ||
android.os.Build.BRAND.contains("BlackBerry") ||
android.os.Build.MANUFACTURER.contains("BlackBerry")) {
bb = true;
}
}
/**
* Create a view showing a remote desktop connection
* @param context Containing context (activity)
* @param bean Connection settings
* @param setModes Callback to run on UI thread after connection is set up
*/
void initializeCanvas(ConnectionBean bean, Database db, final Runnable setModes) {
this.setModes = setModes;
connection = bean;
database = db;
decoder.setColorModel(COLORMODEL.valueOf(bean.getColorModel()));
// Startup the connection thread with a progress dialog
pd = ProgressDialog.show(getContext(), getContext().getString(R.string.info_progress_dialog_connecting),
getContext().getString(R.string.info_progress_dialog_establishing),
true, true, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
closeConnection();
handler.post(new Runnable() {
public void run() {
Utils.showFatalErrorMessage(getContext(), getContext().getString(R.string.info_progress_dialog_aborted));
}
});
}
});
// Make this dialog cancellable only upon hitting the Back button and not touching outside.
pd.setCanceledOnTouchOutside(false);
Thread t = new Thread () {
public void run() {
try {
// Initialize SSH key if necessary
if (connection.getConnectionType() == Constants.CONN_TYPE_SSH &&
connection.getSshHostKey().equals("") &&
Utils.isNullOrEmptry(connection.getIdHash()))
{
handler.sendEmptyMessage(Constants.DIALOG_SSH_CERT);
// Block while user decides whether to accept certificate or not.
// The activity ends if the user taps "No", so we block indefinitely here.
synchronized (RemoteCanvas.this) {
while (connection.getSshHostKey().equals("")) {
try {
RemoteCanvas.this.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
if (isSpice) {
startSpiceConnection();
} else if (isRdp) {
startRdpConnection();
} else {
startVncConnection();
}
} catch (Throwable e) {
if (maintainConnection) {
Log.e(TAG, e.toString());
e.printStackTrace();
// Ensure we dismiss the progress dialog before we finish
if (pd.isShowing())
pd.dismiss();
if (e instanceof OutOfMemoryError) {
disposeDrawable ();
showFatalMessageAndQuit (getContext().getString(R.string.error_out_of_memory));
} else {
String error = getContext().getString(R.string.error_connection_failed);
if (e.getMessage() != null) {
if (e.getMessage().indexOf("SSH") < 0 &&
( e.getMessage().indexOf("authentication") > -1 ||
e.getMessage().indexOf("Unknown security result") > -1 ||
e.getMessage().indexOf("password check failed") > -1)
) {
error = getContext().getString(R.string.error_vnc_authentication);
}
error = error + "<br>" + e.getLocalizedMessage();
}
showFatalMessageAndQuit(error);
}
}
}
}
};
t.start();
clipboardMonitor = new ClipboardMonitor(getContext(), this);
if (clipboardMonitor != null) {
clipboardMonitorTimer = new Timer ();
if (clipboardMonitorTimer != null) {
try {
clipboardMonitorTimer.schedule(clipboardMonitor, 0, 500);
} catch (NullPointerException e){}
}
}
}
/**
* Starts a SPICE connection using libspice.
* @throws Exception
*/
private void startSpiceConnection() throws Exception {
// Get the address and port (based on whether an SSH tunnel is being established or not).
String address = getAddress();
// To prevent an SSH tunnel being created when port or TLS port is not set, we only
// getPort when port/tport are positive.
int port = connection.getPort();
if (port > 0)
port = getPort(port);
int tport = connection.getTlsPort();
if (tport > 0)
tport = getPort(tport);
spicecomm = new SpiceCommunicator (getContext(), this, connection);
rfbconn = spicecomm;
pointer = new RemoteSpicePointer (rfbconn, RemoteCanvas.this, handler);
keyboard = new RemoteSpiceKeyboard (getResources(), spicecomm, RemoteCanvas.this,
handler, connection.getLayoutMap());
spicecomm.setUIEventListener(RemoteCanvas.this);
spicecomm.setHandler(handler);
spicecomm.connect(address, Integer.toString(port), Integer.toString(tport),
connection.getPassword(), connection.getCaCertPath(),
connection.getCertSubject(), connection.getEnableSound());
}
/**
* Starts an RDP connection using the FreeRDP library.
* @throws Exception
*/
private void startRdpConnection() throws Exception {
// Get the address and port (based on whether an SSH tunnel is being established or not).
String address = getAddress();
int rdpPort = getPort(connection.getPort());
// This is necessary because it initializes a synchronizedMap referenced later.
freeRdpApp = new GlobalApp();
// Create a manual bookmark and populate it from settings.
BookmarkBase bookmark = new ManualBookmark();
bookmark.<ManualBookmark>get().setLabel(connection.getNickname());
bookmark.<ManualBookmark>get().setHostname(address);
bookmark.<ManualBookmark>get().setPort(rdpPort);
bookmark.<ManualBookmark>get().setUsername(connection.getUserName());
bookmark.<ManualBookmark>get().setDomain(connection.getRdpDomain());
bookmark.<ManualBookmark>get().setPassword(connection.getPassword());
// Create a session based on the bookmark
session = GlobalApp.createSession(bookmark, this.getContext());
// Set a writable data directory
//LibFreeRDP.setDataDirectory(session.getInstance(), getContext().getFilesDir().toString());
BookmarkBase.DebugSettings debugSettings = session.getBookmark().getDebugSettings();
debugSettings.setAsyncChannel(false);
debugSettings.setAsyncTransport(false);
// Set screen settings to native res if instructed to, or if height or width are too small.
BookmarkBase.ScreenSettings screenSettings = session.getBookmark().getActiveScreenSettings();
waitUntilInflated();
int remoteWidth = getRemoteWidth(getWidth(), getHeight());
int remoteHeight = getRemoteHeight(getWidth(), getHeight());
screenSettings.setWidth(remoteWidth);
screenSettings.setHeight(remoteHeight);
screenSettings.setColors(16);
// Set performance flags.
BookmarkBase.PerformanceFlags performanceFlags = session.getBookmark().getPerformanceFlags();
performanceFlags.setRemoteFX(false);
performanceFlags.setWallpaper(connection.getDesktopBackground());
performanceFlags.setFontSmoothing(connection.getFontSmoothing());
performanceFlags.setDesktopComposition(connection.getDesktopComposition());
performanceFlags.setFullWindowDrag(connection.getWindowContents());
performanceFlags.setMenuAnimations(connection.getMenuAnimation());
performanceFlags.setTheming(connection.getVisualStyles());
BookmarkBase.AdvancedSettings advancedSettings = session.getBookmark().getAdvancedSettings();
advancedSettings.setRedirectSDCard(connection.getRedirectSdCard());
advancedSettings.setConsoleMode(connection.getConsoleMode());
advancedSettings.setRedirectSound(connection.getRemoteSoundType());
advancedSettings.setRedirectMicrophone(connection.getEnableRecording());
rdpcomm = new RdpCommunicator (session);
rfbconn = rdpcomm;
pointer = new RemoteRdpPointer (rfbconn, RemoteCanvas.this, handler);
keyboard = new RemoteRdpKeyboard (rfbconn, RemoteCanvas.this, handler);
session.setUIEventListener(RemoteCanvas.this);
LibFreeRDP.setEventListener(RemoteCanvas.this);
session.connect();
pd.dismiss();
}
/**
* Starts a VNC connection using the TightVNC backend.
* @throws Exception
*/
private void startVncConnection() throws Exception {
Log.i(TAG, "Connecting to: " + connection.getAddress() + ", port: " + connection.getPort());
String address = getAddress();
int vncPort = getPort(connection.getPort());
boolean sslTunneled = connection.getConnectionType() == Constants.CONN_TYPE_STUNNEL;
try {
rfb = new RfbProto(decoder, this, address, vncPort, connection.getPrefEncoding(), connection.getViewOnly(),
connection.getUseLocalCursor(), sslTunneled, connection.getIdHashAlgorithm(),
connection.getIdHash(), connection.getSshHostKey());
Log.v(TAG, "Connected to server: " + address + " at port: " + vncPort);
rfb.initializeAndAuthenticate(connection.getUserName(), connection.getPassword(),
connection.getUseRepeater(), connection.getRepeaterId(),
connection.getConnectionType(), connection.getSshHostKey());
} catch (AnonCipherUnsupportedException e) {
showFatalMessageAndQuit (getContext().getString(R.string.error_anon_dh_unsupported));
} catch (Exception e) {
e.printStackTrace();
throw new Exception (getContext().getString(R.string.error_vnc_unable_to_connect) +
Utils.messageAndStackTraceAsString(e));
}
rfbconn = rfb;
pointer = new RemoteVncPointer (rfbconn, RemoteCanvas.this, handler);
boolean rAltAsIsoL3Shift = Utils.querySharedPreferenceBoolean(this.getContext(),
Constants.rAltAsIsoL3ShiftTag);
keyboard = new RemoteVncKeyboard (rfbconn, RemoteCanvas.this, handler, rAltAsIsoL3Shift);
rfb.writeClientInit();
rfb.readServerInit();
initializeBitmap (displayWidth, displayHeight);
decoder.setPixelFormat(rfb);
handler.post(new Runnable() {
public void run() {
pd.setMessage(getContext().getString(R.string.info_progress_dialog_downloading));
}
});
sendUnixAuth ();
if (connection.getUseLocalCursor())
initializeSoftCursor();
handler.post(drawableSetter);
handler.post(setModes);
// Hide progress dialog
if (pd.isShowing())
pd.dismiss();
rfb.processProtocol();
}
/**
* Sends over the unix username and password if this is VNC over SSH connectio and automatic sending of
* UNIX credentials is enabled for AutoX (for x11vnc's "-unixpw" option).
*/
void sendUnixAuth () {
// If the type of connection is ssh-tunneled and we are told to send the unix credentials, then do so.
if (connection.getConnectionType() == Constants.CONN_TYPE_SSH && connection.getAutoXUnixAuth()) {
keyboard.processLocalKeyEvent(KeyEvent.KEYCODE_UNKNOWN, new KeyEvent(SystemClock.uptimeMillis(),
connection.getSshUser(), 0, 0));
keyboard.processLocalKeyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
keyboard.processLocalKeyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
keyboard.processLocalKeyEvent(KeyEvent.KEYCODE_UNKNOWN, new KeyEvent(SystemClock.uptimeMillis(),
connection.getSshPassword(), 0, 0));
keyboard.processLocalKeyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
keyboard.processLocalKeyEvent(KeyEvent.KEYCODE_ENTER, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
}
}
/**
* Starts a VeNCrypt connection using the TigerVNC backend.
* @throws Exception
*/
private void startVencryptConnection() throws Exception {
cc = new CConn(RemoteCanvas.this, sock, null, false, connection);
rfbconn = cc;
pointer = new RemoteVncPointer (rfbconn, RemoteCanvas.this, handler);
boolean rAltAsIsoL3Shift = Utils.querySharedPreferenceBoolean(this.getContext(),
Constants.rAltAsIsoL3ShiftTag);
keyboard = new RemoteVncKeyboard (rfbconn, RemoteCanvas.this, handler, rAltAsIsoL3Shift);
initializeBitmap(displayWidth, displayHeight);
// Initialize the protocol before we dismiss the progress dialog and request for the right
// modes to be set.
for (int i = 0; i < 6; i++)
cc.processMsg();
handler.post(new Runnable() {
public void run() {
pd.setMessage(getContext().getString(R.string.info_progress_dialog_downloading));
}
});
for (int i = 0; i < 3; i++)
cc.processMsg();
// Hide progress dialog
if (pd.isShowing())
pd.dismiss();
cc.processProtocol();
}
/**
* Retreives the requested remote width.
*/
private int getRemoteWidth (int viewWidth, int viewHeight) {
int remoteWidth = 0;
int reqWidth = connection.getRdpWidth();
int reqHeight = connection.getRdpHeight();
if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_CUSTOM &&
reqWidth >= 2 && reqHeight >= 2) {
remoteWidth = reqWidth;
} else if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_NATIVE_PORTRAIT) {
remoteWidth = Math.min(viewWidth, viewHeight);
} else {
remoteWidth = Math.max(viewWidth, viewHeight);
}
// We make the resolution even if it is odd.
if (remoteWidth % 2 == 1) remoteWidth--;
return remoteWidth;
}
/**
* Retreives the requested remote height.
*/
private int getRemoteHeight (int viewWidth, int viewHeight) {
int remoteHeight = 0;
int reqWidth = connection.getRdpWidth();
int reqHeight = connection.getRdpHeight();
if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_CUSTOM &&
reqWidth >= 2 && reqHeight >= 2) {
remoteHeight = reqHeight;
} else if (connection.getRdpResType() == Constants.RDP_GEOM_SELECT_NATIVE_PORTRAIT) {
remoteHeight = Math.max(viewWidth, viewHeight);
} else {
remoteHeight = Math.min(viewWidth, viewHeight);
}
// We make the resolution even if it is odd.
if (remoteHeight % 2 == 1) remoteHeight--;
return remoteHeight;
}
/**
* Closes the connection and shows a fatal message which ends the activity.
* @param error
*/
void showFatalMessageAndQuit (final String error) {
closeConnection();
handler.post(new Runnable() {
public void run() {
Utils.showFatalErrorMessage(getContext(), error);
}
});
}
/**
* If necessary, initializes an SSH tunnel and returns local forwarded port, or
* if SSH tunneling is not needed, returns the given port.
* @return
* @throws Exception
*/
int getPort(int port) throws Exception {
int result = 0;
if (connection.getConnectionType() == Constants.CONN_TYPE_SSH) {
if (sshConnection == null) {
sshConnection = new SSHConnection(connection, getContext(), handler);
}
// TODO: Take the AutoX stuff out to a separate function.
int newPort = sshConnection.initializeSSHTunnel ();
if (newPort > 0)
port = newPort;
result = sshConnection.createLocalPortForward(port);
} else {
result = port;
}
return result;
}
/**
* Returns localhost if using SSH tunnel or saved VNC address.
* @return
* @throws Exception
*/
String getAddress() {
if (connection.getConnectionType() == Constants.CONN_TYPE_SSH) {
return new String("127.0.0.1");
} else
return connection.getAddress();
}
/**
* Initializes the drawable and bitmap into which the remote desktop is drawn.
* @param dx
* @param dy
* @throws IOException
*/
void initializeBitmap (int dx, int dy) throws IOException {
Log.i(TAG, "Desktop name is " + rfbconn.desktopName());
Log.i(TAG, "Desktop size is " + rfbconn.framebufferWidth() + " x " + rfbconn.framebufferHeight());
int fbsize = rfbconn.framebufferWidth() * rfbconn.framebufferHeight();
capacity = BCFactory.getInstance().getBCActivityManager().getMemoryClass(Utils.getActivityManager(getContext()));
if (connection.getForceFull() == BitmapImplHint.AUTO) {
if (fbsize * CompactBitmapData.CAPACITY_MULTIPLIER <= capacity*1024*1024) {
useFull = true;
compact = true;
} else if (fbsize * FullBufferBitmapData.CAPACITY_MULTIPLIER <= capacity*1024*1024) {
useFull = true;
} else {
useFull = false;
}
} else
useFull = (connection.getForceFull() == BitmapImplHint.FULL);
if (!useFull) {
bitmapData=new LargeBitmapData(rfbconn, this, dx, dy, capacity);
android.util.Log.i(TAG, "Using LargeBitmapData.");
} else {
try {
// TODO: Remove this if Android 4.2 receives a fix for a bug which causes it to stop drawing
// the bitmap in CompactBitmapData when under load (say playing a video over VNC).
if (!compact) {
bitmapData=new FullBufferBitmapData(rfbconn, this, capacity);
android.util.Log.i(TAG, "Using FullBufferBitmapData.");
} else {
bitmapData=new CompactBitmapData(rfbconn, this, isSpice);
android.util.Log.i(TAG, "Using CompactBufferBitmapData.");
}
} catch (Throwable e) { // If despite our efforts we fail to allocate memory, use LBBM.
disposeDrawable ();
useFull = false;
bitmapData=new LargeBitmapData(rfbconn, this, dx, dy, capacity);
android.util.Log.i(TAG, "Using LargeBitmapData.");
}
}
decoder.setBitmapData(bitmapData);
}
/**
* Disposes of the old drawable which holds the remote desktop data.
*/
private void disposeDrawable () {
if (bitmapData != null)
bitmapData.dispose();
bitmapData = null;
System.gc();
}
/**
* The remote desktop's size has changed and this method
* reinitializes local data structures to match.
*/
public void updateFBSize () {
try {
bitmapData.frameBufferSizeChanged ();
} catch (Throwable e) {
boolean useLBBM = false;
// If we've run out of memory, try using another bitmapdata type.
if (e instanceof OutOfMemoryError) {
disposeDrawable ();
// If we were using CompactBitmapData, try FullBufferBitmapData.
if (compact == true) {
compact = false;
try {
bitmapData = new FullBufferBitmapData(rfbconn, this, capacity);
} catch (Throwable e2) {
useLBBM = true;
}
} else
useLBBM = true;
// Failing FullBufferBitmapData or if we weren't using CompactBitmapData, try LBBM.
if (useLBBM) {
disposeDrawable ();
useFull = false;
bitmapData = new LargeBitmapData(rfbconn, this, getWidth(), getHeight(), capacity);
}
decoder.setBitmapData(bitmapData);
}
}
handler.post(drawableSetter);
handler.post(setModes);
bitmapData.syncScroll();
}
/**
* Displays a short toast message on the screen.
* @param message
*/
public void displayShortToastMessage (final CharSequence message) {
screenMessage = message;
handler.removeCallbacks(showMessage);
handler.post(showMessage);
}
/**
* Displays a short toast message on the screen.
* @param messageID
*/
public void displayShortToastMessage (final int messageID) {
screenMessage = getResources().getText(messageID);
handler.removeCallbacks(showMessage);
handler.post(showMessage);
}
/**
* Lets the drawable know that an update from the remote server has arrived.
*/
public void doneWaiting () {
bitmapData.doneWaiting();
}
/**
* Indicates that RemoteCanvas's scroll position should be synchronized with the
* drawable's scroll position (used only in LargeBitmapData)
*/
public void syncScroll () {
bitmapData.syncScroll();
}
/**
* Requests a remote desktop update at the specified rectangle.
*/
public void writeFramebufferUpdateRequest (int x, int y, int w, int h, boolean incremental) throws IOException {
bitmapData.prepareFullUpdateRequest(incremental);
rfbconn.writeFramebufferUpdateRequest(x, y, w, h, incremental);
}
/**
* Requests an update of the entire remote desktop.
*/
public void writeFullUpdateRequest (boolean incremental) {
bitmapData.prepareFullUpdateRequest(incremental);
rfbconn.writeFramebufferUpdateRequest(bitmapData.getXoffset(), bitmapData.getYoffset(),
bitmapData.bmWidth(), bitmapData.bmHeight(), incremental);
}
/**
* 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);
}
}
/**
* Method that disconnects from the remote server.
*/
public void closeConnection() {
maintainConnection = false;
if (keyboard != null) {
// Tell the server to release any meta keys.
keyboard.clearMetaState();
keyboard.processLocalKeyEvent(0, new KeyEvent(KeyEvent.ACTION_UP, 0));
}
// Close the rfb connection.
if (rfbconn != null)
rfbconn.close();
// Close the SSH tunnel.
if (sshConnection != null) {
sshConnection.terminateSSHTunnel();
sshConnection = null;
}
onDestroy();
}
/**
* Cleans up resources after a disconnection.
*/
public void onDestroy() {
Log.v(TAG, "Cleaning up resources");
removeCallbacksAndMessages();
if (clipboardMonitorTimer != null) {
clipboardMonitorTimer.cancel();
// Occasionally causes a NullPointerException
//clipboardMonitorTimer.purge();
clipboardMonitorTimer = null;
}
clipboardMonitor = null;
clipboard = null;
setModes = null;
decoder = null;
scaling = null;
drawableSetter = null;
screenMessage = null;
desktopInfo = null;
disposeDrawable ();
}
public void removeCallbacksAndMessages() {
if (handler != null) {
handler.removeCallbacksAndMessages(null);
}
}
/*
* f(x,s) is a function that returns the coordinate in screen/scroll space corresponding
* to the coordinate x in full-frame space with scaling s.
*
* This function returns the difference between f(x,s1) and f(x,s2)
*
* f(x,s) = (x - i/2) * s + ((i - w)/2)) * s
* = s (x - i/2 + i/2 + w/2)
* = s (x + w/2)
*
*
* f(x,s) = (x - ((i - w)/2)) * s
* @param oldscaling
* @param scaling
* @param imageDim
* @param windowDim
* @param offset
* @return
*/
/**
* Computes the X and Y offset for converting coordinates from full-frame coordinates to view coordinates.
*/
public void computeShiftFromFullToView () {
shiftX = (rfbconn.framebufferWidth() - getWidth()) / 2;
shiftY = (rfbconn.framebufferHeight() - getHeight()) / 2;
}
/**
* Change to Canvas's scroll position to match the absoluteXPosition
*/
void scrollToAbsolute() {
float scale = getScale();
scrollTo((int)((absoluteXPosition - shiftX) * scale),
(int)((absoluteYPosition - shiftY) * scale));
}
/**
* Make sure mouse is visible on displayable part of screen
*/
public void panToMouse() {
if (rfbconn == null)
return;
boolean panX = true;
boolean panY = true;
// Don't pan in a certain direction if dimension scaled is already less
// than the dimension of the visible part of the screen.
if (rfbconn.framebufferWidth() <= getVisibleWidth())
panX = false;
if (rfbconn.framebufferHeight() <= getVisibleHeight())
panY = false;
// We only pan if the current scaling is able to pan.
if (scaling != null && ! scaling.isAbleToPan())
return;
int x = pointer.getX();
int y = pointer.getY();
boolean panned = false;
int w = getVisibleWidth();
int h = getVisibleHeight();
int iw = getImageWidth();
int ih = getImageHeight();
int wthresh = 30;
int hthresh = 30;
int newX = absoluteXPosition;
int newY = absoluteYPosition;
if (x - absoluteXPosition >= w - wthresh) {
newX = x - (w - wthresh);
if (newX + w > iw)
newX = iw - w;
} else if (x < absoluteXPosition + wthresh) {
newX = x - wthresh;
if (newX < 0)
newX = 0;
}
if ( panX && newX != absoluteXPosition ) {
absoluteXPosition = newX;
panned = true;
}
if (y - absoluteYPosition >= h - hthresh) {
newY = y - (h - hthresh);
if (newY + h > ih)
newY = ih - h;
} else if (y < absoluteYPosition + hthresh) {
newY = y - hthresh;
if (newY < 0)
newY = 0;
}
if ( panY && newY != absoluteYPosition ) {
absoluteYPosition = newY;
panned = true;
}
if (panned) {
//scrollBy(newX - absoluteXPosition, newY - absoluteYPosition);
scrollToAbsolute();
}
}
/**
* Pan by a number of pixels (relative pan)
* @param dX
* @param dY
* @return True if the pan changed the view (did not move view out of bounds); false otherwise
*/
public boolean pan(int dX, int dY) {
// We only pan if the current scaling is able to pan.
if (scaling != null && ! scaling.isAbleToPan())
return false;
double scale = getScale();
double sX = (double)dX / scale;
double sY = (double)dY / scale;
if (absoluteXPosition + sX < 0)
// dX = diff to 0
sX = -absoluteXPosition;
if (absoluteYPosition + sY < 0)
sY = -absoluteYPosition;
// Prevent panning right or below desktop image
if (absoluteXPosition + getVisibleWidth() + sX > getImageWidth())
sX = getImageWidth() - getVisibleWidth() - absoluteXPosition;
if (absoluteYPosition + getVisibleHeight() + sY > getImageHeight())
sY = getImageHeight() - getVisibleHeight() - absoluteYPosition;
absoluteXPosition += sX;
absoluteYPosition += sY;
if (sX != 0.0 || sY != 0.0) {
//scrollBy((int)sX, (int)sY);
scrollToAbsolute();
return true;
}
return false;
}
/* (non-Javadoc)
* @see android.view.View#onScrollChanged(int, int, int, int)
*/
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (bitmapData != null) {
bitmapData.scrollChanged(absoluteXPosition, absoluteYPosition);
pointer.mouseFollowPan();
}
}
/**
* This runnable sets the drawable (contained in bitmapData) for the VncCanvas (ImageView).
*/
private Runnable drawableSetter = new Runnable() {
public void run() {
if (bitmapData != null)
bitmapData.setImageDrawable(RemoteCanvas.this);
}
};
/**
* 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 causes a toast with information about the current connection to be shown.
*/
private Runnable desktopInfo = new Runnable() {
public void run() {
showConnectionInfo();
}
};
/**
* 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 = getScale();
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 = getScale();
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));
}
/**
* Displays connection info in a toast message.
*/
public void showConnectionInfo() {
if (rfbconn == null)
return;
String msg = null;
int idx = rfbconn.desktopName().indexOf("(");
if (idx > 0) {
// Breakup actual desktop name from IP addresses for improved
// readability
String dn = rfbconn.desktopName().substring(0, idx).trim();
String ip = rfbconn.desktopName().substring(idx).trim();
msg = dn + "\n" + ip;
} else
msg = rfbconn.desktopName();
msg += "\n" + rfbconn.framebufferWidth() + "x" + rfbconn.framebufferHeight();
String enc = rfbconn.getEncoding();
// Encoding might not be set when we display this message
if (decoder.getColorModel() != null) {
if (enc != null && !enc.equals(""))
msg += ", " + rfbconn.getEncoding() + getContext().getString(R.string.info_encoding) + decoder.getColorModel().toString();
else
msg += ", " + decoder.getColorModel().toString();
}
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
}
/**
* Invalidates (to redraw) the location of the remote pointer.
*/
public void invalidateMousePosition() {
if (bitmapData != null) {
bitmapData.moveCursorRect(pointer.getX(), pointer.getY());
RectF r = bitmapData.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 (bitmapData.isNotInitSoftCursor()) {
initializeSoftCursor();
}
if (!inScrolling) {
pointer.setX(x);
pointer.setY(y);
RectF prevR = new RectF(bitmapData.getCursorRect());
// Move the cursor.
bitmapData.moveCursorRect(x, y);
// Show the cursor.
RectF r = bitmapData.getCursorRect();
reDraw(r.left, r.top, r.width(), r.height());
reDraw(prevR.left, prevR.top, prevR.width(), prevR.height());
}
}
/**
* Initializes the data structure which holds the remote pointer data.
*/
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.
bitmapData.setCursorRect(pointer.getX(), pointer.getY(), w, h, 0, 0);
// Set softCursor to whatever the resource is.
bitmapData.setSoftCursor (tempPixels);
bm.recycle();
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
android.util.Log.d(TAG, "onCreateInputConnection called");
int version = android.os.Build.VERSION.SDK_INT;
BaseInputConnection bic = new BaseInputConnection(this, false);
outAttrs.actionLabel = null;
outAttrs.inputType = InputType.TYPE_NULL;
String currentIme = Settings.Secure.getString(getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
android.util.Log.d(TAG, "currentIme: " + currentIme);
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
return bic;
}
public RemotePointer getPointer() {
return pointer;
}
public RemoteKeyboard getKeyboard() {
return keyboard;
}
public float getScale() {
if (scaling == null)
return 1;
return scaling.getScale();
}
public int getVisibleWidth() {
return (int)((double)getWidth() / getScale() + 0.5);
}
public void setVisibleHeight(int newHeight) {
visibleHeight = newHeight;
}
public int getVisibleHeight() {
if (visibleHeight > 0)
return (int)((double)visibleHeight / getScale() + 0.5);
else
return (int)((double)getHeight() / getScale() + 0.5);
}
public int getImageWidth() {
return rfbconn.framebufferWidth();
}
public int getImageHeight() {
return rfbconn.framebufferHeight();
}
public int getCenteredXOffset() {
return (rfbconn.framebufferWidth() - getWidth()) / 2;
}
public int getCenteredYOffset() {
return (rfbconn.framebufferHeight() - getHeight()) / 2;
}
public float getMinimumScale() {
if (bitmapData != null) {
return bitmapData.getMinimumScale();
} else
return 1.f;
}
public float getDisplayDensity() {
return displayDensity;
}
public boolean isColorModel(COLORMODEL cm) {
return (decoder.getColorModel() != null) && decoder.getColorModel().equals(cm);
}
public void setColorModel(COLORMODEL cm) {
decoder.setColorModel(cm);
}
public boolean getMouseFollowPan() {
return connection.getFollowPan();
}
public int getAbsoluteX () {
return absoluteXPosition;
}
public int getAbsoluteY () {
return absoluteYPosition;
}
/**
* Used to wait until getWidth and getHeight return sane values.
*/
private 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();
}
}
}
//////////////////////////////////////////////////////////////////////////////////
// Implementation of LibFreeRDP.EventListener. Through the functions implemented
// below, FreeRDP communicates connection state information.
//////////////////////////////////////////////////////////////////////////////////
@Override
public void OnPreConnect(long instance) {
Log.v(TAG, "OnPreConnect");
}
@Override
public void OnConnectionSuccess(long instance) {
Log.v(TAG, "OnConnectionSuccess");
rdpcomm.setIsInNormalProtocol(true);
}
@Override
public void OnConnectionFailure(long instance) {
Log.v(TAG, "OnConnectionFailure");
rdpcomm.setIsInNormalProtocol(false);
if (maintainConnection)
handler.sendEmptyMessage(Constants.RDP_UNABLE_TO_CONNECT);
}
@Override
public void OnDisconnecting(long instance) {
rdpcomm.setIsInNormalProtocol(false);
Log.v(TAG, "OnDisconnecting");
if (maintainConnection)
handler.sendEmptyMessage(Constants.RDP_CONNECT_FAILURE);
}
@Override
public void OnDisconnected(long instance) {
Log.v(TAG, "OnDisconnected");
if (maintainConnection) {
if (!rdpcomm.isInNormalProtocol()) {
handler.sendEmptyMessage(Constants.RDP_UNABLE_TO_CONNECT);
} else {
handler.sendEmptyMessage(Constants.RDP_CONNECT_FAILURE);
}
}
}
//////////////////////////////////////////////////////////////////////////////////
// Implementation of LibFreeRDP.UIEventListener. Through the functions implemented
// below libspice and FreeRDP communicate remote desktop size and updates.
//////////////////////////////////////////////////////////////////////////////////
@Override
public void OnSettingsChanged(int width, int height, int bpp) {
android.util.Log.e(TAG, "onSettingsChanged called, wxh: " + width + "x" + height);
// If this is aSPICE, we need to initialize the communicator and remote keyboard and mouse now.
if (isSpice) {
spicecomm.setFramebufferWidth(width);
spicecomm.setFramebufferHeight(height);
waitUntilInflated();
int remoteWidth = getRemoteWidth(getWidth(), getHeight());
int remoteHeight = getRemoteHeight(getWidth(), getHeight());
if (width != remoteWidth || height != remoteHeight) {
android.util.Log.e(TAG, "Requesting new res: " + remoteWidth + "x" + remoteHeight);
rfbconn.requestResolution(remoteWidth, remoteHeight);
}
}
disposeDrawable ();
try {
// TODO: Use frameBufferSizeChanged instead.
bitmapData = new CompactBitmapData(rfbconn, this, isSpice);
} catch (Throwable e) {
showFatalMessageAndQuit (getContext().getString(R.string.error_out_of_memory));
return;
}
android.util.Log.i(TAG, "Using CompactBufferBitmapData.");
// TODO: In RDP mode, pointer is not visible, so we use a soft cursor.
initializeSoftCursor();
// Set the drawable for the canvas, now that we have it (re)initialized.
handler.post(drawableSetter);
handler.post(setModes);
// If this is aSPICE, set the new bitmap in the native layer.
if (isSpice) {
spiceUpdateReceived = true;
rfbconn.setIsInNormalProtocol(true);
handler.sendEmptyMessage(Constants.SPICE_CONNECT_SUCCESS);
}
}
@Override
public boolean OnAuthenticate(StringBuilder username, StringBuilder domain, StringBuilder password) {
android.util.Log.e(TAG, "onAuthenticate called.");
if (maintainConnection)
handler.sendEmptyMessage(Constants.RDP_AUTH_FAILED);
return false;
}
@Override
public int OnVerifiyCertificate(String commonName, String subject,
String issuer, String fingerprint, boolean mismatch) {
android.util.Log.e(TAG, "OnVerifiyCertificate called.");
// Send a message containing the certificate to our handler.
Message m = new Message();
m.setTarget(handler);
m.what = Constants.DIALOG_RDP_CERT;
Bundle strings = new Bundle();
strings.putString("subject", subject);
strings.putString("issuer", issuer);
strings.putString("fingerprint", fingerprint);
m.obj = strings;
handler.sendMessage(m);
// Block while user decides whether to accept certificate or not.
// The activity ends if the user taps "No", so we block indefinitely here.
synchronized (RemoteCanvas.this) {
while (!certificateAccepted) {
try {
RemoteCanvas.this.wait();
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
return 1;
}
@Override
public boolean OnGatewayAuthenticate(StringBuilder username,
StringBuilder domain, StringBuilder password) {
return this.OnAuthenticate(username, domain, password);
}
@Override
public int OnVerifyChangedCertificate(String commonName, String subject,
String issuer, String fingerprint, String oldSubject,
String oldIssuer, String oldFingerprint) {
return this.OnVerifiyCertificate(commonName, subject, issuer, fingerprint, true);
}
@Override
public void OnGraphicsUpdate(int x, int y, int width, int height) {
//android.util.Log.e(TAG, "OnGraphicsUpdate called: " + x +", " + y + " + " + width + "x" + height );
if (isRdp) {
if (bitmapData != null && bitmapData.mbitmap != null && session != null) {
synchronized (bitmapData.mbitmap) {
LibFreeRDP.updateGraphics(session.getInstance(), bitmapData.mbitmap, x, y, width, height);
}
}
} else {
synchronized (bitmapData.mbitmap) {
spicecomm.UpdateBitmap(bitmapData.mbitmap, x, y, width, height);
}
}
reDraw(x, y, width, height);
}
@Override
public void OnGraphicsResize(int width, int height, int bpp) {
android.util.Log.e(TAG, "OnGraphicsResize called.");
OnSettingsChanged(width, height, bpp);
}
@Override
public void OnRemoteClipboardChanged(String data) {
serverJustCutText = true;
setClipboardText(data);
}
/**
* Handler for the dialogs that display the x509/RDP/SSH key signatures to the user.
* Also shows the dialogs which show various connection failures.
*/
public Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
FragmentManager fm = null;
switch (msg.what) {
case Constants.PRO_FEATURE:
if (pd != null && pd.isShowing()) {
pd.dismiss();
}
showFatalMessageAndQuit(getContext().getString(R.string.pro_feature_mfa));
break;
case Constants.GET_VERIFICATIONCODE:
if (pd != null && pd.isShowing()) {
pd.dismiss();
}
fm = ((FragmentActivity)getContext()).getSupportFragmentManager();
GetTextFragment getPassword = GetTextFragment.newInstance(
RemoteCanvas.this.getContext().getString(R.string.verification_code),
sshConnection, GetTextFragment.Plaintext, R.string.verification_code_message, R.string.verification_code);
getPassword.setCancelable(false);
getPassword.show(fm, RemoteCanvas.this.getContext().getString(R.string.verification_code));
break;
case Constants.DIALOG_X509_CERT:
validateX509Cert ((X509Certificate)msg.obj);
break;
case Constants.DIALOG_SSH_CERT:
initializeSshHostKey();
break;
case Constants.DIALOG_RDP_CERT:
Bundle s = (Bundle)msg.obj;
validateRdpCert (s.getString("subject"), s.getString("issuer"), s.getString("fingerprint"));
break;
case Constants.SPICE_CONNECT_SUCCESS:
if (pd != null && pd.isShowing()) {
pd.dismiss();
}
break;
case Constants.SPICE_CONNECT_FAILURE:
if (maintainConnection) {
if (pd != null && pd.isShowing()) {
pd.dismiss();
}
if (!spiceUpdateReceived) {
showFatalMessageAndQuit(getContext().getString(R.string.error_spice_unable_to_connect));
} else {
showFatalMessageAndQuit(getContext().getString(R.string.error_connection_interrupted));
}
}
break;
case Constants.RDP_CONNECT_FAILURE:
showFatalMessageAndQuit(getContext().getString(R.string.error_rdp_connection_failed));
break;
case Constants.RDP_UNABLE_TO_CONNECT:
showFatalMessageAndQuit(getContext().getString(R.string.error_rdp_unable_to_connect));
break;
case Constants.RDP_AUTH_FAILED:
showFatalMessageAndQuit(getContext().getString(R.string.error_rdp_authentication_failed));
break;
}
}
};
/**
* If there is a saved cert, checks the one given against it. If a signature was passed in
* and no saved cert, then check that signature. Otherwise, presents the
* given cert's signature to the user for approval.
*
* The saved data must always win over any passed-in URI data
*
* @param cert the given cert.
*/
private void validateX509Cert (final X509Certificate cert) {
boolean certMismatch = false;
int hashAlg = connection.getIdHashAlgorithm();
byte[] certData = null;
boolean isSigEqual = false;
try {
certData = cert.getEncoded();
isSigEqual = SecureTunnel.isSignatureEqual(hashAlg, connection.getIdHash(), certData);
} catch (Exception ex) {
ex.printStackTrace();
showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_signature));
return;
}
// If there is no saved cert, then if a signature was provided,
// check the signature and save the cert if the signature matches.
if (connection.getSshHostKey().equals ("")) {
if (!connection.getIdHash().equals("")) {
if (isSigEqual) {
Log.i(TAG, "Certificate validated from URI data.");
saveAndAcceptCert (cert);
return;
} else {
certMismatch = true;
}
}
// If there is a saved cert, check against it.
} else if (connection.getSshHostKey().equals(Base64.encodeToString(certData, Base64.DEFAULT))) {
Log.i(TAG, "Certificate validated from saved key.");
saveAndAcceptCert (cert);
return;
} else {
certMismatch = true;
}
// Show a dialog with the key signature for approval.
DialogInterface.OnClickListener signatureNo = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// We were told not to continue, so stop the activity
Log.i(TAG, "Certificate rejected by user.");
closeConnection();
((Activity) getContext()).finish();
}
};
DialogInterface.OnClickListener signatureYes = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.i(TAG, "Certificate accepted by user.");
saveAndAcceptCert (cert);
}
};
// Display dialog to user with cert info and hash.
try {
// First build the message. If there was a mismatch, prepend a warning about it.
String message = "";
if (certMismatch) {
message = getContext().getString(R.string.warning_cert_does_not_match) + "\n\n";
}
byte[] certBytes = cert.getEncoded();
String certIdHash = SecureTunnel.computeSignatureByAlgorithm(hashAlg, certBytes);
String certInfo =
String.format(Locale.US, getContext().getString(R.string.info_cert_tunnel),
certIdHash,
cert.getSubjectX500Principal().getName(),
cert.getIssuerX500Principal().getName(),
cert.getNotBefore(),
cert.getNotAfter()
);
certInfo = message + certInfo.replace(",", "\n");
// Actually display the message
Utils.showYesNoPrompt(getContext(),
getContext().getString(R.string.info_continue_connecting) + connection.getAddress () + "?",
certInfo,
signatureYes, signatureNo);
} catch (NoSuchAlgorithmException e2) {
e2.printStackTrace();
showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_signature));
} catch (CertificateEncodingException e) {
e.printStackTrace();
showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_encoding));
}
}
/**
* Saves and accepts a x509 certificate.
* @param cert
*/
private void saveAndAcceptCert (X509Certificate cert) {
String certificate = null;
try {
certificate = Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT);
} catch (CertificateEncodingException e) {
e.printStackTrace();
showFatalMessageAndQuit(getContext().getString(R.string.error_x509_could_not_generate_encoding));
}
connection.setSshHostKey(certificate);
connection.save(database.getWritableDatabase());
database.close();
// Indicate the certificate was accepted.
certificateAccepted = true;
synchronized (RemoteCanvas.this) {
RemoteCanvas.this.notifyAll();
}
}
public boolean isCertificateAccepted() {
return certificateAccepted;
}
/**
* Permits the user to validate an RDP certificate.
* @param subject
* @param issuer
* @param fingerprint
*/
private void validateRdpCert (String subject, String issuer, final String fingerprint) {
// Since LibFreeRDP handles saving accepted certificates, if we ever get here, we must
// present the user with a query whether to accept the certificate or not.
// Show a dialog with the key signature for approval.
DialogInterface.OnClickListener signatureNo = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// We were told not to continue, so stop the activity
closeConnection();
((Activity) getContext()).finish();
}
};
DialogInterface.OnClickListener signatureYes = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Indicate the certificate was accepted.
certificateAccepted = true;
synchronized (RemoteCanvas.this) {
RemoteCanvas.this.notifyAll();
}
}
};
Utils.showYesNoPrompt(getContext(), getContext().getString(R.string.info_continue_connecting) + connection.getAddress () + "?",
getContext().getString(R.string.info_cert_signatures) +
"\nSubject: " + subject +
"\nIssuer: " + issuer +
"\nFingerprint: " + fingerprint +
getContext().getString(R.string.info_cert_signatures_identical),
signatureYes, signatureNo);
}
/**
* Function used to initialize an empty SSH HostKey for a new VNC over SSH connection.
*/
private void initializeSshHostKey() {
// If the SSH HostKey is empty, then we need to grab the HostKey from the server and save it.
Log.d(TAG, "Attempting to initialize SSH HostKey.");
displayShortToastMessage(getContext().getString(R.string.info_ssh_initializing_hostkey));
sshConnection = new SSHConnection(connection, getContext(), handler);
if (!sshConnection.connect()) {
// Failed to connect, so show error message and quit activity.
showFatalMessageAndQuit(getContext().getString(R.string.error_ssh_unable_to_connect));
} else {
// Show a dialog with the key signature.
DialogInterface.OnClickListener signatureNo = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// We were told to not continue, so stop the activity
sshConnection.terminateSSHTunnel();
pd.dismiss();
((Activity) getContext()).finish();
}
};
DialogInterface.OnClickListener signatureYes = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// We were told to go ahead with the connection.
connection.setIdHash(sshConnection.getIdHash()); // could prompt based on algorithm
connection.setSshHostKey(sshConnection.getServerHostKey());
connection.save(database.getWritableDatabase());
database.close();
sshConnection.terminateSSHTunnel();
sshConnection = null;
synchronized (RemoteCanvas.this) {
RemoteCanvas.this.notify();
}
}
};
Utils.showYesNoPrompt(getContext(),
getContext().getString(R.string.info_continue_connecting) + connection.getSshServer() + "?",
getContext().getString(R.string.info_ssh_key_fingerprint) + sshConnection.getHostKeySignature() +
getContext().getString(R.string.info_ssh_key_fingerprint_identical),
signatureYes, signatureNo);
}
}
}