Forwarding Android Wear app’s crash report to phone

About a week ago, I posted a question on how to retrieve crash reports from wearable apps. Apparently, neither Crashlytics, nor Android Wear itself supports this feature out of the box, so we’re required to do it manually. Luckily, I received a useful answer from a Google employee hinting to try DataApi instead of MessageApi, which I originally intended to use.

To put it simply, MessageApi is a “fire and forget” communication protocol, while DataApi takes care of possible connection breakdowns, e.g. losing Bluetooth link between watch and handset. It also handles device reboots – the data will still be transferred eventually. Well, one problem solved. I’m posting a draft of my solution below.

First, we have to implement Thread.UncaughtExceptionHandler interface. The Application subclass is a perfect place for this:

public class WApplication extends Application
        implements Thread.UncaughtExceptionHandler {

    private static final String LOG_TAG = WApplication.class.getSimpleName();

    private Thread.UncaughtExceptionHandler mDefaultUncaughtExceptionHandler;

    ...

    @Override
    public void onCreate() {
        super.onCreate();
        mDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    @Override
    public void uncaughtException(Thread thread, final Throwable throwable) {
        Log.e(LOG_TAG, "Uncaught exception thrown.");

        WearableService.launchService(throwable, WApplication.this);

        mDefaultUncaughtExceptionHandler.uncaughtException(thread, throwable);
    }

}

Inside the uncaughtException() method, we’re launching a service and passing the crash to it as an intent’s extra. Then, we need to create a PutDataRequest instance with the stack trace, and sent it to the handset:

public class WearableService extends Service {
    ...
    public static void launchService(Throwable throwable, Context context) {
        Intent startServiceIntent = new Intent(context, WearableService.class);
        startServiceIntent.putExtra(EXTRA_KEY_EXCEPTION, throwable);
        context.startService(startServiceIntent);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Throwable throwable = (Throwable) intent.getSerializableExtra(KEY_EXCEPTION);

        sendExceptionToMobile(throwable);

        return super.onStartCommand(intent, Service.START_REDELIVER_INTENT, startId);
    }

    private void sendExceptionToMobile(final Throwable throwable) {
        if (throwable == null) {
            return;
        }
        Log.d(LOG_TAG, "Sending exception to mobile...");
    
        PutDataMapRequest putDataMapReq = PutDataMapRequest
                .create(WearCommunicationConstants.PATH_EXCEPTION);
        DataMap dataMap = putDataMapReq.getDataMap();
    
        StringWriter sw = new StringWriter();
        throwable.printStackTrace(new PrintWriter(sw));
        String stackTrace = sw.toString();
    
        dataMap.putString(WearCommunicationConstants.KEY_STACK_TRACE, stackTrace);
        PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();
        PendingResult<DataApi.DataItemResult> pendingResult =
                Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq);
        
        pendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
            @Override
            public void onResult(final DataApi.DataItemResult result) {
                if (result.getStatus().isSuccess()) {
                    Log.d(LOG_TAG,
                            "DataItem synced: " + result.getDataItem().getUri());
                } else {
                    Log.e(LOG_TAG,
                            "Failed to sync DataItem: " + result.getStatus().getStatusCode() + ", "
                                    + result.getStatus().getStatusMessage());
                }
            }
        });
    }
}

Now, all that’s left is retrieve the message in a subclass of WearableListenerService and log it (or forward it to Crashlytics):

public class MobileService extends WearableListenerService {
    ...
    @Override
    public void onDataChanged(DataEventBuffer dataEvents) {
        Log.d(LOG_TAG, "Data changed, data event(s) received.");

        for (DataEvent event : dataEvents) {
            Log.d(LOG_TAG, "Data event type: " + event.getType());
            switch (event.getType()) {
                case DataEvent.TYPE_CHANGED:
                    DataItem item = event.getDataItem();
                    DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
                    switch (item.getUri().getPath()) {
                        case WearCommunicationConstants.PATH_EXCEPTION:
                            Log.e(LOG_TAG, "Received exception from a wearable device.");
                            String stackTrace = dataMap
                                    .getString(WearCommunicationConstants.KEY_STACK_TRACE);
                            Utils.logWithCrashlytics(stackTrace);

                            break;

                        // ...
                    }
                    break;
                case DataEvent.TYPE_DELETED:
                    // ...
            }
        }
    }
}

That’s it!

Of course, there’s always an easier way to go: write an app which never crashes.

Storing user’s profile – the easiest way

It’s quite obvious that if the only thing you need to store in your app are user profile, app’s settings and other equally simple values, there’s no need to spend extra time creating a whole new relational database just for this purpose.

Still, even with SharedPreferences, I’ve seen some developers going the long way and putting/getting all the key-value pairs manually in and from SharedPreferences, while there’s a much easier way – use Gson (or any other serialization library like Square’s Moshi) for a single read/write operation.

Let’s take a look at a simple UserProfile class:

public class UserProfile {
    private String mFirstName, mLastName;
    private String mEmail;
    private List<Item> mItems;
    //... dozens of other fields
}

Now, let’s create a basic Gson instance:

Gson gson = new GsonBuilder().create();

We’re now ready to store the whole object by serializing it to a single String:

mSharedPreferences.edit().putString(KEY_USER_PROFILE, gson.toJson(userProfile)).apply();

and deserialize that String later to get our proper Java object back:

UserProfile userProfile = gson.
fromJson(mSharedPreferences.getString(KEY_USER_PROFILE, null), UserProfile.class);

That’s it. No boilerplate, no ORMs. Mix it with singleton pattern, and you’re ready to go.