Managing Account Data - Android Development Patterns: Best Practices for Professional Developers (2016)

Android Development Patterns: Best Practices for Professional Developers (2016)

Chapter 14. Managing Account Data

Android has at one time or another been labeled as a difficult system to work with due to the fragmentation of the system. While detractors to the platform are quick to point out the potential flaws of working with a myriad of devices and hardware platforms, part of the strength of Android is the abundance of APIs and libraries available to back up, restore, and synchronize data. This allows users to move from one device to another without missing their information and applications. In this chapter, you learn about many of the Google-provided services as well as how to integrate with other services to handle the transportation and synchronization of user data.

Getting Accounts

Many Android devices require users to create or use an existing Google account to sign in and start using them. Some devices run customized versions of Android and do not require a Google account to be used; in these instances, the device provider implements their own user-authentication process.

When working with Android devices that have users sign in with a Google user account, you can request some information from the user profile. This is accomplished by leveraging the AccountManager class and adding a couple of permissions to your application.

Starting with the permissions, you will need to add the following to your application manifest XML:

<uses-permission android:name="android.permission.GET_ACCOUNTS"></uses-permission>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS">
</uses-permission>

With the permissions in place, you can now use the AccountManager to retrieve the accounts that are available on the device.


Note

The AccountManager gives you the ability to find all accounts on the device. This allows you to work with more than a Google account, and will also allow you to work with multiple Google accounts.


The following shows a snippet of code that uses the AccountManager to create an object that is then stored in a list and iterated over for a matching Google account:

AccountManager myAccountManager = (AccountManager)
getSystemService(ACCOUNT_SERVICE);
Account[] list = myAccountManager.getAccounts();
String googleAccount = "No Google Account";

for(Account account: list) {
if(account.type.equalsIgnoreCase("com.google")) {
googleAccount = account.name;
break;
}
}
// set text view to googleAccount
TextView tv = (TextView) findViewById(R.id.myTextView);
tv.setText(googleAccount);

Note that for demonstration purposes, a String named googleAccount is created and is later populated during iteration through list. It is populated based on looking for a specific type of account (in this case, com.google). This means that any account that is connected to Google will be returned. Because this is just a sample snippet looking for a specific account, you should be aware that some users may have more than one account tied to their device.


Tip

If you’re working with an Android emulator and are having trouble getting the snippet to work, make sure you are using an emulator that was built using a target that supports the Google APIs and is a minimum of API level 5.


You can also use the getAccountsByType() and getAccountsByTypeAndFeatures() methods to return objects that are more specific to what you need. If you are using an account for authentication purposes, remember to check that the account exists in the list of returned accounts. Failure to do so will result in the app requesting an authorization for an account that doesn’t exist and will give an error of undefined.

Android Backup Service

The Android Backup Service is provided for applications that need to store a small amount of user data. This is a great solution for saving preferences, scores, notes, and similar resources that should be transferable between devices or during device restoration.


Note

The maximum storage you are allowed to use with the Android Backup Service is 1MB per account per application. The service is also not intended for use as a data-synchronization service, but instead as a means to restore application data.


To use Android Backup Services, you must register your application with Google to receive a Backup Service Key. At the time of writing, the URL for this is http://code.google.com/android/backup/signup.html. Registration is a short process that requires you to read and agree to a terms of service agreement with Google. After you read and accept the terms, you are asked to provide the package name of your application. If you are developing multiple applications, you need to agree to the terms and enter the package name for each application.

After registering, you are given an XML element that you need to place in your application Manifest XML as a child of the <application> element. The following demonstrates what a generated key looks like:

<meta-data android:name="com.google.android.backup.api_key"
android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" />

While you are still working inside of the application manifest XML file, you need to add a parameter of androidbackupAgent to the <application> element. The value of this property should match the name you use for your backup agent. The following gives an example of using MyBackupAgent for the name of the backup agent:

<application android:label="MyApp" android:backupAgent="MyBackupAgent">

Take specific notice of the naming convention used on the backup agent. Rather than using camel-case to signify a variable, it uses upper-camel-case or PascalCase formatting. This is because you need to create a class of that name and extend the BackupAgentHelper. It should also implement an override for the onCreate() method. The following snippet shows a demonstration class:

import android.app.backup.BackupAgentHelper;
import android.app.backup.FileBackupHelper;

