ARCore is a platform for building augmented reality apps on Android. Cloud Anchors gives you the ability to create AR apps that share a common frame-of-reference, enabling multiple users to place virtual content in the same real world location that can be seen on different devices in the same position and orientation relative to the environment.

This codelab guides you through modifying a pre-existing ARCore app to use the Cloud Anchors APIs, and demonstrates how you can create a shared AR experience.

ARCore Anchors and Cloud Anchors

A fundamental concept in ARCore is that of an Anchor, which describes an approximation of a fixed location and orientation in the real world. Anchor poses are automatically adjusted by ARCore as its understanding of the environment improves based on the device's motion.

Cloud Anchors are Anchors that are hosted in the cloud and can be used (or resolved) by multiple users to establish a common frame of reference across users and their devices.

Hosting an Anchor

When an Anchor is hosted, the anchor's pose and limited data about the user's physical surroundings is uploaded to the ARCore Cloud Anchor Service. Once an anchor is hosted successfully, the anchor becomes a Cloud Anchor and is assigned a unique cloud anchor ID.

Exchanging Cloud Anchor IDs

ARCore apps that use Cloud Anchors are responsible for sharing and exchanging app specific cloud anchor IDs with other users in the same location. In this codelab, anchor IDs are exchanged using Firebase. App developers are free to share anchor IDs using other means.

Resolving an anchor

Once a cloud anchor ID for a previously hosted anchor is shared with another user, that user can use the Cloud Anchor API to create a local anchor by resolving the provided Cloud Anchor ID. This creates a new anchor with the original anchor's pose, provided that the two devices are present in the same physical environment as the original hosting device, and that the Cloud Anchor has not expired. Cloud Anchors expire after 24 hours.

What you will build

In this codelab, you're going to build upon a pre-existing ARCore app. By the end of the codelab, your app will:

  • Be able to host anchors to the Google ARCore Cloud, and obtain a Cloud Anchor ID for anchors.
  • Save anchor IDs on the device for easy retrieval using Android SharedPreferences.
  • Use saved anchor IDs to resolve previously hosted anchors. This makes it easy for us to simulate a multi user experience with a single device for the purposes of this codelab.
  • Share anchor IDs with another device running the same app, so multiple users see the Android statue in the same position.

What you'll learn

What you'll need

Setting up the development machine

Connect your ARCore device to your computer via the USB cable. Make sure that your device allows USB debugging. Open a terminal and run adb devices, as shown below:

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

The <DEVICE_SERIAL_NUMBER> will be a string unique to your device. Make sure that you see exactly one device before continuing.

Download the latest ARCore APK from the ARCore SDK for Android releases page:

If you're using...

Then download...

A (physical) supported ARCore device...

ARCore_1_2.apk

The Android Emulator from Android Studio...

ARCore_1_2_x86_for_emulator.apk

Install the downloaded APK onto your device using adb:

adb install -r ARCore_1_2*.apk

Downloading and installing the Code

You can either clone the github repository as follows:

$ git clone https://github.com/googlecodelabs/arcore-cloud-anchors

Or download a zip file, which you will need to extract:

Download ZIP

There are four Gradle projects, one for each part's solution.

Make a copy of the part-1 directory, and call it work. You'll be working in the work directory for the duration of the codelab. If you get stuck, you can refer to each of the partially completed solutions in the part-1 through part-4 directories.

Open the work project in Android Studio. If you see a dialog recommending that you upgrade the Android Gradle Plugin, click Update:

Click on Run > Run 'app'. Make sure you can see your device in the Select Deployment Target menu that pops up. Select your device and click OK. Android Studio will build the initial app and run it on your device.

When you run the app for the first time, it will request the CAMERA permission. Tap ALLOW to continue.

How to use the app

  1. Move the device around to help the app find a plane. Planes are shown as a triangulated mesh when they are found.
  2. Tap somewhere on the plane to place an anchor. An Android figure will be drawn where the anchor was placed.
  3. Move the device around. The figure should appear to stay in the same place even though the device is moving around.
  4. Press the CLEAR button to remove the anchor. This will allow you to place another anchor. This app only allows for the placement of one anchor at a time.

