How to create and locate anchors using Azure Spatial Anchors in Java

Azure Spatial Anchors allow you to share anchors in the world between different devices. It supports several different development environments. In this article, we'll dive into how to use the Azure Spatial Anchors SDK, in Java, to:

  • Correctly set up and manage an Azure Spatial Anchors session.
  • Create and set properties on local anchors.
  • Upload them to the cloud.
  • Locate and delete cloud spatial anchors.

Prerequisites

To complete this guide, make sure you have:

Cross Platform

Initialize the session

The main entry point for the SDK is the class representing your session. Typically you'll declare a field in the class that manages your view and native AR session.

Learn more about the CloudSpatialAnchorSession class.

    private CloudSpatialAnchorSession mCloudSession;
    // In your view handler
    mCloudSession = new CloudSpatialAnchorSession();

Set up authentication

To access the service, you need to provide an account key, access token, or Microsoft Entra auth token. You can also read more about this in the Authentication concept page.

Account Keys

Account Keys are a credential that allows your application to authenticate with the Azure Spatial Anchors service. The intended purpose of Account Keys is to help you get started quickly. Especially during the development phase of your application's integration with Azure Spatial Anchors. As such, you can use Account Keys by embedding them in your client applications during development. As you progress beyond development, it's highly recommended to move to an authentication mechanism that is production-level, supported by Access Tokens, or Microsoft Entra user authentication. To get an Account Key for development, visit your Azure Spatial Anchors account, and navigate to the "Keys" tab.

Learn more about the SessionConfiguration class.

    mCloudSession.getConfiguration().setAccountKey("MyAccountKey");

Access Tokens

Access Tokens are a more robust method to authenticate with Azure Spatial Anchors. Especially as you prepare your application for a production deployment. The summary of this approach is to set up a back-end service that your client application can securely authenticate with. Your back-end service interfaces with AAD at runtime and with the Azure Spatial Anchors Secure Token Service to request an Access Token. This token is then delivered to the client application and used in the SDK to authenticate with Azure Spatial Anchors.

    mCloudSession.getConfiguration().setAccessToken("MyAccessToken");

If an access token isn't set, you must handle the TokenRequired event, or implement the tokenRequired method on the delegate protocol.

You can handle the event synchronously by setting the property on the event arguments.

Learn more about the TokenRequiredListener interface.

    mCloudSession.addTokenRequiredListener(args -> {
        args.setAccessToken("MyAccessToken");
    });

If you need to execute asynchronous work in your handler, you can defer setting the token by requesting a deferral object and then completing it, as in the following example.

    mCloudSession.addTokenRequiredListener(args -> {
        CloudSpatialAnchorSessionDeferral deferral = args.getDeferral();
        MyGetTokenAsync(myToken -> {
            if (myToken != null) args.setAccessToken(myToken);
            deferral.complete();
        });
    });

Microsoft Entra authentication

Azure Spatial Anchors also allows applications to authenticate with user Microsoft Entra ID (Active Directory) tokens. For example, you can use Microsoft Entra tokens to integrate with Azure Spatial Anchors. If an Enterprise maintains users in Microsoft Entra ID, you can supply a user Microsoft Entra token in the Azure Spatial Anchors SDK. Doing so allows you to authenticate directly to the Azure Spatial Anchors service for an account that's part of the same Microsoft Entra tenant.

    mCloudSession.getConfiguration().setAuthenticationToken("MyAuthenticationToken");

Like with access tokens, if a Microsoft Entra token isn't set, you must handle the TokenRequired event, or implement the tokenRequired method on the delegate protocol.

You can handle the event synchronously by setting the property on the event arguments.

    mCloudSession.addTokenRequiredListener(args -> {
        args.setAuthenticationToken("MyAuthenticationToken");
    });

