mdoery ~ software development

adventures in coding

an android hiccup

| Comments

Summary: if you switch to https watch out for this Android bug

One of the Android projects that I’m working on uses the Apache HTTP library, which is located in a library – android.jar – that comes with Android. The project uses the library to connect to an external server using an HTTP GET command. I’ve experienced a couple of problems with this project due to this connection requirement.

(Note: this project is not mine, but belongs to one of my customers, so I’m not the original developer who wrote this code! I’m just dealing with the fallout.)

The first problem occurred when the IP address for the server was changed by the hosting service. I do not know why someone would hard-code an IP address when making a connection, but that was done in this app. The problem was obvious, and the fix was simple enough: change the line of code to point to the domain name (like, “http://example.com”) rather than the IP address. Problem fixed!

The second problem occurred more recently, when the hosting server was switched to use HTTPS. The URL had to be changed – again! – to read something like “https://example.com”. This happened months ago, and no issues were noticed… until a couple days ago, when I was adding a new feature to the app, and it suddenly stopped working!

I checked the logs and noticed an Exception with a lengthy stack trace. Here is the stack trace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
08-23 08:11:23.863 W/System.err(25119): javax.net.ssl.SSLException: hostname in certificate didn't match: <example.com> != <*.hostmonster.com> OR <*.hostmonster.com> OR <hostmonster.com>
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:185)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:54)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:114)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:95)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.conn.ssl.SSLSocketFactory.createSocket(SSLSocketFactory.java:381)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:165)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:164)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:119)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:360)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)
08-23 08:11:23.863 W/System.err(25119):   at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:465)
...
08-23 08:11:23.863 W/System.err(25119):   at android.os.Handler.handleCallback(Handler.java:615)
08-23 08:11:23.863 W/System.err(25119):   at android.os.Handler.dispatchMessage(Handler.java:92)
08-23 08:11:23.863 W/System.err(25119):   at android.os.Looper.loop(Looper.java:137)
...

The first line is key: “javax.net.ssl.SSLException: hostname in certificate didn’t match: <example.com> != <.hostmonster.com> OR <.hostmonster.com> OR <hostmonster.com>”. My customer is using hostmonster to host their external server. What’s going on?

When I saw this message, I thought the server might be down. The logs contained the URL being requested, so I accessed that URL in a browser, and found it was working just fine.

A simple search for javax.net.ssl.SSLException did not reveal a simple explanation for this error. After substantial hunting, I found a StackOverflow question which provided a clue – it seemed to be related to SNI (server name indication) support. That page pointed to another question at StackOverflow – Why does android get the wrong ssl certificate? (two domains, one server) which made things more clear.

The answer is that the client (the app, in this case) needs to support SNI, and if it does not, the server may send the wrong SSL certificate. That definitely seemed to be happening here. As recommended in the answer to that SO article, I rewrote the app code to use java.net.HttpURLConnection, and the app was fixed immediately. You might think it would be required to use HttpsURLConnection, but it worked perfectly fine with HttpURLConnection.

I did not research the problem much further. I noticed another open-ended question at SO asking what’s the deal with SNI support in Android.

Most of these questions and responses are fairly old. I found a discussion at Google’s issue tracker with the following remarks (from 2010 and 2011!):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[bd...@google.com says:]
The main problem with supporting SNI in the browser is that many sites choke on TLS extensions, especially lesser known ones. Chrome and other browsers try to retry the connection without TLS (and without compression, which technically is SSL and not TLS) 

We have moved forward with this in 2.3 (Gingerbread) and javax.net.ssl.HttpsURLConnection does attempt to handshake with SNI (and automatically falls back to SSL w/o compression if there are problems). Unfortunately the browser uses Apache HTTP Client and there was not a simple local fix to make it retry failed connections, so it is overly conservative and does not include SNI information.

[vi...@gmail.com says:]
Non "corporate gibberish" summary : 
 * Gingerbread (2.3) Will never get this update officially. 
 * Honeycomb (3.0) contains the fix but isn't available on phones. 
 * Ice Cream Sandwich (4.0) will provide this feature to phones but isn't already available (soon). 