This app, right now, is only using the motion tracking provided by ARCore to track an anchor in a single run of the app. If you decide to quit the app, kill it, and then restart the app, the previously placed anchor and any information related to it, including its pose, is lost..

In the next few sections, we're going to build on this app to see how anchors can be shared across AR sessions.

In this section you will modify the work project to host an anchor. Before we write code, we need to implement a few modifications to the app's configuration.

Declare INTENRET permissions

Because Cloud Anchors uses a cloud service, your app must have permission to access the internet. Add the following line just below the android.permission.CAMERA permission declaration in your AndroidManifest.xml file:

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

<!-- Add the line below -->
<uses-permission android:name="android.permission.INTERNET"/>

Add an API Key

To use cloud anchors, you'll need to add a Google ARCore Cloud Anchors API Key to your app.

  1. Obtain an API Key. See Setting up API keys in the Google Cloud Platform Console Help Center if you are new to working with API keys.
  2. Enable the ARCore Cloud Anchor API for your Google Cloud Platform project.
  3. In Android Studio, add your ARCore Cloud Anchors API key to your project. To do this, include the API key in the following code and add it to your AndroidManifest.xml file as follows:
<meta-data android:name="com.google.ar.core" android:value="required" />

<!-- Add the lines below -->
<meta-data
   android:name="com.google.android.ar.API_KEY"
   android:value="<YOUR_API_KEY>"/>

ARCore Config

Next, we will modify the app to create a hosted anchor on a user tap instead of a regular one. For that you need to configure the ARCore Session and enable support for Cloud Anchors.

In the MainActivity class, find the onResume() method. There are a couple of lines of code in there that create an ARCore Config object, and then configure ARCore with the default config. Change those two lines to enable cloud anchor mode:

Config config = new Config(session);
config.setCloudAnchorMode(Config.CloudAnchorMode.ENABLED); // Add this line.
session.configure(config);

Creating a Hosted Anchor

It's time to create a hosted anchor that will be uploaded to the Google ARCore Cloud Service.

First, create the following new enum and field in MainActivity:

private enum AppAnchorState {
  NONE,
  HOSTING,
  HOSTED
}

@GuardedBy("singleTapAnchorLock")
private AppAnchorState appAnchorState = AppAnchorState.NONE;

Find the handleTapOnDraw() method in the MainActivity class, and look at the code there. You should see the following lines:

Anchor newAnchor = hit.createAnchor();
setNewAnchor(newAnchor);

Modify these two lines as follows:

// Create a hosted anchor from a standard anchor.
Anchor newAnchor = session.hostCloudAnchor(hit.createAnchor()); // Change this line.
setNewAnchor(newAnchor);

// Add some UI to show you that the anchor is being hosted.
appAnchorState = AppAnchorState.HOSTING; // Add this line.
snackbarHelper.showMessage(this, "Now hosting anchor..."); // Add this line.

The hostCloudAnchor() method schedules a background task that will begin the hosting process, but the method call returns immediately, so the app's UI thread is not blocked. You can check to see if the anchor was successfully hosted or not by polling the state of the anchor after every session.update() call.

Showing the Cloud Anchor ID

Create a new checkUpdatedAnchor() method in MainActivity, and then invoke the new method from the onDrawFrame method as follows:

private void checkUpdatedAnchor() {
  // We'll fill this in later...
}

@Override
public void onDrawFrame(GL10 gl) {
  ...
  TrackingState cameraTrackingState = camera.getTrackingState();
  checkUpdatedAnchor(); // Add this line.
  ...
}

The new checkUpdatedAnchor() method will now be executed on every frame update. ARCore sets the anchor's cloud anchor state and cloud anchor ID at the successful completion of the hosting process.

We want to keep track of the current state of the anchor to make sure that we only handle a hosting request success or failure exactly once.

Modify the setNewAnchor() method:

@GuardedBy("singleTapAnchorLock")
private void setNewAnchor(@Nullable Anchor newAnchor) {
  ...
    anchor = newAnchor;
    appAnchorState = AppAnchorState.NONE; // Add this line.
    snackbarHelper.hide(this); // Add this line.
  ...
}

