Count words in Android string resource XML files

I put together a simple Python script that counts the words in one or more Android string resource XML file, and thought other people might find use for it as well.

The script is written for Python 2.7, but may work in other versions.

You run the script from the commandline and pass as arguments the string resource file(s) you want to count words in, for example

android-string-wc.py MyApp/res/values/strings.xml MyApp/res/values-es/strings.xml

And here is the source code for the script:

from xml.dom.minidom import parse
import sys
import os
import codecs
import re

def count( files ):
    for f in files:
        if not os.path.exists( f ):
            print "File %s does not exist" % f
            sys.exit( -1 )
            
    total_count = {}
    for filename in files:
        count = 0
        with open( filename, 'r' ) as f:
            xml = parse( f )
            for item in xml.getElementsByTagName( "string" ):
                for cn in item.childNodes:
                    if cn.nodeType == cn.TEXT_NODE:
                        count += count_words( cn.data )
            for plural in xml.getElementsByTagName( "plurals" ):
                for item in plural.childNodes:
                    for cn in item.childNodes:
                        if cn.nodeType == cn.TEXT_NODE:
                            count += count_words( cn.data )
        total_count[ filename ] = count
        
    print total_count
    
def count_words( str ):
    words = re.findall(ur'\w+', str)
    return len( words )
    
if __name__ == "__main__":
    if len( sys.argv ) <= 1:
        print "Enter string resource file(s) as arguments to this script."
        sys.exit( -1 )
        
    count( sys.argv[1:] )

PDF reader from URL hybrid approach

As many StackOverflow questions and answers hint at, there is no built-in support for reading PDF files on Android, which is kind of a shame, especially since iOS has native support for the format. The vision must have been that user’s will select their own favorite PDF reader app, and then read all other apps’ PDF files through that reader app.

However, as an app developer, you cannot make assumptions on which PDF reader apps exist on a user’s device – you cannot even assume that one is installed! Several StackOverflow posts suggest redirecting the user to Google Drive’s (formerly Google Docs) online HTML PDF viewer. This solution is kind of bulky though, and doesn’t work as well as using a native app (Like Adobe Reader or other PDF reader apps).

This inspired me to come up with a hybrid solution for reading that checks if a PDF reader app is installed and does the follwing:

  • If a reader is installed, download the PDF file to the device and start a PDF reader app
  • If no reader is installed, ask the user if he wants to view the PDF file online through Google Drive

NOTE! This solution uses the Android DownloadManager class, which was introduced in API9 (Android 2.3 or Gingerbread). This means that it doesn’t work on Android 2.2 or earlier.

The main method for the code is PDFTools.showPDFUrl( Context context, String pdfUrl );

The code can be found on GitHub or just right here:

public class PDFTools {
	private static final String GOOGLE_DRIVE_PDF_READER_PREFIX = "http://drive.google.com/viewer?url=";
	private static final String PDF_MIME_TYPE = "application/pdf";
	private static final String HTML_MIME_TYPE = "text/html";
	
	/**
	 * If a PDF reader is installed, download the PDF file and open it in a reader. 
	 * Otherwise ask the user if he/she wants to view it in the Google Drive online PDF reader.<br />
	 * <br />
	 * <b>BEWARE:</b> This method
	 * @param context
	 * @param pdfUrl
	 * @return
	 */
	public static void showPDFUrl( final Context context, final String pdfUrl ) {
		if ( isPDFSupported( context ) ) {
			downloadAndOpenPDF(context, pdfUrl);
		} else {
			askToOpenPDFThroughGoogleDrive( context, pdfUrl );
		}
	}