Also : 
 * The fix from Honeycomb could have been backported by the manufacturers that got it's source code but none that I know off did it. 
 * As Honeycomb is a closed source release none of the alternative distribution could merge the patch without re-developing it. 
 * As Ice Cream Sandwitch will include this patch it will be in all distributions and could even be backported to Gingerbread. 

Sadly coupled with the fact that google don't control at all the updates of previous phones it lock SNI in an "Unusable for most of Android users" state for 2+ Years at least for environements where you don't control users hardware. 

At least now for companies you could plan an upgrade (software or hardware depending on the phone models your employees got) as soon as ICS is released

I still don’t know why this problem appeared so suddenly. It could be due to some change at hostmonster (seems most likey), or it could be due to a change in the OS on my test device.

For the record, here’s the fixed code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
URL testUrl = new URL(urlString);
HttpURLConnection urlConnection = (HttpURLConnection)testUrl.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.connect();
int code = urlConnection.getResponseCode();
InputStream in = urlConnection.getInputStream();
BufferedReader bin = new BufferedReader(new InputStreamReader(in));
String inputLine;
StringBuffer sBuffer = new StringBuffer();
while ((inputLine = bin.readLine()) != null) {
  sBuffer.append(inputLine);
}
in.close();
String result = sBuffer.toString();
// do something with the result...

in praise of testing

| Comments

Summary: testing is good… no, great

I’ve been working on a website based on node.js for about 1.5 years now. I didn’t create the site, and when I got it, I saw that 1) it had a very extensive code base and 2) a small number of tests had been written relative to the size of the code base.

It’s understandable; I can see the temptation to avoid coding tests when you’re not really sure if the code will just end up being thrown away. The time spent writing tests could (perhaps) be more productively spent adding new, vital features.

In any case, I decided I needed more code coverage, because I hate it when I fix a bug or add some feature, only to find that it has been broken in some obscure way months later. Having a large suite of tests to run won’t always protect against this kind of thing, but it does help some.

Most recently, I’ve been writing a new, fairly complicated feature. It’s nearing completion. I’ve been writing unit-like tests all along, but I decided in the end that it would be worth writing some integration-like tests that run against a working dev server. The second integration test that I wrote kept failing – immediately. It looked okay. I kept debugging it until I was nearly sure that it should pass. So I went back and looked at my server code. Voilà! My server code was at fault. Nerds! To be honest, I rarely find that tests so quickly uncover a bug. But it happens often enough, and it gives me a warm fuzzy feeling about testing!

coursera algorithms part i

| Comments

Summary: The coursera course Algorithms Part I is worth it, with some caveats

My educational background is in physics and math, not computer science. I’ve taken exactly one computer course (Pascal) as an undegraduate. Aside from that, the programming knowledge that I’ve got was all self-taught: from the Basic programs that I wrote as a teenager on my father’s Commodore 64, to the Fortran code that I wrote to study physical systems in grad school, it was all stuff I had learned on my own or from building on the code of others.

When my career took a left turn, and I started developing for web applications, my background served me well. However, I had the feeling that I was missing some things that I would have learned if I’d gone into a computer science degree program. This is hammered in when you go for job interviews where they ask you abstruse computer science questions.

Even so, it’s been hard to make myself study something like algorithms – there’s just so much time in life, and there always seem to be more urgent things to do… But then I noticed the Coursera course in algorithms, and finally decided to take a swing at it. This is Algorithms, Part I. The promotional material reads: “This course covers the essential information that every serious programmer needs to know about algorithms and data structures.” To top it off, it’s a Princeton University course – at no charge! What more could you ask for? And they tell you that the course requires “6-12 hours of work / week” for 6 weeks. I can certainly afford that! It seems time well spent.

