2011年6月2日木曜日

Write a console app on Android using Java

During development of Android application, sometimes I want to write a small class and test its functions in a fashion similar to a normal desktop console program:

Read input from 'stdin' and write output to 'stdout'.

However, Android virtually requires all programs to be a GUI program. In order to fulfill the above needs, I decide to write a console-like program on Android which acts like normal desktop console: display a command prompt and let user issue commands to bring up a third-party program to execute.

Architecture overview

arch.png

As shown from the diagram, this CmdConsole program consists of

  • Console GUI
    • to forward user input to command dispatcher or the currently running custom console app 
    • to print output from currently running custom console app
  • Command dispatcher (Run on its own thread)
    • if user issue an internal command (e.g. 'ls', 'cd', 'del', etc), forward it to appropriate module to execute
    • if user want to run an external console app, ApkRunner is launched to execute it
  • ApkRunner (Run on its own thread)
    • bring up the custom console app to run

Console GUI

Screenshot 1: Console running built-in commands

screenshot1.png

Screenshot 2: Console running a 3rd party app

screenshot2.png

Console UI is very simple. On the top, you got an edit box to input command; on the top right is an 'ENTER' enter.pngbutton; and finally below is the area like normal console to output text.
The text view which outputs text is actually a modified
ImageView. Its 'onDraw' method is overridden to draw the output text line by line.

ApkLoader

It is a class (not shown in the diagram) to load a third party apk and retrieve the 'main' entry point method inside the apk for later execution. The magic to load the apk is performed by 'dalvik.system.DexClassLoader', which is a class inside the android SDK. This class isn't difficult to use. Only a few function calls can load and retrieve the method of any class you want. The logic to retrieve the 'main' method resides in ApkLoader.loadEntryPoint(). The 'main' method is then passed to ApkRunner to execute.

One point to note here is: loading a third party apk by 'DexClassLoader' will generate a 'dex' file for caching purpose. It is not bad but when you want to run a newer version of the third party app and the original cached 'dex' file still exists, sometimes, not always, the class loader failed strangely. Because of this, CmdConsole will remove the cached 'dex' file every time before the apk is loaded.

Writing a 3rd party console app

Let's start writing a console app so as to consolidate your experience with it. Here, I only list the steps to build a project by command line tools but it is perfectly OK to build a project by Eclipse.

  1. Create a project using android SDK script:

Collapse

> android create project \

--target 3 \

--name MyHello \

--path ./MyHello \

--activity MyHelloActivity \

--package com.xyz.testhello

  1. The Activity class is useless. Just delete it.
  2. Comment out the whole "application" tag in "AndroidManifest.xml" as they are useless, too:

Collapse

<!--

<application android:label="@string/app_name" android:icon="@drawable/icon">

  <activity android:name="dummy"

            android:label="@string/app_name">

     <intent-filter>

        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />

     </intent-filter>

  </activity>

</application>

-->

  1. Create a class, say 'MyHello', with a static method "main(HashMap<Integer, Object> args)" and use the 'CmdApp' (a helper class in the source attached) to initialize the arguments passed in:

Collapse

package com.xyz.testhello;

 

import java.util.HashMap;

import com.sss.consolehelper.CmdApp;

 

public class MyHello

{

   public static void main(HashMap<Integer, Object> args)

   {

      CmdApp cmdApp = new CmdApp(args);

      ...

      ..

      .. // write your program here

      .

   }

}

The 'args' passed in is a hash table (constructed inside ApkRunner before calling 'main') containing some environment variables useful to a console app. Please see appendix for the content of the hash table.

The 'CmdApp' is a helper class, provided in the sample console apps, to retrieve the content inside 'args' and provide convenient functions (like reading a line from stdin) to access and use the content. After initializing 'CmdApp', just write your console program as usual.

  1. Delete all unnecessary resource files inside 'res'.

Create a file "res/raw/entrypoint.txt" with the following single-line content:

Collapse

com.xyz.testhello.MyHello

This states the class which has the entry point function "main".

  1. Inside 'build.properties' at the project root, add this line

Collapse

source.dir=src:../_consolehelper_src

It is the root path where com.sss.consolehelper.CmdApp resides, thus, CmdApp can be found and built later on. (For Eclipse users, right click your project in 'Package Explorer' → 'Build Path' → 'Link Source ...' to add the linking)

  1. Compile the project by:

Collapse

> ant debug

  1. Put the resultant "apk" file to anywhere of the file system in the emulator/phone. I usually happen to put it at the root of the SD card. To put it there, just type:

Collapse

> adb push MyHello.apk /sdcard/MyHello.apk

  1. Launch CmdConsole and run the apk as shown in the screenshot above:

Collapse

> run /sdcard/MyHello.apk

or

Collapse

> cd /sdcard

> run MyHello.apk

Appendix

Console menu

When running a 3rd party app, currently, 2 menu options can be selected:

Kill running app

Actually, this option only sends a java.lang.InterruptedException to the running app and so this does not ensure the running app can be killed. The 'InterruptedException' only breaks out functions like 'wait', blocking 'read', 'sleep' and 'join'.
If the running app does not hang on one of these functions or the app has a try-catch statement that catches all exceptions, this option can't break your 3rd party app.

Kill console

This one will call 'android.os.Process.killProcess' to kill CmdConsole itself

Arguments passed to the console app

The entry point 'main' method of a console app should be declared as

Collapse

public static void main(HashMap<Integer, Object> args)

The arguments passed in by 'args' is a hash table with the following content:

args.get(0) 

android.app.Application

application context

args.get(1) 

String[] 

array of command line arguments; the 0th element is the start of arguments (which is *NOT* the program name); may be null if no arguments

args.get(2) 

java.io.InputStream

act as stdin for the console program

args.get(3) 

java.io.PrintStream

act as stdout for the console program

args.get(4) 

String

specifies the stream (stdin/stdout) encoding, which is "UTF-8" at present

It is recommanded to use 'CmdApp' to indirectly access the content.

Available internal commands

Typing 'help' in CmdConsole can list the available commands and typing 'help [command]' will print a more detailed description of the command.

Currently available commands are: 

  • help
  • ls
  • pwd
  • cd
  • clear
  • run
  • history
  • del
  • mkdir
  • ren 
  • cp
  • cleardex
  • ver
  • sres
  • netinfo
  • fontsize
  • exit

AndroidManifest.xml of CmdConsole

Since the 3rd party console app is actually running in the context of CmdConsole, thus, the 3rd party app is restricted by the permission set inside 'AndroidManifest.xml' of CmdConsole. Currently, CmdConsole only states the permissions of 'WRITE_EXTERNAL_STORAGE', 'INTERNET', 'ACCESS_WIFI_STATE' and 'ACCESS_NETWORK_STATE'. You may want to add more if you want to write a 3rd party app with more access rights.

 

0 件のコメント:

コメントを投稿