Search This Blog

28 May 2010

Menus

Let's start with creating a basic app:
F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
Choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: MenuTest
version: 1.0-SNAPSHOT
package: org.eoti.android


Assuming your emulator is running...
F:\work> cd MenuTest
F:\work\MenuTest> mvn clean install

Create a new folder src\main\android\res\menu

Create a new file src\main\android\res\menu\mainmenu.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:id="@+id/first_menu">
        <item android:id="@+id/second"
              android:title="Open Second Menu" />
        <item android:id="@+id/about_first"
              android:title="About First Menu" />
    </group>
    <group android:id="@+id/second_menu">
        <item android:id="@+id/first"
              android:title="Open First Menu" />
        <item android:id="@+id/about_second"
              android:title="About Second Menu" />
    </group>
    <item android:id="@+id/quit"
          android:title="Quit" />
</menu>

Let's write your activity (src\main\java\org\eoti\android\MenuTestActivity.java in my case):

public class MenuTestActivity extends Activity {
    private static String TAG = "MenuTest";
    private Menu menu;

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.mainmenu, menu);
        this.menu = menu;
        menu.setGroupVisible(R.id.first_menu, true);
        menu.setGroupVisible(R.id.second_menu, false);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId())
        {
            case R.id.first:
                menu.setGroupVisible(R.id.first_menu, true);
                menu.setGroupVisible(R.id.second_menu, false);
                return true;
            case R.id.second:
                menu.setGroupVisible(R.id.first_menu, false);
                menu.setGroupVisible(R.id.second_menu, true);
                return true;
        }
        return false;
    }
}
   
Redeploy (mvn clean install) and run the app.
When you hit the menu, you will see 3 options: Open Second Menu, About First Menu, and Quit.
Only the first one does anything.  If you select Open Second Menu, then the menu will change (though hidden).
Click menu again and you will now see: Open First Menu, About Second Menu, and Quit.
Again, only Open First Menu does anything.

Customizing the Views

Let's start by creating a basic class:
F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml
Choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: CustomViewTest
version: 1.0-SNAPSHOT
package: org.eoti.android


Assuming your emulator is running...
F:\work> cd CustomViewTest
F:\work\CustomViewTest> mvn clean install

Create a new class.  In my case I created src\main\java\org\eoti\android\CustomTextView.java

public class CustomTextView
extends TextView
{
    public CustomTextView(Context context) {
        super(context);
    }

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Paint cornsilk = new Paint(Paint.ANTI_ALIAS_FLAG);
        cornsilk.setColor(Color.rgb(255,248,220));

        canvas.drawLine(0, 0, this.getMeasuredWidth(), this.getMeasuredHeight(), cornsilk);
        canvas.drawLine(this.getMeasuredWidth(), 0, 0, this.getMeasuredHeight(), cornsilk);
       
        super.onDraw(canvas);
    }
}


Update src\main\android\res\layout\main.xml
Replace this line:
    <TextView 
with this line:
    <org.eoti.android.CustomTextView

Redeploy (mvn clean install) and run the app. You should see lines behind the text.

Singleton, Lifecycles and the Application class

Start by creating a basic app:
F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml

Choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: ApplicationTest
version: 1.0-SNAPSHOT
package: org.eoti.android

Make sure you have your emulator running and:
F:\work> cd ApplicationTest
F:\work\ApplicationTest> mvn clean install

Create a new class in the same package as your activity... In my case, I created src\main\java\org\eoti\android\MyMainApplication.java

public class MyMainApplication extends Application
{
    private static String TAG = "MyMainApplication";
    private static MyMainApplication singleton;
    public static MyMainApplication getInstance(){return singleton;}
    private int count = 0;

    @Override
    public void onCreate() {
        super.onCreate();
        singleton = this;
        Log.i(TAG, "Singleton created");
    }

    public int count(){return count++;}
}

In your AndroidManifest.xml, replace this line:
    <application android:icon="@drawable/icon" android:label="@string/app_name"> 
with this line:
    <application android:icon="@drawable/icon" android:label="@string/app_name" android:name="MyMainApplication">


Open your activity (src\main\java\org\eoti\android\ApplicationTestActivity in my case)
Add this to the end of your onCreate method:
        Log.i(TAG, "Created instance#" + MyMainApplication.getInstance().count());

Redeploy (mvn clean install) and watch the logs (adb logcat) as you launch the app.
  • You'll see that the singleton is created and the counter is reported.
  • Hit the back button and relaunch the app a few times.  
  • You'll see that the counter increments each time.
  • Hit the home button. 
  • Now, hold down the home button and reselect your app.  
  • You'll notice it is started again, but NOT created again (no counter reported).

27 May 2010

XPath