Also modify the handleTapOnDraw() method: Add an appAnchorState == AppAnchorState.NONE extra condition to the if-block:

private void handleTapOnDraw(TrackingState currentTrackingState, Frame currentFrame) {
    synchronized (singleTapLock) {
      synchronized (anchorLock) {
        if (anchor == null
           && queuedSingleTap != null
            && currentTrackingState == TrackingState.TRACKING
            && appAnchorState == AppAnchorState.NONE) { // Add this condition.
          ...

Implement the checkUpdatedAnchor method to see if the anchor was successfully hosted or not.

private void checkUpdatedAnchor() {
  synchronized (singleTapAnchorLock) {
    if (appAnchorState != AppAnchorState.HOSTING) {
      return;
    }
    Anchor.CloudAnchorState cloudState = anchor.getCloudAnchorState();
    if (cloudState.isError()) {
      snackbarHelper.showMessageWithDismiss(this, "Error hosting anchor: " + cloudState);
      appAnchorState = AppAnchorState.NONE;
    } else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
      snackbarHelper.showMessageWithDismiss(
          this, "Anchor hosted successfully! Cloud ID: " + anchor.getCloudAnchorId());
      appAnchorState = AppAnchorState.HOSTED;
    }
  }
}

Run your app from Android Studio again. You should see the message "Now hosting anchor..." when you place an anchor. You should see another message when the hosting completes successfully. If you see "Error hosting anchor: ERROR_NOT_AUTHORIZED", verify that your AndroidManifest.xml contains a valid API key.

Immediately after placing an anchor

After waiting for a bit

Anyone who knows the anchor ID and is present in the same physical space as the anchor can use the anchor ID to create an anchor at the exact same pose (position and orientation) relative to the environment around them.

However, the anchor ID is long, and not easy for another user to enter manually. In the following sections, we show how to store anchor IDs in an easy-to-retrieve fashion in order to allow anchor recreation on the same or another device.

In this part, we assign short codes to the long anchor IDs. We store the anchor IDs as values in a key-value map, and the short codes are the keys in that map. This map will be stored in your app's private storage space using the Shared Preferences API, and will persist even if the app is killed and restarted.

Creating a StorageManager

Create a new class called StorageManager in the com.google.ar.core.codelab.cloudanchor package. This class has methods for:

  1. Creating new short codes
  2. Storing Cloud Anchor IDs for a given short code.
  3. Retrieving a Cloud Anchor ID for a given short code.

The SharedPreferences API makes this very simple to do. Copy-paste the following code snippet into the new file you created for StorageManager.

package com.google.ar.core.codelab.cloudanchor;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;

/** Helper class for managing on-device storage of cloud anchor IDs. */
class StorageManager {
  private static final String NEXT_SHORT_CODE = "next_short_code";
  private static final String KEY_PREFIX = "anchor;";
  private static final int INITIAL_SHORT_CODE = 142;

  /** Gets a new short code that can be used to store the anchor ID. */
  int nextShortCode(Activity activity) {
    SharedPreferences sharedPrefs = activity.getPreferences(Context.MODE_PRIVATE);
    int shortCode = sharedPrefs.getInt(NEXT_SHORT_CODE, INITIAL_SHORT_CODE);
    // Increment and update the value in sharedPrefs, so the next code retrieved will be unused.
    sharedPrefs.edit().putInt(NEXT_SHORT_CODE, shortCode + 1)
        .apply();
    return shortCode;
  }

  /** Stores the cloud anchor ID in the activity's SharedPrefernces. */
  void storeUsingShortCode(Activity activity, int shortCode, String cloudAnchorId) {
    SharedPreferences sharedPrefs = activity.getPreferences(Context.MODE_PRIVATE);
    sharedPrefs.edit().putString(KEY_PREFIX + shortCode, cloudAnchorId).apply();
  }

  /**
   * Retrieves the cloud anchor ID using a short code. Returns an empty string if a cloud anchor ID
   * was not stored for this short code.
   */
  String getCloudAnchorID(Activity activity, int shortCode) {
    SharedPreferences sharedPrefs = activity.getPreferences(Context.MODE_PRIVATE);
    return sharedPrefs.getString(KEY_PREFIX + shortCode, "");
  }
}

Note that the class and associated methods are package-private, because they are only used by MainActivity.

Using StorageManager

Here we modify MainActivity to use StorageManager to store anchor IDs with short codes, so they can be retrieved easily.

Create the following new field in MainActivity:

private final StorageManager storageManager = new StorageManager();

Now, modify the checkUpdatedAnchor() method to use the StorageManager. Modify the code in the cloudState == Anchor.CloudAnchorState.SUCCESS case as follows:

  ...
} else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
  int shortCode = storageManager.nextShortCode(this); // Add this line.
  storageManager.storeUsingShortCode(this, shortCode, anchor.getCloudAnchorId()); // Add this line.

  snackbarHelper.showMessageWithDismiss(
    this, "Anchor hosted successfully! Cloud Short Code: " + shortCode); // Change this line.
  appAnchorState = AppAnchorState.HOSTED;
}

Now, run the app from Android Studio. You should see short codes being displayed instead of the long anchor IDs when you create and host an anchor.

Immediately after placing an anchor

After waiting for a bit

Note that the short codes generated by StorageManager are currently always assigned in increasing order.

Next we add a few UI elements that will allow us to enter short codes and recreate the anchors.

Adding ResolveDialogFragment

We're going to add another button next to the CLEAR button. This will be the RESOLVE button. Clicking the RESOLVE button opens a dialog box that prompts the user for a short code. The short code is used to retrieve the anchor ID from the app's SharedPreferences, and resolve the anchor.

To make the UI, you need to make some changes to the strings.xml and activity_main.xml files in your project.

Add the following strings to your strings.xml file:

<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="resolve_dialog_title">Resolve Anchor</string>
<string name="resolve_button_text">Resolve</string>

Add the following XML element to your activity_main.xml layout file, after the existing Button element. Use the Text view rather than the Design view to edit the XML directly.

<Button
    android:id="@+id/resolve_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/resolve_button_text"/>

Now add a new class called ResolveDialogFragment to the com.google.ar.core.codelab.cloudanchor package. This represents the Dialog box that will be created when the RESOLVE button is pressed. You can copy-paste the following code snippet into the new file you created for ResolveDialogFragment.

package com.google.ar.core.codelab.cloudanchor;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.text.Editable;
import android.text.InputType;
import android.text.InputFilter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;

/** A DialogFragment for the Resolve Dialog Box. */
public class ResolveDialogFragment extends DialogFragment {

  interface OkListener {
    void onOkPressed(String dialogValue);
  }

  private OkListener okListener;
  private EditText shortCodeField;

  /** Sets a listener that is invoked when the OK button on this dialog is pressed. */
  void setOkListener(OkListener okListener) {
    this.okListener = okListener;
  }

  /**
   * Creates a simple layout for the dialog. This contains a single user-editable text field whose
   * input type is retricted to numbers only, for simplicity.
   */
  private LinearLayout getDialogLayout() {
    Context context = getContext();
    LinearLayout layout = new LinearLayout(context);
    shortCodeField = new EditText(context);
    shortCodeField.setInputType(InputType.TYPE_CLASS_NUMBER);
    shortCodeField.setLayoutParams(
        new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
    shortCodeField.setFilters(new InputFilter[]{new InputFilter.LengthFilter(8)});
    layout.addView(shortCodeField);
    layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
    return layout;
  }

  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder
        .setView(getDialogLayout())
        .setTitle(R.string.resolve_dialog_title)
        .setPositiveButton(
            R.string.ok,
            (dialog, which) -> {
              Editable shortCodeText = shortCodeField.getText();
              if (okListener != null && shortCodeText != null && shortCodeText.length() > 0) {
                // Invoke the callback with the current checked item.
                okListener.onOkPressed(shortCodeText.toString());
              }
            })
        .setNegativeButton(R.string.cancel, (dialog, which) -> {});
    return builder.create();
  }
}

Add a new method called onResolveOkPressed() to MainActivity, and then add a callback for the new RESOLVE button in the UI.

private void onResolveOkPressed(String dialogValue) {
  // TODO: We'll add more code here later...
}

@Override
public void onCreate(Bundle savedInstance) {
  ...

  Button clearButton = findViewById(R.id.clear_button);
  clearButton.setOnClickListener(
      (unusedView) -> {
        synchronized (singleTapAnchorLock) {
          setNewAnchor(null);
        }
      });


  // Add these lines:
  Button resolveButton = findViewById(R.id.resolve_button);
  resolveButton.setOnClickListener(
    (unusedView) -> {
      synchronized (singleTapAnchorLock) {
        if (anchor != null) {
          snackbarHelper.showMessageWithDismiss(this, "Please clear anchor first.");
          return;
        }
      }
      ResolveDialogFragment dialog = new ResolveDialogFragment();
      dialog.setOkListener(this::onResolveOkPressed);
      dialog.show(getSupportFragmentManager(), "Resolve");
    });
}

Before proceeding further, run your app from Android Studio and make sure that it builds. Try clicking on the new RESOLVE button you see.

New RESOLVE button in the top-left

The dialog shown when the RESOLVE button is pressed.

Clicking on OK in the Resolve Dialog does nothing yet. You'll now add code to make it functional.

Resolving Anchors

Now, we can finally use all of this work to resolve anchors previously hosted. We will use the Resolve Dialog to get a short code from the user, use that short code to query the StorageManager for the cloud anchor ID, and then use that cloud anchor ID to resolve an anchor.

To make this happen, we will implement the onResolveOkPressed() method you added to the MainActivity class in the previous section.

Add two new values to the AppAnchorState enum the MainActivity class:

private enum AppAnchorState {
  NONE, HOSTING, HOSTED,

  // Add these two values:
  RESOLVING, RESOLVED
}

Implement the onResolveOkPressed() method in your file as follows:

private void onResolveOkPressed(String dialogValue) {
  int shortCode = Integer.parseInt(dialogValue);
  String cloudAnchorId = storageManager.getCloudAnchorID(this, shortCode);
  synchronized (singleTapAnchorLock) {
    Anchor resolvedAnchor = session.resolveCloudAnchor(cloudAnchorId);
    setNewAnchor(resolvedAnchor);
    snackbarHelper.showMessage(this, "Now resolving anchor...");
    appAnchorState = AppAnchorState.RESOLVING;
  }
}

However, we now need to modify the checkUpdatedAnchor() method to also check for the cloud state of the anchor if it is being resolved. Use new implementation of the method given below to update the method in your file:

private void checkUpdatedAnchor() {
  synchronized (singleTapAnchorLock) {
    if (appAnchorState != AppAnchorState.HOSTING && appAnchorState != AppAnchorState.RESOLVING) {
      return;
    }
    Anchor.CloudAnchorState cloudState = anchor.getCloudAnchorState();
    if (appAnchorState == AppAnchorState.HOSTING) {
      if (cloudState.isError()) {
        snackbarHelper.showMessageWithDismiss(this, "Error hosting anchor: " + cloudState);
        appAnchorState = AppAnchorState.NONE;
      } else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
        int shortCode = storageManager.nextShortCode(this);
        storageManager.storeUsingShortCode(this, shortCode, anchor.getCloudAnchorId());
        snackbarHelper.showMessageWithDismiss(
            this, "Anchor hosted successfully! Cloud Short Code: " + shortCode);
        appAnchorState = AppAnchorState.HOSTED;
      }
    } else if (appAnchorState == AppAnchorState.RESOLVING) {
      if (cloudState.isError()) {
        snackbarHelper.showMessageWithDismiss(this, "Error resolving anchor: " + cloudState);
        appAnchorState = AppAnchorState.NONE;
      } else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
        snackbarHelper.showMessageWithDismiss(this, "Anchor resolved successfully!");
        appAnchorState = AppAnchorState.RESOLVED;
      }
    }
  }
}

