Migration to Google Play In-app Billing

So your Android app has a free version and also a paid/pro version with some extra features and the whole thing isn’t easy to maintain, and you heard about this new (now pretty old) billing API version 3 (which is a whole lot better than version 2) and you think maybe I can use this to bring everything in a single app for good. Now what? I’m going to talk you through my requirements and concerns in a similar situation, and tell you what I have done about them. I had a very limited time to spend and I’m sure there are better ways doing this, that’s what the comment box down the page is for 😉

Everything should be in a single app

I had to stop users buying the pro app and communicate to the existing users that it has been deprecated. I did that by updating the Play Store listing and increasing the price so that people don’t buy it. Then I merged the common library module with the free version of the app and integrated with in-app billing so that users of free app can get pro features via in-app purchases. Now is time to let the existing users know they can migrate to the new app. There are a few things to take care of, for a smooth transition.

Current users of pro app shouldn’t need to pay again

The existence of the pro app can work as a proof of purchase and unlock the extra features in the new app, just like going through in-app purchase. I can rename the pro app to unlocker at this stage. It is important that users keep their unlocker app on their phone, until I come up with a more elegant way of dealing with this in the billing API. In order to verify that users have the unlocker, all I need to do is making sure the app exists and is signed with the same certificate that my new app is. This is how to do it:

    public boolean isUnlockerAppPresent()
    {
        try
        {
            PackageInfo info = context.getPackageManager().getPackageInfo("com.example.unlocker",
                    PackageManager.GET_META_DATA);
            return context.getPackageManager().checkSignatures("com.example.app",
                    "com.example.unlocker") == PackageManager.SIGNATURE_MATCH;
        }
        catch (NameNotFoundException e)
        {
            return false;
        }
    }

Users should be able to import settings from the old app

According to Android docs if my apps are signed with the same signature they can share a user ID and as a result run in the same process or access their data (share preferences for example). All I need to do is adding two tags to the manifest of both apps:

<manifest android:sharedUserId="com.example.shared.userid" android:sharedUserLabel="@string/shared_user_id" ... package="com.example.unlocker" />
<manifest android:sharedUserId="com.example.shared.userid" android:sharedUserLabel="@string/shared_user_id" ... package="com.example.app" />

Please note that sharedUserId has to be in a package-like structure, i.e. dot separated, and sharedUserLabel has to be a string resource not a hardcoded value. Once all this is in place in can do this in my app:

Context context = createPackageContext("com.example.unlocker", 0);
SharedPreferences unlockerAppPrefs = PreferenceManager.getDefaultSharedPreferences(context);
// Read data from shared preferences of the old app.

Looks pretty easy and straightforward, right? WRONG!

You should’ve known this before

The problem is, the apps can’t get updated with this changes because I have changed the user id the apps will run as. I should’ve thought about this in day one! Now I have to implement a “real and proper” messaging mechanism between the apps.

What I came up with was a pretty simple content provider on top of shared preferences protected by the signature check, meaning only apps with the matching signature will be able to access it. This is how it is defined it in the manifest:

<provider android:name="com.example.unlocker.PreferencesContentProvider" android:authorities="com.example.unlocker.preferences" android:exported="true" android:protectionLevel="signature"/>

Here is the code for the content provider, of course simplified to fit the blog post:

public class PreferencesContentProvider extends ContentProvider
{
    @Override
    public boolean onCreate()
    {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
    {
        String[] columns = new String[] { "NAME", "VALUE" };
        if (projection == null)
            projection = new String[] { "NAME", "VALUE" };
        if (!Arrays.asList(columns).containsAll(Arrays.asList(projection)))
            throw new IllegalArgumentException("Unknown columns requested.");

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        MatrixCursor matrixCursor = new MatrixCursor(columns);
        matrixCursor.addRow(new Object[] { "TEST_DATE", prefs.getString("TEST_DATE") });
        // ...
        return matrixCursor;
    }

    @Override
    public String getType(Uri uri)
    {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values)
    {
        throw new UnsupportedOperationException("Cannot insert into this content provider.");
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs)
    {
        throw new UnsupportedOperationException("Cannot delete from this content provider.");
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    {
        throw new UnsupportedOperationException("Cannot update this content provider.");
    }
}

This is how I will read data from the old app, again simplified:

    ContentResolver cr = getContentResolver();
    Cursor cur = cr.query(Uri.parse("content://com.example.unlocker.preferences/preferences"),
            null, null, null, null);
    int count = cur.getCount();
    cur.moveToFirst();
    String testData = null;
    for (int i = 0; i < count; i++)
    {
        String key = cur.getString(0);
        String value = cur.getString(1);
        if (key.equals("TEST_DATA"))
            testData = value;
        cur.moveToNext();
    }

What’s left?

There isn’t much left. It is all about making sure the user is properly guided through the migration process in both apps. I need to make sure the latest unlocker (with the content provider) is installed before trying to import settings. I also need to implement the update method for the content provider to be able to flag transition complete. I might need to do other things, but at least I’ve got a base in place.

Lesson learned

There are some parts of your application that can not change. User ID is also one of them.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s