I didn’t have much time during the month of June, but I figured I could catch up. It has not been easy! Each week covers 2 topics. Week 1 covers the “Union-Find” algorithm, and also “Analysis of Algorithms”. I just finished the first programming assignment for “Union-Find”. It didn’t take long for me to get running code which did what the spec wanted – perhaps 3 or 4 hours. But that’s not enough. When you submit your code, it’s run through an automated grading system. Little bugs which seemingly shouldn’t count for much can result in nearly complete failure.

In my case, I uploaded the assignment 5 different times, each time resulting in an improvement until it passed with 100% the very last time. The first couple times I had to debug a few items because I had misunderstood the spec for a couple of API methods. In one particularly nasty bug, I had somehow managed to use the wrong version of their algorithm class (QuickFindUF instead of WeightedQuickUnionUF) in my code – oops! That turned out to be very difficult and time-consuming to discover. The automated grading system gives you hints about what is going wrong, but in some cases it just appears to be annoyingly cryptic.

Fortunately, there’s a tipsheet that goes with each assignment. And there are also discussion forums for when you get really desperate. With all this help, it still took me at least 10 hours to finish just this initial programming assignment, and I haven’t even touched the next one yet. I hope things go faster from here on out, but I’m skeptical about that.

code coverage for node and vows

| Comments

Summary: you can get code coverage stats for your node.js code

(FYI my dev box uses Ubuntu Linux 13.10.)

I’ve been developing new features in my node.js application, and concurrently writing vows.js tests for the new code. Yesterday, I decided that it was about time that I check to see just how much code my tests are covering.

What to do? When developing Java software with the eclipse IDE, it’s relatively simple to get code coverage information for your JUnit tests. With node, things are more complicated.

I looked at code coverage in npm and saw a number of potentially useful libraries, but didn’t want to research each one to see if they worked with vows. Then I noticed that the vows page says “Code coverage reporting is available if instrumented code is detected. Currently only instrumentation via node-jscoverage is supported.” Well, that makes it a no-brainer. They give you instructions for downloading and installing node-jscoverage, which is not in npm!

While I was hunting around, I had found Alex Seville’s post about some problems he had encountered using node-jscoverage. That made me concerned that the instructions on the vows page were incomplete. So I followed his directions. In his step 2, I ran into a little permission trouble; I had to run each command as sudoer:

1
2
3
sudo ./configure
sudo make
sudo make install

From then on, almost everything worked. Here’s my node.js app code:

1
2
3
/srv/app/node-app
/srv/app/node-tests
/srv/app/node_modules

And here’s how I produced the instrumented code to test:

1
jscoverage /srv/app/node-app /home/mdoery/instrumented/node-app

Notice I put my instrumented code into a directory which is completely independent from my app directory structure. You don’t want instrumented code running in your app! It’s only used for generating coverage reports.

I followed Alex’s link to Jeff Kunkle’s post about switching between testing instrumented and non-instrumented code. I grabbed Jeff’s sample code to do this, and it worked like a charm. Then I ran a test –

1
2
3
4
5
6
7
8
9
10
11
12
/srv/app/node-tests$ ../node_modules/vows/bin/vows test-something.js
ERROR   => Error: Cannot find module 'moment'
  at Function.Module._resolveFilename (module.js:338:15)
    at Function.Module._load (module.js:280:25)
    at Module.require (module.js:364:17)
    at require (module.js:380:17)
    at exports.undefined.window.C (/home/mdoery/instrumented/node-app/C.js:208:22)
    at Object.<anonymous> (/home/mdoery/instrumented/node-app/C.js:607:3)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)

Oops! The instrumented code couldn’t find my app’s libraries (such as moment) in /srv/app/node_modules. So I created a soft link to these in my instrumented directory:

1
ln -s /srv/app/node_modules /home/mdoery/instrumented/node_modules

After doing that, my instrumented code appeared to run fine; I got the usual happy output from vows:

1
2
3
/srv/app/node-tests$ ../node_modules/vows/bin/vows test-something.js
·············
  ✓ OK » 13 honored (0.004s)

