Dialog As Activity

Advanced Topic: Launching a Second Activity Using Intents

In this lesson we will gather password information by launching a separate activity called ManageKey which extends Activity. We will learn how to push data from the ConfuseText activity (int length of key) to the ManageKey activity; and then push data from the ManageKey activity (string newPassword) back to the ConfuseText activity. We then learn how to handle events within the ManageKey class.  Here is the manage key view as an activity:


We can reuse the layout password_dialog from the previous lesson! Our foresight in separating out the presentation into an XML file now allows us to reuse this layout. First we create a simple class that extends Activity, MangageKeys.java.

package jalcomputing.confusetext;

import android.app.Activity;
import android.os.Bundle;

public class ManageKeys extends Activity {
    private String pushValue;
   
    @Override
    protected void onCreate(Bundle b){
        super.onCreate(b);
        setContentView(R.layout.password_dialog);
    }
}

We then edit the AndroidManifest.xml file to register our new Activity as:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jalcomputing.confusetext"
      android:versionCode="5"
      android:versionName="1.2">
    <application android:icon="@drawable/ic_launcher_confusetext"
                    android:label="@string/app_name">
        <activity android:name=".ConfuseText"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name= "ManageKeys"
            android:label="@string/manage_keys_title">
        </activity>
    </application>
    <uses-sdk android:minSdkVersion="3" />
</manifest>

Of course, we should add the manage_keys_title value to strings.xml (not shown). Back in the ConfuseText.java file we edit the onOptionsItemSelected method to launch our new Activity as:


    // HANDLE MENU CHOICES HERE
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
        case R.id.manage_password:
            Intent i= new Intent(this, ManageKeys.class);
            startActivity(i);
            return true;
        case R.id.options:
            ;
            return true;
        case R.id.about:
            this.showDialog(DIALOG_ABOUT);
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

Now when the user clicks on the context menu and choose "Manage Keys" our ManageKeys class will launch! It may be insightful to note how this use of Intents forces us to instantiate an instance of ManageKeys using a "constructor" that takes no parameters.

Note: If you try to launch ManageKeys from MyActivity.onCreate use the pattern new Intent(MyActivity.this, ManageKeys.class);


Pushing Data to ManageKeys

Now we want to push some data to our new view. We can do this using the putExtra method of Intent.

  
  // HANDLE MENU CHOICES HERE
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
        case R.id.manage_password:
            Intent i= new Intent(this, ManageKeys.class);
            i.putExtra("TestValue","Test");
            startActivity(i);
            return true;
        case R.id.options:
            ;
            return true;
        case R.id.about:
            this.showDialog(DIALOG_ABOUT);
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

The call i.putExtra("TestValue","Test") hides the calls to:

Bundle b= new Bundle();
b.putString("TestValue","Test");
i.putExtras(b);

We can retrieve the extra data "TestValue" in the ManageKeys class as:


package jalcomputing.confusetext;

import android.app.Activity;
import android.os.Bundle;

public class ManageKeys extends Activity {
    private String pushValue;
   
    @Override
    protected void onCreate(Bundle b){
        super.onCreate(b);
        setContentView(R.layout.password_dialog);
       
        try {
            pushValue= getIntent().getExtras().getString("TestValue");
        }
        catch(Exception e){
            pushValue= "";
        }
    }
}

Note the try catch to trap for a null bundle. If the key does not exist, the return value may be unexpected (int may return 0, string may return null, arrays may return null)! Another approach is:


Bundle extras= getIntent().getExtras();

 and do null checking on extras.


Pushing Data Back to ConfuseText

Now let us explore pushing data the other way, from the ManageKeys activity back to the ConfuseText activity. First, instead of calling startActivity(i), we will call startActivityForResult. Here is the modified onOptionsItemSelected method in ConfuseText:

   // HANDLE MENU CHOICES HERE
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
        case R.id.manage_password:
            Intent i= new Intent(this, ManageKeys.class);
            startActivityForResult(i,REQUEST_MANAGE_KEYS);
            return true;
        case R.id.options:
            ;
            return true;
        case R.id.about:
            this.showDialog(DIALOG_ABOUT);
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

We have defined a constant REQUEST_MANAGE_KEYS to identify this result as:

    private static final int REQUEST_MANAGE_KEYS= 0;

Now we trap for the result REQUEST_MANAGE_KEYS in the ConfuseText method onActivityResult as:

   
    protected void onActivityResult(int request, int result, Intent data)
    {
        switch (request){
        case REQUEST_MANAGE_KEYS:
            String newPassword;
            if (result == RESULT_OK) {
                try {
                    newPassword= data.getExtras().getString("PasswordValue"); // default on failure is null
                    }
                    catch(Exception e){ // data == null, extras == null
                        newPassword= "";
                    }
                editTextPassword.setText(newPassword); // setText accepts null
            }
            break;
        }
    }

