package org.elasticdroid; import static org.elasticdroid.utils.ResultConstants.RESULT_ERROR; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.elasticdroid.db.ElasticDroidDB; import org.elasticdroid.model.EC2InstancesModel; import org.elasticdroid.model.ds.SerializableInstance; import org.elasticdroid.tpl.GenericListActivity; 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.util.TypedValue; 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.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; public class InstanceGroupEditView extends GenericListActivity { /** The selected region */ private String selectedRegion; /** The connection data */ private HashMap<String,String> connectionData; /**The model object */ private EC2InstancesModel ec2InstancesModel; /** * 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; /** Dialog box for credential verification errors */ private AlertDialog alertDialogBox; /** * boolean to indicate if an error that occurred is sufficiently serious to * have the activity killed. */ private boolean killActivityOnError; /**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 = InstanceGroupEditView.class.getName(); @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(); selectedRegion = intent.getStringExtra("selectedRegion"); try { this.connectionData = (HashMap<String, String>)intent.getSerializableExtra( "org.elasticdroid.EC2DashboardView.connectionData"); } //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. @Override 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) { InstanceGroupEditView.this.finish(); Intent loginIntent = new Intent(); loginIntent.setClassName("org.elasticdroid", "org.elasticdroid.LoginView"); startActivity(loginIntent); } } }); //set the content view setContentView(R.layout.instancegroupedit); //set the title this.setTitle(connectionData.get("username") + " (" + selectedRegion +")"); } /** * 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"); //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 SelectableInstanceDisplayAdapter( this, R.layout.selectableinstancerow, instanceData) ); getListView().setItemsCanFocus(false); getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); } } } /** * 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); //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); ec2InstancesModel.execute(); } @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. Log.v(this.getClass().getName(), "populating the list"); setListAdapter(new SelectableInstanceDisplayAdapter( this, R.layout.selectableinstancerow, instanceData) ); getListView().setItemsCanFocus(false); getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); } //if no data found, just show a String adapter else { Log.v(this.getClass().getName(), "no data found"); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new String[]{"No instances found."})); } } 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 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()); setResult(RESULT_OK, resultIntent); //let the calling activity know that the user chose to //cancel } 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) { //this cannot be called UNLESS the user has the model running. //i.e. the prog bar is visible progressDialogDisplayed = false; ec2InstancesModel.cancel(true); } /** * 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) { Log.d(TAG, "Position clicked: " + position); /*getListView().setItemChecked(position, !getListView().isItemChecked(position)); getListView().isItemChecked(position);*/ } /** * 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.instancegroup_menu, menu); return true; } /** * Overriden method to handle selection of menu item */ @Override public boolean onOptionsItemSelected(MenuItem selectedItem) { switch (selectedItem.getItemId()) { case R.id.instancegroup_menuitem_save: saveInstanceGroupDataToDb(); return true; default: return super.onOptionsItemSelected(selectedItem); } } private List<String> instanceIds; /** * Method to save instance group data to DB. */ private void saveInstanceGroupDataToDb() { instanceIds = new ArrayList<String>(); ListView instancesListView = getListView(); for (int listItemPos = 0; listItemPos < instancesListView.getCount(); listItemPos ++) { if (getListView().isItemChecked(listItemPos)) { Log.d(TAG, "Adding instnace : " + instanceData.get(listItemPos).getInstanceId() + " to " + "group"); instanceIds.add(instanceData.get(listItemPos).getInstanceId()); } } Log.d(TAG, "Adding " + instanceIds.size() + " instances..."); if (instanceIds.size() == 0) { alertDialogMessage = getString(R.string.ec2instancegroupsview_select_instance); alertDialogDisplayed = true; killActivityOnError = false; } else { AlertDialog.Builder groupNameDialog = new AlertDialog.Builder(this); // Set an EditText view to get user input LinearLayout groupNameDialogLayout = new LinearLayout(this); groupNameDialogLayout.setOrientation(LinearLayout.VERTICAL); final EditText groupNameInput = new EditText(this); groupNameInput.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); TextView groupNameTextView = new TextView(this); groupNameTextView.setText(getString(R.string.ec2instancegroupsview_input)); groupNameTextView.setPadding(0, 0, 0, 10); groupNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22.0f); groupNameTextView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); groupNameDialogLayout.addView(groupNameTextView); groupNameDialogLayout.addView(groupNameInput); groupNameDialog.setView(groupNameDialogLayout); //set the listener up. groupNameDialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { String newGroupName = groupNameInput.getText().toString(); //write to DB try { new ElasticDroidDB(getApplicationContext()).writeInstanceGroupsToDb(connectionData.get("username"), selectedRegion, newGroupName, instanceIds); Toast.makeText( getApplicationContext(), getApplicationContext().getString(R.string.ec2instancegroupsview_new_group_notification), Toast.LENGTH_LONG) .show(); } catch (SQLException e) { alertDialogMessage = "Group creation failed: " + e.getLocalizedMessage(); alertDialogDisplayed = true; killActivityOnError = true; } //display alert dialog if requested if (alertDialogDisplayed) { alertDialogBox.setMessage(alertDialogMessage); alertDialogBox.show(); } } }); groupNameDialog.show(); } //display alert dialog if requested if (alertDialogDisplayed) { alertDialogBox.setMessage(alertDialogMessage); alertDialogBox.show(); } } } /** * Adapter to display instances with checkboxes in a list view. * @author Rodolfo Cartas * * 18 Jan 2010 */ class SelectableInstanceDisplayAdapter extends ArrayAdapter<SerializableInstance> implements OnClickListener{ /** Instance list */ private ArrayList<SerializableInstance> instanceData; /** Context; typically the Activity that sets an object of this class as the Adapter */ private Context context; /**Logging tag */ private static String TAG = "org.elasticdroid.InstanceGroupEditView$SelectableInstanceDisplayAdapter"; /** * @param context * @param textViewResourceId */ public SelectableInstanceDisplayAdapter(Context context, int textViewResourceId, ArrayList<SerializableInstance> instanceData) { super(context, textViewResourceId, instanceData); //save the context, data, and list type this.context = context; this.instanceData = instanceData; } /** * 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; if (instanceDataRow == null) { LayoutInflater inflater = (LayoutInflater) context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); instanceDataRow = inflater.inflate(R.layout.selectableinstancerow, parent, false); } //set main text view CheckedTextView checkedTextView = (CheckedTextView)instanceDataRow.findViewById(R.id. CheckedTextView01); if (instanceData.get(position).getTag() != null) { checkedTextView.setText(instanceData.get(position).getTag()); } else { checkedTextView.setText(instanceData.get(position).getInstanceId()); } checkedTextView.setOnClickListener(this); return instanceDataRow; } @Override public void onClick(View checkedView) { CheckedTextView checkedTextView = (CheckedTextView) checkedView; Log.d(TAG, "Clicked: " + checkedTextView.getText().toString()); checkedTextView.setChecked(!checkedTextView.isChecked()); } }