We now have different logic for handling updates for anchors being hosted or anchors being resolved. An alternative would have been to split these two into different methods, with one checking only for hosting anchors, and one checking only for resolving anchors, but even with both the code paths, the function is still less than 30 lines of code.

Now, run the app from Android Studio, and perform the following steps:

  1. Create an anchor on a plane and wait for the anchor to be hosted.
    Remember the short code.
  2. Press the CLEAR button to delete the anchor.
  3. Press the RESOLVE button. Enter the short code from (1).
  4. You should see an anchor in the same position relative to the enviornment as you originally placed it.
  5. Quit and kill the app, and then open it again.
  6. Repeat steps (3) and (4). You should see a new anchor, again in the same position.

Entering a short code

Anchor is successfully resolved

Congratulations! You can now make AR anchors persist in the real world using ARCore Cloud Anchors!

You've seen how you can store the cloud anchor ID of an anchor to your device's local storage, and then retrieve it later to recreate the same anchor. But the full potential of Cloud Anchors is only unlocked when you can share the cloud anchor IDs between different devices.

The mechanism of sharing the cloud anchor IDs is up to the app developer and anything can be used to transfer the string from one device to another. For this codelab, we use the Firebase Realtime Database to transfer anchor IDs between instances of the app.

Setting up Firebase