public class MyBackupAgent extends BackupAgentHelper {

// set the name(s) of a preference file to backup
static final String HIGH_SCORES_FILENAME = "scores";
static final String INVENTORY_FILENAME = "inventory";

// create a key to identify the backup data set
static final String FILES_BACKUP_KEY = "mybackupfileskey";

// allocate the helper and add it to the backup agent
@Override
void onCreate() {
FileBackupHelper helper = new FileBackupHelper(this,
HIGH_SCORES_FILENAME, INVENTORY_FILENAME);
addHelper(FILES_BACKUP_KEY, helper);
}
}

To back up multiple files, two strings are created and set to the filenames that need to be backed up. The strings are then passed as arguments to the FileBackupHelper() method. The FILES_BACKUP_KEY will be used when restoration is needed. Because the value is a “key,” it does not have to be lower-, camel-, upper-, or mixed-case.

Files are not the only resources you can back up from your application. If you want to back up application preferences, you can use the SharedPreferencesBackupHelper. Using the SharedPreferencesBackupHelper is almost identical to using theFileBackupHelper. The following class demonstrates how preferences are backed up:

import android.app.backup.BackupAgentHelper;
import android.app.backup.SharedPreferencesBackupHelper;

public class MyBackupAgent extends BackupAgentHelper {
// set the names of the preferences to back up
// these should match values used in getSharedPreferences()

static final String PREFS_OPTIONS = "optionsprefs";
static final String PREFS_SCORES = "highscores";

// create a key to use with your preferences backup
static final String PREFS_BACKUP_KEY = "myprefsbackupkey";

// allocate the helper and add it the backup agent
void onCreate() {
SharedPreferencesBackupHelper helper =
new SharedPreferencesBackupHelper(this, PREFS_OPTIONS, PREFS_SCORES);
addHelper(PREFS_BACKUP_KEY, helper);
}
}

To begin a backup process, you need to use the BackupManager and the dataChanged() method to request a backup. After the request happens, the backup manager calls the onBackup() method and the backup will be performed. The following shows the snippet that is used to create the backup request:

public void requestBackup() {
BackupManager bm = new BackupManager(this);
bm.dataChanged();
}

Note that in the class where you place that, you also need to use import android.app.backup.BackupManager. You should also remember that the backup service is not run “on demand,” but you should still call it whenever data is changed so that a user has a better chance at having the most up-to-date information saved.

Using Google Drive Android API

Many Android users have a device that is compatible with Google services that gives them access to Google Drive. Google Drive is a storage service that works with other Google services, including Google Play Services. This allows users to store many gigabytes of data for free with an option for them to buy more space if needed.

As apps have become more sophisticated, users have started to increase their dependency on being able to move and access data from wherever they are, on whatever device they have. To make things more complex, users expect to be able to not only read data, they expect to be able to write data and have that data be saved without having to worry about getting in an elevator, taking a subway, or going through a tunnel. In each of these scenarios there is a chance that connectivity will drop and a potential write or save operation will fail due to the sudden loss of connectivity.

The Google Drive Android API allows you to overcome these issues by offering access to user data through an Android native picker, giving users transparent offline synchronization for data to maintain write integrity, and by working with devices running Gingerbread and above.

To get started implementing the API into your app, you need to register your app in the Google Developers console. This is the console that allows access to Google Services and is separate from the Play Store console (https://console.developers.google.com).

If you are working with a new application that has not been registered in the Developers Console, you can register your application and the Developers Console walks you through creating and signing it.

If you have already registered your app, you can select it and use the APIs & Auth menu to select APIs and find Drive API in the list. This will then allow you to turn on access for your application. Note that you will need to generate and sign your .apk file. If you need to submit authorized requests, you have to add OAuth 2.0 credentials to your app and use the Developer Console to generate a client ID.

Once you have all of your credentials set up and access to the Drive API enabled, you are ready to create a client in your Android application to start accessing data. This is done by building a client in the onCreate() method of an Activity and connecting it in the onStart()Activity. If a user has never authenticated when using the application, the onConnectionFailed() callback method will be invoked. This allows the user to authorize access to their data from within the app. The following snippet demonstrates the creation using the Builder pattern, the connection of the client, and a snippet for the onConnectionFailed() callback method:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstance);

myGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Drive.API)
.addScope(Drive.SCOPE_FILE)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}

