org.elasticdroid.SshConnectorView.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticdroid.SshConnectorView.java

Source

/**
 *  This file is part of ElasticDroid.
 *
 * ElasticDroid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * ElasticDroid 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 ElasticDroid.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authored by siddhu on 18 Dec 2010
 */
package org.elasticdroid;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import org.apache.http.ConnectionClosedException;
import org.elasticdroid.model.SecurityGroupsModel;
import org.elasticdroid.model.SshConnectorModel;
import org.elasticdroid.model.ds.SerializableIpPermission;
import org.elasticdroid.model.ds.SerializableSecurityGroup;
import org.elasticdroid.tpl.GenericActivity;
import org.elasticdroid.utils.DialogConstants;

import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.model.Filter;

/**
 * Collects information from user and uses ConnectBot to connect to the instance using SSH.
 * @author siddhu
 *
 * 18 Dec 2010
 */
public class SshConnectorView extends GenericActivity implements OnClickListener {

    /** Connection data */
    private HashMap<String, String> connectionData;
    /** The hostname to connect to */
    private String hostname;
    /** Security groups available for the instance to connect to*/
    private String[] securityGroupNames;
    /** Selected AWS region */
    private String selectedRegion;

    /** Dialog box for displaying errors */
    private AlertDialog alertDialogBox;
    /**
     * set to show if alert dialog displayed. Used to decide whether to restore
     * progress dialog when screen rotated.
     */
    private boolean alertDialogDisplayed;
    /** message displayed in {@link #alertDialogBox alertDialogBox}. */
    private String alertDialogMessage;
    /**
     * boolean to indicate if an error that occurred is sufficiently serious to
     * have the activity killed.
     */
    private boolean killActivityOnError;
    /** The model used to retrieve security group info */
    private SecurityGroupsModel securityGroupsModel;
    /** The model used to verify input and retrieve SSH URI */
    private SshConnectorModel sshConnectorModel;
    /** Information on security groups */
    private ArrayList<String> openPorts;
    /** The tag used in log messages in this class*/
    private static final String TAG = "org.elasticdroid.SshConnectorView";