I spent quite a bit of time trying to find an easy way to do XPath on my NexusOne.  While 2.2 adds that as part of the API (why oh why can't we have the entire JDK?), it does not handle malformed HTML very well (like Google's homepage including an unclosed <link> tag).  Well, that and it isn't been OTA'd to my device yet.

One thing I really did *not* want to do is resort to walking the DOM tree or writing 50 times more code to handle it via SAX.  Yes, I understand there are a few classes available in Android 2.1; but using them reminds me of the pre-JAXB days (which I have no desire to repeat).

For this test, we'll use HtmlCleaner.  It does OK with some XPath and sanitizes the page we are searching...

Start by creating a basic project:

F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml


choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: XPathTest
version: 1.0-SNAPSHOT


Assuming you have your emulator running...


F:\work> cd XPathTest
F:\work\XPathTest> mvn clean install

Add this repository to your pom.xml (or Nexus):
    <repositories>
        <repository>
            <id>xwiki</id>
            <name>xwiki</name>
            <url>http://maven.xwiki.org/externals</url>
        </repository>
    </repositories>
   
Add this dependency to your pom.xml:
        <dependency>
            <groupId>net.sourceforge.htmlcleaner</groupId>
            <artifactId>htmlcleaner</artifactId>
            version>2.1</version>
        </dependency>
   

Add this to your AndroidManifest.xml


            <uses-permission android:name="android.permission.INTERNET" />
           
Update your activity (src\main\java\org\eoti\android\XPathTestActivity.java in my case):
public class XPathTestActivity extends Activity {
    private static String TAG = "XPathTest";
    private static String TEST_URL = "http://www.google.com/profiles/malachid";
    private static String XPATH_GUSER = "//div[@class='g-unit']/h1/span[@class='fn']/text()";
    private static String XPATH_LOCATION = "//span[@class='adr']/text()";

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        try{
            HtmlCleaner cleaner = new HtmlCleaner();
            CleanerProperties props = cleaner.getProperties();
            props.setAllowHtmlInsideAttributes(true);
            props.setAllowMultiWordAttributes(true);
            props.setRecognizeUnicodeChars(true);
            props.setOmitComments(true);
            URL url = new URL(TEST_URL);
            URLConnection conn = url.openConnection();
            TagNode node = cleaner.clean(new InputStreamReader(conn.getInputStream()));
            Log.v(TAG, node.evaluateXPath(XPATH_GUSER)[0] + " is from " + node.evaluateXPath(XPATH_LOCATION)[0]);
        }catch(Exception e){
            Log.e(TAG, "Failed", e);
        }
    }
}


Redeploy (mvn clean install). Make sure you are watching 'adb logcat' for messages and launch the app.

You should see something like: V/XPathTest(  271): Malachi de Ælfweald is from Beaverton, OR

AdMob and AdWhirl

Do you want to display advertisements on your app in hopes of bringing in some money, displaying cross-productization ads or even just so you can make a Lite vs Pro version?  In this example, we will do that using both AdMob (ad server) and AdWhirl (ad aggregator).

To get started, we are going to need to setup a couple of online accounts.