@Override
protected void onStart() {
super.onStart();
myGoogleApiClient.connect();
}

@override
public void onConnectionFailed(ConnectionResult connectionResult) {
if (connectionResult.hasResolution()) {
try {
connectionResult
.startResolutionForResult(this, RESOLVE_CONNECTION_REQUEST_CODE);
} catch (IntentSender.SendIntentException e) {
// The app cannot resolve the connection, add error logic here
}
} else {
GooglePlayServicesUtil
.getErrorDialog(connectionResult.getErrorCode(), this, 0).show();
}
}

If the user is prompted to authenticate their app, then the onActivityResult() method for the activity will be called. This also passes back an argument that should be checked to match RESULT_OK; if it does, the client will need to be connected again. The following snippet shows an example of overriding the method to handle this scenario:

@Override
protected void onActivityResult(final int requestCode,
final int resultCode, final Intent data) {
switch (requestCode) {
// put your cases here
case RESOLVE_CONNECTION_REQUEST_CODE:
if (resultCode == RESULT_OK) {
myGoogleApiClient.connect();
}
break;
// put other cases or default here
}
}

After your connection has been made and authenticated, you can use the DriveFile interface to read and write files. Due to the architecture of Drive, there are essentially two copies of every file you work with—one that is created locally and one that is stored out in Drive. Using the DriveFile.open() method allows you to check locally for a file and, if it’s not found, attempt to retrieve it from the Drive service.


Note

If you intend to retrieve files only for reading, you can use an InputStream. If you intend to only save a file, you can use an OutputStream. If you intend to do both, you should use ParcelFileDescriptor because it can handle both reading and writing. You need to use a ParcelFileDescriptor when appending to a file because WRITE_ONLY truncates the file you are writing to.


When you retrieve a file from Drive, a resource called DriveContents will be available as a temporary copy of the file you are working with. This resource does require that you verify that it was able to get the file you want to work with. The following snippet shows a request made for a file as well as the process of verifying the contents of the DriveContents resource:

// either create a file object, or use Drive.DriveApi.getFile()
// MODE_READ_ONLY signifies working with an InputStream
file.open(myGoogleApiClient, DriveFile.MODE_READ_ONLY, null)
.setResultCallback(contentsOpenedCallback);

ResultCallback<DriveContentsResult> contentsOpenedCallback =
new ResultCallback<DriveContentsResult>() {
@Override
public void onResult(DriveContentsResult result) {
if (!result.getStatus().isSuccess()) {
// File cannot be opened, display appropriate message
return;
}
// set contents to the binary return
DriveContents contents = result.getDriveContents();
}
};

To read the binary contents that were just opened, you need to create a BufferedReader, a StringBuilder, and a String. When you are finished working with the file, remember to close the file with either DriveContents.commit or DriveContents.discard. The following snippet shows how to convert the binary data into a String as well as how to close the file:

// add this snippet where you are working with the binary read
DriveContents contents = result.getDriveContents();
BufferedReader reader =
new BufferedReader(new InputStreamReader(contents.getInputStream()));
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
// Create a String to house the contents
String contentsAsString = builder.toString();
// Perform logic with the string

// The following will close the file
contents.commit(mGoogleApiClient, null)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
// handle based on result status
}
});

Writing to files is similar to reading from them in that you need to retrieve and open the file you want to write to, perform the write, and then close the file. Remember that you will need to use a ParcelFileDescriptor when you are appending to a file rather than using anOutputStream. The following snippet demonstrates opening a file to work with, appending a String message to the file and then closing the file:

// create a file object, use Drive.DriveApi.getFile(), or use DriveContents
file.open(mGoogleApiClient, DriveFile.MODE_WRITE_ONLY, null)
.setResultCallback(new ResultCallback<DriveContentsResult>() {
@Override
public void onResult(DriveContentsResult result) {
if (!result.getStatus().isSuccess()) {
// File cannot be opened, display appropriate message
return;
}
DriveContents contents = result.getDriveContents();
}
});

// append a string to the file that was opened
try {
ParcelFileDescriptor parcelFileDescriptor =
contents.getParcelFileDescriptor();
FileInputStream fileInputStream =
new FileInputStream(parcelFileDescriptor.getFileDescriptor());
// read to the end of the file
fileInputStream.read(new byte[fileInputStream.available()]);

// append to the file
FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor
.getFileDescriptor());
Writer writer = new OutputStreamWriter(fileOutputStream);
writer.write("Howdy World!");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}

