/**
* 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 11 Dec 2010
*/
package org.elasticdroid;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import org.elasticdroid.model.ControlInstancesModel;
import org.elasticdroid.model.EC2InstancesModel;
import org.elasticdroid.model.ElasticIPsModel;
import org.elasticdroid.model.ControlInstancesModel.ControlType;
import org.elasticdroid.model.ds.SerializableAddress;
import org.elasticdroid.model.ds.SerializableInstance;
import org.elasticdroid.tpl.GenericListActivity;
import org.elasticdroid.utils.DialogConstants;
import org.elasticdroid.utils.AWSConstants.InstanceStateConstants;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.model.Filter;
import com.amazonaws.services.ec2.model.InstanceStateChange;
/**
* Activity to display details on a single instance.
* Called by EC2DisplayInstancesView. Will handle all of its errors internally, and not return a
* result.
*
* @author siddhu
*
* 11 Dec 2010
*/
public class EC2SingleInstanceView extends GenericListActivity {
/**
* The instance that is being displayed.
*/
private SerializableInstance instance;
/**
* The AWS connection data
*/
private HashMap<String, String> connectionData;
/**
* The selected 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;
/**
* Elastic IP Model object
*/
private ElasticIPsModel elasticIpsModel;
/**
* EC2 Instances Model object
*/
private EC2InstancesModel ec2InstancesModel;
/**
* Control Instances model: to start/stop instances
*/
private ControlInstancesModel controlInstancesModel;
/**
* Is an Elastic IP assigned to this instance.
* Model answers this question.
* Note: Only Elastic IP per instance (i.e. one public n/w i/f per machine).
*
* Using Boolean instead of boolean because we also use this to find if the model has been
* executed.
*/
private Boolean isElasticIpAssigned;
/**
* Is EC2InstanceModel being used for autorefreshing after starting/stopping instances
*/
private boolean autoRefresh;
/**
* This boolean indicates we have changed the state of this instance since when it was first
* displayed. This is to force the EC2DisplayInstancesView to update.
*/
private boolean instanceStateChanged;
/**
* Tag for logging
*/
private static final String TAG = "org.elasticdroid.EC2SingleInstanceView";
/**
* 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 data from intent
selectedRegion = intent.getStringExtra("selectedRegion");
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}.
}
try {
this.instance = (SerializableInstance)intent.getSerializableExtra(
"org.elasticdroid.model.SerializableInstance");
}
catch(Exception exception) {
Log.e(TAG, exception.getMessage());
finish(); //this will cause it to return to {@link EC2DisplayInstancesView}.
}
// 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) {
EC2SingleInstanceView.this.finish();
}
}
});
setContentView(R.layout.ec2singleinstance); //tell the activity to set the xml file
this.setTitle(connectionData.get("username")+ " (" + selectedRegion +")"); //set title
}
/**
* Restore instance state when the activity is reconstructed after a destroy
*
* This method restores:
* <ul>
* <li>isElasticIpAssigned: Has the instance been assigned an Elastic IP?</li>
* </ul>
*/
@Override
public void onRestoreInstanceState(Bundle stateToRestore) {
//restore alertDialogDisplayed boolean
alertDialogDisplayed = stateToRestore.getBoolean("alertDialogDisplayed");
Log.v(TAG, "alertDialogDisplayed = "
+ alertDialogDisplayed);
alertDialogMessage = stateToRestore.getString("alertDialogMessage");
//was a progress dialog being displayed? Restore the answer to this question.
progressDialogDisplayed = stateToRestore.getBoolean("progressDialogDisplayed");
Log.v(TAG + ".onRestoreInstanceState", "progressDialogDisplayed:" +
progressDialogDisplayed);
//restore the instance if the instance has been saved. This is for when we
//may have changed the state of the instance due to start/stop operations by the
//ControlInstancesModel.
if (stateToRestore.getSerializable("instance") != null) {
instance = (SerializableInstance)stateToRestore.getSerializable("instance");
}
//restore the boolean that indicates if state has changed.
instanceStateChanged = stateToRestore.getBoolean("instanceStateChanged");
//check if the key exists before assigning it.
//This is because getBoolean returns false if key doesn't exist.
//See onSaveInstanceState(). It shows that isElasticIpAssigned is not always saved.
if (stateToRestore.get("isElasticIpAssigned") != null) {
isElasticIpAssigned = stateToRestore.getBoolean("isElasticIpAssigned");
}
/*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 ElasticIpsModel
if (retained instanceof ElasticIPsModel) {
Log.i(TAG + ".onRestoreInstanceState()","Reclaiming previous " +
"background task");
elasticIpsModel = (ElasticIPsModel)retained;
elasticIpsModel.setActivity(this);//tell the model of the new activity created
ec2InstancesModel = null; //set EC2 instances model to null (redundant assignment)
controlInstancesModel = null; //set ControlInstancesModel to null (redundant assn)
}
else if (retained instanceof EC2InstancesModel) {
Log.i(TAG + ".onRestoreInstanceState()","Reclaiming previous " +
"background task");
ec2InstancesModel = (EC2InstancesModel) retained;
ec2InstancesModel.setActivity(this);
}
else if (retained instanceof ControlInstancesModel) {
Log.i(TAG, "Reclaiming ControlInstancesModel");
controlInstancesModel = (ControlInstancesModel) retained;
controlInstancesModel.setActivity(this);
}
else {
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;
}
}
}
/**
* Executed when activity is resumed. Calls ElasticIpModel to determine if public IP
* is elastic.
*/
@Override
public void onResume() {
super.onResume(); //call superclass onResume()
Log.v(TAG + ".onResume()", "onResume");
//set the rest of the UI elements
//if there is no tag called "Name" (case sensitive), set instance ID
if (instance.getTag() == null) {
((TextView)findViewById(R.id.ec2SingleInstanceName)).setText(Html.fromHtml(String.format(
this.getString(R.string.ec2singleinstance_tag), instance.getInstanceId())));
}
else {
((TextView)findViewById(R.id.ec2SingleInstanceName)).setText(Html.fromHtml(String.format(
this.getString(R.string.ec2singleinstance_tag), instance.getTag())));
}
//don't execute ElasticIpModel if instance is stopped or summat
if (instance.getStateCode() != InstanceStateConstants.RUNNING) {
isElasticIpAssigned = false;
}
//if there was a dialog box, display it
//if failed, then display dialog box.
if (alertDialogDisplayed) {
alertDialogBox.setMessage(alertDialogMessage);
alertDialogBox.show();
}
//execute Elastic IPs model only if EC2 instances model is not running
else if ((elasticIpsModel == null) && (ec2InstancesModel == null) && (isElasticIpAssigned ==
null)) {
executeElasticIpModel();
}
else {
//populate the list
if (isElasticIpAssigned != null) {
setListAdapter(new EC2SingleInstanceAdapter(this, R.layout.ec2singleinstance,
instance, isElasticIpAssigned));
}
}
}
/**
* Save state of the activity on destroy/stop.
* Saves:
* <ul>
* <li> instanceData: The instance data collected.</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 if progress dialog is being displayed.
saveState.putBoolean("progressDialogDisplayed", progressDialogDisplayed);
//save if instance state has been changed
saveState.putBoolean("instanceStateChanged", instanceStateChanged);
//save whether there is an elastic IP assigned to this instance IF it has been initialised
if (isElasticIpAssigned != null) {
saveState.putBoolean("isElasticIpAssigned", isElasticIpAssigned);
}
if (instance != null) {
saveState.putSerializable("instance", instance);
}
}
/**
* 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 (elasticIpsModel != null) {
elasticIpsModel.setActivityNull();
return elasticIpsModel;
}
else if (ec2InstancesModel != null) {
//the user has asked the single instance view to be refreshed
ec2InstancesModel.setActivityNull();
return ec2InstancesModel;
}
else if (controlInstancesModel != null) {
//the user has asked for the instance to be stopped or restarted.
controlInstancesModel.setActivityNull();
return controlInstancesModel;
}
return null;
}
/**
* Executes the model which will return the Elastic IP(s) assigned to this instance.
*/
private void executeElasticIpModel() {
Log.v(TAG + ".executeModel()", "Going to execute model!");
elasticIpsModel = new ElasticIPsModel(this, connectionData);
//filter results that are not relevant to this instance
//we have to pass an array of filters to the doInBackground method, and we need to construct
//an array list from the String array constructed to hold the instance.getInstanceId() string
//so much function chaining, i feel ill. Or maybe I'm just stupid!
elasticIpsModel.execute(new Filter[]{
new Filter("instance-id",
new ArrayList<String>(Arrays.asList(
new String[]{instance.getInstanceId()})
)
)
});
}
/**
* Executes the model that will refresh the instance.
*/
private void executeEC2InstanceModel() {
//create a new filter
Filter singleInstanceFilter = new Filter("instance-id").withValues(new String[]{
instance.getInstanceId()
});
Log.v(TAG, "Region:" + connectionData.get("region"));
Log.v(TAG, "Instance ID:" + instance.getInstanceId());
ec2InstancesModel = new EC2InstancesModel(this, connectionData, selectedRegion);
ec2InstancesModel.execute(singleInstanceFilter);
}
/**
* Execute ControlInstancesModel
* @param Do we wish to tag this instance, or start/restart it.
*/
private void executeControlInstancesModel(boolean tag) {
instanceStateChanged = true;
Log.v(TAG, "Instance state changed: " + instanceStateChanged);
if (!tag) {
if (instance.getStateCode() == InstanceStateConstants.RUNNING) {
controlInstancesModel = new ControlInstancesModel(this, connectionData,
ControlType.STOP_INSTANCE);
//execute model
controlInstancesModel.execute(instance.getInstanceId());
}
else if (instance.getStateCode() == InstanceStateConstants.STOPPED) {
controlInstancesModel = new ControlInstancesModel(this, connectionData,
ControlType.START_INSTANCE);
//execute model
controlInstancesModel.execute(instance.getInstanceId());
}
}
else {
Log.v(TAG, "Tagging instance...");
if (instance.getTag() != null) {
controlInstancesModel = new ControlInstancesModel(this, connectionData,
ControlType.TAG_INSTANCE, Arrays.asList(new String[]{instance.getTag()}));
}
//send an empty array over
else {
controlInstancesModel = new ControlInstancesModel(this, connectionData,
ControlType.TAG_INSTANCE, Arrays.asList(new String[]{}));
}
controlInstancesModel.execute(instance.getInstanceId());
}
}
/**
* Keep refreshing the EC2 Instance model until the state is as expected
*/
private void autoRefreshEC2InstanceModel(int expectedState) {
//create a new filter
Filter singleInstanceFilter = new Filter("instance-id").withValues(new String[]{
instance.getInstanceId()
});
Log.v(TAG, "Expected instance state: " + expectedState);
autoRefresh = true;//set autorefresh to true so that progress dialog is not displayed.
ec2InstancesModel = new EC2InstancesModel(this, connectionData, selectedRegion,
expectedState);
ec2InstancesModel.execute(singleInstanceFilter); //this will now auto-refresh till done.
}
/**
* Process results from model
* @param Object, which can be a List<Address> or exceptions
*
* This method can be invoked by:
* <ul>
* <li>{@link ControlInstancesModel}: Start/stop instance.</li>
* <li>{@link ElasticIPsModel}: Check if the instance is using an Elastic IP.</li>
* <li>{@link EC2InstancesModel}: Refresh (or auto-refresh) the instance</li>
* </ul>
*/
@Override
public void processModelResults(Object result) {
Log.v(TAG + ".processModelResults()", "Processing model results...");
// dismiss the progress dialog if displayed. Check redundant
if (progressDialogDisplayed) {
progressDialogDisplayed = false;
removeDialog(DialogConstants.PROGRESS_DIALOG.ordinal());
}
//irrespective of the model, if cancelled, display the cancelled toast.
if ((result == null) && (!autoRefresh)) {
Toast.makeText(this, Html.fromHtml(this.getString(R.string.cancelled)), Toast.
LENGTH_LONG).show();
return; //don't execute the rest of this method.
}
if (elasticIpsModel != null) {
processElasticIpsModelResult(result);
}
else if (ec2InstancesModel != null) {
processEC2InstanceModelResult(result);
}
//the control instances model returns after stoppign or starting an instance
else if (controlInstancesModel != null) {
processControlInstancesModelResult(result);
}
//if failed, then display dialog box.
if (alertDialogDisplayed) {
alertDialogBox.setMessage(alertDialogMessage);
alertDialogBox.show();
}
}
/**
*
* @param result. Result can be one of:
* <ul>
* <li>ArrayList<SerializableAddress>: An ArrayList (known in this case to be of size 1)
* containing the data on the IP. This model is execd when a running instance's view
* is created.</li>
* <li>AmazonClientException: Issues with the client.</li>
* <li>AmazonServiceException: Issues with the service itself.</li>
* </ul>
*/
@SuppressWarnings("unchecked")
private void processElasticIpsModelResult(Object result) {
elasticIpsModel = null;
// if the model returned a result; i.e. success.
if (result instanceof ArrayList<?>) {
//if there is data, set boolean to true
if (((ArrayList<SerializableAddress>) result).size() != 0) {
isElasticIpAssigned = true;
}
else {
isElasticIpAssigned = false; //this has to be done manually because we are
//using a Boolean and not a boolean. When null, we execute the model.
}
//populate the list
setListAdapter(new EC2SingleInstanceAdapter(this, R.layout.ec2singleinstance,
instance, isElasticIpAssigned));
}
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.
}
}
/**
*
* @param result. Result can be one of:
* <ul>
* <li>ArrayList<SerializableInstance>: An ArrayList (known in this case to be of size 1)
* containing the data on the instance. This model is execd when the instance is refreshed,
* or TODO when the instance state is changed and we want the display to auto-refresh.</li>
* <li>AmazonClientException: Issues with the client.</li>
* <li>AmazonServiceException: Issues with the service itself.</li>
* </ul>
*/
@SuppressWarnings("unchecked")
private void processEC2InstanceModelResult(Object result) {
ec2InstancesModel = null; //set model to null
autoRefresh = false; //set autorefresh to false even if it already was faslse
// dismiss the progress dialog if displayed. Check redundant
if (progressDialogDisplayed) {
progressDialogDisplayed = false;
removeDialog(DialogConstants.PROGRESS_DIALOG.ordinal());
}
if (result instanceof ArrayList<?>) {
//there's going to be only 1 entry. Tested in unit tests
Log.v(TAG, "Result size: " + ((ArrayList<SerializableInstance>) result).size());
instance = ((ArrayList<SerializableInstance>) result).get(0);
//change the title, just in case
((TextView)findViewById(R.id.ec2SingleInstanceName)).setText(Html.fromHtml(String.format(
this.getString(R.string.ec2singleinstance_tag), instance.getTag())));
if (isElasticIpAssigned != null) {
//populate the list
setListAdapter(new EC2SingleInstanceAdapter(this, R.layout.ec2singleinstance,
instance, isElasticIpAssigned));
}
else {
//elastic IP not assigned; rerun model.
executeElasticIpModel();
}
}
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.
}
}
/**
* Process the results returned by the ControlInstancesModel
*
* @param result. Result can be:
* <ul>
* <li>List<InstanceStateChange>: An ArrayList containing the state changes to
* each of the instances stopped/started.</li>
* <li>AmazonClientException: Issues with the client.</li>
* <li>AmazonServiceException: Issues with the service itself.</li>
* </ul>
*/
@SuppressWarnings("unchecked")
private void processControlInstancesModelResult(Object result) {
//set the model to null
controlInstancesModel = null;
//if successful it would have returned a ArrayList<InstanceStateChange>.
if (result instanceof List<?>) {
int expectedState = -1;
//there's only going to be one entry in the list, but nevertheless...
for (InstanceStateChange change : (List<InstanceStateChange>) result) {
Log.v(TAG, "Instance ID: " + change.getInstanceId() + ", State: " + change.
getCurrentState());
if (change.getInstanceId().equals(instance.getInstanceId())) {
//set the state code and state name
instance.setStateCode(change.getCurrentState().getCode());
instance.setStateName(change.getCurrentState().getName());
//get the state we should eventually get to
if (change.getPreviousState().getCode() == InstanceStateConstants.STOPPED) {
expectedState = InstanceStateConstants.RUNNING;
}
else {
expectedState = InstanceStateConstants.STOPPED;
}
}
}
//populate the list
setListAdapter(new EC2SingleInstanceAdapter(this, R.layout.ec2singleinstance,
instance, isElasticIpAssigned));
//start refresh until state is not equal to expected state.
//i.e. running if stopped, stopped if running.
if (expectedState >= 0) {
autoRefreshEC2InstanceModel(expectedState);
}
}
else if (result instanceof Boolean) {
Log.v(TAG, "Successfully tagged instance.");
}
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.
}
}
/**
* Overridden method to display the menu on press of the menu key
*
* Inflates and shows menu for displayed instances view.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.singleinstance_menu, menu);
return true;
}
/**
* Overriden. Prepares menu
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
Log.v(TAG, "Preparing Options menu....");
if (instance.getStateCode() == InstanceStateConstants.RUNNING) {
menu.findItem(R.id.singleinstance_menuitem_monitor).setEnabled(true);
menu.findItem(R.id.singleinstance_menuitem_ssh).setEnabled(true);
menu.findItem(R.id.singleinstance_menuitem_controlinstance).setEnabled(true);
//make sure that the start/stop instance state is saying Stop
menu.findItem(R.id.singleinstance_menuitem_controlinstance).setTitle
(R.string.ec2singleinstance_menu_stopinstance);
menu.findItem(R.id.singleinstance_menuitem_controlinstance).setIcon(R.drawable.
ic_menu_stop);
}
else if (instance.getStateCode() == InstanceStateConstants.STOPPED) {
//enable the control instance button
menu.findItem(R.id.singleinstance_menuitem_controlinstance).setEnabled(true);
//if the instance is stopped, then change the control instance menu item's text and img
//to show "start instance" and a "play" button respectively.
menu.findItem(R.id.singleinstance_menuitem_controlinstance).setTitle
(R.string.ec2singleinstance_menu_startinstance);
menu.findItem(R.id.singleinstance_menuitem_controlinstance).setIcon(R.drawable.
ic_menu_play_clip);
}
else //in between running and stopped
if (instance.getStateCode() != InstanceStateConstants.RUNNING) {
//if the instance is not running, disable monitoring and SSH
Log.v(TAG + ".onPrepareOptionsMenu()", "Removing monitoring and" +
"SSH connect options.");
menu.findItem(R.id.singleinstance_menuitem_monitor).setEnabled(false);
menu.findItem(R.id.singleinstance_menuitem_ssh).setEnabled(false);
}
else {
}
return true;
}
/**
* Overriden method to handle selection of menu item
*/
@Override
public boolean onOptionsItemSelected(MenuItem selectedItem) {
switch (selectedItem.getItemId()) {
case R.id.singleinstance_menuitem_about:
Intent aboutIntent = new Intent(this, AboutView.class);
startActivity(aboutIntent);
return true;
case R.id.singleinstance_menuitem_ssh:
Log.v(TAG + ".onOptionsItemSelected()", "User wishes to SSH!");
//call the SSH connector view using the intent
Intent sshConnectorIntent = new Intent();
sshConnectorIntent.setClassName("org.elasticdroid","org.elasticdroid.SshConnectorView");
//if the user deallocates Elastic IP address in the middle of all of this you won't have
//a public DNS name. Just issue an error and fuck off.
if (instance.getPublicDnsName() == null) {
alertDialogMessage = this.getString(R.string.ec2singleinstance_nopublicdns);
alertDialogDisplayed = true;
killActivityOnError = true;
instanceStateChanged = true; //force refresh of Display Instances list.
alertDialogBox.setMessage(alertDialogMessage);
alertDialogBox.show();
}
else {
//not using IP address as theoretically DHCP lease can expire when connecting
//if Elastic IP is not used.
sshConnectorIntent.putExtra("hostname", instance.getPublicDnsName());
List<String> secGroupNames = instance.getSecurityGroupNames();
//breaking 100 character per line unwritten rule here as the code looks better this way
sshConnectorIntent.putExtra("securityGroups",
secGroupNames.toArray(new String[secGroupNames.size()]));
sshConnectorIntent.putExtra("selectedRegion", selectedRegion);
sshConnectorIntent.putExtra(
"org.elasticdroid.EC2DashboardView.connectionData",
connectionData); // aws connection info
//start the activity
startActivity(sshConnectorIntent);
}
return true;
case R.id.singleinstance_menuitem_refresh:
//create the model and launch it with the filter
//execute Elastic IP model as well as EC2 instance model
//this is because Elastic IPs may have been assigned/deassigned.
executeElasticIpModel();
executeEC2InstanceModel();
return true;
case R.id.singleinstance_menuitem_monitor:
Intent monitorIntent = new Intent();
monitorIntent.setClassName("org.elasticdroid",
"org.elasticdroid.MonitorInstanceView");
//send it the AWS connection data.
monitorIntent.putExtra("org.elasticdroid.EC2SingleInstanceView.connectionData",
connectionData);
monitorIntent.putExtra("instanceId", instance.getInstanceId());
monitorIntent.putExtra("selectedRegion", selectedRegion);
startActivity(monitorIntent);
return true;
case R.id.singleinstance_menuitem_controlinstance:
//do not execute if the user cancelled when getting elastic IP data (applicable only
//to running instances)
if ( (isElasticIpAssigned == null) && (instance.getStateCode() ==
InstanceStateConstants.RUNNING)) {
return false;
}
executeControlInstancesModel(false); //execute control instances model
//to start/stop instance
return true;
case R.id.singleinstance_menuitem_tag:
Intent startIntent = new Intent();
startIntent.setClassName("org.elasticdroid", "org.elasticdroid.TagView");
Log.v(TAG, "Instance ID: " + instance.getInstanceId());
startIntent.putExtra("instanceId", instance.getInstanceId());
if (instance.getTag() != null) {
startIntent.putExtra("tag", instance.getTag());
}
startActivityForResult(startIntent, 0); //second arg ignored
return true;
default:
return super.onOptionsItemSelected(selectedItem);
}
}
/**
* Handle back button.
* If back button is pressed, UI should die.
*/
@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) {
Intent resultIntent = new Intent();
resultIntent.setType(this.getClass().getName());
Log.v(TAG, "Force refresh: " + instanceStateChanged);
resultIntent.putExtra("forceRefresh", instanceStateChanged);
//if EC2 instances model is stil running autorefresh, cancel it.
if (ec2InstancesModel != null) {
ec2InstancesModel.setActivityNull(); //tell the model the activity is goign bye bye
ec2InstancesModel.cancel(true);
}
setResult(RESULT_OK, resultIntent); //return result to calling party
finish();
}
return super.onKeyDown(keyCode, event);
}
/**
* Handle cancel of progress dialog
* @see android.content.DialogInterface.OnCancelListener#onCancel(android.content.
* DialogInterface)
*/
@Override
public void onCancel(DialogInterface dialog) {
//do not allow the ControlInstancesModel to be cancelled.
if (controlInstancesModel != null) {
//do not allow cancel. Redisplay.
showDialog(DialogConstants.PROGRESS_DIALOG.ordinal());
}
else {
//this cannot be called UNLESS the user has the model running.
//i.e. the prog bar is visible
progressDialogDisplayed = false;
if (elasticIpsModel != null) {
elasticIpsModel.cancel(true);
}
else if (ec2InstancesModel != null) {
ec2InstancesModel.cancel(true);
}
}
}
/**
* Called when the tag changer returns.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultIntent) {
if (resultCode == RESULT_OK) {
if (!resultIntent.getStringExtra("tag").trim().equals("")) {
instance.setTag(resultIntent.getStringExtra("tag"));
}
//the user deleted the tag.
else {
instance.setTag(null);
}
executeControlInstancesModel(true);
}
}
}
/**
* ListView Adapter that extends ArrayAdapter<RowData>
*
* Displays the metrics of each instance.
*
* @author siddhu
*
* 12 Dec 2010
*/
class EC2SingleInstanceAdapter extends ArrayAdapter<RowData> {
/** Instance datum */
private SerializableInstance instance;
/** Does the above instance have an Elastic IP assigned? */
private boolean isElasticIpAssigned;
/** Context */
private Context context;
/**
* Constructor: Calls ArrayAdapter constructor with a RowData enum that contains all of the
* fields we want to display in the ListView.
*
* @param context
* @param textViewResourceId
* @param objects
*/
public EC2SingleInstanceAdapter(Context context, int textViewResourceId,
SerializableInstance instance, boolean isElasticIpAssigned) {
super(context, textViewResourceId, RowData.values());
//save context and instance data
this.context = context;
this.instance = instance;
this.isElasticIpAssigned = isElasticIpAssigned;
}
/**
* Overriden method called when ListView is initialised with data.
* @param position The position in {@link #instanceData}.
* @param convertView The view to set.
* @param parent
* @return Configured row to add to ListView
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View instanceMetricRow = convertView;
if (instanceMetricRow == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
instanceMetricRow = inflater.inflate(R.layout.ec2singleinstancerow, parent,
false);
}
Log.v(context.getClass().getName(), "Instance state code:" + instance.getStateCode());
TextView instanceMetricTextView = (TextView)instanceMetricRow.findViewById(R.id.
instanceMetric);
TextView instanceDataTextView = (TextView)instanceMetricRow.findViewById(R.id.
instanceData);
ImageView instanceStatusIcon = ((ImageView) instanceMetricRow.findViewById(R.id.
instanceStatusIcon));
RowData selectedRowDatum = RowData.values()[position];
switch(selectedRowDatum) {
case STATE_NAME:
instanceMetricTextView.setText(context.getString(R.string.ec2singleinstance_state));
instanceDataTextView.setText(Html.fromHtml(instance.getStateName()));
if (instance.getStateCode() == InstanceStateConstants.RUNNING) {
((ImageView) instanceMetricRow.findViewById(R.id.instanceStatusIcon)).
setImageResource(R.drawable.green_light);
}
else if ((instance.getStateCode() == InstanceStateConstants.STOPPED) || (instance.
getStateCode() == InstanceStateConstants.TERMINATED)) {
instanceStatusIcon.setImageResource(R.drawable.red_light);
}
//instance is between starting and stopping/terminating.
else if ( (instance.getStateCode() == InstanceStateConstants.SHUTTING_DOWN)
||(instance.getStateCode() == InstanceStateConstants.PENDING)
||(instance.getStateCode() == InstanceStateConstants.STOPPING))
{
instanceStatusIcon.setImageResource(R.drawable.yellow_light);
}
break;
case TYPE:
instanceMetricTextView.setText(context.getString(R.string.ec2singleinstance_type));
instanceDataTextView.setText(Html.fromHtml(instance.getInstanceType()));
instanceStatusIcon.setImageResource(R.drawable.instance);
break;
case OS:
instanceMetricTextView.setText(context.getString(R.string.ec2singleinstance_os));
if (instance.getPlatform() != null) {
instanceDataTextView.setText(Html.fromHtml(instance.getPlatform()));
instanceStatusIcon.setImageResource(R.drawable.droid_guy);
}
else {
instanceDataTextView.setText(Html.fromHtml("Linux"));
instanceStatusIcon.setImageResource(R.drawable.linux_penguin);
}
break;
case LAUNCHED:
instanceMetricTextView.setText(context.getString(R.string.
ec2singleinstance_launchtime));
instanceStatusIcon.setImageResource(R.drawable.ic_dialog_time);
if (instance.getStateCode() == InstanceStateConstants.RUNNING) {
//get period running in hours.
float timeRunning = ((new Date().getTime() - instance.getLaunchTime()) /
(1000 * 60 * 60)); //convert from milliseconds to hours
String launchDetails;
//if been running greater than 24 hours, convert to days
if (timeRunning > 24) {
timeRunning /= 24;
launchDetails = String.format(
context.getString(R.string.ec2singleinstance_launchdetails_days),
timeRunning
) ;
}
else {
launchDetails = String.format(
context.getString(R.string.ec2singleinstance_launchdetails_hrs),
timeRunning
);
}
instanceDataTextView.setText(Html.fromHtml(launchDetails));
}
else {
instanceDataTextView.setText("N/A");
}
break;
case KEYNAME:
instanceMetricTextView.setText(context.getString(R.string.ec2singleinstance_keypair));
instanceDataTextView.setText(Html.fromHtml(instance.getKeyName()));
instanceStatusIcon.setImageResource(R.drawable.keypair);
break;
case SECURITY_GROUP:
//concatenate all security groups in list into one long String separated by spaces
String securityGroupString = "";
List<String> securityGroupNames = instance.getSecurityGroupNames();
//O(n) solution
for (String securityGroupName : securityGroupNames) {
securityGroupString += securityGroupName + ", ";
}
//remove last comma
securityGroupString = securityGroupString.substring(0, securityGroupString.length() - 2);
instanceMetricTextView.setText(context.getString(R.string.ec2singleinstance_secgroup));
instanceDataTextView.setText(Html.fromHtml(securityGroupString));
instanceStatusIcon.setImageResource(R.drawable.ic_lock_lock);
break;
case AMI_ID:
instanceMetricTextView.setText(context.getString(R.string.ec2singleinstance_ami));
instanceDataTextView.setText(Html.fromHtml(instance.getImageId()));
instanceStatusIcon.setImageResource(R.drawable.ami);
break;
case IP_ADDRESS:
//set IP address only if instance is running
if (instance.getStateCode() == InstanceStateConstants.RUNNING) {
if (isElasticIpAssigned) {
instanceMetricTextView.setText(context.getString(R.string.
ec2singleinstance_elastic_ip));
//to handle bug: http://code.google.com/p/elastic-droid/issues/detail?id=13
//AWS doesn't seem to assign public DNS names to instances that
//have their Elastic IPs removed midway.
if (instance.getPublicIpAddress() != null) {
instanceDataTextView.setText(Html.fromHtml(instance.getPublicIpAddress()));
}
else {
instanceDataTextView.setText(context.getString(R.string.
ec2singleinstance_nopublicdns));
}
}
else {
instanceMetricTextView.setText(context.getString(R.string.
ec2singleinstance_public_ip));
if (instance.getPublicIpAddress() != null) {
instanceDataTextView.setText(Html.fromHtml(instance.getPublicIpAddress()));
}
else {
instanceDataTextView.setText(context.getString(R.string.
ec2singleinstance_nopublicdns));
}
}
}
else {
instanceMetricTextView.setText(context.getString(R.string.
ec2singleinstance_public_ip));
instanceDataTextView.setText("N/A");
}
instanceStatusIcon.setImageResource(R.drawable.ic_menu_mylocation);
break;
}
return instanceMetricRow;
}
/**
* Function to disable all items in the ListView, as we do not want users clicking on
* them.
*/
@Override
public boolean areAllItemsEnabled()
{
return false;
}
/**
* Another function that does the same as hte function above
*/
@Override
public boolean isEnabled(int position)
{
return false;
}
}
/**
* An enumeration that gets the text to be used for each data
* type displayed by the EC2SingleInstanceAdapter.
* @author siddhu
*
* 12 Dec 2010
*/
enum RowData {
STATE_NAME,
TYPE,
OS,
LAUNCHED,
KEYNAME,
SECURITY_GROUP,
AMI_ID,
IP_ADDRESS;
}