If the result was sent from ManageKeys, we check for a valid flag, RESULT_OK, and extract the password from the intent. Now let us look at the code changes to ManageKeys.java. In this simple example, we simply call setResult in the onCreate method. This will send data back to the ConfuseText activity. Here is the modified onCreate method in ManageKeys.java:


    @Override
    protected void onCreate(Bundle b){
        super.onCreate(b);
        setContentView(R.layout.password_dialog);
       
        // Test pushing values back to main activity using Intent.putExtra
        editTextPasswordFirst.setText(pushValue);
        getIntent().putExtra("PasswordValue","JALComputing2011");
        setResult(RESULT_OK,getIntent());   
    }

Now when the user dismisses the ManageKey activity by hitting the back button, the data "JALComputing2011" is pushed to ConfuseText!


Exiting ManageKeys

Well, it took me a while figuring out how to exit the ManageKey view and go back to the ConfuseText view. The simple answer is to call this.finish(), but that does not work from an inner class. Well, finish() does! One strangeness, on my system RESULT_CANCEL is not defined. Go figure.

    @Override
    protected void onCreate(Bundle b){
        super.onCreate(b);
...

        // CANCEL BUTTON HANDLER
        final Button buttonCancel= (Button)findViewById(R.id.ButtonPasswordCancel);
        buttonCancel.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                editTextPasswordFirst.setText("");   
                editTextPasswordSecond.setText(""); 
                //setResult(RESULT_CANCEL, getIntent()); // cannot find RESULT_CANCEL!
                finish();
            }
        });

...

We never really are able to kill the ManageKey activity, only the OS (user) can do that!

KNOWN ISSUES

  1. If the we call setResult in a child activity and we do NOT call finish() immediately, we cannot be guaranteed that our result will be received in the parent activity with RESULT_OK. For instance, if the user changes the phone orientation the update will not take place. Consider calling activity.finish() immediately after setResult to commit changes.
  2. Be very careful when you name button widgets, perhaps by using fully qualified names such as buttonManageKeysPasswordCancel. If you try to touch a button in another Activity, your Activity will not load.


Working ManageKeys.java Class with Event Handlers

So here is the ManageKeys class with the event handlers as anonymous classes. I like this approach rather than handling events in ConfuseText since these events "belong" to ManageKeys. Here is our ManageKey view in action.



And here is the ManageKeys.java class.

package jalcomputing.confusetext;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class ManageKeys extends Activity {
    private static final String TAG= "Confuse Text";
    private int lengthPassword= -1; // default, do NOT check length
    private EditText editTextPasswordFirst;
    private EditText editTextPasswordSecond;
    
    @Override
    protected void onCreate(Bundle b){
        super.onCreate(b);
        final Activity a= this;
        setContentView(R.layout.password_dialog);
        editTextPasswordFirst= (EditText)findViewById(R.id.EditTextPasswordFirst);
        editTextPasswordSecond= (EditText)findViewById(R.id.EditTextPasswordSecond);
       
        // Get length of Key from intent data
        // Get length of Key from intent data
        try {
            lengthPassword= getIntent().getExtras().getInt("PasswordLength"); // returns 0 on no key!
        }
        catch(Exception e){
            lengthPassword= -1;
        }
           
        // CANCEL BUTTON HANDLER
        final Button buttonCancel= (Button)findViewById(R.id.ButtonPasswordCancel);
        buttonCancel.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                editTextPasswordFirst.setText("");           
                editTextPasswordSecond.setText("");    
                //setResult(RESULT_CANCEL, getIntent()); // cannot find RESULT_CANCEL!
                a.finish(); // result should be RESULT_CANCEL
            }
        });
        // UPDATE BUTTON HANDLER
        final Button buttonUpdate= (Button)findViewById(R.id.ButtonPasswordUpdate);
        buttonUpdate.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
               
                String entryFirst= editTextPasswordFirst.getText().toString();
                String entrySecond= editTextPasswordSecond.getText().toString();   
                
                if (entryFirst.equals(entrySecond)) { // keys match
                    if (lengthPassword != -1 && entryFirst.length() != lengthPassword) { // invalid key length
                        editTextPasswordFirst.setText("Invalid Key Length of "+
                                new Integer(entryFirst.length()).toString() +
                                ". Valid Key length is "+
                                new Integer(lengthPassword).toString()+".");                       
                        editTextPasswordSecond.setText("");
                        editTextPasswordFirst.selectAll();
                        editTextPasswordFirst.requestFocus();
                    }
                    else { // valid key length or we do not check key length (lengthPassword == -1)
                        // *** Clear Key Fields ***
                        editTextPasswordFirst.setText("");                  
                        editTextPasswordSecond.setText("");
                       
                        // Save new password and push using intent data
                        getIntent().putExtra("PasswordValue", entryFirst);
                        setResult(RESULT_OK,getIntent());    
                        a.finish();
                    }
                }
                else { // entries do not match
                    editTextPasswordFirst.setText("Entries did not match!");                  
                    editTextPasswordSecond.setText("");
                    editTextPasswordFirst.selectAll();
                    editTextPasswordFirst.requestFocus();
                }
            }
        });
    }
}



Minimize Dependencies