	/**
	 * Downloads a PDF with the Android DownloadManager and opens it with an installed PDF reader app.
	 * @param context
	 * @param pdfUrl
	 */
	@TargetApi(Build.VERSION_CODES.GINGERBREAD)
	public static void downloadAndOpenPDF(final Context context, final String pdfUrl) {
		// Get filename
		final String filename = pdfUrl.substring( pdfUrl.lastIndexOf( "/" ) + 1 );
		// The place where the downloaded PDF file will be put
		final File tempFile = new File( context.getExternalFilesDir( Environment.DIRECTORY_DOWNLOADS ), filename );
		if ( tempFile.exists() ) {
			// If we have downloaded the file before, just go ahead and show it.
			openPDF( context, Uri.fromFile( tempFile ) );
			return;
		}

		// Show progress dialog while downloading
		final ProgressDialog progress = ProgressDialog.show( context, context.getString( R.string.pdf_show_local_progress_title ), context.getString( R.string.pdf_show_local_progress_content ), true );
		
		// Create the download request
		DownloadManager.Request r = new DownloadManager.Request( Uri.parse( pdfUrl ) );
		r.setDestinationInExternalFilesDir( context, Environment.DIRECTORY_DOWNLOADS, filename );
		final DownloadManager dm = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
		BroadcastReceiver onComplete = new BroadcastReceiver() {
			@Override
			public void onReceive(Context context, Intent intent) {
				if ( !progress.isShowing() ) {
					return;
				}
				context.unregisterReceiver( this );
				
				progress.dismiss();
				long downloadId = intent.getLongExtra( DownloadManager.EXTRA_DOWNLOAD_ID, -1 );
				Cursor c = dm.query( new DownloadManager.Query().setFilterById( downloadId ) );
				
				if ( c.moveToFirst() ) {
					int status = c.getInt( c.getColumnIndex( DownloadManager.COLUMN_STATUS ) );
					if ( status == DownloadManager.STATUS_SUCCESSFUL ) {
						openPDF( context, Uri.fromFile( tempFile ) );
					}
				}
				c.close();
			}
		};
		context.registerReceiver( onComplete, new IntentFilter( DownloadManager.ACTION_DOWNLOAD_COMPLETE ) );
		
		// Enqueue the request
		dm.enqueue( r );
	}
	
	/**
	 * Show a dialog asking the user if he wants to open the PDF through Google Drive
	 * @param context
	 * @param pdfUrl
	 */
	public static void askToOpenPDFThroughGoogleDrive( final Context context, final String pdfUrl ) {
		new AlertDialog.Builder( context )
			.setTitle( R.string.pdf_show_online_dialog_title )
			.setMessage( R.string.pdf_show_online_dialog_question )
			.setNegativeButton( R.string.pdf_show_online_dialog_button_no, null )
			.setPositiveButton( R.string.pdf_show_online_dialog_button_yes, new OnClickListener() {
				@Override
				public void onClick(DialogInterface dialog, int which) {
					openPDFThroughGoogleDrive(context, pdfUrl);	
				}
			})
			.show();
	}
	
	/**
	 * Launches a browser to view the PDF through Google Drive
	 * @param context
	 * @param pdfUrl
	 */
	public static void openPDFThroughGoogleDrive(final Context context, final String pdfUrl) {
		Intent i = new Intent( Intent.ACTION_VIEW );
		i.setDataAndType(Uri.parse(GOOGLE_DRIVE_PDF_READER_PREFIX + pdfUrl ), HTML_MIME_TYPE );
		context.startActivity( i );
	}
	/**
	 * Open a local PDF file with an installed reader
	 * @param context
	 * @param localUri
	 */
	public static final void openPDF(Context context, Uri localUri ) {
		Intent i = new Intent( Intent.ACTION_VIEW );
		i.setDataAndType( localUri, PDF_MIME_TYPE );
		context.startActivity( i );
	}
	/**
	 * Checks if any apps are installed that supports reading of PDF files.
	 * @param context
	 * @return
	 */
	public static boolean isPDFSupported( Context context ) {
		Intent i = new Intent( Intent.ACTION_VIEW );
		final File tempFile = new File( context.getExternalFilesDir( Environment.DIRECTORY_DOWNLOADS ), "test.pdf" );
		i.setDataAndType( Uri.fromFile( tempFile ), PDF_MIME_TYPE );
		return context.getPackageManager().queryIntentActivities( i, PackageManager.MATCH_DEFAULT_ONLY ).size() > 0;
	}

}

SQLite INSERT OR REPLACE through ContentProvider

Introduction

A lot of times in an app, you need to update some locally cached data stored in a database with newer data fetched from e.g. a webservice. A normal way (or so it was for me) to do this is with the “if-not-update-then-insert” pattern, like this:

if ( getContentProvider().update( uri, values, selection, selectionArgs ) == 0 ) {
    getContentProvider().insert( uri, values );
}

Or with many rows, using bulkInsert():