You need to set up a Firebase Realtime Database with your Google account to use with this app. This is easy with the Firebase Assistant in Android Studio.

In Android Studio, click on Tools > Firebase. In the menu that pops up, click on Realtime Database, then click on Save and retrieve data:

Click on the Connect to Firebase button to connect your Android Studio project to a new or existing Firebase project.

Once you've signed in with your Google account and successfully connected your project, review the instructions linked from the configure your rules for public access link to configure your Firebase Realtime Database to be world writable. This helps simplify testing in this codelab:

From the Firebase Console (https://console.firebase.google.com/), select the project you connected your Android Studio project to, then select DEVELOP > Database.

Click GET STARTED to configure and setup the Realtime Database:

If offered, select the test mode security rules and click ENABLE:

At any time, you can use the Datbase RULES tab to change your database's security rules:

Your app is now configured to use the Firebase database.

Modifying the StorageManager class

In this section we modify the StorageManager class to use Firebase as a storage mechanism instead of local on-device storage. Because Firebase query results are normally available through callbacks instead of synchronously returned values, we modify the methods of StorageManager.

Replace the contents of your existing StorageManager.java file with the following:

package com.google.ar.core.codelab.cloudanchor;

import android.content.Context;
import android.util.Log;
import com.google.firebase.FirebaseApp;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;

/** Helper class for Firebase storage of cloud anchor IDs. */
class StorageManager {

  /** Listener for a new Cloud Anchor ID from the Firebase Database. */
  interface CloudAnchorIdListener {
    void onCloudAnchorIdAvailable(String cloudAnchorId);
  }

  /** Listener for a new short code from the Firebase Database. */
  interface ShortCodeListener {
    void onShortCodeAvailable(Integer shortCode);
  }

  private static final String TAG = StorageManager.class.getName();
  private static final String KEY_ROOT_DIR = "shared_anchor_codelab_root";
  private static final String KEY_NEXT_SHORT_CODE = "next_short_code";
  private static final String KEY_PREFIX = "anchor;";
  private static final int INITIAL_SHORT_CODE = 142;
  private final DatabaseReference rootRef;

  StorageManager(Context context) {
    FirebaseApp firebaseApp = FirebaseApp.initializeApp(context);
    rootRef = FirebaseDatabase.getInstance(firebaseApp).getReference().child(KEY_ROOT_DIR);
    DatabaseReference.goOnline();
  }

  /** Gets a new short code that can be used to store the anchor ID. */
  void nextShortCode(ShortCodeListener listener) {
    // Run a transaction on the node containing the next short code available. This increments the
    // value in the database and retrieves it in one atomic all-or-nothing operation.
    rootRef
        .child(KEY_NEXT_SHORT_CODE)
        .runTransaction(
            new Transaction.Handler() {
              @Override
              public Transaction.Result doTransaction(MutableData currentData) {
                Integer shortCode = currentData.getValue(Integer.class);
                if (shortCode == null) {
                  shortCode = INITIAL_SHORT_CODE - 1;
                }
                currentData.setValue(shortCode + 1);
                return Transaction.success(currentData);
              }

              @Override
              public void onComplete(
                  DatabaseError error, boolean committed, DataSnapshot currentData) {
                if (!committed) {
                  Log.e(TAG, "Firebase Error", error.toException());
                  listener.onShortCodeAvailable(null);
                } else {
                  listener.onShortCodeAvailable(currentData.getValue(Integer.class));
                }
              }
            });
  }

  /** Stores the cloud anchor ID in the configured Firebase Database. */
  void storeUsingShortCode(int shortCode, String cloudAnchorId) {
    rootRef.child(KEY_PREFIX + shortCode).setValue(cloudAnchorId);
  }

  /**
   * Retrieves the cloud anchor ID using a short code. Returns an empty string if a cloud anchor ID
   * was not stored for this short code.
   */
  void getCloudAnchorID(int shortCode, CloudAnchorIdListener listener) {
    rootRef
        .child(KEY_PREFIX + shortCode)
        .addListenerForSingleValueEvent(
            new ValueEventListener() {
              @Override
              public void onDataChange(DataSnapshot dataSnapshot) {
                listener.onCloudAnchorIdAvailable(String.valueOf(dataSnapshot.getValue()));
              }

              @Override
              public void onCancelled(DatabaseError error) {
                Log.e(TAG, "The database operation for getCloudAnchorID was cancelled.",
                    error.toException());
                listener.onCloudAnchorIdAvailable(null);
              }
            });
  }
}

You aren't required to pay attention to this code, but it's not too complicated if you want to read it.

Using the new Firebase StorageManager

We now need to modify the code in MainActivity to use this new StorageManager, since the method signatures have changed, and now use callbacks.

First of all, we have to change how the StorageManager is instantiated. It can no longer be final, and has to be constructed at the end of the onCreate method in MainActivity, as follows:

private StorageManager storageManager; // Change this line.

public void onCreate(Bundle savedInstanceState) {
  ...

  storageManager = new StorageManager(this); // Add this line.
}

Next, we need to modify the way it is used in the checkUpdatedAnchor() method. Replace the cloudState == Anchor.CloudAnchorState.SUCCESS if-block with the following:

  ...
} else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
  storageManager.nextShortCode(
    (shortCode) -> {
      if (shortCode == null) {
        snackbarHelper.showMessageWithDismiss(this, "Could not obtain a short code.");
        return;
      }
      synchronized (singleTapAnchorLock) {
        storageManager.storeUsingShortCode(shortCode, anchor.getCloudAnchorId());
        snackbarHelper.showMessageWithDismiss(
            this, "Anchor hosted successfully! Cloud Short Code: " + shortCode);
      }
    });
    appAnchorState = AppAnchorState.HOSTED;
}

Instead of using a synchronously returned value, we now define a callback which is invoked with the short code as an argument when it is available.

Modify the onResolveOkPressed() method to be the following:

private void onResolveOkPressed(String dialogValue) {
  int shortCode = Integer.parseInt(dialogValue);
  storageManager.getCloudAnchorID(
      shortCode,
      (cloudAnchorId) -> {
        synchronized (singleTapAnchorLock) {
          Anchor resolvedAnchor = session.resolveCloudAnchor(cloudAnchorId);
          setNewAnchor(resolvedAnchor);
          snackbarHelper.showMessage(this, "Now resolving anchor...");
          appAnchorState = AppAnchorState.RESOLVING;
        }
      });
}

Build and run your app. You should see the same UI flow as in the previous section, except that now, the Firebase database is being used to store the cloud anchor IDs and short codes, instead of device-local storage.

Multi-user testing

To test what a multi-user experience is like, use two different phones:

  1. Install the app on two devices.
  2. Use one device to host an anchor and generate a short code.
  3. Use the other device to resolve the anchor using that short code.

You should be able to host anchors from one device, get a short code, and use the short code on the other device to see the anchor in the same place!

Congratulations! You've reached the end of this codelab!