/**
* 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();
}
}