List<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
for ( ContentValues values: myValues ) {
    if ( getContentProvider().update( uri, values, selection, selectionArgs ) == 0 ) {
        valuesToInsert.add( values );
    }
}
getContentProvider.bulkInsert( uri, valuesToInsert.toArray( new ContentValues[0] ) );

While this might seem like a solid way to do it, there are a few problems:

First of all, some of the code is boilerplate and could be reduced a bit, but the major issue here is…

Thread safety

If multiple threads are running this code simultaneously, it is possible to violate your database constraints:

For example if you have a unique key in your table that the update() tries to match with, and two threads executing the code, the following could happen:

Both threads have run all the update() calls without updating anything and are about to run the insert()/bulkInsert() method. This means that both threads think that it is safe to insert the values, because they have already checked that no constraints are violated when no updates took place. The threads are, however, not aware of eachother’s plans to insert data with the same unique key, which undoubtedly will result in a constraint violation, and possibly an exception like the following:

  
android.database.sqlite.SQLiteConstraintException: error code 19: constraint failed
	at android.database.sqlite.SQLiteStatement.native_execute(Native Method)
	at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:61)
	at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1677)
	at android.database.sqlite.SQLiteDatabase.insertOrThrow(SQLiteDatabase.java:1547)

Why not just synchronize it?

An easy solution for this thread safety problem is simply to wrap the code in a synchronized block, like this:

synchronized( this ) {
    List<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
    for ( ContentValues values: myValues ) {
        if ( getContentProvider().update( uri, values, selection, selectionArgs ) == 0 ) {
            valuesToInsert.add( values );
        }
    }
    getContentProvider.bulkInsert( uri, valuesToInsert.toArray( new ContentValues[0] ) );
}

This works, and no constraint violations occur anymore. But now we added more code and slowed down the execution of the code by locking it to one thread at a time.

Better solution: Using SQLite’s INSERT OR REPLACE statement

The better solution to this is to use one of SQLite’s already builtin features, the INSERT OR REPLACE statement. However, the ContentProvider only has a insert() method and not a replace method, so we’ll have to work around this in some clever way:

Fortunately, the ContentValues objects can hold an arbitrary amount of parameters to the ContentProvider, so we’ll use that to indicate that we want to do an INSERT OR REPLACE instead of just a normal INSERT.

First we’ll define some static string constant that we would never use as column name:

public static final String SQL_INSERT_OR_REPLACE = "__sql_insert_or_replace__";

Next, we’ll pass that along with the other values in our ContentValues object:

ContentValues values = new ContentValues();
values.put( SQL_INSERT_OR_REPLACE, true );
// ... fill the ContentValues with the actual values ...

// ... notice we only need one line to insert/replace
getContentProvider().insert( uri, values );

Now we have passed the parameter on, so we’ll modify our ContentProvider to handle this:

public Uri insert(Uri uri, ContentValues values) {
    // ...

    boolean replace = false;
    if ( values.containsKey( SQL_INSERT_OR_REPLACE ) {
        replace = values.getAsBoolean( SQL_INSERT_OR_REPLACE );

        // Clone the values object, so we don't modify the original.
        // This is not strictly necessary, but depends on your needs
        values = new ContentValues( values );

        // Remove the key, so we don't pass that on to db.insert() or db.replace()
        values.remove( SQL_INSERT_OR_REPLACE );
    }

    long rowId;
    if ( replace ) {
        rowId = db.replace(tableName, null, values);
    } else {
        rowId = db.insert(tableName, null, values);
    }

    // ...

}

Conclusion

There you have it. We can now call SQLite’s INSERT OR REPLACE function through our ContentProvider,
and even takes even less code, since we only have to call insert(), and not update(). Neat huh? :-)

Push message differences between Android and iPhone

Introduction

Having recently written a library to unify the sending of push messages for Android devices with GCM (Google Cloud Messaging for Android) and iOS devices with APNs (Apple Push Notification Service), I have gained a valuable insight on the technical differences between how push messaging works identical and differently between the two platforms.

In my opinion, there are more differences than similarities between them, so let’s start with what is the same for Android and iOS push messages:

Similarities between Android and iOS push messages