The overall effect is that the ManageKeys class does not access the UI elements or variables of the ConfuseText class and vice-versa. ManageKeys handles its own events. We use intent data to communicate between the two classes. The end result is a near full separation from dependency, so that name changes in one activity class does not affect the other activity. The outstanding dependencies are the names of the variables passed between the classes using intent data. This means that we can use the ManageKeys class in another project with minimal, if any, modification!

In the end, I used a custom immutable data class to pass state between activities (see below).


Adding CheckBox and Setting Password Mode Programmatically

We can add a CheckBox to our interface that shows or hides the entries. The hard part is setting the EditText to and from password mode programmatically. Here is the code that does this magical transformation. These changes may not "stick" when the user changes phone orientation. See Saving Instance State.


       // CHECKBOX HIDE HANDLER
        final CheckBox cb= (CheckBox)d.findViewById(R.id.CheckBoxHide);
        cb.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                if (cb.isChecked()) {
                    editTextPasswordFirst.setTransformationMethod(new PasswordTransformationMethod());           
                    editTextPasswordSecond.setTransformationMethod(new PasswordTransformationMethod());     
                }
                else {
                    editTextPasswordFirst.setTransformationMethod(null);                
                    editTextPasswordSecond.setTransformationMethod(null);   
                }
            }
        });



Another Better Approach to Getting a Pushed Int

Intent provides a number of get methods that may simplify coding, at least for an int, so that you can use:

lengthPassword= getIntent().getIntExtra("PasswordLength",-1);

which sets the fail value (no extras or no matching key) to -1.

Passing Objects Between Activities

It is possible to pass an immutable object between Activities by calling putExtras(Bundle) and myBundle.putSerializable. The object and the entire object tree would need to implement serializable. This is a basic tenet of Object Oriented Programming, passing of stateful messages.

First we create the immutable object by declaring a new class:

import java.io.Serializable;
import java.util.Date;

/*
 * Immutable messaging object to pass state from Activity Main to Activity ManageKeys
 * No error checking
 */
public final class PasswordState implements Serializable {

    private static final long serialVersionUID = 1L;
    public static final int MIN_PASSWORD_LENGTH= 8;
    public final int lengthKey;  // in bytes
    public final long timeExpire; // in milliseconds as a Calendar object
    public final boolean isValidKey;
    public final int timeoutType;
    public final String password;
    public final boolean isHashPassword;
   
   
    public PasswordState(int lengthKey,
            long timeExpire,
            boolean isValidKey,
            int timeoutType,
            String password,
            boolean isHashPassword){
        this.lengthKey= lengthKey;
        this.timeExpire= timeExpire;
        this.isValidKey= isValidKey;
        this.timeoutType= timeoutType;
        this.password= password;
        this.isHashPassword= isHashPassword;
    }

Then we create an immutable stateful instance of the class, a message, in the parent activity, and send it in an intent as in:
  
    private void launchManagePassword() {
        Intent i= new Intent(this, ManagePassword.class); // no param constructor
        PasswordState outState= new PasswordState(lengthKey,timeExpire,isValidKey,timeoutType,"",model.getIsHashPassword());
        Bundle b= new Bundle();
        b.putSerializable("jalcomputing.confusetext.PasswordState", outState);
        i.putExtras(b);
        startActivityForResult(i,REQUEST_MANAGE_PASSWORD); // used for callback
    }

Finally, we retrieve the object in the child activity.

         try {
            inPWState= (PasswordState) getIntent().getSerializableExtra("jalcomputing.confusetext.PasswordState");
            lengthKey= inPWState.lengthKey;
            timeoutType= inPWState.timeoutType;
            isValidKey= inPWState.isValidKey;
            timeExpire= inPWState.timeExpire;
            isHashPassword= inPWState.isHashPassword;
            // password= inPWState.password; // not required
        } catch(Exception e){
            lengthKey= PasswordState.MIN_PASSWORD_LENGTH;
            timeoutType= TIMEOUT_NEVER;
            isValidKey= true;
            timeExpire= LONG_YEAR_MILLIS;
            isHashPassword= false;
        }



Parcels

We pass data between the parent and child activites using bundles. A bundle, in turn, is a "type safe container for key/value maps." You can also send data between activities using Parcels, actually instances of classes that implement the Parcelable interface. A Parcel can contain flattened data and is designed for "high-performance IPC transport." Parcels are not designed for persistent storage. At least one author has suggested that Parcels were not designed to be passed between Activities. Consider prototyping with objects and refactor to use Parcels.

Here is some code straight from the Android docs:

public class MyParcelable implements Parcelable {
     private int mData;
    
     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(mData);
     }

     public static final Parcelable.Creator CREATOR
             = new Parcelable.Creator() {
         public MyParcelable createFromParcel(Parcel in) {
             return new MyParcelable(in);
         }

         public MyParcelable[] newArray(int size) {
             return new MyParcelable[size];
         }
     }
    
     private MyParcelable(Parcel in) {
         mData = in.readInt();
     }
 }


Are you still having fun?
JAL
Comments