But how could I be sure that the instrumented code was being exercised? This time, I reran my test with the --cover-html option:

1
../node_modules/vows/bin/vows test-something.js --cover-html

Sure enough, I saw a file called coverage.html had been generated in my current directory. I opened it in my browser window to view an html display of what code is covered, and what still needs to be tested. Nice!

android example using GridLayout

| Comments

Summary: I demonstrate building a simple app which uses android.support.v7.widget.GridLayout

I’m trying out a GridLayout for one of my projects. I built a small app to test it. The app looks like this on my tablet:

Here’s how to do it.

First, create an Android project in Eclipse (File > New > Project… > Android Application Project). In setting up my project I used a Minimum Required SDK of 2.2 (Froyo) and Target SDK 4.2 (Jelly Bean), and otherwise kept all the default settings proposed by Eclipse.

Next, import five image files into the res/drawable folder. These will be displayed in the grid. You can grab these images from this page (right-click on the image and “Save as…”). They are all in the public domain.

Open activity_main.xml in “Graphical Layout” view, click on “Layouts” and drag a “GridLayout” onto the view. A dialog opens which says “Warning android.widget.GridLayout requires API level 14 or higher, or a compatibility library for older versions. Do you want to install the compatibility library?” – do that. Set “Use Default Margins” to true, and set the “Column Count” to 2 (these two items are found under the Properties panel):

When I did this last step, something weird happened. I saw a couple of errors in the xml:

Unexpected namespace prefix “app” found for tag android.support.v7.widget.GridLayout. The activity_main.xml had the property app:columnCount=“2” – I hadn’t added this manually. I hunted around the internet and found a bug report about it, including a workaround – adding the attribute tools:ignore=“MissingPrefix” to the RelativeLayout tag gets rid of the error.

Finally, set the Android id for the GridLayout to be android:id=“@+id/gridLayout1”

The rest is simple; add all the images programmatically to the GridLayout, and it’s done. This is all the code that’s in my MainActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.mdoery.tut.gridlayout;

import android.os.Bundle;
import android.app.Activity;
import android.support.v7.widget.GridLayout;
import android.widget.ImageView;

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      GridLayout gl = (GridLayout)findViewById(R.id.gridLayout1);
      setupGridLayout(gl);
  }

  private void setupGridLayout(GridLayout gl) {
      gl.addView(getImageView(R.drawable.apple));
      gl.addView(getImageView(R.drawable.ladybeetle));
      gl.addView(getImageView(R.drawable.moon));
      gl.addView(getImageView(R.drawable.sheep));
      gl.addView(getImageView(R.drawable.strawberry));
      
  }
  private ImageView getImageView(int resource) {
      ImageView iv = new ImageView(this);
      iv.setBackgroundResource(resource);
      return iv;
  }

}

where did my GridLayout go?

| Comments

Summary: The class android.support.v7.widget.GridLayout vanished from my Eclipse project! Here’s how I retrieved it.

I was working on an Android project in Eclipse a while back, when I got pulled off of it to do some contract work. When I finally returned to the project today, I was surprised to see it had this compiler error: “The import android.support.v7.widget cannot be resolved”.

Periodically I’m dumbfounded by things which happen in Eclipse, but I’ve begun to take surpises in a stride. I searched around the internet for posts which reported this problem and eventually found that the solution is to import a “support” library which is found in the Android SDK directory. For me, that jar file is located here: C:\Program Files\Android\android-sdk\extras\android\support\v7\gridlayout\libs\android-support-v7-gridlayout.jar

This is a quick rundown of the simple procedure I followed to solve the problem:

  1. Open Eclipse’s Navigator view (from the menu up top, select Window > Show view > Navigator)
  2. Right-click on the project’s libs directory
  3. From the menu which opens, choose “Import…” and then choose “File System”.
  4. In the “File System” dialog which opens, navigate to wherever your android-support-v7-gridlayout.jar has been stored. Like I said above, for me it was under C:\Program Files\Android\android-sdk\extras\android\support\v7\gridlayout\libs\android-support-v7-gridlayout.jar where presumably the Android SDK Manager placed it. Select the jar file, and click the Finish button.
  5. Tada! The compiler error goes away.

