Tuesday, December 20, 2011

Sharing with Gmail

I read through the Android Training docs recently and came across a section which I thought I could use to improve sharing on Rage Faces (by getting rid of the need for an SD card-based share system):

Write the data to a file in your own application directory using openFileOutput() with mode MODE_WORLD_READABLE after which getFileStreamPath() can be used to return a File. As with the previous option, Uri.fromFile() will create a file:// style Uri for your share intent.
However, this setup fails to work with the Gmail.  When you try to share with Gmail in this manner, an error message pops up in the logs:

file:// attachment paths must point to file:///mnt/sdcard. Ignoring attachment file:///data/data/com.idunnolol.ragefaces/files/share.png
I'm not sure why Gmail arbitrarily rejects attachments not on the SD card, but it pretty much cuts you off from sharing files with MODE_WORLD_READABLE.  Unfortunately sharing with Gmail feels like an essential part of the app so I'm going to have to stick with SD card sharing.

Monday, October 31, 2011

TextWatchers and onRestoreInstanceState()

There's a small timing issue I'd like to mention because it's bitten me a few times now.

When you want to observe changes to an EditText, you add a TextWatcher to it (via TextView. addTextChangedListener()). There are a lot of uses for TextWatchers, like implementing your own autocomplete or filters based on a dynamic EditText. The only thing you have to be careful of is whether the text was changed by the user or the code - the listener fires either way.

What's worth knowing about an EditText is that it will save and restore the state of the text inside of it within your Activity. That means that when you rotate the screen, the EditText will restore the text that was inside of it. And most importantly, the automated restoring of the text on rotation causes the TextWatcher's methods to fire. Like I said - the TextWatcher doesn't discern between whether the user changed the EditText or the system did.

The solution is simple - just don't add the TextWatcher until after the EditText's content has been restored. It restores itself in Activity.onRestoreInstanceState(), which makes Activity.onResume() the preferred time to add TextWatchers to any EditTexts.

Monday, October 10, 2011

Android Drawable XML Talk

I recently gave a talk on the basics of Android drawable resources (via XML).

There's a recording I made of the talk - not super, but functional. I had to split it into two parts because of time restrictions on YouTube. Here's part one and part two.

I also have a few links that may be useful (whether you watch the talk or not):

- The slides for the presentation

- Official Android drawable resource documentation

- My drawable XML documentation

- The github project with my samples

Monday, September 26, 2011

Logc.at

A few developers and I have formed a new Android development blog. We snagged what I think is an awesome and nerdy url: logc.at.

I will still be writing here - this blog is intended for one-off coding issues that I want to document. The new blog will be a place for me to write about Android development in a more general manner. Instead of assisting with individual hurdles, I hope my articles there will help one become a better Android developer (or at least pick up a few tips).

My first post is about working with JSON on Android. Check it out!

Tuesday, August 2, 2011

New to Android: More Style Restrictions

There have been two changes the Android platform build tools which have caught me off guard. You may run into these problems when you next update your aapt. Both of them cause compilation errors, so they're not easy to ignore, but it's not immediately obvious how to fix either.

Change #1: Implicit parenting now requires a parent

Suppose I have this style defined in styles.xml:

<style name="Widget.MyWidget.Small"> ... </style>

It was named that way after how Android sets up their widget styles (for example, "Widget.RatingBar.Small"). One thing to know is the implicit aspects to using dots in the naming is that you automatically parent all the styles before the dot. So "Widget" is the parent of "Widget.MyWidget", and "Widget.MyWidget" is the parent of "Widget.MyWidget.Small".

In the new version of aapt, you'll see this error:

Error retrieving parent for item: No resource found that matches the given name '@Style/Widget.MyWidget'.

This happens now because aapt is stricter with regards to style parenting. Before, if there was no parent, it would just ignore this oversight; now it requires a parent.

There are two possible solutions:

1. Rename the style so that it doesn't use implicit parenting with dots.

2. Create the parent styles. They could either be empty, or you could actually use them for something.

I think #1 makes more sense, unless you actually have a use for the parent styles.

Change #2: Some android styles are now enforced as private

Before, you used to be able to get away with parenting some non-public Android styles:

<style name="MyRatingBar" parent="@android:style/Widget.RatingBar.Small"> ... </style>