If you need to execute asynchronous work in your handler, you can defer setting the token by requesting a deferral object and then completing it, as in the following example.

    mCloudSession.addTokenRequiredListener(args -> {
        CloudSpatialAnchorSessionDeferral deferral = args.getDeferral();
        MyGetTokenAsync(myToken -> {
            if (myToken != null) args.setAuthenticationToken(myToken);
            deferral.complete();
        });
    });

Set up the session

Invoke Start() to enable your session to process environment data.

To handle events raised by your session, attach an event handler.

    mCloudSession.setSession(mSession);
    mCloudSession.start();

Provide frames to the session

The spatial anchor session works by mapping the space around the user. Doing so helps to determine where anchors are located. Mobile platforms (iOS & Android) require a native call to the camera feed to obtain frames from your platform's AR library. In contrast, HoloLens is constantly scanning the environment, so there's no need for a specific call like on mobile platforms.

    mCloudSession.processFrame(mSession.update());

Provide feedback to the user

You can write code to handle the session updated event. This event fires every time the session improves its understanding of your surroundings. Doing so, allows you to:

  • Use the UserFeedback class to provide feedback to the user as the device moves and the session updates its environment understanding. To do this,
  • Determine at what point there's enough tracked spatial data to create spatial anchors. You determine this with either ReadyForCreateProgress or RecommendedForCreateProgress. Once ReadyForCreateProgress is above 1, we have enough data to save a cloud spatial anchor, though we recommend you wait until RecommendedForCreateProgress is above 1 to do so.

Learn more about the SessionUpdatedListener interface.

    mCloudSession.addSessionUpdatedListener(args -> {
        auto status = args->Status();
        if (status->UserFeedback() == SessionUserFeedback::None) return;
        NumberFormat percentFormat = NumberFormat.getPercentInstance();
        percentFormat.setMaximumFractionDigits(1);
        mFeedback = String.format("Feedback: %s - Recommend Create=%s",
            FeedbackToString(status.getUserFeedback()),
            percentFormat.format(status.getRecommendedForCreateProgress()));
    });

Create a cloud spatial anchor

To create a cloud spatial anchor, you first create an anchor in your platform's AR system, and then create a cloud counterpart. You use the CreateAnchorAsync() method.

Learn more about the CloudSpatialAnchor class.

    // Create a local anchor, perhaps by hit-testing and creating an ARAnchor
    Anchor localAnchor = null;
    List<HitResult> hitResults = mSession.update().hitTest(0.5f, 0.5f);
    for (HitResult hit : hitResults) {
        Trackable trackable = hit.getTrackable();
        if (trackable instanceof Plane) {
            if (((Plane) trackable).isPoseInPolygon(hit.getHitPose())) {
                localAnchor = hit.createAnchor();
                break;
            }
        }
    }

    // If the user is placing some application content in their environment,
    // you might show content at this anchor for a while, then save when
    // the user confirms placement.
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(localAnchor);
    Future createAnchorFuture = mCloudSession.createAnchorAsync(cloudAnchor);
    CheckForCompletion(createAnchorFuture, cloudAnchor);

    // ...

    private void CheckForCompletion(Future createAnchorFuture, CloudSpatialAnchor cloudAnchor) {
        new android.os.Handler().postDelayed(() -> {
            if (createAnchorFuture.isDone()) {
                try {
                    createAnchorFuture.get();
                    mFeedback = String.format("Created a cloud anchor with ID=%s", cloudAnchor.getIdentifier());
                }
                catch(InterruptedException e) {
                    mFeedback = String.format("Save Failed:%s", e.getMessage());
                }
                catch(ExecutionException e) {
                    mFeedback = String.format("Save Failed:%s", e.getMessage());
                }
            }
            else {
                CheckForCompletion(createAnchorFuture, cloudAnchor);
            }
        }, 500);
    }