I don’t understand how this project broke while I wasn’t working on it. I can only imagine that the imported library was somehow deleted, but I don’t recall doing that, so it’s another one of those fun Android mysteries!

P.S. This evening, I discovered that I had added the jar file to a separate, Android library Eclipse project and had set that project as an Android reference for my current project (under Properties > Android). The library project had been closed some time between now and then. So that explains why the file was “missing;” it was never there to begin with!

working with json in android apps

| Comments

Summary: Extracting data from a JSON formatted file is pretty simple in Android.

I’m planning to store data in the JSON format for the app that I’m currently building. So here’s a quick post which demonstrates the procedure for reading JSON into data which can be used in your app.

  1. Create an Android application project called MainJson in Eclipse.
  2. Set the Minimum Required SDK property to API 8: Android 2.2 (Froyo), and the Target SDK to API 17: Android 4.2 (Jelly Bean). I tested this tutorial with these parameters.
  3. Set the package name to be com.mdoery.tut.json.
  4. Start the project with a blank Activity.
  5. Create a sub-directory called json under the assets folder; we’ll store our JSON file here.

Now that the project is ready, create a file with JSON-formatted data as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{"sculptors": [
    {"sculptor": {
        "firstname": "Isamu",
        "lastname": "Noguchi",
        "sculptures": [
            {"sculpture": {"name": "Red Cube", "date": "1968"}},
            {"sculpture": {"name": "Black Sun", "date": "1969"}},
            {"sculpture": {"name": "Sky Gate", "date": "1977"}}
        ]
    }},
    {"sculptor": {
      "firstname": "David",
      "lastname": "Smith",
      "sculptures": [
        {"sculpture": {"name": "Medals for Dishonor", "date": "1937-40"}},
        {"sculpture": {"name": "Agricola I", "date": "1952"}},
        {"sculpture": {"name": "CUBI VI", "date": "1963"}}
      ]
    }},
    {"sculptor": {
      "firstname": "Rachel",
      "lastname": "Whiteread",
      "sculptures": [ {"sculpture": {"name": "House", "date": "1993"}} ]
    }}
  ]
}

Call the file sculptors.json, and add it to the assets/json folder.

Finally, open the MainActivity java class, which was created by default, and edit it to contain the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.mdoery.tut.json;

import java.io.IOException;
import java.io.InputStream;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.text.Html;
import android.text.Spanned;
import android.widget.TextView;

public class MainActivity extends Activity {

  private static String sep = System.getProperty("line.separator");
  private static String br = "<br>";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    TextView tv = (TextView)this.findViewById(R.id.textView1);
    try {
      // Read contents of file sculptors.json 
      String json = getText("json/sculptors.json",
        getApplicationContext()) + sep;
      // Display it in TextView
      displaySculptorNames(tv, json);
    } catch (Exception e) {
      // If there is an exception, show it in the TextView
      tv.setText("Exception: " + sep + e + sep);
    }
  }

  private String getText(String filename, Context ctx)
    throws IOException {
    InputStream is = ctx.getAssets().open(filename);
    // We guarantee that the available method returns 
    // the total size of the asset not more than 2G.
    int size = is.available();
    // Read the entire asset into a local byte buffer.
    byte[] buffer = new byte[size];
    is.read(buffer);
    is.close();
    String text = new String(buffer);
    return text;
  }

  /**
   * Extracts information from JSON text, and displays it in TextView 
   * @param tv TextView
   * @param json String of json from file sculptors.json
   * @throws JSONException
   */
  private void displaySculptorNames(TextView tv, String json)
    throws JSONException {
    String text = "";
    JSONTokener tokener = new JSONTokener(json);
    JSONObject object = (JSONObject) tokener.nextValue();
    JSONArray sculptors = object.getJSONArray("sculptors");
    for (int ii = 0; ii < sculptors.length(); ii++) {
      text += (ii + 1) + ") ";
      JSONObject obj = sculptors.getJSONObject(ii);
      JSONObject sculptor = obj.getJSONObject("sculptor");
      String firstname = sculptor.getString("firstname");
      String lastname = sculptor.getString("lastname");
      JSONObject sculpture = sculptor.getJSONArray("sculptures")
        .getJSONObject(0).getJSONObject("sculpture");
      String name = "<i>" + sculpture.get("name") + "</i>";
      text += firstname + " " + lastname + " sculpted " + name;
      String date = sculpture.getString("date");
      try {
        Integer.parseInt(date);
        text += " in " + date;
      } catch (NumberFormatException e) { // catch date range
        text += " during " + date;
      }
      text += br;
    }
    Spanned span = Html.fromHtml(text);
    tv.setText(span);
  }
}