However, these styles have always been intended to be private, and the latest aapt will not allow you to build with a private parent defined. A Google employee explained the reason why this is no longer allowed:

For the framework, only public resources are guaranteed to only have the same integer, build after build. The integer of private resources integer will change from build to build. This means that your custom style is referencing a parent that *will not* be valid once installed on a device. It'll referenced either another resources or none at all, and it won't do what you want.


That seems reasonable to me. The solution is to just import the entire style into your styles.xml; all the styles are open source anyways.

Thursday, June 30, 2011

Honeycomb App Widget and Backwards Compatibility

Honeycomb added some neat functionality to Android widgets. However, there's a problem that arises from adding a widget using collections to your app - what if your APK is supposed to work on all versions of Android? How do you prevent the Honeycomb-only widget from appearing in previous versions?

Normally when you want to do something different between versions of Android, you either use resource qualifiers (for XML) or check Build (for code). But in this case, the widget itself is defined in AndroidManifest.xml, which can only be at the root of your project.

There's a way of removing the app widget from the listing in previous versions: use resource qualifiers on the AppWidgetProviderInfo resource.

When you define an app widget in AndroidManifest.xml, you must also define a <meta-data> element which points towards the AppWidgetProviderInfo (that is usually contained inside of a file in /res/xml/). Simply move that provider file to a directory that only Honeycomb can see - /res/xml-v11/. Earlier versions of Android will try to load the widget only to find no AppWidgetProviderInfo and will thus ignore the widget.

UDPATE (Jan 26th, 2012): I've now written about an improved solution to this problem.

Tuesday, May 31, 2011

Passwords: Longer, Not More Complex

I was going through a mandatory security training course for my company when I came across a slide about passwords. Its advice was that the password should be at least 7 characters long and include a good mixture of lower/upper case characters, plus numbers and special symbols. Sony is using the same tactics. I had to change my PSN password and it forced me to use at least one number and one letter in the password.

This is totally bunk advice. What you want is a long password, and it doesn't have to be complex.

Suppose there's a server with your credentials that has the bare essentials of good security (hash + salt). If someone compromises the server, they will have to crack your password by checking every single password against the hash in the database. Therefore, the more possible passwords your scheme contains, the longer it takes to crack.

You can calculate an approximation on number of attempted passwords a cracker will have to attempt with this simple formula: (size of character set)^(password length). Given a rudimentary understanding of exponents, it's pretty evident that increasing the password length will have a lot more impact than expanding the character set size.

Let's look at some numbers. On my keyboard, there are 95 characters I could insert into a password. Let's compare a length eight password using that character set to a slightly longer password using only lower case letters (a 26 size character set):

95^8 ~= 6 quadrillion (or ~10^15)
26^12 ~= 90 quadrillion (or ~10^16)
26^15 ~= 1 sextillion (or ~10^21)

With four extra characters, my simpler password is already harder to crack than the complex eight character password.

The best part about using a limited character set is that you can create a pass phrases instead of pass words. It's much easier to remember "please do not break into my account" than "x82@.Zij", and it's also far more resilient to cracking, too.

Unfortunately, not all sites allow for long passwords. In those cases, it's better to use a more complex character set. But when given the opportunity, a long and simple password is vastly superior to a short but complicated one.

Addendum: A friend pointed out that pass phrases are more vulnerable to dictionary attacks. This is true, but only if your pass phrase contains very few words. The approximate effort for pass phrases is (number of possible words)^(number of words). Given how many possible words there are, it only takes a phrase of a few words to be as secure as 15 simple characters of nonsense. So while "supercalifragilisticexpialidocious" is a crappy password despite how long it is, my example from above ("please do not break into my account") is still very secure.

Tuesday, May 24, 2011

Using Subquery Columns on Android with SQLite

Take a look at this (contrived) query, as might be passed to SQLiteDatabase.rawQuery():

SELECT *
FROM t1 A, (SELECT T2.id FROM t2 T2) B
WHERE A.id = B.id

On Android 2.1 and below, this query will cause your app to crash. You'll get an error like this:

Caused by: android.database.sqlite.SQLiteException: no such column: B.id: , while compiling: SELECT * FROM t1 A, (SELECT T2.id FROM t2 T2) B WHERE A.id = B.id
at android.database.sqlite.SQLiteProgram.native_compile(Native Method)
at android.database.sqlite.SQLiteProgram.compile(SQLiteProgram.java:110)
at android.database.sqlite.SQLiteProgram.<init>(SQLiteProgram.java:59)
at android.database.sqlite.SQLiteQuery.<init>(SQLiteQuery.java:49)
at android.database.sqlite.SQLiteDirectCursorDriver.query(SQLiteDirectCursorDriver.java:49)
at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1220)
at android.database.sqlite.SQLiteDatabase.rawQuery(SQLiteDatabase.java:1193)
at com.mycompany.myapp.MyActivity.onCreate(QuickTestActivity.java:22)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2459)
... 11 more

It turns out that SQLite throws this error if two conditions are met:
  1. You use a subquery (in this case, the select subquery from table t2).

  2. Your subquery has a table alias (in this case, referencing table t2 as "T2").

The solution is simple: alias the selected field names. This query will work:

SELECT *
FROM t1 A, (SELECT T2.id AS id FROM t2 T2) B
WHERE A.id = B.id

I'm hardly an expert on SQL or SQLite, so I don't know whether this is a bug or just me running into undefined behavior. I ran into this problem when doing JOINs on multiple tables, so as contrived as my example is, it can happen. Regardless, it is easy to work around.

Wednesday, April 27, 2011

Detecting the Keystore Signature in Code

Here's a problem I'm sure many of us have faced: you've got code that runs one way when you're developing and another way when you're releasing. A few examples I've run into:

- Logging during development, but disabled for release.
- Targeting development vs. production servers.
- MapView requires an API key tied to the signing key.

Before, I just setup static parameters in code that I'd have to remember to change before making a release build. It's a pain and error prone.

I've just determined a much better (and more automatic) way - detecting the signing keystore in code. Through use of the PackageManager you can actually determine the Signature of the app that's currently running.

First, you need to get the Signature of your release keystore. Put this code into your application temporarily, then sign it with your release keystore and run it:

PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
Log.i("test", pi.signatures[0].toCharsString());

Take the output from logcat and put it into this method:

private static final Signature SIG_RELEASE = new Signature("<YOUR SIGNATURE HERE>");

public static boolean isRelease(Context context) {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
for (Signature sig : pi.signatures) {
if (sig.equals(SIG_RELEASE)) {
return true;
}
}
}
catch (Exception e) {
Log.w("Exception thrown when detecting if app is signed by a release keystore.", e);

// Return true if we can't figure it out, just to play it safe
return true;
}

return false;
}

Now you can always call isRelease() and check whether your app was signed by your release keystore. No more having to edit files to create a release version of your app - just sign it with the correct keystore and you're ready to go!

Wednesday, April 13, 2011

HTML in TextViews

There's a trick I've been increasingly using as I've learned how powerful it is: HTML in TextViews.

I used to think that TextViews were pretty plain; they contained a CharSequence, and that was it. But it can actually contain a Spanned (or its mutable cousin, Spannable) buffer type as well. This means that there are a host of HTML tags that you can use to modify your TextView, which allows for different typefaces/functionality - all in the same TextView. Check out what you can do with just one string resource in a TextView:



Here's the string resource for above:

<string name="html">This <b>is</b> <i>all</i> <sub>in</sub> <u><sup>one</sup></u> <a href="http://google.com">TextView</a>.</string>

Android has tips on the basics of Html in TextViews, but it doesn't nearly cover some of the use cases. It also doesn't have a reference on which tags are supported (scroll to bottom for a reference I created).

Here are a few tips:

Links in TextViews

The simplest way to add links to TextView is to use the android:autoLink attribute. However, this only allows you to link visible URIs in the text and can sometimes lead to undesired situations (I had an app that was detecting the copyright dates "2009-2011" as a phone number). By using the <a> tag, you can create links with any text that leads to any URI:

<string name="my_site"><a href="http://idunnolol.com">My Website</a></string>

There's only one catch: when you try to use this in a TextView, it won't be clickable unless you set the movement method in code:

TextView myTextView = (TextView) findViewById(R.id.my_textview);
myTextView.setMovementMethod(LinkMovementMethod.getInstance());

Dynamic Html

