Backup / Restore / Cloud Save

Cloud Save – Part 2

Previously in this series we got a basic project set up to use Google Play services. In this article we’ll start implementing Cloud Save to persist our app state across multiple devices.

First we’ll create a simple layout consisting of and EditText control which will contain some text that we want to persist, two Buttons to save and load the app state, and a TextView in which we can display status and error messages.



    

    

        

The thing that we need to do is create an AppStateClient which will be responsible for managing our state persistence. We do this is onCreate():

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    persistent = (EditText)findViewById(android.R.id.text1);
    messages = (TextView)findViewById(android.R.id.text2);
    saveButton = (Button)findViewById(R.id.saveButton);
    loadButton = (Button)findViewById(R.id.loadButton);

    String[] scopes = new String[]{Scopes.APP_STATE};

    appStateClient = new AppStateClient.Builder(this, 
            this, this)
        .setScopes(scopes)
        .create();
    messages.setText(getString(R.string.connecting));
}

The AppStateClient.Builder constructor takes three arguments: The first is a Context, the second and third are a ConnectionCallbacks implementation, and an OnConnectionFailedListener implementation both of which are implemented by our Activity class.

Next we need to implement the ConnectionCallbacks methods:

@Override
public void onConnected(Bundle bundle)
{
    messages.setText("Connected");
    loadState(null);
}

@Override
public void onDisconnected()
{
    saveButton.setEnabled(false);
    loadButton.setEnabled(false);
    messages.setText("Disconnected");
}

These are pretty straightforward. On connection and disconnection we display an appropriate message. When we have successfully connected we initiate a load to ensure that we get the latest data from the cloud. WHen we disconnect we want to diable to load and save buttons, as we cannot perform these operations unless we are connected. The behaviour of these buttons will be that they are disabled at app startup. Whenever we perform a save, or we are disconnected they will be disabled, and when we have loaded data they will be enabled. Therefore they will not be enabled until our initial load has completed.

We also need to implement the OnConnectionFailedListener method:

@Override
public void onConnectionFailed(
    ConnectionResult connectionResult)
{
    messages.setText("Connection failed: " + 
        connectionResult.getErrorCode());
    if (connectionResult.hasResolution())
    {
        messages.setText(
            "Attempting to resolve connection failure: " + 
            connectionResult.getErrorCode());
        try
        {
            connecting = true;
            connectionResult.startResolutionForResult(
                this, RC_RESOLVE);
        } catch (IntentSender.SendIntentException e)
        {
            appStateClient.connect();
        }
    } else
    {
        messages.setText(
            "Unresolvable connection failure: " + 
            connectionResult.getErrorCode());
    }
}

The interesting thing to note here is that we may get a connection failure which is recoverable. The library provides us with the necessary mechanisms to attempt to resolve the failure, but we must invoke it manually – so we have a degree of control over whether or not recovery is attempted. The startResolutionForResult() method is rather similar to startActivityForResult(), and we basically pass control to the library to resolve the issue which could require it displaying it’s own Activity to resolve the problem. On first invocation of our app, the user will need to permit our app to persist using Google Play services. While this is done for us, we need to handle the result, and this is done using the standard onActivityReturned() mechanism:

public void onActivityResult(int requestCode, int responseCode, Intent intent)
{
    if (requestCode == RC_RESOLVE)
    {
        connecting = false;
        Log.d(TAG, "onActivityResult, req " + 
            requestCode + " response " + responseCode);
        if (responseCode == Activity.RESULT_OK)
        {
            appStateClient.connect();
        } else
        {
            Log.e(TAG, "Unable to connect");
            connecting = false;
        }
    }
}

Next we need a couple of utility methods to handle connection and disconnection:

private void connect()
{
    connecting = true;
    appStateClient.connect();
}

private void disconnect()
{
    appStateClient.disconnect();
}

We use the connecting semaphore to ensure that we only try and have one connection attempt in progress.

We also need to connect and disconnect from the onStart and onStop methods:

@Override
protected void onStart()
{
    super.onStart();
    if (!connecting)
    {
        connect();
    }
}

@Override
protected void onStop()
{
    disconnect();
    super.onStop();
}

We now implement the onCLick handlers for our save and load buttons:

public void loadState(View view)
{
    saveButton.setEnabled(false);
    loadButton.setEnabled(false);
    appStateClient.loadState(this, 0);
}

public void saveState(View view)
{
    saveButton.setEnabled(false);
    loadButton.setEnabled(false);
    Log.v(TAG, "Max keys: " + 
        appStateClient.getMaxNumKeys());
    appStateClient.updateStateImmediate(this, 
        0, 
        persistent.getText().toString().getBytes(charset));
}

The loadState method takes two parameters: An OnStateLoadedListener instance, a key which represents which of the four 128KB slots we wish to save to.

The updateStateImmediate method takes three parameters: An OnStateLoadedListener instance, the key which represents which of the four 128KB slots we wish to save to, and a byte array representing the data to persist. Normally we’d call the asynchronous updateState method instead (which doesn’t require the OnStateLoadedListener), but we want to update the UI (enabling the buttons) once the save is complete, and the onStateLoaded() method of the OnStateLoadedListener will be called once the save is complete.

Finally we need to implement to OnStateLoadedListener:

@Override
public void onStateLoaded(int i, 
    int i2, byte[] bytes)
{
    saveButton.setEnabled(true);
    loadButton.setEnabled(true);
    if(bytes != null)
    {
        persistent.setText(
            new String(bytes, charset));
    }
}

@Override
public void onStateConflict(int i, 
    String s, byte[] bytes, byte[] bytes2)
{
}

So all we’re doing is updating the text of our EditText when the data is loaded.

If we run this on multiple devices, we can save data from one device and hitting load on another device will result in the text being synchronised:

basic

It is worth noting that we have to manually load in order to check for updates, so in a real app you will need to periodically check for updates.

There is also another potential pitfall: when multiple updates occur causing conflits ion the data. In the concluding article in this series we’ll look at how we can detect and resolve conflicts in our data.

The source code for this article is available here.

© 2013 – 2014, Mark Allison. All rights reserved.

Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. Great stuff! I’ve imported all this into my proof of concept project, but every time the onStateLoaded callback fires, the incoming bytes are null. Any ideas? Maybe APP_ID has a problem?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.