com.iiordanov.bVNC.RemoteCanvas.java Source code

Java tutorial

Introduction

Here is the source code for com.iiordanov.bVNC.RemoteCanvas.java

Source

/**
 * 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.MessageDigest;
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.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;

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);

        // Set a writable data directory
        LibFreeRDP.setDataDirectory(session.getInstance(), getContext().getFilesDir().toString());

        // 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) {
            throw new Exception(getContext().getString(R.string.error_vnc_unable_to_connect)
                    + e.getStackTrace().toString() + e.getLocalizedMessage());
        }

        rfbconn = rfb;
        pointer = new RemoteVncPointer(rfbconn, RemoteCanvas.this, handler);
        keyboard = new RemoteVncKeyboard(rfbconn, RemoteCanvas.this, handler);

        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);
        handler.post(desktopInfo);

        // 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);
        keyboard = new RemoteVncKeyboard(rfbconn, RemoteCanvas.this, handler);
        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);
        handler.post(desktopInfo);
        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 = 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;
    }

    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 OnConnectionSuccess(int instance) {
        rdpcomm.setIsInNormalProtocol(true);
        Log.v(TAG, "OnConnectionSuccess");
    }

    @Override
    public void OnConnectionFailure(int instance) {
        rdpcomm.setIsInNormalProtocol(false);
        Log.v(TAG, "OnConnectionFailure");
        if (maintainConnection)
            handler.sendEmptyMessage(Constants.RDP_UNABLE_TO_CONNECT);
    }

    @Override
    public void OnDisconnecting(int instance) {
        rdpcomm.setIsInNormalProtocol(false);
        Log.v(TAG, "OnDisconnecting");
        if (maintainConnection)
            handler.sendEmptyMessage(Constants.RDP_CONNECT_FAILURE);
    }

    @Override
    public void OnDisconnected(int instance) {
        rdpcomm.setIsInNormalProtocol(false);
        Log.v(TAG, "OnDisconnected");
        if (maintainConnection)
            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);
        handler.post(desktopInfo);

        // 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 boolean OnVerifiyCertificate(String subject, String issuer, String fingerprint) {
        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 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);
        }
    }
}