If you run this in the emulator, you should see a screen with text created from the information extracted from the JSON file, like this:

As an aside, notice that some of the text is formatted in italics. That was done using Html.fromHtml in the code. I found out how to do that at Stealthcopter.com.

BTW, here’s a handy tip if you want to try this project with your own JSON: validate JSON before trying to read it in your app. It may save you some trouble! I used jsonlint to do so.

how to share resources among two or more android apps

| Comments

Summary: This post explains how to use the attribute sharedUserId to make data accessible from one Android app to another. To follow along, you should be familiar with building Android projects in Eclipse. To implement the solution, you will need an Android device; it won’t work in the emulator.

For the current app that I’m building, the user will be given the choice of downloading one or more “packs” which contain new content. Here, “content” means resources such as images or text files in the assets folder of an Android project. Every new pack will be implemented in its own, independent Android project.

As a concrete example, suppose I’ve built a crossword app which allows users to solve crosswords on a mobile device. The app comes with a limited supply of crossword puzzle content. If users want to solve more crosswords, they have to download more packs of puzzles from the Google Play store, or elsewhere. For a real-life example, take a look at Standalone’s Crossword Light app.

The key to making this work lies in the AndroidManifest.xml attribute called sharedUserId.

In keeping with the example above, open Eclipse, and first create the project MainCrossword. This project represents the app which lets users solve crosswords. Note that we won’t implement a real interface; for our purposes, we’ll just provide a simple TextView to display debugging information:

  1. This is a launchable application - meaning it will show up as an App when installed on your device. As you create the new project, accept all the defaults supplied by Eclipse. Pay attention to these options, in particular:
    1. Set the package name to be `com.mdoery.tut.maincrossword`.
    2. Set the Minimum Required SDK property to API 8: Android 2.2 (Froyo), and the Target SDK to API 17: Android 4.2 (Jelly Bean). This tutorial has been tested with that configuration.
    3. When creating the workspace, just choose a blank `Activity` to start with.
  2. Open the `AndroidManifest.xml` in `MainCrossword` and edit the <manifest> tag to add the following property: `android:sharedUserId=”com.mdoery.tut.maincrossword.uniqueid”`.

Next, create another project in Eclipse, called AnimalCrosswordPack. This will be a “data pack” which will contain the content for a number of animal-themed crosswords (note that for our example we will use fake data; we’re not going to build a real app, just enough of it to demonstrate the solution).

  1. Choose the same options as for `MainCrossword`, but this time, name the package `com.mdoery.tut.maincrossword.pack`. There’s nothing special about this package name; I’m just using a different package from the previous one so it’s clear that they don’t have to be the same.
  2. Otherwise, repeat the process as for the `MainCrossword` project (same Target SDK, etc.).
  3. In `AnimalCrosswordPack`, there should be a folder called `assets`. Create a subfolder here called `txt`, and then create the file `animal_theme.txt` within `assets/txt`. Edit this file to contain the text “Lions and tigers and bears, oh my!”.
  4. Open the `AndroidManifest.xml` in `AnimalCrosswordPack` and perform two edits:
    1. delete the <intent-filter> tag. This makes it impossible to launch the app. We don’t want this app to be launched by the user; it’s only there as a source of data to be accessed by the `MainCrossword` app.
    2. edit the <manifest> tag to add the same `sharedUserId`: `android:sharedUserId=”com.mdoery.tut.maincrossword.uniqueid”`.