AdMob
  • Create an account at AdMob.
  • From your dashboard, click on "Sites & Apps".
  • Click on "Add Site/App".
  • Click on "Android App"
  • Fill in the form (I'd ignore the URL for now but eventually it will look like http://market.android.com/search?q=pname:com.example.ExampleApp)
  • Hit "Continue"
  • Click on "Download AdMob Android SDK"
  • Extract it to the disk somewhere and navigate to it from the command line
 C:\java\admob-sdk-android> mvn install:install-file -Dfile=admob-sdk-android.jar -DgroupId=com.admob.android.ads -DartifactId=AdMobAndroid -Dversion=20100331 -Dpackaging=jar
  • Note: I got the version from the top of the Changelog.txt file
  • Click on "Go to Sites/Apps"
  • Under the row for your new app, click on "Manage Settings" (it uses mouse-hover, so if you don't see it move your mouse)
  • Copy the id# from "Publisher ID: a14bfed196e8994"
AdWhirl
  • Create an account at AdWhirl.  AdWhirl will act as aggregator, allowing you to mix-n-match ad hosting accounts (like AdMob).
  • Under "Apps" click on "Add Application"
  • Fill out the form. Make sure to choose 'Android'.  Click "Add App"
  • Note: At this point, I have seen an issue where it shows up on the screen twice. If I go into it and come back it resolves itself.  Don't worry about it.
  • Click on your new app's name
  • Copy your SDK key from: "SDK Key:d34d05761f0544dab08b396807778d23"
  • Next to AdMob, click on (mouse-hover again) "Edit Settings"
  • Put the id# from above into the "PublisherID" box and apply it
  • Under "Ad Serving" make sure AdMob is enabled
  • Save Changes
  • Download the latest Android zip from here. In this case, it is "AdWhirlSDK_Android_2.0.8.zip".
  • Extract it to the disk somewhere and navigate to it from the command line
C:\java\AdWhirlSDK_Android_2.0.8> mvn install:install-file -Dfile=AdWhirlSDK_Android_2.0.8.jar -DgroupId=com.adwhirl -DartifactId=AdWhirlSDKAndroid -Dversion=2.0.8 -Dpackaging=jar
  • Note: I took the version from the filename 
Your Code

Whew!  Ok, with your accounts setup, let's create a basic project:

F:\work>  mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml

choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: AdWhirlTest
version: 1.0-SNAPSHOT

Assuming you have your emulator running...

F:\work>  cd AdWhirlTest
F:\work\AdWhirlTest>  mvn clean install


Add these dependencies to your pom.xml [change versions to match what you did earlier]:
        <dependency>
          <groupId>com.adwhirl</groupId>
          <artifactId>AdWhirlSDKAndroid</artifactId>
          <version>2.0.8</version>
        </dependency>
        <dependency>
          <groupId>com.admob.android.ads</groupId>
          <artifactId>AdMobAndroid</artifactId>
          <version>20100331</version>
        </dependency>



Add this to your AndroidManifest.xml:  

        <uses-permission android:name="android.permission.INTERNET" />
       
Inside your src\main\android\res\layout\main.xml, insert this just above the bottom '</LinearLayout> '
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="bottom"
        android:id="@+id/layout_ad"
    />
   
   
Open your activity (src\main\java\org\eoti\android\AdWhirlTestActivity.java in my case)
Add this into the bottom of your onCreate method:
        try{
            AdManager.setTestDevices( new String[] { AdManager.TEST_EMULATOR } );
            LinearLayout layout = (LinearLayout)findViewById(R.id.layout_ad);
            AdWhirlLayout adWhirlLayout = new AdWhirlLayout(this, "d34d05761f0544dab08b396807778d23");
            Display d = this.getWindowManager().getDefaultDisplay();
            RelativeLayout.LayoutParams adWhirlLayoutParams = new RelativeLayout.LayoutParams(d.getWidth(), 72);
            layout.addView(adWhirlLayout, adWhirlLayoutParams);
        }catch(Exception e){
            Log.e(TAG, "Unable to create AdWhirlLayout", e);
        }
Note: Make sure to use the SDK Key from the AdWhirl setup earlier when creating the AdWhirlLayout.

Redeploy (mvn clean install) and launch it.  You should see an ad at the bottom of the screen.  If not, check your logs (you do have 'adb logcat' running, right?)

12 May 2010

Installing TTS voice data into Emulator

I have a couple different test environments... one of them was able to use voice out of the box... the other, however, could not.  Sure, maybe I screwed something up... end result though was that when attempting to install the voice data (via Settings | Text-to-Speech) it crashed due to there not being a Market application in my emulator...

So what to do?

If you go to the TTS download page, you can download the com.svox.langpack.installer_1.0.1.apk.  At that point, simply do 'adb install com.svox.langpack.installer_1.0.1.apk' and it will install!

Grab content from a URL

First we create a basic project:

    F:\work> mvn archetype:generate -DarchetypeCatalog=http://kallisti.eoti.org:8081/content/repositories/snapshots/archetype-catalog.xml

choose the galatea-archetype plugin
groupId: org.eoti.android
artifactId: URLTest
version: 1.0-SNAPSHOT


    F:\work> cd URLTest
    F:\work\URLTest> mvn clean install

Add this to your AndroidManifest.xml:   
    <uses-permission android:name="android.permission.INTERNET" />

And here's the code for your activity (rename as needed):

public class URLTestActivity extends Activity {
    private Handler handler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                Toast.makeText(URLTestActivity.this, "Time: " + msg.obj, Toast.LENGTH_SHORT).show();
            }
        };
        Thread t = new Thread(){
            @Override
            public void run() {
                try {
                    URL url = new URL("http://tycho.usno.navy.mil/cgi-bin/timer.pl");
                    URLConnection connection = url.openConnection();
                    BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    String line = "";
                    String s = "";
                    while( (line = in.readLine()) != null )
                    {
                        int idx = line.indexOf("Pacific Time");
                        if( idx != -1)
                        {
                            s = line.substring(4, idx-1).trim(); // strip off "<BR>" and "Pacific Time"
                        }
                    }
                    Message msg = new Message();
                    msg.obj = s;
                    handler.sendMessage(msg);
                } catch (Exception e) {
                    Toast.makeText(URLTestActivity.this, "ERROR: " + e.getMessage(), Toast.LENGTH_SHORT).show();
                }
            }
        };
        t.start();
    }
}

Redeploy (mvn clean install) and launch the app. You should see the time (Pacific Time) displayed as a popup.