/**
* 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 Warrier on 5 Dec 2010
*/
package org.elasticdroid;
import static org.elasticdroid.utils.ResultConstants.RESULT_ERROR;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import org.elasticdroid.model.EC2InstancesModel;
import org.elasticdroid.model.ds.SerializableInstance;
import org.elasticdroid.tpl.GenericListActivity;
import org.elasticdroid.utils.AWSConstants.InstanceStateConstants;
import org.elasticdroid.utils.DialogConstants;
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.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.model.Filter;
/**
* This class will display a list of instances that are
* running or stopped.
*
* This class extends GenericListActivity.
*
* This class may later be extended to also handle
* keypair and security group display.
* @author Siddhu Warrier
*
* 5 Dec 2010
*/
public class EC2DisplayInstancesView extends GenericListActivity {
/**
* The type of list to display. Accepted values atm are RUNNING and STOPPED
*/
private int listType;
/** The selected region */
private String selectedRegion;
/** The connection data */
private HashMap<String,String> connectionData;
/**The model object */
private EC2InstancesModel ec2InstancesModel;
/** Dialog box for credential verification 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;
/**
* boolean to indicate if any of the individual instances in this list have had
* their states changed.
*/
private boolean instanceStateChanged;
/**The model result: an ArrayList of corresponding instances
* Uses Serializable Instance and not AWS Instance. {@link SerializableInstance}
* */
private ArrayList<SerializableInstance> instanceData;
/**
* Logging Tag
*/
private static final String TAG = "org.elasticdroid.EC2DisplayInstancesView";
/**
* This method is called when the activity is (re)started.
*
* @param savedInstanceState Instance state saved (if any) on screen destroy. See
* @see EC2DisplayInstancesView#onSaveInstanceState(Bundle)
*/
@SuppressWarnings("unchecked")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); //call superclass onCreate
/* get intent data */
//get the type of list to display from the intent.
Intent intent = this.getIntent();
listType = intent.getIntExtra("listType", InstanceStateConstants.RUNNING);
selectedRegion = intent.getStringExtra("selectedRegion");
try {
this.connectionData = (HashMap<String, String>)intent.getSerializableExtra(
"org.elasticdroid.EC2DashboardView.connectionData");
if (connectionData == null) {
Log.d(TAG, "Connection data null.");
}
}
//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
catch(Exception exception) {
Log.e(this.getClass().getName(), exception.getMessage());
//return the failure to the mama class
Intent resultIntent = new Intent();
resultIntent.setType(this.getClass().getName());
resultIntent.putExtra("EXCEPTION_MSG", this.getClass().getName() + ":" +
exception.getMessage());
setResult(RESULT_ERROR, resultIntent);
}
// create and initialise the alert dialog
alertDialogBox = new AlertDialog.Builder(this).create(); // create alert
// box to
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) {
EC2DisplayInstancesView.this.finish();
Intent loginIntent = new Intent();
loginIntent.setClassName("org.elasticdroid",
"org.elasticdroid.LoginView");
startActivity(loginIntent);
}
}
});
//set the content view
setContentView(R.layout.ec2displayinstances);
//set the title
this.setTitle(connectionData.get("username") + " (" + selectedRegion +")");
//set the heading appropriately
if (listType == InstanceStateConstants.RUNNING) {
((TextView)findViewById(R.id.ec2DisplayInstancesTextView)).setText(this.getString(
R.string.ec2displayinstances_running_title));
}
else if (listType == InstanceStateConstants.STOPPED) {
((TextView)findViewById(R.id.ec2DisplayInstancesTextView)).setText(this.getString(
R.string.ec2displayinstances_stopped_title));
}
}
/**
* Restore instance state when the activity is reconstructed after a destroy
*
* This method restores:
* <ul>
* <li>instanceData: The list of instances</li>
* <li>progressDialogDisplayed: Was a progress dialog displayed?</li>
* <li>ec2DisplayInstancesModel: The retained config object containing the model object.</li>
* </ul>
*/
@SuppressWarnings("unchecked")
@Override
public void onRestoreInstanceState(Bundle stateToRestore) {
// restore dialog data
alertDialogDisplayed = stateToRestore.getBoolean("alertDialogDisplayed");
Log.v(this.getClass().getName(), "alertDialogDisplayed = "
+ alertDialogDisplayed);
alertDialogMessage = stateToRestore.getString("alertDialogMessage");
//restore instance data if any
instanceData = (ArrayList<SerializableInstance>)stateToRestore.getSerializable("instanceData");
//has the instance state been changed.
instanceStateChanged = stateToRestore.getBoolean("instanceStateChanged");
//was a progress dialog being displayed.
progressDialogDisplayed = stateToRestore.getBoolean("progressDialogDisplayed");
Log.v(this.getClass().getName() + ".onRestoreInstanceState", "progbar:" +
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.
if (retained instanceof EC2InstancesModel) {
Log.i(this.getClass().getName() + ".onRestoreInstanceState()","Reclaiming previous " +
"background task");
ec2InstancesModel = (EC2InstancesModel) retained;//force typecast
ec2InstancesModel.setActivity(this);//pass the model reference to activity
}
else {
ec2InstancesModel = null;
Log.v(this.getClass().getName(),"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;
}
//if we have instance data, reload the list
if (instanceData != null) {
setListAdapter(new EC2DisplayInstancesAdapter(this, R.layout.ec2displayinstancesrow,
instanceData, listType));
}
}
}
/**
* Executed last in the (re)start lifecycle, this method starts the model if both these
* conditions are met:
*
* <ul>
* <li>There is no currently running model.</li>
* <li>There is no instance data already computed.</li>
* </ul>
*/
@Override
public void onResume() {
super.onResume(); //call base class method
//if there was a dialog box, display it
//if failed, then display dialog box.
if (alertDialogDisplayed) {
alertDialogBox.setMessage(alertDialogMessage);
alertDialogBox.show();
} else if ((ec2InstancesModel == null) && (instanceData == null)) {
executeModel();
}
}
/**
* 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);
//were any of the instances' states changed by the user?
saveState.putBoolean("instanceStateChanged", instanceStateChanged);
//if we have instance data, save it.
//but don't bother saving it if the model is not null, i.e. a new model
//is executing.
if ((instanceData != null) && (ec2InstancesModel == null)) {
saveState.putSerializable("instanceData", instanceData);
}
//save if progress dialog is being displayed.
saveState.putBoolean("progressDialogDisplayed", progressDialogDisplayed);
}
/**
* Save reference to {@link org.elasticdroid.model.EC2DisplayInstancesModel 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() {
Log.v(this.getClass().getName(), "Object about to destroyed...");
// if the model is being executed when the onDestroy method is called.
//tell the model that the activity has now disappeared. Hopefully, the
//activity will return.
if (ec2InstancesModel != null) {
ec2InstancesModel.setActivityNull();
return ec2InstancesModel;
}
//if there was no model being executed, just return null
return null;
}
//private methods
/**
* Execute the model to retrieve EC2 instance data for the selected region. The model
* runs in a different thread and calls processModelResults when done.
*/
private void executeModel() {
ec2InstancesModel = new EC2InstancesModel(this, connectionData, selectedRegion);
// add the endpoint for this region to connectionData
// it's not nice to modify a member like this, now, is it?
Filter instanceStateFilter = new Filter("instance-state-code");
if (listType == InstanceStateConstants.RUNNING) {
instanceStateFilter.setValues(Arrays.asList(
new String[]{String.valueOf(InstanceStateConstants.RUNNING)}));
}
else {
instanceStateFilter.setValues(Arrays.asList(
new String[]{String.valueOf(InstanceStateConstants.STOPPED)}));
}
ec2InstancesModel.execute(instanceStateFilter);
}
//overriden methods
/**
* This method processes results generated by the model.
* @see org.elasticdroid.tpl.GenericListActivity#processModelResults(java.lang.Object)
*/
@SuppressWarnings("unchecked")
@Override
public void processModelResults(Object result) {
Log.v(this.getClass().getName()+".processModelResults()", "Model returned...");
// dismiss the progress dialog if displayed. Check redundant
if (progressDialogDisplayed) {
removeDialog(DialogConstants.PROGRESS_DIALOG.ordinal());
progressDialogDisplayed = false;
}
//i.e. user did not cancel
if (result != null) {
//set reference to model object to null
ec2InstancesModel = null;
//get the model data
if (result instanceof ArrayList<?>) {
try {
instanceData = (ArrayList<SerializableInstance>)result;
}
catch(Exception exception) {
Log.e(this.getClass().getName(), exception.getMessage());
//return the failure to the mama class
Intent resultIntent = new Intent();
resultIntent.setType(this.getClass().getName());
resultIntent.putExtra("EXCEPTION_MSG", this.getClass().getName() + ":" +
exception.getMessage());
setResult(RESULT_ERROR, resultIntent);
}
if (instanceData.size() != 0) {
//add the instances to the list adapter to display.
setListAdapter(new EC2DisplayInstancesAdapter(this, R.layout.ec2displayinstancesrow,
instanceData, listType));
}
//if no data found, just show a String adapter
else {
ArrayAdapter<String> listAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
new String[]{getString(R.string.ec2displayinstances_menu_no_instances)});
setListAdapter(listAdapter);
}
}
else if (result instanceof AmazonServiceException) {
// if a server error
if (((AmazonServiceException) result).getErrorCode()
.startsWith("5")) {
alertDialogMessage = "AWS server error.";
} 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
}
else if (result instanceof IllegalArgumentException) {
alertDialogMessage = this
.getString(R.string.ec2dashview_illegal_arg_exception);
alertDialogDisplayed = true;
killActivityOnError = true;
}
}
else {
Toast.makeText(this, Html.fromHtml(this.getString(R.string.cancelled)), Toast.
LENGTH_LONG).show();
}
//if failed, then display dialog box.
if (alertDialogDisplayed) {
alertDialogBox.setMessage(alertDialogMessage);
alertDialogBox.show();
}
}
/**
* Handle the selection of a given instance, and pass the relevant SerializableInstance object
* on.
*/
@Override
protected void onListItemClick(ListView list, View v, int position, long id) {
if (instanceData.size() <= position) {
return;
}
Intent displaySingleInstanceIntent = new Intent();
displaySingleInstanceIntent.setClassName("org.elasticdroid",
"org.elasticdroid.EC2SingleInstanceView");
//send it the AWS connection data.
displaySingleInstanceIntent.putExtra(
"org.elasticdroid.EC2DashboardView.connectionData",
connectionData); // aws connection info
//send it a single SerializableInstance
displaySingleInstanceIntent.putExtra("org.elasticdroid.model.SerializableInstance",
instanceData.get(position));
//send it the selected region
displaySingleInstanceIntent.putExtra("selectedRegion", selectedRegion);
//start the activity
startActivityForResult(displaySingleInstanceIntent, 0); //second argument ignored.
}
/**
* Handle back button.
*/
@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) {
//return the failure to the mama class
Intent resultIntent = new Intent();
resultIntent.setType(this.getClass().getName());
Log.v(TAG, "Force Refresh: " + instanceStateChanged);
resultIntent.putExtra("forceRefresh", instanceStateChanged);
setResult(RESULT_OK, resultIntent); //let the calling activity know that the user chose to
//cancel
}
return super.onKeyDown(keyCode, event);
}
/**
* 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.displayinstances_menu, menu);
return true;
}
/**
* Overriden method to handle selection of menu item
*/
@Override
public boolean onOptionsItemSelected(MenuItem selectedItem) {
switch (selectedItem.getItemId()) {
case R.id.displayinstances_menuitem_about:
Intent aboutIntent = new Intent(this, AboutView.class);
startActivity(aboutIntent);
return true;
case R.id.displayinstances_menuitem_refresh:
executeModel();
return true;
default:
return super.onOptionsItemSelected(selectedItem);
}
}
/**
* Called when the following views return:
* <ul>
* <li> {@link EC2SingleInstanceView}
* </ul>
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (result.getType().equals(EC2SingleInstanceView.class.getName())) {
Log.v(TAG, "EC2 SingleInstanceView returned...");
//if the list should be force refreshed, well, uhm, DO IT for chrissake.
if (result.getBooleanExtra("forceRefresh", false)) {
Log.v(TAG, "Forcing refresh of list as single instance just viewed has changed " +
"state.");
instanceStateChanged = true;
executeModel(); //force refresh
}
}
}
/**
* 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;
ec2InstancesModel.cancel(true);
}
}
/**
* Adapter to display the instances in a list view.
* @author Siddhu Warrier
*
* 6 Dec 2010
*/
class EC2DisplayInstancesAdapter extends ArrayAdapter<SerializableInstance>{
/** Instance list */
private ArrayList<SerializableInstance> instanceData;
/** Context; typically the Activity that sets an object of this class as the Adapter */
private Context context;
/** List type */
private int listType;
/**
* @param context
* @param textViewResourceId
*/
public EC2DisplayInstancesAdapter(Context context, int textViewResourceId,
ArrayList<SerializableInstance> instanceData, int listType) {
super(context, textViewResourceId, instanceData);
//save the context, data, and list type
this.context = context;
this.instanceData = instanceData;
this.listType = listType;
}
/**
* Overriden method called when ListView is initialised with data.
* @param position The position in {@link #instanceData}.
* @param convertView The view to set.
* @param parent
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View instanceDataRow = convertView;
String details = "";
if (instanceDataRow == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
instanceDataRow = inflater.inflate(R.layout.ec2displayinstancesrow, parent, false);
}
//set main text view
TextView textViewHeadline = (TextView)instanceDataRow.findViewById(R.id.instanceHeadline);
TextView textViewDetails = (TextView)instanceDataRow.findViewById(R.id.instanceDetails);
//set Instance ID as headline if no tag named "name"(case-insensitive) found.
if (instanceData.get(position).getTag() == null) {
textViewHeadline.setText(String.format(
context.getString(R.string.ec2displayinstances_instanceID),
instanceData.get(position).getInstanceId()));
}
else {
textViewHeadline.setText(String.format(
context.getString(R.string.ec2displayinstances_tag),
instanceData.get(position).getTag()));
}
details += String.format(
context.getString(R.string.ec2displayinstances_type),
instanceData.get(position).getInstanceType()) + ", ";
//get platform
details += String.format(
context.getString(R.string.ec2displayinstances_os),
(instanceData.get(position).getPlatform() == null?"Linux": instanceData.
get(position).getPlatform())) + ", ";
//don't bother getting day launched if the instance is stopped
if (listType == InstanceStateConstants.RUNNING) {
//get period running in hours.
float timeRunning = ((new Date().getTime() - instanceData.get(position).getLaunchTime()) /
(1000 * 60 * 60)); //convert from milliseconds to hours
//if been running greater than 24 hours, convert to days
if (timeRunning > 24) {
timeRunning /= 24;
details += String.format(
context.getString(R.string.ec2displayinstances_rundays),
timeRunning
) ;
}
else {
details += String.format(
context.getString(R.string.ec2displayinstances_runhrs),
timeRunning
);
}
}
textViewDetails.setText(Html.fromHtml(details));
return instanceDataRow;
}
}