So the AndroidManifest.xml file in AnimalCrosswordPack should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mdoery.tut.maincrossword.pack"
    android:versionCode="1"
    android:versionName="1.0"
    android:sharedUserId="com.mdoery.tut.maincrossword.uniqueid" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.mdoery.tut.maincrossword.pack.MainActivity">
        </activity>
    </application>
</manifest>

Notice the sharedUserId property. The MainCrossword project also has the android:sharedUserId="com.mdoery.tut.maincrossword.uniqueid" property set. However, it has not had <intent-filter> tag removed.

Eclipse should have created a file activity_main.xml in the directory MainCrossword/res/layout (if it hasn’t, add this now). Edit the file so that it contains a TextView to display the output of our test, as follows:

1
2
3
4
5
6
7
8
9
10
11
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:minLines="25"/>
</RelativeLayout>

Now open com.mdoery.tut.maincrossword.MainActivity and edit the code as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.mdoery.tut.maincrossword;

import java.io.InputStream;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.widget.TextView;

/**
 * Demo to show how data can be shared between two apps.
 * 
 * You must install the Android app generated from this project and 
 * from AnimalCrosswordPack project to make this demo work correctly.
 * 
 * @author mdoery
 *
 */
public class MainActivity extends Activity {
    private static String sep = System.getProperty("line.separator");
    // package name of AnimalCrosswordPack
    private static String pkg = "com.mdoery.tut.maincrossword.pack";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // We will write the results of our test to the TextView.
        setContentView(R.layout.activity_main);
        TextView tv = (TextView)this.findViewById(R.id.textView1);
        // Start of displayed content.
        String text = "BEGIN ...." + sep;
        try {
            Context ctx = createPackageContext(pkg,
                    Context.CONTEXT_IGNORE_SECURITY);
            // Add contents of file animal_theme.txt 
            // from AnimalCrosswordPack app.  
            text += getText("txt/animal_theme.txt", ctx) + sep;
            // Print out the directory listing 
            text += "DIRECTORY CONTENTS: " + getFileList(ctx) + sep;
        } catch (Exception e) {
            // If there is an exception, show it in the TextView
            text += sep + e + sep;
        }
        text += "END";
        tv.setText(text);
    }

    private String getText(String filename, Context ctx) {
        String text = sep + "FILE " + filename + " CONTENT = ";
           try {
               InputStream is = ctx.getAssets().open(filename);
               // We guarantee that the available method returns 
               // the total size of the asset not more than 2G.
               int size = is.available();
               // Read the entire asset into a local byte buffer.
               byte[] buffer = new byte[size];
               is.read(buffer);
               is.close();
               text += " " + size + " " + new String(buffer);
           } catch (Exception e) {
               text += "EXCEPTION " + e;
           }
           text += getFileList(ctx);
           return text + sep;
    }

    private String getFileList(Context ctx) {
        String text = sep + "FILE LIST = " + sep;
        try {
            String[] l = ctx.getAssets().list("txt");
            for (String s : l) {
                text += s + ", ";
            }
        } catch (Exception e) {
            text += sep + "EXCEPTION: " + e + sep;
        }
        return text + sep;
    }
}

Now build each of the projects, and export them as Android Applications (right-click on the project name in Navigator view, then choose Export > Export Android Application and follow the instructions). In the final step, install both apps on your Android device. You should see MainCrossword appear in your list of apps. Open it, and you should see the following output:

Tada! The data from AnimalCrosswordPack has been read by the MainCrossword app. You’ve successfully shared data from one app to another.

You can also verify that the AnimalCrosswordPack doesn’t show up as a launchable app. You can’t open it.