// close the file
contents.commit(mGoogleApiClient, null)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
// handle based on response status
}
});

Once a file has been closed, it will be flagged to synchronize with the Drive service. The synchronization service is run automatically and will perform connectivity checks to ensure that any files that need to be updated will complete when the network is available and that this operation performs successfully.

Using Google Play Games Services

Google Play Games is a service that allows game developers to create achievements, track login information, grant user permissions, and add a social aspect to gaming that allows you to provide smoother and more addictive gameplay to your users. There is a lot to learn and cover to implement all of the available services, and in this section we are going to focus on what has been the most troublesome aspect of handling user data.

There has long been an issue of figuring out how to provide a quality “save” experience for users who get a new device or have multiple devices. When users only have one device, saving game data is a manageable affair. As a developer, you could save to the local file system or database and you were done.

The problem with this strategy is that many users have more than one device. They may not actually “own” multiple devices, but within the life of your app they may upgrade or change devices. When this happens, a user does not want to spend more of their time doing what they had already done previously to get back to where they were in your game.

Developers have ended up using a variety of strategies to save, restore, and synchronize game data between devices. Any implementation is better than none, but to ease this particular burden, Google has provided a free service that can help you handle this process with Google Play Games Services.

To use Google Play Games Services, you will need to log in to the Google Play Developer Console and add your name to it. This includes a description of your game as well as the name of it. You also need to make sure you have credentials set up for your game. This usually includes creating an OAuth client and linking it to the console.

Google has written up a guide that you can follow, with detailed steps on how this initial setup is done. It is also the best place to go to for reference because it is updated to reflect what the Google Play Developer Console looks like and how it is used. Visit this guide athttps://developers.google.com/games/services/console/enabling.

With your app registered in the console, you can now access all of the features of Google Play Games Services. For examples on all of the features you can use, you should visit the samples code repository that is hosted on GitHub at https://github.com/playgameservices/android-basic-samples.

Working with Saved Games

To add game saving through Google Play Games Services, you need to provide only two things:

Image A binary blob of game data

Image Metadata containing Google-provided data as well as data you provide, which includes an ID, name, description, last modified timestamp, time played, and a cover image

Note that you are given 3MB of data for the binary data blob, and 800KB for your cover image. The cover image is used to help the player visually understand what and where they were in your game. The cover image should be something to not only show your game but should be something to help entice the player to continue playing in case they haven’t played the game in a while.

The data and cover image are stored in the Drive account for the user playing the game. This folder is hidden from them and contains the game blob and cover image. Due to the Drive service being used, when you create your Google Services API client, you will need to include Games and Drive as part of the client. The following shows an example of creating the API using the builder method that allows access to Google Plus, Google Games, and Google Drive:

@Override
public void onCreate(Bundle savedInstanceState) {
// create Services API with Play, Games, and Drive access
myGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Plus.API).addScope(Plus.SCOPE_PLUS_LOGIN)
.addApi(Games.API).addScope(Games.SCOPE_GAMES)
.addApi(Drive.API).addScope(Drive.SCOPE_APPFOLDER)
.build();
}

In code, a game is referred to as a Snapshot. This is the combination of the required blob and metadata for the saved game. To save the Snapshot, you need to obtain a reference to it, use the open() and writeBytes() methods to write current game data, and then use thecommitAndClose() method to save the Snapshot. The following snippet shows you how these methods are used to save a game:

private PendingResult<Snapshots.CommitSnapshotResult>
writeSnapshot(Snapshot snapshot,
byte[] data, Bitmap coverImage, String desc) {

// get the contents of the snapshot and write it
snapshot.getSnapshotContents().writeBytes(data);

// set the metadata change
SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder()
.setCoverImage(coverImage)
.setDescription(desc)
.build();

// commit the snapshot
return Games.Snapshots.commitAndClose(myGoogleApiClient, snapshot, metadataChange);
}

To load a saved game, you should use an asynchronous method to move the process from the main thread. This can be done using AsyncTask with an override to the doInBackground() method. You can then call the load() method:

private byte[] mySaveGameData;