All of the above examples have been with static text - that is, when the TextView's android:text attribute is set in XML from a string resource. What if you want to set a TextView's string resource in code? This is especially important when you use string formatting (which I'm a big fan of).

Android discusses this already, but for posterity I'll repeat the process here:

1. HTML-escape the string resource:

<string name="loud">Loud text here: &lt;b>%s&lt;/b></string>

2. Use Html.fromHtml() to dynamically create a Spanned that a TextView can be styled with:

Spanned spanned = Html.fromHtml(context.getString(R.string.loud, "this is loud"));
myTextView.setText(spanned);

Handling Custom Tags

So far so good, but I ran into a problem the other day: while the <strike> tag works if it's linked as a static string, it doesn't work when using Html.fromHtml(). I looked through the source code and discovered that Html.fromHtml() handles a different set of tags from static resources. Luckily, there's a way to handle tags that Html.fromHtml() doesn't: the TagHandler interface.

It's a little complex, but here's one possible solution for how to handle the <strike> tag. (My own solution differed a bit, but the essentials are the same.)

Tags Supported in String Resources

Tags in static string resources are parsed by android.content.res.StringBlock, which is a hidden class. I've looked through the class and determined which tags are supported:
  • <a> (supports attributes "href")
  • <annotation>
  • <b>
  • <big>
  • <font> (supports attributes "height", "size", "fgcolor" and "bicolor", as integers)
  • <i>
  • <li>
  • <marquee>
  • <small>
  • <strike>
  • <sub>
  • <sup>
  • <tt>
  • <u>

Tags Supported by Html.fromHtml()

For some reason, Html.fromHtml() handles a different set of of tags than static text supports. Here's a list of the tags (gleaned from Html.java's source code):
  • <a> (supports attribute "href")
  • <b>
  • <big>
  • <blockquote>
  • <br>
  • <cite>
  • <dfn>
  • <div>
  • <em>
  • <font> (supports attributes "color" and "face")
  • <i>
  • <img> (supports attribute "src". Note: you have to include an ImageGetter to handle retrieving a Drawable for this tag)
  • <p>
  • <small>
  • <strong>
  • <sub>
  • <sup>
  • <tt>
  • <u>

The font "color" attribute supports some color names (along with the normal integer-based color scheme):
  • aqua
  • black
  • blue
  • fuchsia
  • green
  • grey
  • lime
  • maroon
  • navy
  • olive
  • purple
  • red
  • silver
  • teal
  • white
  • yellow

Wednesday, April 6, 2011

Image Sharing on Android

One of the sillier projects I've worked on in my spare time is Rage Faces for Android. It has almost no value to anyone, which is why it's free (in every sense of the word). However, it's been quite useful to me, as it's taught me a few things about sharing images on Android. Overall, I think this is a good reason for Android developers (or any developer, really) to have fun side projects to work on - there's almost no app so trivial that you can't learn something from doing it.

Here's three big ones I learned from working on Rage Faces.

Sharing Images

The first thing it taught me is that you need space on the SD card in order to share images. This makes sense, as sending images (typically) uses the ACTION_SEND intent along with a URI to the image. You can't link a URI to a resource inside your application package, so you have to put it somewhere else (I chose the SD card).

