Implementing Whispersync for Games in your Android or Fire OS game


To integrate Whispersync for Games 2.2 in your game, follow the steps in this section. With Whispersync, you get and set values in a data map. It is the first solution to offer both auto-conflict resolution and player-choice conflict resolution as options, and it queues when a device is offline. You can set up the data map in just a few minutes.

The GameCircle SDK incorporates the Login with Amazon service to manage Whispersync authentication.

How Whispersync for Games Works

Prior to Whispersync for Games, many developers implemented separate methods to store game data to disk and to cloud. Whispersync replaces your local storage solution and provides the added benefit of background synchronization with the cloud and Android & Amazon Fire devices.

With the GameDataMap interface, you get and set numbers and strings, and organize them in lists and maps. GameDataMap is a first-class citizen that you can treat as a variable, pass into a method, or return from a method.

Here’s how you get your game data:

GameDataMap gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();

Unlike the GameCircle leaderboard and achievements clients, the Whispersync client is available immediately after initializing AmazonGamesClient. Whispersync data is always accessible, even when the player is not registered with Amazon or is offline.

GameDataMap provides several ways to access your data. For example, to retrieve a player’s highest score:

SyncableNumber highScore = gameDataMap.getHighestNumber("highScore");

In this example, because you retrieve highScore as a highest number, it will always reflect the maximum value ever assigned to it, from any device.

To set the high score:

// Where 1000 represents a player's score, not a maximum

Conflict Resolution Options

Amazon offers two conflict resolution options to provide the best possible player experience for each game:

  • Auto resolution, in which your game automatically syncs the best score (highest or lowest, depending on the game), most recent achievements, and most recent purchases across all devices and to the Amazon cloud, without further action on both the player and the developer’s part. To implement auto-conflict resolution, use any of the Whispersync data type described below, except for DeveloperString (which is used exclusively for manual conflict resolution).
  • Manual resolution via DeveloperString, in which the developer manually performs the conflict resolution. You should first get and deserialize both locally and remotely stored strings, and then set specific values via game logic to determine the current game state. If there is no easy way to auto-resolve, you can optionally prompt the player for input.

For implementation details of manual resolution via DeveloperString, see Example 4.

When Syncing Occurs

The GameCircle SDK automatically synchronizes Whispersync data with the cloud. With Whispersync, syncing occurs based on two event types:

  • Active sync events occur when you call the synchronize() method, and when a customer signs in to your game. However, syncing occurs even if you never call the synchronize() method, due to passive sync events.
  • Passive sync events occur any time that you choose to change game data, such as by setting a high score or accumulating number. Passive sync events are batched, rather than synced immediately. Whispersync throttles passive sync events to conserve network bandwidth and battery life.
  • Regardless of throttling, local storage is always synced. Whispersync stores local game data in your application’s storage space in an obfuscated text file.
  • Throttling does not cause data to be lost; the customer’s data is synced on the next active or passive sync attempt.
  • When throttling occurs, a callback is made to WhispersyncEventListener.onThrottled(), a Java class that you can choose to extend.