Both Android and iOS requires the device to contact their respective push message provider (GCM or APNs) to receive a special string (which I will call the identifier), that uniquely identifies a specific app on a specific device. In GCM this is called the device ID, while APNs calls it a device token. It is the app’s responsibilty to receive this identifier from the provider and send it to your application server, so your app can receive push messages from your application server.

Both platforms allow for sending messages from one application server to multiple devices, and for multiple application servers to send to the same device.

Lastly, both platforms allow for an arbitrary JSON object to be sent from your application server to the device, though the maximum allowed size of this object differs (more on that later).

Differences between Android and iOS push messages

The format of the identifier is different: APNs’ device token is always a 64-byte hex-string, while I’m not so sure about GCM’s device ID (As an example, I got a 183-byte string which seemed to follow the regex pattern [a-zA-Z0-9_\-] (letters, numbers, underscores and dashes).

Push messages sent to GCM always returns a reply to the application server informing whether or not the delivery of the message to GCM was successful, and this reply can tell the application server if a device ID has changed or if it has become invalid and can no longer receive push messages. With APNs, you have to periodically connect to a APNs feedback server, that will tell you if any device tokens used previously have become invalid.

Note that none of the providers can or will tell you if the message actually got delivered to the device, they merely tell you whether or not the message was accepted for delivery. The primary reason for this is that a device may be unavailable (e.g. turned off, no network coverage) and the provider then stores the message for later delivery up to a maximum timeperiod (APNs does not inform of this timeperiod, while for GCM it is 4 weeks – in fact you can choose a time-to-live for GCM messages anywhere for 0 seconds to 4 weeks).

And now that we are on the subject of delivery, GCM has quite an advantage over APNs: APNs only stores one unsent message – the most recent: If you send another message to a device that already has a queued message on APNs, the queued message is discarded in favor of the newer. GCM supports this behaviour through the use of collapse keys, but it is not the default behaviour. GCM’s default is attempting to deliver every accepted message.

Differences in the contents of the push messages

The raw contents and interpretations of the push messages are probably the biggest differences between the GCM and APNs:

Where GCM simply passes raw JSON data to a background service from your app, free for the app to process whether it is in the foreground, background or not even started. You can choose to ignore the message, post a notification in the notification bar, trigger a synchronization action, display a dialog (though frowned upon if your app isn’t in the foreground) or something entirely different. It is totally up to you.

APNs makes up for iOS’ limited support for background services in the following way: You can specify an alert dialog (possibly localized with format arguments) to pop up to tell the user that something has happened in your app. Furthermore you can play an optional sound from your app and put a badge on your app’s icon (The number in the red circle in the top right corner of the icon). This is all done by iOS. You don’t have to do any programming on the app side for this. This is default behaviour if the app isn’t in the foreground. If it is in the foreground, you receive the parameters for the alert, badge and sound in your code along with an arbitrary JSON payload, where you can do whatever you want with it.

Notice, however, that there is limitations on the size of the payload in the messages. For GCM the payload is limited to 4 KB (4096 bytes), while APNs only allows 256 bytes(!) for payload. Also, with APNs, the alert, badge and sound parameters are part of these 256 bytes, so there isn’t much space for additional data: An alert with format parameters can easily take up 75-100 bytes, so you have to be conservative with the data that you pass along. It is better to notify your app that something has changed than to send along the change in the push message (which is practically impossible for real life data)

Conclusion

These are, in my opinion, the most important differences and similarities between Google Cloud Messaging for Android and Apple Push Notification Service.

Though I am heavily biased in favor of the Android platform, this is not a bashing of Apple’s push service. It is intended to give an overview of the differences, so you get a better understanding of how to implement your cross-platform push message solution

Android Layout Finder online tool

Hi guys, hope you had a great new years!

If you’ve ever developed an Android app, you’ve probably used the findViewById() method for finding views and findFragmentById() for finding fragments from your inflated layout. This has to be done with every View and Fragment that you want to access from your code.

After getting tired of writing the same code over and over again, I created a tool to generate that code for me from an arbitrary XML layout:

Behold: The Android Layout Finder!

You can try it out on a sample XML layout by pressing the ‘Paste example XML’ button in the right side of the screen.

Android Layout Finder is open source and licensed under the GNU General Public License. The code can be found on GitHub: http://www.github.com/jesperborgstrup/android-layout-finder. Enjoy!