It's also possible to put the media into a ContentProvider using MediaStore.Images.Media.insertImage(). The downside is that the image is then inserted into the Gallery, which I didn't want just for sharing images (the whole point of the app is to avoid having to fill up your gallery with images you don't want, instead keeping them in one organized place). You can avoid putting images in the Gallery by putting them into a folder on the SD card and creating a file ".nomedia".

Picture Frame and ACTION_GET_CONTENT

ACTION_SEND can be used to send the images to other applications. The opposite of that is having the intent filter ACTION_GET_CONTENT enabled; that way, other applications can request data from your app. They both work essentially the same way, by passing a URI to the image in an Intent. In the case of ACTION_SEND, you pass the URI in EXTRA_STREAM. For ACTION_GET_CONTENT, it's actually provided in the return Intent's data.

However, there is one exception: the standard Picture Frame widget doesn't play by these rules. It uses ACTION_GET_CONTENT to allow anyone to hook into their system, but they won't read a stream URI. If you try it, the picture frame widget crashes. The relevant part of the crash log is here:

 Caused by: java.lang.NullPointerException
at com.cooliris.media.PhotoAppWidgetProvider$PhotoDatabaseHelper.setPhoto(PhotoAppWidgetProvider.java:121)
at com.cooliris.media.PhotoAppWidgetConfigure.onActivityResult(PhotoAppWidgetConfigure.java:92)
at android.app.Activity.dispatchActivityResult(Activity.java:3908)
at android.app.ActivityThread.deliverResults(ActivityThread.java:2528)
... 11 more

What they expect is for you to pass an extra called "data", as a Bitmap. This means you need to pass back both the URI and the Bitmap as a Parcelable in the return Intent:

Intent return = new Intent();
return.setData(myImageUri);
return.putExtra("data", myBitmap);
setResult(RESULT_OK, return);
finish();

This neglect in following the rules of the road is, I assume, due to Cool Iris both writing the standard Picture Frame widget and the standard Gallery application. As a result, the two play together nicely, but you'll have to follow their rules to be part of the gang.

Messaging on HTC

There's another player who doesn't quite follow the rules: HTC Sense's messaging application. Instead of filtering on the ACTION_SEND action like everyone else, HTC Sense's messaging application listens to "android.intent.action.SEND_MSG". This means two things for you as a developer: You have to test for the existence of an app that accepts the action "android.intent.action.SEND_MSG", and then give the user an explicit option to send to HTC Sense's messaging application, in addition to the normal picker.

You can test whether the HTC Sense messaging app is on the phone by querying for the intent filter:

Intent dummy = new Intent("android.intent.action.SEND_MSG");
dummy.putExtra(Intent.EXTRA_STREAM, myImageUri);
dummy.setType("image/png");
PackageManager packageManager = getPackageManager();
List list = packageManager.queryIntentActivities(dummy, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() > 0) { // This means the HTC Sense messaging app is on the phone }

If you do have HTC Sense, you'll have to give your users the option to send messages to it. This means another layer of dialogs, which is a pain to users. At this point, I think the best solution is to stop using the Android chooser altogether. As useful as it is, the Android ecosystem is too abused to keep using it. In an ideal world, you'd only need one Intent to pass your data around, but that's just not been the case. If you roll your own chooser (which is not particularly difficult), you can include multiple possible Intents to pass out.

It's also worth noting that HTC Sense's messaging system doesn't implement ACTION_GET_CONTENT, so you can't have users insert your app's content into a conversation already in progress. This is not an insignificant oversight - people have told me they switched text messaging apps because of this lacking feature!

Wednesday, March 9, 2011

Tiled bitmaps in layer drawables

This is a bizarre bug that a coworker came across.

There's a cool feature of XML drawables that allows you to tile bitmaps, using android:tileMode. There is also the ability to layer drawables in XML, via the layer list. Naturally, there are some circumstances where you might want to layer bitmaps that are tiled.

It turns out there is a bug in Android with trying to layer a tiled bitmap. Every once in a while, a tiled bitmap in a layer list won't tile. It's not every time (nor even that common) but anything less than a 100% success rate for graphics is problematic.

The solution is to not define the bitmap drawable tileMode in XML, instead defining it dynamically in code. Inflate the drawable in code somehow (either directly, or by inflating a layout that uses the drawable). Grab the drawable, cast it to a LayerDrawable, then retrieve the BitmapDrawable from the layer list (there are a few methods for it, like LayerDrawable.getDrawable()). Then, set the tileMode in code via BitmapDrawable.setTileModeXY().

I've looked through the code and haven't figured out exactly why this bug occurs. I would appreciate any insight; in the meantime, the fix works well enough.

Thursday, March 3, 2011

Custom styles in Android libraries

About nine months ago, Android added library support. It allows you to define multiple Android projects where (at build time) it combines them all into one. These libraries have proven very useful in my work because there's a lot of common code between applications. In some cases, I'm even reusing entire Activities.

But this brings up a problem - how do you restyle an Activity in a library? Say you've got a custom date picker in your library which you want to customize to each application's look and feel. You could subclass the library's Activity and (in code) change the style, but then you miss out on all the advantages of resource qualifiers. You could try subclassing but using your own XML, but then you're tightly coupling your code to the library and will likely end up duplicating code.

The answer is to use attributes and styles/themes. Chances are you've already worked with styles and themes before (if you haven't, you should read up on them), but attributes aren't commonly needed when you're working on a one-off application. Attributes are a tad under-documented, but with attributes, you can define references which are filled in later by styles.

Attributes are one level of abstraction beyond the normal use of XML references in Android. Let's take the simple example of a background: the most direct way to set a background is directly in code (android:background="#FF0000"). Instead of putting the background in the code, you can create a color resource (<color name="red">#FF0000</color>) then reference that (android:background="@color/red"). Going one level of abstraction higher is to create an attribute(<attr name="red" format="color" />), which doesn't directly define the color but can be set by a style or theme (android:background="?style/red").

A high-level layout of the solution looks like this:

1. Define attributes for what XML values you want to customize.

2. Link to those attributes in the library's XML.

3. In the application using the library, create a style which defines values for those attributes.

4. Apply that style as a theme for the application.

Here's a specific example. Suppose we want the background of an Activity to be black in one application and white in another.

1. Create a file in /values/ called "attrs.xml" and add an attribute for the background color:

<resources>
<attr name="myBackground" format="reference|color" />
</resources>

The "format" field defines what sort of values can be entered for this attribute. There's no official documentation, but people have written about possible values.

2. Now let's create a simple layout in /layout/:

<LinearLayout android:layout_height="fill_parent" android:layout_width="fill_parent"
android:background="?attr/myBackground" />

Note how you use the "?" instead of "@" when referencing an attribute.

3. Now switch away from the library to the application using it. Inside, we create a "styles.xml" in /values/, and create a style to fill in the values for the attributes we defined in the library:

<resources>
<style name="LibraryTheme">
<item name="myBackground">@android:color/black</item>
</style>
<resources>

4. Now in AndroidManifest.xml, we set the overall application theme:

<application android:icon="@drawable/icon" android:label="My App"
android:theme="@style/LibraryTheme">

If you wanted to set another theme for the application already, use style parenting to lump both styles together.

Ta-da! Now you've written a layout in a library that can be customized in each application that uses it. You can repeat steps #3 and #4 for any number of applications, setting the background to a different color for each.

There is only one caveat about this method, which is that you need to define all the attributes in each application that uses the library. If you miss an attribute, your app will blow up in your face when the app tries to use it. What's worse, the stack trace when this occurs for missing attributes is complete gibberish and unrelated to the actual problem, making it difficult to debug.

This technique can be used for any attributes from a library. Use your imagination; at this point I've just been using it to style layouts, but you can apply attributes to just anything you can define in XML.

Friday, February 25, 2011

Sending Emails on Android, Take 2

A while back, I wrote a post about how to send specifically send only emails on Android. Unfortunately, the solution I posted does not work 100% of the time.

My previous solution was a hack and (as hacks are wont to do) it came back to bite my in the ass. It turns out that some email programs have buggy implementations of the mailto protocol. This led us to some users not getting the body content when using the mailto trick.

The correct solution is to use the normal ACTION_SEND intent, but setting the type to "message/rfc822", so only programs that implement emails will accept the intent.

Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("message/rfc822");
intent.putExtra(Intent.EXTRA_EMAIL, "foo@bar.com");
intent.putExtra(Intent.EXTRA_SUBJECT, "A Subject");
intent.putExtra(Intent.EXTRA_TEXT, "Here's my message.");
startActivity(intent);


Not only does this work more often than my hack, but it's far more elegant, too.

One other choice I've made recently is to just use startActivity() on the original intent instead of creating a chooser first. That way, the user can opt for a default email program to use instead of being asked each time which app should handle the intent.

Wednesday, February 23, 2011

Drop shadow text

Here's a little tip for creating a decent drop shadow effect in a TextView.

The issue I ran into initially when trying the shadow effects on TextView is that I could rarely get it to look right - instead of a clean, single pixel shadow, it'd always come out fuzzy. It turns out that the secret is that shadowRadius can be a float, and that you should make that float very small. Here's an example style you can apply to TextViews:

<style name="GreyEtchedText">
<item name="android:textColor">#2F3541</item>
<item name="android:shadowColor">#88FFFFFF</item>
<item name="android:shadowRadius">.1</item>
<item name="android:shadowDx">0</item>
<item name="android:shadowDy">1</item>
</style>

Of course, you can use whatever colors you want for the foreground/shadow.