As described earlier, you need sufficient environment data captured before trying to create a new cloud spatial anchor. That means ReadyForCreateProgress has to be above 1, though we recommend you wait until RecommendedForCreateProgress is above 1 to do so.

    Future<SessionStatus> sessionStatusFuture = mCloudSession.getSessionStatusAsync();
    CheckForCompletion(sessionStatusFuture);

    // ...

    private void CheckForCompletion(Future<SessionStatus> sessionStatusFuture) {
        new android.os.Handler().postDelayed(() -> {
            if (sessionStatusFuture.isDone()) {
                try {
                    SessionStatus value = sessionStatusFuture.get();
                    if (value.getRecommendedForCreateProgress() < 1.0f) return;
                    // Issue the creation request...
                }
                catch(InterruptedException e) {
                    mFeedback = String.format("Session status error:%s", e.getMessage());
                }
                catch(ExecutionException e) {
                    mFeedback = String.format("Session status error:%s", e.getMessage());
                }
            }
            else {
                CheckForCompletion(sessionStatusFuture);
            }
        }, 500);
    }

Set properties

You may choose to add some properties when saving your cloud spatial anchors. Like the type of object being saved, or basic properties like whether it should be enabled for interaction. Doing so can be useful upon discovery: you can immediately render the object for the user, for example a picture frame with blank content. Then, a different download in the background gets additional state details, for example, the picture to display in the frame.

    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(localAnchor);
    Map<String,String> properties = cloudAnchor.getAppProperties();
    properties.put("model-type", "frame");
    properties.put("label", "my latest picture");
    Future createAnchorFuture = mCloudSession.createAnchorAsync(cloudAnchor);
    // ...

Update properties

To update the properties on an anchor, you use the UpdateAnchorProperties() method. If two or more devices try to update properties for the same anchor at the same time, we use an optimistic concurrency model. Which means that the first write will win. All other writes will get a "Concurrency" error: a refresh of the properties would be needed before trying again.

    CloudSpatialAnchor anchor = /* locate your anchor */;
    anchor.getAppProperties().put("last-user-access", "just now");
    Future updateAnchorPropertiesFuture = mCloudSession.updateAnchorPropertiesAsync(anchor);
    CheckForCompletion(updateAnchorPropertiesFuture);

    // ...

    private void CheckForCompletion(Future updateAnchorPropertiesFuture) {
        new android.os.Handler().postDelayed(() -> {
            if (updateAnchorPropertiesFuture.isDone()) {
                try {
                    updateAnchorPropertiesFuture.get();
                }
                catch(InterruptedException e) {
                    mFeedback = String.format("Updating Properties Failed:%s", e.getMessage());
                }
                catch(ExecutionException e) {
                    mFeedback = String.format("Updating Properties Failed:%s", e.getMessage());
                }
            }
            else {
                CheckForCompletion1(updateAnchorPropertiesFuture);
            }
        }, 500);
    }

You can't update the location of an anchor once it has been created on the service - you must create a new anchor and delete the old one to track a new position.

If you don't need to locate an anchor to update its properties, you can use the GetAnchorPropertiesAsync() method, which returns a CloudSpatialAnchor object with properties.

    Future<CloudSpatialAnchor> getAnchorPropertiesFuture = mCloudSession.getAnchorPropertiesAsync("anchorId");
    CheckForCompletion(getAnchorPropertiesFuture);

    // ...

    private void CheckForCompletion(Future<CloudSpatialAnchor> getAnchorPropertiesFuture) {
        new android.os.Handler().postDelayed(() -> {
            if (getAnchorPropertiesFuture.isDone()) {
                try {
                    CloudSpatialAnchor anchor = getAnchorPropertiesFuture.get();
                    if (anchor != null) {
                        anchor.getAppProperties().put("last-user-access", "just now");
                        Future updateAnchorPropertiesFuture = mCloudSession.updateAnchorPropertiesAsync(anchor);
                        // ...
                    }
                } catch (InterruptedException e) {
                    mFeedback = String.format("Getting Properties Failed:%s", e.getMessage());
                } catch (ExecutionException e) {
                    mFeedback = String.format("Getting Properties Failed:%s", e.getMessage());
                }
            } else {
                CheckForCompletion(getAnchorPropertiesFuture);
            }
        }, 500);
    }

