Launching Android Apps with Implicit Intents
One of the coolest features of Android Apps is chaining of applications. Unix users understand the concept of passing the results of one command to another command using pipes. This can result in new combinations of commands not envisioned by the writers of the individual commands! An example of chaining using the pipe (|) and the Unix command line is:
ls -l | lpr
which pipes the output of the ls -l command, listing detailed information about files in the current directory, to the line printer.
Well, we can pipe data in Android using Intents. So we can pipe the output of Confuse Text to the default Android Short Text Messaging Application (sms) using an Intent. Chaining intents leverages the power of applications by letting coders create new uses for existing applications. In this example, we leverage the power of the default Android SMS application by sending encrypted text as "sms_body" using Intent.putExtra and an Implicit intent to message the sms application.
We simply add a button and write the buttonClicked event handler as:
// LAUNCH SMS EVENT HANDLER
final Button buttonLaunchSMS= (Button)findViewById(R.id.ButtonLaunchSMSMessage);
buttonLaunchSMS.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
String outCipherText= editTextSMSCipherText.getText().toString();
String phoneNumber= editTextPhoneNumber.getText().toString();
// pre-conditions
if (outCipherText.length() < 1){
editTextSMSCipherText.setError("Cipher Text is Empty");
editTextSMSCipherText.requestFocus();
return;
}
if (outCipherText.length()>MAX_SMS_CHAR){
editTextSMSCipherText.setError("Error. Message Is Too Large.");
editTextSMSCipherText.requestFocus();
return;
}
String uri= "smsto:"+phoneNumber;
Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(uri));
intent.putExtra("sms_body", outCipherText);
intent.putExtra("compose_mode", true);
startActivity(intent);
finish();
}
});
According to the docs, an (Implicit) intent is "an abstract description of an operation to be performed" that "provides for performing late runtime binding between code in different applications." Gee, sounds like polymorphism. Furthermore, "the primary pieces of information in an intent are" action and data. Intents can be used to launch activities, can be embedded into a PendingIntent for broadcasting and can be passed as a parameter for starting or binding to a service. In this example, we will use an intent to launch the Android SMS activity. Examine the previous code snippet new Intent(action,data) where:
action --> Intent.ACTION_SENDTO
data--> Uri.parse(uri)
To receive this implicit intent, the receiving application must declare an <intent-filter> in its manifest that matches as:
<action android:name="android.intent.action.ACTION_SENDTO" />
A URI (Uniform Resource Identifier) is used to "specify a file or resource." A URI may be a URL (Uniform Resource Locator). Examples of URIs include:
file:/C:/mydir/myfile.txt
http://google.com
In our case the URI is smsto:someValidPhoneNumber. The call to Uri.parse returns an immutable Uri object for a given URI "RFC 2396-compliant" string. So we pass both the action and the data using the constructor Intent(String action,Uri data). We then add two name:value pairs to a bundle attached to the Intent. This is enough information for the Android OS to determine that the message is best handled by the Android SMS application, launching the SMS application and passing two extras to the SMS application. The data in "sms_body" is then placed into the body of the sms message. The data in smsto: is placed into the "to" of the sms message as seen below.
Emulated SMS client.
Cool!
Enabling Other Apps to Launch Confuse Text Using Explicit Intents
We can test launching ConfuseText using an Explicit intent. You could think of an implicit intent as analogous to invoking a method over the wire using an interface and an explicit intent as invoking a method over the wire using a concrete class and method name. The ConfuseText manifest contains an <intent-filter> as:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Where:
ACTION_MAIN --> "Start as a main entry point, does not expect to receive data."
CATEGORY_LAUNCHER--> "Should be displayed in the top-level launcher."
Since we want to receive data, we add a new <intent-filter> to the ConfuseText manifest as:
<intent-filter>
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Where:
ACTION_EDIT --> "Provide explicit editable access to the given data"
We can then create a new project with a "dummy" activity TestIntent and start the ConfuseText activity as in:
package jalcomputing.confusetext.test;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import jalcomputing.confusetext.*;
public class TestIntent extends Activity {
private static final String TAG="Confuse Text";
/** Called when the activity is first created.
* initialize and UI setup here */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main); // uses main.xml and strings.xml for the XLM-layout UI
Log.d(TAG,"onCreate");
Intent intent = new Intent(Intent.ACTION_EDIT);
intent.putExtra("plain_text", "Testing");
intent.setClassName("jalcomputing.confusetext", "jalcomputing.confusetext.ConfuseText"); // Explicit Intent
try {
startActivity(intent);
}
catch (Exception e)
{
Log.d("Confuse Text","onCreate",e);
}
}
}
We can then extract the data "plain_text" in ConfuseText.onCreate as in:
// SUPPORT CHAINING OF INTENTS
try { // see if we were launched by an intent
this.editTextPlainText.setText(getIntent().getExtras().getString("plain_text"));
}
catch (Exception e){
}
We can also call intent.getAction() to determine if the intent was sent with a given action type as in:
private static String ACTION_STRING= "android.intent.action.EDIT";
// SUPPORT CHAINING OF INTENTS
// names:values plain_text:string, cipher_text:string, clear_prefs:boolean
Intent i= getIntent();
if (i != null && i.getAction().equals(ACTION_STRING)) {
Bundle b= i.getExtras(); // may well be null
if (b != null) {
Log.d(Utilities.TAG,"gotIntent");
// support "plain_text"
this.editTextPlainText.setText(b.getString("plain_text")); // accepts null
// support cipher_text
this.editTextCipherText.setText(b.getString("cipher_text"));
// support "clear_prefs"
if (b.getBoolean("clear_prefs", false)){
SharedPreferences prefsClear = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = prefsClear.edit();
editor.clear();
editor.commit();
Eula.clearPreferences(this);
}
Log.d(Utilities.TAG,"clearedPrefs");
}
}
Avoiding Launching Multiple Instances of ConfuseText using FLAG_ACTIVITY_REORDER_TO_FRONT
You can set a flag in an intent to avoid launching a new instance of an application if an instance of the application is currently running. Just add the flag Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) as in:
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
Where:
FLAG_ACTIVITY_REORDER_TO_FRONT --> If set in an Intent passed to Context.startActivity(), this flag will cause the launched activity to be brought to the front of its task's history stack if it is already running.
In our sample case, if an instance of ConfuseText is already running, then it appears that the new call to startActivity will be ignored and the main activity will be brought to the front.
Launching ConfuseText from another Class using FLAG_ACTIVITY_NEW_TASK
If you try to launch ConfuseText from another class in the same application, an exception may be thrown. To enable the launching of another task, add the flag FLAG_ACTIVITY_NEW_TASK as in:
outIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Using Intents for Debugging
This class TestIntent can also be used for debugging. For instance, I can signal the ConfuseText class to clear the application preference by adding the line:
intent.putExtra("clear_prefs", true);
We can trap this boolean value in onCreate as:
// SUPPORT CHAINING OF INTENTS
try { // see if we were launched by an intent on initial startup
Bundle b= getIntent().getExtras(); // may well be null
// support "plain_text"
this.editTextPlainText.setText(b.getString("plain_text")); // accepts null
// support "clear_prefs"
if (b.getBoolean("clear_prefs", false)){ // may throw if b == null
SharedPreferences prefsClear = getPreferences(MODE_PRIVATE);
prefsClear.edit().clear().commit();
Eula.clearPreferences(this);
}
Log.d(Utilities.TAG,"clearedPrefs");
}
catch (Exception e){ // caller failed to call putExta("plain_text",someValue) OR not launched by Intent
this.editTextPlainText.setText("");
//Log.d(Utilities.TAG,"getIntent",e);
}
This code will clear the preferences file, but does not appear to delete the preferences file, so that we are left with empty preference files. This technique is useful as a supplement to JUnit and ActivityInstrumentationTestCase2 where you should be able to add intent data in setUp().
Note: In Eclipse you can go to Run --> Run Configurations... --> Target and click on Wipe User Data. This should prompt the emulator to confirm that you wish to completely clear the prefs file when the emulator restarts.
JAL