org.elasticdroid.EC2DisplayInstancesView.java Source code

Java tutorial

Introduction

Here is the source code for org.elasticdroid.EC2DisplayInstancesView.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 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;
    }

}