Set expiration

It's also possible to configure your anchor to expire automatically at a given date in the future. When an anchor expires, it will no longer be located or updated. Expiration can only be set when the anchor is created, before saving it to the cloud. Updating expiration afterwards isn't possible. If no expiration is set during anchor creation, the anchor will only expire when deleted manually.

    Date now = new Date();
    Calendar cal = Calendar.getInstance();
    cal.setTime(now);
    cal.add(Calendar.DATE, 7);
    Date oneWeekFromNow = cal.getTime();
    cloudAnchor.setExpiration(oneWeekFromNow);

Locate a cloud spatial anchor

Being able to locate a previously saved cloud spatial anchor is one of the main reasons for using Azure Spatial Anchors. For this, we're using "Watchers". You can only use one Watcher at a time; multiple Watchers are not supported. There are several different ways (also known as Anchor Locate Strategies) a Watcher can locate a cloud spatial anchor. You can use one strategy on a watcher at a time.

  • Locate anchors by identifier.
  • Locate anchors connected to a previously located anchor. You can learn about anchor relationships here.
  • Locate anchor using Coarse relocalization.

Note

Each time you locate an anchor, Azure Spatial Anchors will attempt to use the environment data collected to augment the visual information on the anchor. If you are having trouble locating an anchor, it can be useful to create an anchor, then locate it several times from different angles and lighting conditions.

If you're locating cloud spatial anchors by identifier, you can store the cloud spatial anchor identifier in your application's back-end service, and make it accessible to all devices that can properly authenticate to it. For an example of this, see Tutorial: Share Spatial Anchors across devices.

Instantiate an AnchorLocateCriteria object, set the identifiers you're looking for, and invoke the CreateWatcher method on the session by providing your AnchorLocateCriteria.

    AnchorLocateCriteria criteria = new AnchorLocateCriteria();
    criteria.setIdentifiers(new String[] { "id1", "id2", "id3" });
    mCloudSession.createWatcher(criteria);

After your watcher is created, the AnchorLocated event will fire for every anchor requested. This event fires when an anchor is located, or if the anchor can't be located. If this situation happens, the reason will be stated in the status. After all anchors for a watcher are processed, found or not found, then the LocateAnchorsCompleted event will fire. There is a limit of 35 identifiers per watcher.

Learn more about the AnchorLocatedListener interface.

    mCloudSession.addAnchorLocatedListener(args -> {
        switch (args.getStatus()) {
            case Located:
                CloudSpatialAnchor foundAnchor = args.getAnchor();
                // Go add your anchor to the scene...
                break;
            case AlreadyTracked:
                // This anchor has already been reported and is being tracked
                break;
            case NotLocatedAnchorDoesNotExist:
                // The anchor was deleted or never existed in the first place
                // Drop it, or show UI to ask user to anchor the content anew
                break;
            case NotLocated:
                // The anchor hasn't been found given the location data
                // The user might in the wrong location, or maybe more data will help
                // Show UI to tell user to keep looking around
                break;
        }
    });

Delete anchors

Deleting anchors when no longer used is a good practice to include early on in your development process and practices, to keep your Azure resources cleaned up.

    Future deleteAnchorFuture = mCloudSession.deleteAnchorAsync(cloudAnchor);
    // Perform any processing you may want when delete finishes (deleteAnchorFuture is done)

Pause, reset, or stop the session

To stop the session temporarily, you can invoke Stop(). Doing so will stop any watchers and environment processing, even if you invoke ProcessFrame(). You can then invoke Start() to resume processing. When resuming, environment data already captured in the session is maintained.

    mCloudSession.stop();

To reset the environment data that has been captured in your session, you can invoke Reset().

    mCloudSession.reset();

To clean up properly after a session, invoke close().

    mCloudSession.close();

Next steps

In this guide, you learned about how to create and locate anchors using the Azure Spatial Anchors SDK. To learn more about anchor relationships, continue to the next guide.