Showing posts with label Android. Show all posts
Showing posts with label Android. Show all posts

Monday, January 26, 2015

Network May Be Monitored By A Third Party (a.k.a. Android KitKat scary warning message part 3)

I guess people care about this annoying warning message that Android has thrown on us.   No other blog post here has generated anywhere near the number of hits that this issue has generated.  So, to all of your that have found my blog and read what I have to say, I thank you!

Unfortunately, Google doesn't seem to care about this issue.   (Newsflash!  Google doesn't care about making certificates easy to use on Android.   And, by extension, they don't care to make wireless authentication on Android usable either!   I could write volumes on my experiences trying to get things improved.  But, I fear that it would inspire depression in anyone that read it.)   So, we need to do something to try to GET them to care!

There have been several bugs opened about this problem, and most of those bugs have been closed with various reasons that make no sense.  (Which is what I base my claim that they don't care on.)  Some of the other people on the Internet that agree with me that this is a problem have continued to open new bugs to continue to push the problem and make Google listen.   If you care about this problem, PLEASE go to the Google bug tracker, and at least star the issue.   If you have the time, you might consider posting a comment to the bug that outlines why this issue causes you grief, and what changes you would like to see made in order to improve the situation.   (If you are only going to complain, do us all a favor and just star the issue.   If you want to complain, and offer suggestions on how to improve things, please post a comment!)

I'll even make it easy for you.   The URL (with link!) to the current version of the bug is at : https://code.google.com/p/android/issues/detail?id=82036 .


As you make your feelings known, please consider this.   There is actual value in having a warning like this, if you are the kind of person that values privacy and is against network operators using man-in-the-middle tactics to monitor what you are doing.   And, thanks to stupid legislation here in the US, if you are attending a K-12 school that provides Internet access, you SHOULD care about this very much!   US law requires the schools to snoop on students as a condition to continue to receive funding!    I'm lazy, so I am not going to track down the exact law that requires this, but I know it is true because of my day job.   (Not going to say any more than that because my opinion might get me in trouble.)

Because I would prefer that people being snooped on are given any warning that it could be happening, I am actually in favor of the warning message that was added.   However, I have problems with how it was implemented.   I think there are a few tweaks that could be made to the implementation that would take this from being a pointlessly scary warning message to a valuable warning message, and an opportunity to educate at least a few people on some of the security issues that are associated with installing new root CA certificates on any device or OS.   Also, as I have stated before, I don't like people that complain about a problem without offering solutions!

So, here are my suggestions on how to transform the annoying mess that is the "scary certificate warning message" in to something useful and significantly less annoying :


  • Don't show the message (ever) if the certificate is used with 802.1X on a Wi-Fi connection -- Starting with Android 4.3, the "Credential Store" (a.k.a.  Certificate store, or key store) has been split in to two parts.  One part holds the certificates that will be used for authentication on Wi-Fi networks.   The other part holds certificates that are used for VPN connections, web site connections, and everything else.   Since you *HAVE* to install a certificate in order to securely use a Wi-Fi connection, showing this warning message is pointless.  The other alternative is not to use a certificate to validate the Wi-Fi network, which leaves you WIDE open to anyone doing a man-in-the-middle.   So, the scary warning actually has the potential to make users LESS secure by making them remove the certificates necessary to secure their networks!   Google must recognize this on some level, because they don't show the warning if an app uses the Wifi API to install certificates.
  • When you tap the warning, give useful information --  Right now, when you tap the warning, you are taken to the security settings screen.   This is COMPLETELY useless.   The user is expecting to be given more information on what this warning is all about.   Take them to a screen that explains what the potential issues are with installing a new CA certificate.   Explain clearly what the problem is, and don't try to scare them in to submission.  Try to give the user enough information to allow them to make a good decision about leaving the certificate installed.   Also, provide links to more detailed information so that those that are really curious can really dig in and understand the problems.
  • Allow users to get rid of the warning -- Right now, you can dismiss the warning, but it comes back any time you install another CA cert, or anytime you reboot your device.   This behavior is annoying.   Tell me once, let me decide what to do, and then leave me alone!  Anything else is a bad user experience.   When the user taps the warning to get more information about what it is about, they should also be given two check boxes.   One check box should allow them to dismiss the warning just for the current certificate, the other should allow them to block ever showing it again.   There really are two types of users where this warning is concerned.  There are those that already understand the risks, and those that don't care.   The purpose of the warning to to try to get some of those that don't care to actually start to care.  But, annoying the crap out of them is only going to make them hate the issue so much that they will never care.

If my three suggestions above were implemented, I suspect most people would no longer hate the warning message as much as they do now.   It also gives the power back to the user, which is what I always thought Android was supposed to be about.

I understand that in addition to warning users that they may be causing themselves problems, they are probably also trying to cover their butts should a bunch of users get hacked and complain in the media, or worse, try to sue.   However, I really believe that showing the warning once is more than enough for Google to cover their butts on this.

In the unlikely event that someone from Google reads this, and wants me to put my money where my mouth is, I will offer to submit patches to the AOSP project to implement all of the things above.  HOWEVER, given my previous experiences with submitting patches to AOSP, I will be expecting a promise that the code will eventually be included.   By "eventually", I mean that someone from Google will be assigned to review my code, and comment on it until the code is in the right state to be fully included in AOSP and eventually the main Android builds.   (As an aside, while I have tried not to give out any information on this blog about who I am, I am sure that Google has the ability to look up my e-mail address based on this blog being hosted on Blogger.   There is also someone on the Android dev team that knows me, and someone on the Android security team that knows me.  So, it should be easy enough to verify that I know what I am talking about, and it should be easy to reach out to me.)

Tuesday, November 26, 2013

Scary Warning Message on KitKat Part 2

Update 1/26/2015 - Google has refused to take this issue seriously.  They have closed several bugs posted on the Android bug tracker.   Fortunately, there are some other people out there that agree that the current implementation is broken.   Unfortunately, Google doesn't seem to think that it is worth fixing.   Since I know a lot of people find this page while looking for information on this annoying warning, I would encourage you to go to the current bug on the Android bug tracker, and star it to show Google that you want this resolved!   Perhaps if enough of us star it, Google will start to pay attention.   You can find the current open bug here.


It always amazes me which posts of mine tend to generate the most traffic.   Not that my blog gets a ton of traffic, but once in a while I get spikes of traffic to a specific post and it surprises me a little.   My previous post on KitKat seems to be one of those that has caused a bit of a spike.   So, I figured a follow up might be interesting.

A couple of hours ago, a coworker sent me a link to Android issue 62076.   Gotta say, I was quite surprised to see a link to my blog as supporting evidence!   I was equally surprised to see that the bug was marked "Works as Intended" and closed.   I read through the statements on how it is intended to work, and came to the conclusion that if it does indeed work that way, this may not be as big a deal as I thought.   But, what was being claimed didn't seem to match up with my experience using KitKat.

For those who don't want to sift through the bug chatter, here is a snip of the response from one of the Android team members :

The "User" portion of the trusted credential store is non-system CA certificates that have been installed and are trusted by the browser and other things that use the system Trusted Certificate Store. This warning is about protecting the user of the device.

Note that EAP-TLS and other Wi-Fi modes do not need to install a CA certificate to the Trusted Certificate Store. If you include your CA certificate in a PKCS#12 bundle when installing the credentials and select "Wi-Fi" as the destination for those credentials, you will not get this warning. You can also create a program that adds Wi-Fi credentials and configurations programmatically using the new WifiEnterpriseConfig API (see http://developer.android.com/reference/android/net/wifi/WifiEnterpriseConfig.html).

Asking for exceptions does not make sense. There is a process for getting a CA into the trusted list of browsers and operating systems. Please see http://www.mozilla.org/projects/security/certs/policy/ for an example of the process that needs to be followed.

This all seems pretty reasonable, although arguing that you can write an app to install certificates in a way that doesn't cause scary messages seems to be a bit of a stretch for most people.   But, lets run with the claim for the PKCS#12 bundle for installing the certificates for a second.

Lets start with a little test.   First, go download a root CA certificate from a known CA provider.  You might need to search around a bit to find one, or you can just use the GeoTrust certificate that I will be using as I run through this test.   I used the first certificate on the list, which is the "Equifax Secure Certificate Authority" certificate.   Next, lets convert that certificate to a PKCS#12 bundle so that we can attempt to install it.   You can either try to figure out the magical incantations to do it using OpenSSL, or you could just hop over to the SSL Shopper.com converter that I will be using.


Whoops.  You mean we have to have a private key to create the bundle that we need to install a root CA in a way that doesn't trigger that scary warning message?!   Awww, c'mon!

A little bit of searching will quickly turn up that a PKCS#12 bundle is intended to store a user certificate and any supporting CA certificates it may need.   Further, even if you could get a PKCS#12 file with just the CA certificates in it, the code in the Android certificate installer would prevent you from using it.  (I have spent FAR too much time digging around in that code.  So you may have to trust me on this one.)

Okay, so lets assume for a second that we are using an EAP-TLS network, and so we can create a PKCS#12 bundle that contains a user certificate and private key.   According to the statement from the bug, we should be able to do this, and as long as we select the "Wi-Fi" store, we should be good to go.   As I happened to have a small home grown CA set up on a Linux box, I tried this method.   After going through the install, I still saw the scary icon in the shade.   But, how could that be?   If the bug was closed because it works as intended, and someone with an @android.com e-mail address says the way it is intended to work shouldn't be showing that scary icon, we must have done something wrong.   Did I maybe forget to select "Wi-Fi" for the target store?   So, I decided to try again.   The first thing I would do is go an clear the "Credential Store" by going to Settings->Security->Clear Credentials.   Then, I tried it again, and again got the icon for the scary warning.   But this time, I made extra sure that I had selected the "Wi-Fi" store like the bug resolution said to.  Interesting...

While poking around with KitKat, I remembered coming across another location that I could use to install certificates.   If you go to Settings->WiFi, select the menu button at the bottom and select "Advanced" there is another option in that menu to install certificates.   But, before we do that, we need to clear out the certificates we had already installed.  So, we go to Settings->Security->Clear Credentials, but the option to clear the credentials isn't available.   Does this mean that the certificates weren't installed?  If you tap on "Trusted Credentials" and then select the "User" tab, you would find that the certificates you had installed are actually listed.   So, either the "Clear Credentials" option has a bug that prevents it from being used when a user has installed certificates to the "Wi-Fi" store, or the certificates are installed someplace other than the "normal" user key store.   The response to the bug indicated they were installed elsewhere, so lets assume that is the case.  (A quick aside.  They are actually stored in the same place as they always were, but they are flagged with a different user ID.   So, it could easily be argued that we are dealing with a bug.  Exactly what the bug is would depend on your view of how things should work, but the fact that historically using "Clear Credentials" has cleared all of the certificates that were installed on the "User" tab would seem to indicate that the same should continue to be true.)

Okay, so the only way to get rid of them appears to be to tap each certificate listed, scroll down and tap the "Remove" button.   Fair enough, lets do that.   Now, we are ready to install certificates using the installed option located in the advanced section of the WiFi settings.   Doing so acts almost the same as installing them through the security menu.  The only difference is that you are not given the option to install to the "Wi-Fi" store.   So, I guess we have to assume that it is going there by default when you install this way.  (Another aside, testing this on the emulator shows that it does go in to the "Wi-Fi store".)

But, look at that, the scary warning is showing up again!

Well crap.   Now what?   Lets try the other option outlined.   Let's write our own code.  But first, lets take a look at this bug.   Uh oh.   Looks like that isn't an option either.   And I won't even bother to point out that the API doesn't allow for certificate chaining.   So, even if it DID work, most people would be outta luck using it anyway.

So there you have it.  This is not a bug.  It works as intended.   Which means that you have *NO* way to access an authenticated wireless network without seeing a scary warning that someone might be monitoring your traffic.


But, before I go, let me relate a story that happened to me a few years ago that, while tangentially related seems like it makes a good warning.   Several years ago, I was on a conference call with a large mobile phone provider from the UK, and higher-ups at a phone manufacturer that had significant market share at the time.  I'm leaving out names because I suspect neither party would be overly happy to be identified publicly.  During this call, the mobile phone provider was asking the manufacturer for APIs to support a product they wanted to deploy.   No matter how they asked, or how they threatened, the manufacturer kept telling the provider that they didn't need those APIs because the product they wanted to use would be obsoleted by something they were working on.   After getting off the call, I talked to my boss (who was also on the call) and said, "Ignoring what your customers want seems to be a good way to kill a company."   In the years since that call, the manufacturer ignored more of what its customers wanted, and its market share has declined to a point that is shameful.   It doesn't matter how big your company is, or how your market share is today, if you ignore what your customers want, you will eventually die.

Since there seems to be some interest in how the wireless authentication all fits together on Android, I plan to post additional information here.   If you want to know far more about the certificate internals of Android than any reasonable person would, you might want to check back.


Wednesday, November 6, 2013

Android KitKat : Network may be monitored by an unknown third party

Update 1/26/2015 - Google has refused to take this issue seriously.  They have closed several bugs posted on the Android bug tracker.   Fortunately, there are some other people out there that agree that the current implementation is broken.   Unfortunately, Google doesn't seem to think that it is worth fixing.   Since I know a lot of people find this page while looking for information on this annoying warning, I would encourage you to go to the current bug on the Android bug tracker, and star it to show Google that you want this resolved!   Perhaps if enough of us star it, Google will start to pay attention.   You can find the current open bug here.

Let me start by making the disclaimer that these observations are based on an AOSP build of Android KitKat on a Galaxy Nexus.   (My Nexus 5 isn't due for two more days.)   However, things like this that exist in AOSP tend to also exist in the release builds.

Along with all of the great new features in KitKat, Google has introduced what is probably the WORST security hole possible.   A ham-fisted implementation of certificate pinning.

Certificate pinning itself is a good idea.   It verifies that when you visit a web site, it provides you with the same certificate every time.   In general, certificates shouldn't be changing on web sites, and if one does you should be made aware of it.

So, how could certificate pinning be a security hole then?   By creating a situation where a harmless certificate creates a scary, and unnecessary, warning message.  When you install any certificate in to the key store, you get a warning icon in the bar at the top of the screen.   Pulling down the shade presents the following screen :


Let's consider the average user at this point.   Given the revelations about all of the snooping by various governments, it seems that a warning like this would make uninformed users very concerned.   (And, lets face it, there are very few informed users when it comes to 802.1X on Android.)   But, okay, maybe I am freaking out over nothing.  What happens when we tap the warning?


Hmm... Maybe not.   So, the assumption that an uninformed user is to draw based on this is that having any form of third party "trusted credential" installed means that a 3rd party will probably be monitoring my traffic.

I'm going to skip over the obvious irony here that something from Google is warning me that a 3rd party might be monitoring the web sites I visit, and reading my e-mail.  (If the irony is lost on you, you may want to do some research.)

Google seems to be trying to argue that the only safe type of certificate is one that comes pre-installed on your device.   Which is a downright silly argument no matter how you slice it.   But, lets go ahead and let that one slide.   Being the security minded individual that I am, I make sure that all of my network connections are as secure as possible.   So, I make the (probably bad) assumption that purchasing a certificate for my RADIUS server from a public CA will provide me what I need in order to have a secure wireless network.

Once I get the network setup, I try to connect my Android device to the network.   Now, being that we have paid even a little attention to the security issues around wireless networks, we know that we need to validate the server certificate in order to have a secure connection.   No problem, we purchased from a public CA, so we will just select that in the configuration settings.   But, you can't.   The pre-installed certificates on Android can't be used with 802.1X authentication.   Okay, no problem.  I'll just install the CA certificate on my device and then use that.   Oh, what is this scary message?

Now, those of us that understand the meaning of this message will just dismiss it.   But, let's assume that you aren't a techno savvy individual that has enough time to spend learning about security.   This warning is going to freak you out!   If this conversation hasn't happened on a message board yet, it will soon :

"Hey, I got the upgrade to KitKat, and it is great!  But, now I get this warning saying that someone is monitoring my network connection!   How do I make that go away!?  I don't want someone monitoring my network connection!"

"Getting rid of that warning is easy.   Go to Settings->Security->Clear credentials.   After that, the warning will go away."

"Thanks!  The warning is gone.  But now I can't connect to my wifi network!  HELP!"

"Not a problem.  Go in to the configuration for your wifi network and change the 'CA certificate' setting to '(undefined)'.  Problem solved!"

"Perfect!  That solved my problem!  Thanks!"



And somehow Google either didn't consider this case, or they really want to decrease the security of wireless networks.

Anyway, if you happen across this post while looking for how to make this scary looking warning go away, and you use a secure wifi network, please just swipe the warning out of the shade.   It is really nothing to worry about.   (I'll do a follow-up post in the next week or so about how authentication on wifi works and why you should care.)

Edit 11/26/2013 : A coworker pointed out that my blog post was referenced in a bug posted to the Android bug tracker.   The same bug post had a response from someone using an @android.com e-mail address outlining why this issue isn't a problem.   I tested their solutions and wrote about it here.

Thursday, July 19, 2012

Code coverage with Android and EMMA

Recently, I have been looking in to tools in Android that would help me improve code quality and find bugs before putting the product out.  I picked up a copy of Diego Torres Milano's "Android Application Testing Guide" as a good place to get me started.   Since there are a lot of tools out there, and not all of them support Android, it was a useful guide to tools that should be working.

One of the tools I was looking in to is code coverage.   Over the years, EMMA has been integrated with the Android build tools, which is supposed to make it easier to use.   Unfortunately, if you don't use them just the right way they can end up being a horrid time suck trying to figure out why your instrumented build just isn't working.

For my testing, I was using the latest tools available as of this writing.  (That would be ADT 20)   Lots of the information available on the Internet is for older versions of tools that don't use the same commands as ADT 20.  I also started with the most basic app I could, and set it up so that I had an easy way to intentionally miss some code, or make sure I hit it all.   The code looked something like this :
@Override
public void onCreate(Bundle savedInstanceState) {
   String appname = "unknown.app";
   super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);
}

public void testme() {
    System.out.println("This is a test!");
}

A pretty simple "app" that mostly comes out of the Android tools when you create a new project.   The "testme()" method is there because it will allow us to explicitly call some code (or not) to let us see that code coverage is really working.

Next, I created a test app using the Android tools and created a new test case.   The test case extends ActivityInstrumentationTestCase2, as it will start up the activity we want to test in a state that is easy to use.  The test class looked something like this :

public class MyTest extends ActivityInstrumentationTestCase2 {
public MyTest()
{
   super(TestActivity.class);
}

public void testCodeCoverage()
{
}

The "TestActivity" referenced in the above code is the name of the Activity that we created earlier.   Also, note that our test doesn't actually do anything.   For now, this is okay.   When the MyTest class is run, ActivityInstrumentationTestCase2 will create the activity, which will result in the onCreate() method being called.   So, once we have our code coverage running, we will have a block of code that was covered, and a block that was not.

At this point, we are going to use the command line.   We also need to make sure that we have a fairly current version of Apache Ant ready to use.   For my tests I was using 1.8.2.

At the command line, we want to convert our apps to use Ant for building.   To do this, we need to use the "android" command that is included in the SDK.  (On Windows, it is "android.bat")  We need to run two commands, one to convert the "TestActivity" app and one to convert the "MyTest" test project.  The command will look something like this :

android update project --path /path/to/TestActivityProject --name TestActivityProject --target android-16
The "--name" parameter should be set to whatever you named your project, and "--target" should be set to the API level that you want to test with.   The "--path" parameter requires the FULL path name to your project.   So, if you are on windows it might be something like "c:\users\foo\workspace\TestActivityProject" I tested with an Android 4.1 VM, so I chose "android-16".  The version you use isn't too important, unless you plan to use Ant to make your final builds.

Next, to convert the test project :

android update test-project --main ../TestActivityProject --path /path/to/MyTestProject
The "--main" parameter confused me a bit at first.   It should be a relative path from the directory you store "MyTestProject" in, to the path of "TestActivityProject".   If you are using Eclipse and putting your code in a workspace, you should normally need to use ../.

Finally, we are ready to build and run our test.  Go in to the directory for "TestActivityProject" and run "ant".  (If ant isn't in your path, you may need to use a full path to ant instead.)

But, whats this?  We get a help screen like the following :


Android Ant Build. Available targets:
   help:      Displays this help.
   clean:     Removes output files created by other targets.
              The 'all' target can be used to clean dependencies
              (tested projects and libraries)at the same time
              using: 'ant all clean'
   debug:     Builds the application and signs it with a debug key.
              The 'nodeps' target can be used to only build the
              current project and ignore the libraries using:
              'ant nodeps debug'
   release:   Builds the application. The generated apk file must be
              signed before it is published.
              The 'nodeps' target can be used to only build the
              current project and ignore the libraries using:
              'ant nodeps release'
   instrument:Builds an instrumented package and signs it with a
              debug key.
   test:      Runs the tests. Project must be a test project and
              must have been built. Typical usage would be:
                  ant [emma] debug install test
   emma:      Transiently enables code coverage for subsequent
              targets.
   install:   Installs the newly build package. Must either be used
              in conjunction with a build target (debug/release/
              instrument) or with the proper suffix indicating
              which package to install (see below).
              If the application was previously installed, the
              application is reinstalled if the signature matches.
   installd:  Installs (only) the debug package.
   installr:  Installs (only) the release package.
   installi:  Installs (only) the instrumented package.
   installt:  Installs (only) the test and tested packages (unless
              nodeps is used as well.
   uninstall: Uninstalls the application from a running emulator or
              device. Also uninstall tested package if applicable
              unless 'nodeps' is used as well.

This is where I really screwed up and spent a lot of time scratching my head.   Since we want to use EMMA, we need an instrumented build, right?   So the command that we need is "ant emma instrument install test" to get a build, install it, and run the test in it.

While this seems like the most intuitive answer, you will quickly find out that it doesn't work.  Everything will compile and install fine, and we can assume that the resulting apps are instrumented.   However, when it gets down to running the test, you will be greeted with something like this :


     [echo] Running tests ...
     [exec] INSTRUMENTATION_RESULT: shortMsg=java.lang.IllegalAccessError
     [exec] INSTRUMENTATION_RESULT: longMsg=java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation     [exec] INSTRUMENTATION_CODE: 0

Hmm..  Something didn't work.  This is where I spent a ton of time trying different combinations and searching to understand what was going on.   Finally, as I was giving up, I noticed a browser page that I had already opened that said for EMMA code coverage, we need to run "ant emma debug install test".  I didn't think it would work, but I gave it a shot.  Crazy as it is, it DID work!  (I literally spent 7 hours trying to figure out what the above error message was all about.)

Once your instrumented build is run, the necessary coverage files will be downloaded, and a coverage.html file will be created in your test project's "bin" directory.   Opening that up should show that your onCreate() method ran all of its code, but the testme() method wasn't touched at all.   If you go in to the testCodeCoverage() method in the test app, you should be able to add the line "getActivity().testme()" and then run the instrumented build again.   This time, you should get 100% code coverage.

Hopefully, this post saves someone out there a significant portion of their time getting started with code coverage!

Monday, April 11, 2011

Color changing rounded rectangles and Android

I recently came across an interesting problem. I needed to have a rounded rectangle as the background of one of my widgets, but I needed to allow the border color of the rounded rectangle to be set by a configuration setting.

Before the color changing requirement showed up, I was using a 9-patch to create my rounded rectangle. It was simple, and worked quite well. So, after some digging I came to the conclusion that I would need to change the colors in the 9-patch image in order to achieve my goals. I ended up burning a lot of time trying to figure out how to do this. (It isn't as simple as changing the colors in a normal .png file, unfortunately.)

While digging, I came across this post that hinted that I might not need the 9-patch to do what I wanted. But, the documentation for the shape XML is pretty much non-existent. Which made things hard.

A little more digging found an article that linked to this page. This provided me the attributes for the XML that allow me to do what I needed to do. So, I set out to give it a shot. I started with a rounded rectangle with a white background and a black line around the outside. The XML looked like this :

<!--?xml version="1.0" encoding="utf-8"?-->
<shape android="http://schemas.android.com/apk/res/android" shape="rectangle">
<solid color="#ffffffff">
<stroke width="2dip" color="#ff000000">
<corners radius="75dip">
</corners></stroke></solid></shape>


When I created the XML file in eclipse, there wasn't an option to create a shape XML file. So, I picked one of the other types, and just deleted what was automatically created, and replaced it with the code above. However, I also needed to move the XML file so that it was in the drawable directory. (If you are like I was, you are thinking, "But an XML file isn't a drawable!" But, you'll just have to trust me.)

This provided me the basic rounded rectangle that I needed. But, changing colors on-the-fly was still an issue. After a bit of digging, I found that using this XML to create a drawable would give me everything I needed to get the job done.

I decided to set the background for the layout that contained everything I was going to use. (Which is why I am using a LinearLayout instead of some other widget. But, any widget that allows a background to be set should work.)

For a test, I changed my onCreate() method to include this code :

workareaLayout = (LinearLayout)findViewById(R.id.rightColumnInner);

GradientDrawable sd = (GradientDrawable)this.getResources().getDrawable(R.drawable.roundrect);
sd.setColor(0xff00ff00);
sd.setStroke(10, 0xffff0000);
workareaLayout.setBackgroundDrawable(sd);

This created a nice rounded rectangle that is suitable for Christmas. (It has a green background with a large red border.)

The sd.setColor() call sets the background color for the rectangle. The color is an ARGB value, so we start with 0xff since we don't want any transparency. The 00ff00 portion indicates that we want to be the brightest green we can have.

The sd.setStroke() call specifies the width of the border line, along with the color. For this example, the width is 10, and the color is bright red, with no transparency. The documentation isn't clear what a width of "10" means. (Is it pixels? Or one of the other measures commonly used in Android development?) But, it appears that it is probably pixels.

And with that, you should have the ability to create all kinds of crazy rounded rectangles. However, keep in mind that this same method should work for other types of shapes that can be created with the shape XML.

Sunday, October 17, 2010

Fancy List Items in a ListView

A while ago, I was working on a project where I wanted to be able to have Android show a ListView, with custom entries in the list. In most other languages and toolkits, doing this is a bit of a pain. You have to implement a special painter to pain the list cell in just the right way. Unless you are a pretty die-hard developer for that toolkit you may find yourself horribly confused as to how to handle the situation.

Fortunately, Android made this REALLY easy. If you know how to do a layout for a form, you know almost everything you need to know in order to make a fancy list item.

First, you want to create a small layout that specifies how a single list entry should look. For my purposes, I wanted to have a single line of large text, with smaller text below. I also wanted the large text to be bright white, and the smaller text to be a darker gray color. Finally, the smaller text should be indented on both sides to indicate that it is subordinate to the larger text.

No problem! A basic linear layout will do the job nicely. My final layout looks like this :


<linearlayout android="http://schemas.android.com/apk/res/android" layout_height="wrap_content" layout_width="fill_parent" orientation="vertical">
<textview layout_height="wrap_content" id="@+id/LargeText" text="Large Text" textcolor="@color/white" textsize="25dip" layout_width="fill_parent"></textview>
<textview layout_height="wrap_content" layout_width="fill_parent" layout_marginright="10dip" layout_marginleft="10dip" id="@+id/SmallText" text="Smaller Text" textcolor="#c0c0c0"></textview>
</linearlayout>


Easy enough. Now that we have that, we need to actually write some code that will use it. I find it easiest to create a convenience class that lets me manipulate the list easily. So, I create a class called MultiLineList, that looks something like this :


import java.util.ArrayList;
import java.util.HashMap;

import android.app.Activity;
import android.widget.SimpleAdapter;

public class MultiLineList {

private ArrayList<HashMap<String,String>> list = new ArrayList<HashMap<String,String>>();
private Activity parentActivity;
private SimpleAdapter taskAdapter;

public MultiLineList(Activity parent)
{
parentActivity = parent;

taskAdapter = new SimpleAdapter(parentActivity, list,
R.layout.multi_item_layout, new String[] { "largetext","smalltext" },
new int[] { R.id.LargeText, R.id.SmallText } );
}

public void clear()
{
list.clear();
}

public void addItem(String largeText, String smallText)
{
HashMap<String,String> item = new HashMap<String,String>();
item.put("largetext", largeText);
item.put("smalltext", smallText);
list.add(item);
}

public SimpleAdapter getData()
{
return taskAdapter;
}
}


In the constructor, we create a SimpleAdapter that we use to map our text in to the different rows of the list view we will show the user. "R.layout.multi_item_layout" is the name of the layout we created above. (You may need to change this if you called your layout something else.) The two strings, followed by the two ints give us a mapping in a hash table for how to identify the two different TextEdits that we put in the layout. There is nothing special about only having two items. We could easily have 10 strings and 10 matching ints. What is important is that each string has a corresponding int that maps to a text edit in your layout.

The addItem() method is where the good stuff happens. As part of creating the SimpleAdapter in the constructor, we told it to use the "list" variable to get its data out of. So, the addItem() method allows us to easily put new text in to the list to be used. So, calling addItem("I am big", "I am not") would give us a single entry in the list view that contained the text "I am big" in the large font, and "I am not" in the small font.

Now that we have all of the ground work in place, it is time to put the pieces together. Moving forward, I am going to assume that you have a basic form set up with a list view configured in it. Also, that a ListView variable points to it called "listView". From there, using our fancy list widgets would look something like this :


myList = new MultiLineList(this);
myList.addItem("Large 1", "Small 1");
myList.addItem("Large 2", "Small 2");

listView.setAdapter(myList.getData());



Now, the next time out list view is drawn, it will contain two items in the list. The first one will have "Large 1" in the larger text, and "Small 1" in the smaller text. The second one will contain "Large 2" in the larger text, and "Small 2" in the smaller text.

From this simple example, you should be able to easily expand your list items to contain any just about any widget type you would want. But, I leave that up to you to experiment with.

Saturday, August 7, 2010

Its all about the layers..

For an Android project I am currently working on, I wanted to take an image of the last state of my game board, and overlay another image on it. While this seems like a simple thing it wasn't terribly simple to figure out. (Though, it is surprisingly simple to implement.)

To make this easy, lets assume you have two bitmaps. We will call them bitmap1, and bitmap2. Also to make it easy, we will assume they are stored as drawables in your Eclipse project.

It is also important to note that bitmap2 must have some transparent areas on it. If bitmap2 is completely opaque, you won't see any of bitmap1, and this code will be a waste of effort. (Since you could just draw bitmap2 by itself.)

The first thing we want to do is get our drawables stored in Bitmap objects. These bitmap objects should be called bitmap1 and bitmap2. This is easily done with the following code :


BitmapDrawable draw1 = (BitmapDrawable)res.getDrawable(R.drawable.bitmap1);
Bitmap bitmap1 = draw1.getBitmap();

BitmapDrawable draw2 = (BitmapDrawable)res.getDrawable(R.drawable.bitmap2);
Bitmap bitmap2 = draw2.getBitmap();



That was easy. Now, we have to figure out how to combine the two. This is where things start to get a little less obvious. After Googling around, I found several different methods that could do it. But, the method I am going to use below seemed the most straightforward to me.

First, the code, then the explanation :



Bitmap layers = Bitmap.createBitmap(bitmap1.getWidth(), bitmap1.getHeight(), bitmap1.getConfig());
Canvas c = new Canvas(layers);
c.drawBitmap(bitmap1, new Matrix(), null);
c.drawBitmap(bitmap2, new Matrix(), null);



Believe it or not, you now have a merged bitmap in memory. (But how? It looks like all I did was draw on a canvas!?)

Simple, if you look at the Canvas class, you will find that one of the constructors allows you to pass in a Bitmap object. When you do this, anything that is drawn on the canvas is put in to the Bitmap that was passed in. So, in our example above, if we wanted to see the result, we could draw the Bitmap object 'layers' to the screen, and we would have the image of bitmap2 laid over bitmap1.

Or, to look at it another way. If you were working with a SurfaceView, and wanted to draw the bitmaps there, you would create a Canvas object, draw the two bitmaps in order, and pass the Canvas to the SurfaceView where it would be drawn. The code here does the same basic thing. However, instead of having the code drawn immediately on the SurfaceView, you have the output sent to a Bitmap.

But, lets look at some assumptions in this code real quick. When we create the 'layers' Bitmap object, we have to tell the Bitmap object a little bit about the bitmap we want to create. Specifically, we have to tell it what the size of the bitmap is, and what the configuration of the bitmap is. The size is fairly obvious. But, in the sample code above, we are assuming that either the two bitmaps are the same size, or that bitmap2 is smaller than bitmap1. If bitmap1 is actually smaller than bitmap2, then the final bitmap will be the same size as bitmap1, and bitmap2 will be clipped to fit. But what about the configuration? What is that? The configuration is a value that tells the bitmap what kind of data it will be storing. Or, another way to look at it, how big each pixel will be in memory. Without getting in to a bunch of detail, under normal circumstances, you will want to use ARGB_8888.

Why the heck would you ever do this? Well, if you needed a composite image that would be used repeatedly in your program you could either try to draw it in real-time every time it was needed, or draw it once and just reuse the drawing. If the image is more than a few pixels, recreating the overlay each time could be an expensive operation. Caching the result and using that instead can save you a fair bit of processing time that you can use for other cool effects in your program!

Monday, July 12, 2010

The right way to do databases in Android

Feel free to comment below on what a loser I am. I am sitting in a cabin outside of Yellowstone National Park, writing a blog entry about doing databases on Android. However, I do think I should get a little geek cred as I am posting it from an EDGE connection on T-Mobile across the FroYo wireless tether. But, anyway...

When I first started working with Android, I found a simple tutorial on how to build SQLite databases. A few weeks ago, I was trying to track down the information again for project for my day job. After searching a bit, I was unable to find the original tutorial, but I found one that was a little better. The newer tutorial pointed me toward a much better way to handle databases.

In the Android SDK is a class called SQLiteOpenHelper. This class has two methods that Eclipse will force you to implement. The first is onCreate(), the second is onUpdate().

The onCreate() method is automagically called when you instantiate your descendant class, and the target database doesn't exist. In this method, you should issue all of the necessary SQLite commands to create the tables in your database. But, hold on a second. How can you put tables in the database when you didn't specify the database name? Simple, when the class is instantiated, the database name is passed in. The SQLiteOpenHelper class then takes care of creating the database for you, before the onCreate() method is called.

The onUpdate() method is also really handy and cool. When you instantiate the descendant class, you also pass in a database version number. If the version number you pass in is higher than the version number when the database was created, the onUpdate() method is called with version number stored in the database, and the version number presented when the class is instantiated. onUpdate() is responsible for issuing the correct SQLite commands needed to bring the database up to the schema version specified when the class was instantiated.

Okay, so blah, blah, blah.. Sample code!?


public class DBOpenHelper extends SQLiteOpenHelper {

private final String MY_DATA_TABLE = "t_Data";
private final String MY_DATA2_TABLE = "t_Data2";

public DBOpenHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}

@Override
public void onCreate(SQLiteDatabase myDB) {

/* Create a Table in the Database. */
myDB.execSQL("CREATE TABLE IF NOT EXISTS "
+ MY_DATA_TABLE
+ " (mystring1 VARCHAR, mystring2 VARCHAR);");

myDB.execSQL("CREATE TABLE IF NOT EXISTS "
+ MY_DATA2_TABLE
+ " (mystring3 VARCHAR);");
}

@Override
public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
// We don't use this just yet.
}

}




Great! So the database gets created, but how do we actually use it?

Well, I am glad you asked. The SQLiteOpenHelper has methods that will return a handle to the database in either read only, or read/write flavors. So, if we wanted to use our open helper to create the database, and add a record to the second table, it would look something like this :



public void addDbData()
{
DBOpenHelper dbHelper = new DBOpenHelper(this, "my_database", null, 1);
SQLiteDatabase myDb = dbHelper.getWriteableDatabase(); // Open for write.

// Now, use the normal SQLiteDatabase methods to work with the DB.
myDb.execSQL("insert into " + MY_DATA2_TABLE + " set mystring3='Foo!';");
}


After you call addDbData(), you will now have a new database created with the name 'my_database', with schema version 1. The MY_DATA2_TABLE will also contain a single record containing the string "Foo!".

Two things worth pointing out in the addDbData() method. First, if you only want to read data from your database, you can change "getWriteableDatabase()" to "getReadableDatabase()". Second, always be sure you close your database after you are finished with it! The Java garbage collector won't do it for you, and you will get leak errors sent to your logcat console.

I am not going to get in to the update method of the SQLiteOpenHelper. I leave that as an exercise for you.

Wednesday, June 9, 2010

Something simple. Get text from a ListView.

Perhaps this topic is considered too simple for most people, which is why there aren't more postings about it. But, it seems like a useful thing to know. Specifically, when you are using a ListView with the android.R.layout.simple_list_item_1 view, how the heck do you get the text from the item clicked in the OnItemClickListener class?

It is pretty simple. The second parameter of the onItemClick is a View that contains just the item that was clicked. (Or tapped, whatever.) If you used Eclipse to automagically generate the callback method for you, the variable will be named "arg1", which is what we will assume for this example.

Inside of the onItemClick method, you can figure out the text using the following code :

String s = ((TextView)arg1.findViewById(android.R.id.text1)).getText().toString();


Pretty simple, huh? But, the code doesn't quite explain itself. So, I'll take a stab at it.

A ListView is exactly what the name says. It is a list of views. When you create the ArrayAdapter that you will use to display the data, you also tell the ArrayAdapter what view it should use. In our case, we chose "android.R.layout.simple_list_item_1" so we could use a simple array for the data. What we are doing is referencing a very simple layout that only displays a single line of text. I am sure if you Google around you can find the XML definition.

Inside the layout is a single definition. It is a TextView named "text1". Since it is a built-in layout, we will find the layout information under "android.R", instead of just "R" where our layout definitions show up.

So, by executing findViewById() on the view that was clicked, and passed in as "arg1", we can get the text that the user clicked on.

Pretty simple huh?

Monday, May 31, 2010

Basic graphics with Android

I have been working on a small game, and come across a real lack of useful information on how to develop a non-real time game. (i.e. A game that is entirely event driven.)

I started with the usual route, attempting to use existing UI objects to create the look that I wanted. However, I was unable to get the look and behavior that I wanted. So, I looked closer at the Lunar Lander sample in the Android SDK. The problem is that the Lunar Lander sample was perfect if you wanted to develop a real-time game. But, I didn't.

So, what to do? After much Googling, I found suggestions that what I really wanted to do was create a specialized view that would contain my game board. Doing this turned out to be easier than I thought it would. Using the Eclipse wizards, I created a new class that would be my game board view. The class was derived from the View class. The simplistic class would look something like this :


package com.example.simplegame;

public class BoardView extends View {
public BoardView(Context context) {
super(context);

}

public BoardView(Context context, AttributeSet attrib)
{
super(context, attrib);

}
}


Now, let me save you some time and pain. When I started this effort, I used "super(context)" inside the second constructor. Doing that will prevent you from using findViewById() to get a pointer to the view in your form.

Okay, so now we have a basic class set up. But, it doesn't actually do anything useful. What we really wanted to do is have a small blue box drawn on our view. So how do we do that?

Simple. We add an onDraw() method to our class. onDraw() takes a single parameter, with is of type Canvas. The canvas is what we will draw on, and what will ultimately be shown on the screen. As previously stated, we want to draw a simple blue square on the screen, so our onDraw method should look something like this :


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

Paint p = new Paint();
p.setAntiAlias(true);
p.setARGB(255, 0, 0, 255);

canvas.drawRect(0, 0, 10, 10, p);
}


Before we move on to talk about how to use our new View in a form, I want to point out a little bit more information on how onDraw() works. According to the Android documentation, you should assume that the canvas passed in to onDraw() is blank. (That is, you should never rely on anything you have previously drawn to still exist on the canvas.) Because of this, you will need to redraw your entire view each time onDraw() is called. In our sample, we draw the blue square each time onDraw() is called.

Now that we have a new view class set up, we need to figure out how to put it in a form, and use it. To make things easy, I first created a simple form with a generic View that fills the entire screen. It looks something like this :


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<View android:id="@+id/View01" android:layout_width="fill_parent" android:layout_height="fill_parent"></View>
</LinearLayout>


Now that we have our layout, we need to modify it to include our custom view. To do this, we change the <View> tags to be <com.example.simplegame.BoardView>. If you then take this class, and this form and wrap it in some boiler plate code that would display the form, you will get a form with a small blue square in the top left corner of the screen.

If you do some experimenting, you will find that you get the blue square on the screen even if you don't establish a pointer to the BoardView object in the form. While it may seem strange at first, if you think about it you will realize that all of the objects on a form exist as soon as the form is inflated. The findViewById() call is simply giving you a pointer to the object that already exists in memory. Therefore, the onDraw() method is called, even if your main program doesn't have a pointer to the view.

Monday, May 17, 2010

Stupid Android ListView tricks

I recently made some major changes to the Movie Track app that I put out. What started as an app that I did for my own use seems to have gained a few users. So, I decided to upgrade the look and feel a bit. In the process of this, I changed several of the screens from using a ListActivity to using a normal XML layout that contained a ListView. After mucking around with all of the settings to get things looking the way I wanted, I ran in to an interesting snag.

Everything displayed the way I wanted it to, but to "click" on an item that was in the ListView, you had to actually "click" on the text of the ListView, and not just on the row in the ListView. Perhaps I am a perfectionist at some level, but this annoyed me to no end.

Much Google later, I couldn't find the answer on how to do it. So, I started to mess with settings. A hair-brained idea or two later, I decided to try changing the ListView's layout-width from "wrap_content" to "fill_parent". Amazingly enough, that did exactly what I wanted!

Sure, this was a bit of a "duh" experience, and a newbie mistake. But, I figured I would publish it here in case some other developer runs across the same thing, and gets as frustrated as I was getting!

Saturday, May 15, 2010

What does it take to add the Mob to your existing Android applications?

When I get bored, I code. And when I get money hungry, I also code. I currently have one app out on the Android marketplace, that has been downloaded several times. In all honesty, I didn't expect the app to become the next big thing. I also wasn't sure it would be useful to people beyond myself. But, it seems that there are at least a few other people out there that believe my Movie Track app has some value.

Now, to be fair, I am all for free apps. I have done a lot of free open source development in my time, and have generally enjoyed it. But, I also like to get a little something extra out of my work when possible. So, I have decided I am going to try adding AdMob to my Movie Track program, and see if I can generate a few bucks along the way. (For those that hate ads, I'll also offer an ad-free version for $1.)

Signing up for AdMob is pretty easy. Just hop over to the AdMob web site, and create a new account. Once the account is created, you will get an e-mail with a link that needs to be clicked in order to verify the e-mail account is yours. But, once you have done that, it starts to get confusing in a hurry as to what to do next.

First, some terminology. When I first logged in with my shiny new account, there was a lot of talk about "Advertisers" and "Publishers". Since we are putting ads in to our programs, we are "Publishers". So, unless you want to advertise things in other people's programs, you should look for information that pertains to publishers.

Once you log in to the site, and click on the Marketplace link, the Campaigns tab shows information that is only relevant to advertisers. Or does it? Clicking on the "New Advertiser Training Videos" takes you to a page with videos about how to use AdMob to advertise new products and services. However, if you scroll down toward the bottom of the page, you will also find that there are videos for publishers down there as well.

A quick look at some of those links tell us that we need to create an Android App profile on the AdMob site in order to allow us to put ads in, and track the results. To do this, we need to hover over the "Sites & Apps" tab, and select the "Add Site/App" option from the drop-down menu. Next, click on the "Android App" icon, and scroll down to enter the rest of the information.

As you fill this form out, there is a section for the Android package URL. The help information that is available says it must be a mobile site, or the URL to your package in the market place. (The FAQ says the URL in the market place will be formatted as : market://details?id) However, digging around, it seems there should be a "=" between the "id" and the "". You can find more information at http://developer.android.com/guide/publishing/publishing.html.

This left me with a couple of questions that I will probably post more about later as I dig deeper. The first one is, "What is this URL used for?". And the second is, "What if I have not yet published the app? What do I put in there?".

Since this field isn't in bold, I am going to guess that it may not be important what goes in there. But, I like to be complete where possible, so I will be putting in the link to my app in the form of "market://details?id=info.geektaco.movietrack". For the category, I selected "Entertainment", and then I entered a description of the Movie Track program, and clicked the "Continue" button.

It would appear that I entered the correct information, as the next screen I was presented with is a link to download the "Install Code" to add to my program. I went ahead and grabbed the SDK, and a copy of the PDF version of the SDK documentation for later use, and then clicked "Go to Sites/Apps".

Now, we are getting somewhere. We should now see the dashboard for AdMob. In the dashboard is a link to update your payment information. That part seems pretty straight forward, so I won't go in to detail here.

And now, for the fun part! Let's add the ads to our program, and go make some money!!!

(Continued in later posts)

Monday, October 12, 2009

Of Androids and USB

In general, I am a fan of the Android phone. (In case you couldn't tell.) But, I just spend 2 hours fighting with a myTouch3g trying to get it set up so I could use it as a debug target.

Now, in general, I suspect most people won't have this type of problem, since I am fairly sure it is the result of previously having my system configured to debug on my ADP1. But, if you happen to run in to this problem, here are some things that might help.

1. Download USBDeview from http://www.nirsoft.net/utils/usb_devices_view.html .
2. Unplug your phone from the computer.
3. Run USBDeview, find everything listed as an Android device, and uninstall it.
4. In the Android SDK, run "adb kill-server"
5. Make sure your phone is configured for development, and plug it back in.

This should cause Windows to find it again as if it had never been plugged in. It should give you the option of installing the ADB driver from the latest version of the SDK.

Once I had done this, the phone didn't immediately show up. To fix that, I left the phone connected, and toggled the development checkbox. After that, it showed up and I could use it.

Friday, September 18, 2009

Calling my app from a MIME type in Android

It seems like this is something that people might want to be able to easily do. But, surprisingly enough when I google this, I get a lot of posts with people saying, "How do I have my app get called when the browser reads a certain MIME type?"

Here is your answer.

It is really fairly simple. In fact, most of what you need to know is included in the Notepad sample app on the Android SDK site! But, there are some minor details missing.


If you follow what is available on the Android SDK site, you would add a new <intent-filter> to your manifest, that would look something like this :

<intent-filter>
<action:name="android.intent.action.VIEW" />
<category:name="android.intent.category.DEFAULT" />
<data:mimetype="testtype/testtype" />
</intent-filter>

Then, you just need to have a web server that can serve up our "testtype/testtype" MIME type to make it work, right?

Well, to test that theory, I hacked up this cute little piece of PHP, and stuck it on a local web server :

<?php

header("Content-type: testtype/testtype");

echo("Testing!");
?>

Note : Your web server will have to be able to run PHP for this to work! You are on your own to figure that out.


I named this PHP script test.php, and dropped it in the root share for my web server. Then, I pulled up the Android browser, and went to the web page : <myserver>/test.php .

What happened, was underwhelming to say the least. The phone downloaded a file called test.php. If I then tapped on the file name, it would start my application. So, I knew I was close.


After more digging around, I discovered the magical incantation. The MIME type line needs to know that we want to look specifically for things downloaded via http. So, I made a small change to my manifest file. The MIME type line now looks like this :

<data:mimetype="testtype/testtype" scheme="http">



Then, I ran the same test again, and my application was now loaded when I attempted to pull up the .php script that issued the testtype MIME type.

Sunday, August 30, 2009

Orientation of an Android screen...

This issue has been vexing me for a few weeks now. I have found several different e-mail threads, and blog posts that talk about this exact issue, but none of them really seemed to have the answer I was looking for. So, hopefully this helps someone else.

I have been working on a small Android app that, in a nutshell, goes out to the internet, grabs an XML file, parses it, and sticks it in the local database on the phone. Currently, I am writing ~937 records in to the database after reading this XML file. Needless to say, this takes some time. As a result, I throw the work in to a thread, and then put a dialog on the screen indicating that I am working on it.

The code I am using to spin up the thread is here (modified to make it short and sweet) :

new Thread(new Runnable(){
public void run(){
try {
dp = new XMLDataParser(getURLPath());

dp.parse();
System.out.println("Update db.");
updateDatabase();

} catch (Exception e) {
System.out.println("Exception : " + e.getMessage());

// Display an error.
}
}
}).start();


Hopefully, this code is pretty straight forward. It spins up a new thread, initializes the XMLDataParser class, while passing in the URL that I am getting the data from. If you were to add this code to a project, you would find that a thread was spun off, and was doing its thing. (Though, you wouldn't really be able to tell that it was actually doing anything. We'll get to that in a second.)

You could just throw up a progress dialog with a spinner before you start the thread, and then attempt to close the dialog when the thread is done. In my adventures, I attempted this, and actually managed to make it work! There are even supported ways to make the dialog go away when the thread is done. However if you do this, you are going to end up in one of two "difficult" situations :

  1. When the screen orientation changes, the thread will finish running, but your progress dialog will go away.
  2. When the screen orientation changes, the thread will try to access something in the Activity that was destroyed by the OS, and you will crash.
While option #1 would work, it probably isn't really what you would want. What you REALLY want is for the progress dialog to continue to display and update.

So, how the heck do we make that happen?

Well, we know that when the screen orientation changes the current Views and Activities are destroyed, and new ones are created. A little bit of messing around will also show us that the thread we started continues to run even after the Views and Activities are destroyed. So, "all" we need to do is figure out how to make it so that the thread can communicate with the newly created Views and Activities.

So, lets break this down in to two parts.

  1. Making something survive the screen rotation.
  2. Having that something update our newly created Activity and Views.

How to survive a rotation

If you have done much development, you may think the magic way to deal with this is to stick something in your bundle that will be passed to the newly created Activity. While it is possible that you might be able to make that work, we want to have an entire object survive the restart. To do that, we need to add onRetainNonConfigurationInstance() to our activity class. When the screen is rotated, this method is called, and allows us to return a class (derived from Object) that can be retrieved from the new Activity. Generally, you want this method to do as little as possible, since it will be called during the screen rotation. Here is a bit of code from my application showing the call :

 @Override
public Object onRetainNonConfigurationInstance() {
return myGUIUpdateHandler;
}


All we are doing is returning the object that we want to have access to when our current activity is destroyed, and the new one created. Once the new Activity is created, we can get the pointer to the object back by calling getLastNonConfigurationInstance(). Once we have done that, we will again have access to the object that we were attempting to save.

Which brings us to :

Having something update our newly created Activity and Views

Now, for the fun part. We can pass an object between the two Activities, so now we can just pass the ProgressDialog object we were using, call .show(), and everything just works, right?

No, not really. The ProgressDialog will be bound to our old View that was part of the Activity that was destroyed. So, attempting to work with it would end up crashing our application.

Instead, we want to create a handler that will allow us to pass messages between threads. In the Handler class, we want to override the handleMessage() method so that when a message comes in, we can do something with it. Here, again, is a sample from my code :


public class ThreadEvents extends Handler {

// Set up a random unique ID for message handler
public static final int UPDATEDATA = 12346;

// @Override
public void handleMessage(Message msg) {
switch (msg.what) {

case UPDATEDATA:
System.out.println("Thread sent us an update data event.");
break;

default:
break;
}

super.handleMessage(msg);
}
}

In this handler class, when the UPDATEDATA case is called, we want to update our UI thread. This could be something such as updating the status bar in our program.

The last piece of the puzzle is how to actually send events to this handle so that we can process them. The first thing that needs to be done before a thread can send a message through a handle is the handle class needs to be created prior to the thread being started. By doing that, the child thread will also have a pointer to the handler class. Then, any time the thread wants to send something to the parent thread, it only has to do something like this :

message = new Message();
message.what = ThreadEvents.UPDATEDATA;
myGUIUpdateHandler.sendMessage(message);


The final trick to making the ProgressDialog survive a screen rotation is to create a new instance of the dialog when an activity is started, and there is an object available from getLastNonConfigurationInstance(). Then, to update that new ProgressDialog instance as events come in from the child thread.


Final thoughts :

Obviously, I have not provided enough sample code here to do a full implementation. That never was the goal, so please don't post a follow-up asking for it. It is my experience that people that have to work a little to obtain knowledge keep it longer. My goal was to point you in the right direction so you can put the remaining pieces in place.

I also suspect that it might be "better" to use an AsyncTask to achieve the same ultimate result. However, this is the first method I discovered to achieve my goal. In the future, I intend to try doing the same thing with an AsyncTask, and will post another entry when I get around to that. In the mean time, this method seems to work well as I have been able to rotate the screen back and forth several times during a long operation without crashing my application.