    /**
     * Called when activity is created.
     * @param savedInstanceState if any
     */
    @SuppressWarnings("unchecked")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); //call superclass onCreate

        Intent intent = this.getIntent();
        //get all of the data stored in the intent
        try {
            this.connectionData = (HashMap<String, String>) intent
                    .getSerializableExtra("org.elasticdroid.EC2DashboardView.connectionData");
        } catch (Exception exception) {
            //the possible exceptions are NullPointerException: the Hashmap was not found, or
            //ClassCastException: the argument passed is not Hashmap<String, String>. In either case,
            //just print out the error and exit. This is very inelegant, but this is a programmer's bug
            Log.e(TAG, exception.getMessage());
            finish(); //this will cause it to return to {@link EC2DisplayInstancesView}.
        }
        securityGroupNames = intent.getStringArrayExtra("securityGroups");
        hostname = intent.getStringExtra("hostname");
        selectedRegion = intent.getStringExtra("selectedRegion");

        // create and initialise the alert dialog
        alertDialogBox = new AlertDialog.Builder(this).create(); // create alert
        alertDialogBox.setCancelable(false);
        alertDialogBox.setButton(this.getString(R.string.loginview_alertdialogbox_button),
                new DialogInterface.OnClickListener() {
                    // click listener on the alert box - unlock orientation when
                    // clicked.
                    // this is to prevent orientation changing when alert box
                    // locked.
                    public void onClick(DialogInterface arg0, int arg1) {
                        alertDialogDisplayed = false;
                        alertDialogBox.dismiss(); // dismiss dialog.
                        // if an error occurs that is serious enough return the
                        // user to the login
                        // screen. THis happens due to exceptions caused by
                        // programming errors and
                        // exceptions caused due to invalid credentials.
                        if (killActivityOnError) {
                            Log.v(TAG, "Ich bin hier.");
                            SshConnectorView.this.finish();
                        }
                    }
                });

        //initialise the display
        setContentView(R.layout.sshconnector); //tell the activity to set the xml file
        this.setTitle(connectionData.get("username") + " (" + selectedRegion + ")"); //set title

        View loginButton = (View) this.findViewById(R.id.sshConnectorLoginButton);
        loginButton.setOnClickListener(this);
    }

    /**
     * Restore instance state when the activity is reconstructed after a destroy
     * 
     * This method restores:
     * <ul>
     * <li></li>
     * </ul>
     */
    @SuppressWarnings("unchecked")
    @Override
    public void onRestoreInstanceState(Bundle stateToRestore) {
        //restore alertDialogDisplayed boolean
        alertDialogDisplayed = stateToRestore.getBoolean("alertDialogDisplayed");
        Log.v(this.getClass().getName(), "alertDialogDisplayed = " + alertDialogDisplayed);
        alertDialogMessage = stateToRestore.getString("alertDialogMessage");

        //was a progress dialog being displayed? Restore the answer to this question.
        progressDialogDisplayed = stateToRestore.getBoolean("progressDialogDisplayed");
        Log.v(this.getClass().getName() + ".onRestoreInstanceState",
                "progressDialogDisplayed:" + progressDialogDisplayed);

        /*get the model data back, so that you can inform the model that the activity
         * has come back up. */
        Object retained = getLastNonConfigurationInstance();
        //if there was a model executing when the object was destroyed, retained will be an 
        //instance of SecurityGroupsModel
        if (retained instanceof SecurityGroupsModel) {
            Log.i(TAG + ".onRestoreInstanceState()", "Reclaiming previous " + "background task");
            securityGroupsModel = (SecurityGroupsModel) retained;
            securityGroupsModel.setActivity(this);//tell the model of the new activity created
        } else {
            securityGroupsModel = null;

            Log.v(TAG, "No model object, or model finished before activity " + "was recreated.");

            //now if there is no model anymore, and progressDialogDisplayed is set to true,
            //reset it to false, because the model finished executing before the restart
            if (progressDialogDisplayed) {
                progressDialogDisplayed = false;
            }
        }

        Log.d(TAG, "Restoring open ports data if any");

        try {
            openPorts = (ArrayList<String>) stateToRestore.getSerializable("openPorts");
        } catch (Exception exception) {
            openPorts = null;
        }

        //if we have openPorts data, populate the spinner
        if (openPorts != null) {
            populateSpinner();
            //populate spinner will have set the spinner to port 22
            //or to the first index
            //reposition the selected index
            ((Spinner) findViewById(R.id.sshConnectorPortSpinner))
                    .setSelection(stateToRestore.getInt("selectedPortPos"));
        }

        //if the user has entered new username, set that into EditText
        if (stateToRestore.getString("sshUsername") != null) {
            ((EditText) findViewById(R.id.sshConnectorUsernameEditTextView))
                    .setText(stateToRestore.getString("sshUsername"));
        }

        //set the pubkey auth checkbox
        ((CheckBox) findViewById(R.id.sshConnectorUsePublicKeyAuth))
                .setChecked(stateToRestore.getBoolean("usePubkeyAuth"));
    }

    /**
     * Executed when activity is resumed. Calls SecurityGroupsModel to get the list of open ports.
     */
    @Override
    public void onResume() {
        super.onResume();

        //if there was a dialog box, display it
        //if failed, then display dialog box.
        if (alertDialogDisplayed) {
            alertDialogBox.setMessage(alertDialogMessage);
            alertDialogBox.show();
        } else if ((securityGroupsModel == null) && (openPorts == null)) {
            executeSecurityGroupsModel();
        }
    }

    /**
     * Save state of the activity on destroy/stop.
     * Saves:
     * <ul>
     * <li></li>
     * </ul>
     */
    @Override
    public void onSaveInstanceState(Bundle saveState) {
        // if a dialog is displayed when this happens, dismiss it
        if (alertDialogDisplayed) {
            alertDialogBox.dismiss();
        }
        //save the info as to whether dialog is displayed
        saveState.putBoolean("alertDialogDisplayed", alertDialogDisplayed);
        //save the dialog msg
        saveState.putString("alertDialogMessage", alertDialogMessage);

        //save the list of open ports 
        if (openPorts != null) {
            saveState.putSerializable("openPorts", openPorts);
            saveState.putInt("selectedPortPos",
                    ((Spinner) findViewById(R.id.sshConnectorPortSpinner)).getSelectedItemPosition());
        }
        //save if progress dialog is being displayed.
        saveState.putBoolean("progressDialogDisplayed", progressDialogDisplayed);

        //save the username entered if it is not the default user name
        String curUsername = ((EditText) findViewById(R.id.sshConnectorUsernameEditTextView)).getText().toString();
        if (!curUsername.equals(this.getString(R.string.ssh_defaultuser))) {
            saveState.putString("sshUsername", curUsername);
        }

        //save the state of the checkbox that specifies whether pubkey auth should be used or not
        saveState.putBoolean("usePubkeyAuth",
                ((CheckBox) findViewById(R.id.sshConnectorUsePublicKeyAuth)).isChecked());
    }

    /**
     * Save reference to {@link org.elasticdroid.model.ElasticIPsModel} Async
     * Task when object is destroyed (for instance when screen rotated).
     * 
     * This has to be done as the Async Task is running in the background.
     */
    @Override
    public Object onRetainNonConfigurationInstance() {
        if (securityGroupsModel != null) {
            securityGroupsModel.setActivityNull();
            return securityGroupsModel;
        }

        return null;
    }

    /**
     * Executes the model which will return the open ports assigned to this instance.
     * Uses SecurityGroupModel
     */
    private void executeSecurityGroupsModel() {
        Log.v(TAG + ".executeModel()", "Going to execute model!");

        Filter filter = new Filter("group-name").withValues(securityGroupNames);//get filters.
        //the filters say: get us info on the security groups with the following names.

        //note this model will show all ports, including ports that may not be accessible from
        //this IP address.
        securityGroupsModel = new SecurityGroupsModel(this, connectionData);
        securityGroupsModel.execute(new Filter[] { filter });
    }

    /**
     * Executes the model which will:
     * <ul>
     * <li>Make sure that the port selected is accessible from the IP address specified.</li>
     * <li>Return the SSH URI expected by ConnectBot
     * </ul>
     */
    private void executeSshConnectorModel() {
        Log.v(TAG + ".executeModel()", "Asking model to retrieve SSH URI for ConnectBot...");

        String username = ((EditText) findViewById(R.id.sshConnectorUsernameEditTextView)).getText().toString();

        int toPort = Integer
                .valueOf(((Spinner) findViewById(R.id.sshConnectorPortSpinner)).getSelectedItem().toString());

        sshConnectorModel = new SshConnectorModel(this, connectionData, username, hostname, toPort);

        sshConnectorModel.execute(securityGroupNames);
    }

    /**
     * Process results returned by SecurityGroupsModel.
     * 
     * @param Result: Result produced by the AsyncTask model.
     */
    @SuppressWarnings("unchecked")
    @Override
    public void processModelResults(Object result) {
        // dismiss the progress dialog if displayed. Check redundant
        if (progressDialogDisplayed) {
            progressDialogDisplayed = false;
            removeDialog(DialogConstants.PROGRESS_DIALOG.ordinal());
        }

        //handle return of securityGroupsModel
        //if this fails, just return the user back to the parent activity (EC2SingleInstanceView)
        if (securityGroupsModel != null) {
            securityGroupsModel = null; //set the model to null. it's usefulness is done.

            //handle result
            if (result instanceof List<?>) {
                try {
                    populateOpenPortsList((List<SerializableSecurityGroup>) result); //populate the spinner
                } catch (ClassCastException exception) {
                    Log.e(TAG, exception.getMessage());
                    finish();
                }

                populateSpinner(); //populate the spinner
            } else if (result instanceof AmazonServiceException) {
                // if a server error
                if (((AmazonServiceException) result).getErrorCode().startsWith("5")) {
                    alertDialogMessage = this.getString(R.string.loginview_server_err_dlg);
                } else {
                    alertDialogMessage = this.getString(R.string.loginview_invalid_keys_dlg);
                }
                alertDialogDisplayed = true;
                killActivityOnError = true;//do not kill activity on server error
                //allow user to retry.
            } else if (result instanceof AmazonClientException) {
                alertDialogMessage = this.getString(R.string.loginview_no_connxn_dlg);
                alertDialogDisplayed = true;
                killActivityOnError = true;//do not kill activity on connectivity error. allow 
                //client to retry.
            }
        }
        //handle return of sshConnectorModel
        else if (sshConnectorModel != null) {
            sshConnectorModel = null;

            if (result instanceof String) {
                Log.v(TAG, "SshConnectorModel returned: " + (String) result);

                Intent connectBotIntent = new Intent(Intent.ACTION_VIEW, Uri.parse((String) result));

                try {
                    connectBotIntent.putExtra("usePubKeyAuth",
                            ((CheckBox) findViewById(R.id.sshConnectorUsePublicKeyAuth)).isChecked());
                    //pass nickname: the last name of the file's path seq
                    startActivity(connectBotIntent);
                } catch (ActivityNotFoundException exception) {
                    alertDialogMessage = this.getString(R.string.connectbot_not_installed);
                    alertDialogDisplayed = true;
                    //kill the activity when user closes the dialog
                    killActivityOnError = true;
                }
            } else if (result instanceof AmazonServiceException) {
                // if a server error
                if (((AmazonServiceException) result).getErrorCode().startsWith("5")) {
                    alertDialogMessage = this.getString(R.string.loginview_server_err_dlg);
                } else {
                    alertDialogMessage = this.getString(R.string.loginview_invalid_keys_dlg);
                }
                alertDialogDisplayed = true;
                killActivityOnError = false;//do not kill activity on server error
                //allow user to retry.
            } else if (result instanceof AmazonClientException) {
                alertDialogMessage = this.getString(R.string.loginview_no_connxn_dlg);
                alertDialogDisplayed = true;
                killActivityOnError = false;//do not kill activity on connectivity error. allow 
                //client to retry.
            } else if (result instanceof ConnectionClosedException) {
                alertDialogMessage = ((ConnectionClosedException) result).getMessage();
                alertDialogDisplayed = true;
                killActivityOnError = false;
            }
        }

        //display the alert dialog if the user set the displayed var to true
        if (alertDialogDisplayed) {
            alertDialogBox.setMessage(alertDialogMessage);
            alertDialogBox.show();//show error
        }
    }

    /**
     * Method called by processModelResults() to populate the openPorts ArrayList<String>
     * with the SecurityGroups data returned by SecurityGroupsModel.
     * 
     * @param securityGroups Security groups returned by the model.
     */
    private void populateOpenPortsList(List<SerializableSecurityGroup> securityGroups) {

        openPorts = new ArrayList<String>(); //(re)initialise openPorts

        //get the data to populate the spinner with
        //very crap O(n2) algo
        for (SerializableSecurityGroup securityGroup : securityGroups) {
            List<SerializableIpPermission> ipPermissions = securityGroup.getIpPermissions();
            for (SerializableIpPermission ipPermission : ipPermissions) {
                openPorts.add(String.valueOf(ipPermission.getToPort()));
            }
        }

        Collections.sort(openPorts); //sort by natural order, i.e. alphabetically
    }

    /**
     * Method called to populate the open ports spinner in the UI
     */
    private void populateSpinner() {
        if (openPorts == null) {
            return;
        }

        int selectedIndex = 0; //set selected index to index of port 22 if available.
        Spinner portSpinner = (Spinner) findViewById(R.id.sshConnectorPortSpinner);

        if (openPorts.contains("22")) {
            Log.d(TAG, "Found port 22 in openPorts.Setting as selected.");
            selectedIndex = openPorts.indexOf("22");
        }
        //create an ArrayAdapter<String> to hold this
        ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item,
                openPorts);
        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

        portSpinner.setAdapter(spinnerAdapter);
        portSpinner.setSelection(selectedIndex); //0 if port 22 unavailable, else indexof(port22)
    }

    /* Overriden methods */
    /**
     * Handle back button. When back button pressed, we want the openPorts to be set to null
     * so that it is recomputed when the user comes back in.
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        //do not allow user to return to previous screen on pressing back button
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            openPorts = null;
            //cancel
        }

        return super.onKeyDown(keyCode, event);
    }

    /* (non-Javadoc)
     * @see android.view.View.OnClickListener#onClick(android.view.View)
     */
    @Override
    public void onClick(View button) {
        switch (button.getId()) {
        case R.id.sshConnectorLoginButton:
            EditText usernameEditText = (EditText) findViewById(R.id.sshConnectorUsernameEditTextView);

            //if no user name entered, do not proceed; warn user and exit
            if (usernameEditText.getText().toString().trim().equals("")) {
                usernameEditText.setError(getString(R.string.loginview_invalid_username_err));
            } else {
                executeSshConnectorModel();//execute the SSH connector model
            }
            break;
        }
    }

    /** 
     * Handle cancel of progress dialog
     * @see android.content.DialogInterface.OnCancelListener#onCancel(android.content.
     * DialogInterface)
     */
    @Override
    public void onCancel(DialogInterface dialog) {
        //this cannot be called UNLESS the user has the model running.
        //i.e. the prog bar is visible
        progressDialogDisplayed = false;
        securityGroupsModel.cancel(true);
        //kill the activity to return to previous view
        finish();
    }
}