void loadFromSnapshot() {
// Consider using a loading message or widget here

AsyncTask<Void, Void, Integer> task =
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... params) {
/*
* Open the saved game using myCurrentSaveName
* using "true" as the third argument of open()
* will create a save game if one has not already been created
*/
Snapshots.OpenSnapshotResult result = Games.Snapshots
.open(myGoogleApiClient, myCurrentSaveName, true).await();

// did the open method work?
if (result.getStatus().isSuccess()) {
Snapshot snapshot = result.getSnapshot();
try {
// read the byte content of the saved game.
mySaveGameData = snapshot.getSnapshotContents().readFully();
} catch (IOException e) {
// Logging the IO error
Log.e(TAG, "Error while reading Snapshot.", e);
}
} else {
// Logging the status code error
Log.e(TAG, "Error while loading: " +
result.getStatus().getStatusCode());
}
return result.getStatus().getStatusCode();
}

@Override
protected void onPostExecute(Integer status) {
// Close the loading message or progress dialog if used
// and update the UI
}
};
task.execute();
}

If you do not want to implement your own design for handling game loading, you can use an out-of-the-box solution that is provided by the Google Play Games Services. This is launched by using two methods that call an Intent that displays any saved games the user has, and may allow the user to delete or create a new saved game based on arguments passed to the methods. The following snippet demonstrates how to show the Saved Games UI as well as how to use the onActivityResult() method to handle creating a new save or loading an existing one:

// display the Saved Games UI
// RC_SAVED_GAMES is set to an int to identify it
private static final int RC_SAVED_GAMES = 1003;

private void showSavedGamesUI() {
// set number of saves to show
int maxNumberOfSavedGamesToShow = 3;
// args 3 and 4 represent allowAddButton and allowDelete
Intent savedGamesIntent =
Games.Snapshots.getSelectSnapshotIntent(myGoogleApiClient,
"See Saved Games", true, true, maxNumberOfSavedGamesToShow);
startActivityForResult(savedGamesIntent, RC_SAVED_GAMES);
}

// save a new game or load an existing one
// start by creating a temp snapshot
private String myCurrentSaveName = "snapshotTemp";


// this callback is triggered after startActivityForResult() is called
// from the showSavedGamesUI() method.

@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent intent) {

if (intent != null) {
if (intent.hasExtra(Snapshots.EXTRA_SNAPSHOT_METADATA)) {
// load a snapshot
SnapshotMetadata snapshotMetadata = (SnapshotMetadata)
intent.getParcelableExtra(Snapshots.EXTRA_SNAPSHOT_METADATA);
// avoid hardcoding names, use the name from the snapshot
myCurrentSaveName = snapshotMetadata.getUniqueName();

// continue logic here to load the game data from the Snapshot

} else if (intent.hasExtra(Snapshots.EXTRA_SNAPSHOT_NEW)) {
// Create a new snapshot, name it with a unique string
String unique = new BigInteger(281, new Random()).toString(13);
myCurrentSaveName = "snapshotTemp-" + unique;

// continue the create the new snapshot logic
}
}
}

If you need further references on how to implement this logic into your game, visit the package docs at https://developers.google.com/android/reference/com/google/android/gms/games/snapshot/package-summary. You can also view the “SavedGames” code sample athttps://github.com/playgameservices/android-basic-samples/tree/master/BasicSamples/SavedGames. This sample also includes how to migrate data from the old Cloud Save service to the Saved Games service that is part of the Google Play Games Services.

Summary

In this chapter, you learned the basics of working with account details by using the AccountManager. This was done by specifying a package name that matched some Google accounts, and a name for the user account was retrieved. You also learned that there may be multiple accounts on a device and that you should get them all and either return a list or allow the user to choose which one to use.

You also learned about the Android Backup Service. This service allows you to make small backups that will restore user settings when they have to wipe, hard-reset, or set up a new device. You learned that this is not a suitable service for data synchronization, but is a helpful and free solution for minor data restoration needs.

You learned about the Google Drive Android API that can be used to load files to and from a device using Google Drive. You learned about the benefits of using this service because it allows for a seamless integration for mobile users who are constantly moving in and out of data or network range. You also learned how to read and write files using this API.

Finally, you were shown a portion of the Google Play Games Services. These services offer a lot of methods and libraries to help make game development easier. You learned how to save games using a snapshot, how to load a game, and how to use the built-in UI solution for performing both saves and loading games to a device.