AmazonGamesClient.getWhispersyncClient().setWhispersyncEventListener(new WhispersyncEventListener() {
  public void onNewCloudData() {
     // refresh visible game data

  // The following three methods are mainly useful for debugging purposes and don't have to be overridden

  public void onDataUploadedToCloud() {

  public void onThrottled() {

  public void onDiskWriteComplete() {

Forcing a Sync

Use this code to force a sync:

  • Java: AmazonGamesClient.getWhispersyncClient().synchronize();`

  • C++, Using our JNI Interface: AmazonGames::WhispersyncClient::synchronize();`

Offline Scenarios

Whispersync easily manages offline scenarios involving mergeable data types. For example, say that a player starts a game online and unlocks six levels, each with a rating of one star. The player then might go offline with another device and play the first three levels again, and earn three stars on each level. When the offline device goes back online and syncs, both devices will then show three stars on levels 1 – 3 and one star on levels 4 – 6.

Syncable Data Types

The table below shows the data types that you can sync, the GameDataMap accessors for each data type, and sample use cases.

Table 1: Syncable Data Types
Whispersync Data Type Data Type Represents GameDataMap Accessors Sample Use Case
SyncableNumber A highest, lowest or most recent number. getHighestNumber Stars earned on level 7
getLowestNumber Fastest lap time
getLatestNumber Most recent lap time
SyncableNumberList A list of numbers in ascending, descending or latest order. getHighNumberList List of N best game scores
getLowNumberList List of N fastest lap times
getLatestNumberList List of N most recent scores
SyncableAccumulatingNumber A number that can be incremented and decremented. getAccumulatingNumber Total distanceRemaining health potions
SyncableString The most recent string. getLatestString Most recent car added to collection
SyncableStringList A list of strings, ordered by most recent. getLatestStringList Names of last N enemies encountered
SyncableStringSet A set of strings.* * Special Case This is an unbounded set of strings, to which items can only be added, as long as the set doesn’t duplicate an existing string. getStringSet Levels completed for a non-linear game
GameDataMap A nested map that enables hierarchical data. getMap A data map for each level in a game

Step 1. Set Up the Data You Want to Sync

Model your game data

Model your game data by using the basic types in the Syncable Data Types table. Add new accessors simply by retrieving them by name; if they don’t exist, they’ll be created on the fly and synchronized from that point on.

You can model both simple and complex game data by using Whispersync’s syncable data types, and, if necessary, by nesting data maps.

Start by setting the name and type of game data that you want to synchronize. It’s not strictly necessary to do this all in one place, or even before the variables are actually used. But having a single, authoritative definition can improve code clarity and make it easier to understand and maintain the structure of your game data.

Example 1: Maintaining a list of spells mastered

In the snippet below, for example, we can see at a glance what is stored and how the developer laid it out. The game maintains a list of all spells the player has ever mastered, as well as the last few power-ups collected. In addition, it will store data for each level, including the number of stars earned, fastest completion time, and total time played for each level.

public static final int NUM_LEVELS = 10;
public static final int MAX_POWERUPS = 5;

. . .

GameDataMap gameDataMap;
SyncableNumber starsEarned;
SyncableNumber fastestTime;
SyncableAccumulatingNumber totalTime;

protected void setupGameData() {
   // Save a reference to the root object for later use.
   gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();

   // Create a sub-map for each level.
   GameDataMap[] levelMaps = new GameDataMap[NUM_LEVELS];
   for(int i = 0; i < NUM_LEVELS; i++) {
	  levelMaps[i] = gameDataMap.getMap("level" + i);

	  // Track the highest score (stars) obtained for each level.
	  starsEarned = levelMaps[i].getHighestNumber("starsEarned");

	  // Remember the fast time to completion.
	  fastestTime = levelMaps[i].getLowestNumber("fastestTime");

	  // Record total time playing each level.
	  totalTime = levelMaps[i].getAccumulatingNumber("totalTime");

   // Maintain an inventory of spells mastered.  Spell names will be
   // added to this over time (and can never be removed).
   SyncableStringSet spells = gameDataMap.getStringSet("spells");

   // Establish the size of a FIFO list of power-ups.
   //This list will always hold the last MAX_POWERUPS power-ups.
   SyncableStringList powerUps = gameDataMap.getLatestStringList("powerUps");

Example 2: Tracking a player’s experience level

Here’s a second example employing getGameData(). The snippet looks up a player’s experience level and updates the level if certain criteria are met. In this case, any player who has performed magic is automatically elevated to magician’s apprentice.

// Get the global game data map for the current player.
GameDataMap gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();

// Look up the object representing the player’s experience level.
// If none exists, one will be created.
SyncableNumber experience = gameDataMap.getHighestNumber("experience");

// If the player has ever used a magic spell, they’re at least a
// magician’s apprentice.
if (hasUsedMagic() && MAGICIANS_APPRENTICE > experience.asLong()) {
  // As long as this device is online (or as soon as it is),
  // local and cloud storage will be synced.

Example 3: Creating hierarchical data

This example illustrates how to use embedded maps to create a hierarchical data structure. In this case, the top-level _GameDataMap_ singleton contains a child _GameDataMap_ object for each level in the game.

This method also shows how to look up the syncable object accessors corresponding to a particular type. You can use this approach to iterate over existing objects, rather than simply passing a string to one of the _GameDataMap_ accessors, which might have the undesirable side effect of instantiating a new syncable element.

GameDataMap getLevelData(String name) {
   GameDataMap gdm = AmazonGamesClient.getWhispersyncClient().getGameData();

   // Get all the string keys associated with GameDataMap objects.
   Set<String> maps = gdm.getMapKeys();

   // Look for a match among the maps.
   for (String s : maps) {
	  if (s.equals(name)) {
		 // A map exists for the name specified.
		 return gdm.getMap(s);

   // No match found. Don't create one.
   return null;

Example 4: Manually resolve conflicts via DeveloperString

To implement manual conflict resolution in your game, write code modeled on these samples:

// The map used by GameCircle to store all the syncable data types
private GameDataMap gameDataMap;

// The structure you are using to save all game data, such as levels unlocked and available coins
private GameData gameData;
private void initializeGameCircle() {
    // Get the GameDataMap
    gameDataMap = AmazonGamesClient.getWhispersyncClient().getGameData();

    // Create your own game data holder
    gameData = new GameData();

    // Set up listeners to handle conflicts after syncing with the cloud
    AmazonGamesClient.getWhispersyncClient().setWhispersyncEventListener(new WhispersyncEventListener() {
        public void onNewCloudData() {

        public void onDataUploadedToCloud() {

private void saveGameData() {
    // Create a developer string where player game data will be stored
    SyncableDeveloperString developerString = gameDataMap.getDeveloperString("gameData");

private void handlePotentialGameDataConflicts() {
    SyncableDeveloperString developerString = gameDataMap.getDeveloperString("gameData");

    // Once cloud data is available on the device, GameCircle can check for conflicts
    if (developerString.inConflict()) {
        // Deserialize both local and cloud strings
        GameData localValue = new GameData(developerString.getValue());
        GameData cloudValue = new GameData(developerString.getCloudValue());

        // Manually merge the local and the cloud value; the logic could be as simple as
        // picking one of them or asking the customer to do so
        GameData mergedValue = mergeGameData(localValue, cloudValue);

        // Set a new value and resolve the conflict
        // Mark the conflict as resolved, which enables GameCircle to save the final data to the cloud

// A class that holds your game data
class GameData {
    // Your game data
    private ArrayList<String> itemsEarned = new ArrayList<String>();
    private ArrayList<String> levelsUnlocked = new ArrayList<String>();
    private int coinsCollected;

    public GameData() {
        // Initialization logic

    public GameData(String serializedGameData) {
        // Game data deserialization logic.

    // Logic to serialize the string; all game data should be stored in a single string
    public String serialize() {
        String serializedGameData = "";

        // Game data serialization logic.

        return serializedGameData;

Step 2. Add Accessor Names to Your Code

The following code snippets demonstrate how to add accessors (getters) to your code.

Table 2: Whispersync Code Snippets
Accessor Name Code Snippet
SyncableNumber    HighestNumber

Lowest Number


     // Retrieve the highest number of stars earned on the current level.
	SyncableNumber starsEarned = gameDataMap.getHighestNumber("starsEarned");
	System.out.println("Stars earned on current level: " + starsEarned.asLong());  
	// Save the current lap time if it's the fastest one (lowest value).
	SyncableNumber fastestLapTime = gameDataMap.getLowestNumber("fastestLapTime");
	System.out.println("Fastest lap time so far: " + fastestLapTime.asDouble());  
	// Determine the id of the last obstacle blown up.
	SyncableNumber lastTargetId = gameDataMap.getLatestNumber("lastTargetId");
	System.out.println("Id of last thing destroyed: " + lastTargetId.asLong());  



	// Set the number of high scores to preserve.
	SyncableNumberList highScoresList = gameDataMap.getHighNumberList("highScoresList");
	. . .

	// Retrieve the a list of the highest scores recorded.
	SyncableNumberElement[] highScores = highScoresList.getValues();
	for(int i = 0; i < highScores.length; i++) {
	  System.out.println("Score #" + i + ": " + highScores[i].asLong());
	// Set the number of fastest lap times to preserve.
	SyncableNumberList fastestLapTimesList = gameDataMap.getLowNumberList("fastestLapTimesList");

	. . .

	// Retrieve the list of fastest laps clocked so far.
	SyncableNumberElement[] fastestLapTimes = fastestLapTimesList.getValues();
	for(SyncableNumberElement sne: fastestLapTimes) {
	  System.out.println("Lap time: " + sne.asDouble());
	// Get a list of the most recent items collected (defaults to length 3).
	SyncableNumberElement[] items = gameDataMap.getLatestNumberList("itemsList").getValues();
	for(int i = 0; i < items.length; i++ ) {
	  System.out.println("item[" + i + "] = " + items[i].asLong());

	. . .

	// Record an item whenever the player collects one.
	// Add to the total time spent solving the current puzzle.
	SyncableAccumulatingNumber totalTime = gameDataMap.getAccumulatingNumber("totalTime");

	. . .

	// Reduce a player's energy, clamping it to 0.0 if necessary.
	SyncableAccumulatingNumber energy = gameDataMap.getAccumulatingNumber("energy");
	if(0.0 > energy.asDouble())
	// Retrieve the value of the current level.
	SyncableString curLevelName = gameDataMap.getLatestString("curLevelName");
	System.out.println("The current level is called " + curLevelName);  
	// Set the number of phrases an NPC parrot will remember.
	SyncableStringList phrasesList = gameDataMap.getLatestStringList("phrasesList");

	. . .

	// Display the last few phrases the player has taught his parrot.
	SyncableStringElement[] phrases = phrasesList.getValues();
	for(int i = 0; i < phrases.length; i++) {
	  System.out.println("phrase[" + i + "] = " + phrases[i].getValue());

	. . .

	// Teach the player's parrot some new phrases.
	phrasesList.add("Ahoy, matey");
	// Display all enemies encountered in the game so far.
	Set<SyncableStringElement> allEnemies = gameDataMap.getStringSet("allEnemiesSet").getValues();
	for(SyncableStringElement sse : allEnemies) {
	  System.out.println("Encountered enemy: " + sse.getValue());
	// Nested maps can be used just like the root map
	GameDataMap antarctica = gameDataMap.getMap("Antarctica");

Step 3. Test Your Whispersync Implementation

You can test your Whispersync for Games implementation by using a single test device. Whispersync doesn’t require nickname whitelisting. If the game is registered, Whispersync will work.

Note: There is no mechanism for clearing Whispersync test data. Amazon recommends that you test Whispersync by using accounts that you’ve created just for testing.

Follow these steps to test Whispersync:

  1. On a test device, sign into your game.
  2. Play your game until you hit a milestone that you’ve set as a syncable event, such as reaching a new level.
  3. Save and close the game.
  4. On the device, navigate to Android’s Settings page and clear your game’s data.
  5. Sign into the game and observe whether the game presents the correct level.

During QA, you may want to know when throttling is occurring. To make throttling more visible during testing, you can implement an event listener, as shown in this snippet:

client.getWhispersyncClient().setWhispersyncEventListener(new WhispersyncEventListener() {
  public void onThrottled() {
	 Toast.makeText(StarGameActivity.this, "SLOW DOWN!", Toast.LENGTH_LONG).show();

Non-Syncable Data

Some types of game state data can’t be merged automatically. For example, say that a player starts a chess match on an online device, and then continues playing the game on two offline devices. The player will almost certainly make different moves on each offline device. When the two offline devices later go online, the individual moves and final game piece positions can’t be reconciled automatically among the three devices. For this type of data, consider using SyncableDeveloperString to manually resolve conflicts. Possible resolution strategies include:

  • Ask the player to select either the local or the cloud value: This is the recommended approach.
  • Select a value programmatically: This approach may work only for games that have measurable progress, such as the number of pieces placed in a jigsaw puzzle.

Best Practices

  • The best customer experience is to enable your customers to sync consumables. If you worry that some customers will use in-game currency to make multiple offline purchases, add code to determine if a device is offline and then block offline purchases.
  • Don’t store personally identifiable information by using Whispersync for Games. Amazon may revoke your access to GameCircle if you use Whispersync to store any data other than game metadata.
  • Calling WhispersyncClient.flush() forces game data to be written to the device’s file system. You don’t need to call flush() each time you change your game data, because changes are automatically persisted to local storage via a background thread. However, if you know your app is about to close, calling flush() will save your game data by performing a blocking write to the file system.
  • Store binary data (byte array) to a String by using Base64, and then use the SyncableString data type to store the data in Whispersync. Do not use device-specific formatting for your data, as Whispersync can synchronize across different Android devices.