Tutorial: instrucciones detalladas para crear una aplicación Android mediante Azure Spatial Anchors

Este tutorial le mostrará cómo crear una aplicación Android que integra la funcionalidad ARCore con Azure Spatial Anchors.

Requisitos previos

Para completar este tutorial, asegúrese de disponer de los siguientes elementos:

Introducción

Inicie Android Studio. En la ventana Welcome to Android Studio (Introducción a Android Studio), haga clic en Start a new Android Studio project (Iniciar un nuevo proyecto de Android Studio).

  1. Seleccione Archivo->Nuevo proyecto.
  2. En la ventana Create New Project (Crear proyecto), en la sección Phone y Tablet (Teléfono y tableta), elija Empty Activity (Actividad vacía) y haga clic en Next (Siguiente).
  3. En la ventana New Project - Empty Activity (Nuevo proyecto > Actividad vacía), cambie los valores siguientes:
    • Cambie el nombre y el nombre del paquete por los valores deseados y guarde la ubicación.
    • Establezca el lenguaje en Java.
    • Establezca el nivel mínimo de la API en API 26: Android 8.0 (Oreo).
    • Deje las restantes opciones como están.
    • Haga clic en Finalizar
  4. Se ejecutará Component Installer (Instalador de componentes). Tras un tiempo de procesamiento Android Studio abrirá el IDE.

Android Studio - New Project

Prueba

Para probar la nueva aplicación, conecte el dispositivo habilitado para desarrolladores en el equipo de desarrollo con un cable USB. En la parte superior derecha de Android Studio seleccione el dispositivo conectado y haga clic en el icono Ejecutar "aplicación" . Android Studio instala la aplicación en el dispositivo conectado y lo inicia. Ahora debería ver "Hola mundo" en la aplicación que se ejecuta en el dispositivo. Haga clic en Ejecutar->Detener "aplicación". Android Studio - Run

Integración de ARCore

ARCore es la plataforma de Google para la creación de experiencias de realidad aumentada, la habilitación del dispositivo para realizar un seguimiento de su posición a medida que se mueve y la creación de su propio conocimiento del mundo real.

Modifique app\manifests\AndroidManifest.xml para incluir las siguientes entradas en el nodo <manifest> raíz. Este fragmento de código realiza varias labores:

  • Permitirá que la aplicación acceda a la cámara del dispositivo.
  • También se garantizará que la aplicación solo podrán verla en Google Play Store los dispositivos que admiten ARCore.
  • Se configurará Google Play Store para descargar e instalar ARCore, en caso de que no esté aún instalado, cuando se instale la aplicación.
<manifest ...>

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera.ar" />

    <application>
        ...
        <meta-data android:name="com.google.ar.core" android:value="required" />
        ...
    </application>

</manifest>

Modifique Gradle Scripts\build.gradle (Module: app) para incluir la siguiente entrada. Este código garantiza que la aplicación tiene como destino la versión 1.25 de ARCore. Después de este cambio, es posible que reciba una notificación de Gradle en la que se le pida que realice la sincronización: haga clic en Sync now (Sincronizar ahora).

dependencies {
    ...
    implementation 'com.google.ar:core:1.25.0'
    ...
}

Integración de Sceneform

Sceneform simplifica la representación de escenas 3D realistas en aplicaciones de realidad aumentada sin tener que aprender OpenGL.

Modifique Gradle Scripts\build.gradle (Module: app) para incluir las siguientes entradas. Este código permitirá que la aplicación use construcciones del lenguaje de Java 8, lo que requiere Sceneform. También se asegurará de que la aplicación tiene como destino la versión 1.15 de Sceneform. Después de este cambio, es posible que reciba una notificación de Gradle en la que se le pida que realice la sincronización: haga clic en Sync now (Sincronizar ahora).

android {
    ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.15.0'
    ...
}

Abra el archivo app\res\layout\activity_main.xmly reemplace el elemento <TextView ... /> de Hello World existente por el siguiente ArFragment. Este código hará que se muestre en pantalla la fuente de la cámara, lo que permite que ARCore realice un seguimiento de la posición del dispositivo mientras se mueve.

<fragment android:name="com.google.ar.sceneform.ux.ArFragment"
    android:id="@+id/ux_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Nota

Para ver el xml sin formato de la actividad principal, haga clic en el botón "Código" o "Dividir" en la parte superior derecha de Android Studio.

Vuelva a implementar la aplicación en el dispositivo para validarla una vez más. Esta vez, se le deberían solicitar los permisos de la cámara. Tras la aprobación, debería ver que la fuente de la cámara se representa en la pantalla.

Colocación de un objeto en el mundo real

Vamos a crear y colocar un objeto mediante la aplicación. En primer lugar, agregue las importaciones siguientes a app\java\<PackageName>\MainActivity:

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import android.view.MotionEvent;

Luego, agregue las siguientes variables de miembro a la clase MainActivity:

private boolean tapExecuted = false;
private final Object syncTaps = new Object();
private ArFragment arFragment;
private AnchorNode anchorNode;
private Renderable nodeRenderable = null;
private float recommendedSessionProgress = 0f;

Después, agregue el código siguiente al método app\java\<PackageName>\MainActivityonCreate(). Este código enlazará un cliente de escucha, llamado handleTap(), que se detectará cuando el usuario toque la pantalla del dispositivo. Si da la casualidad de que el toque se produce en una superficie del mundo real que ya se ha reconocido en el seguimiento de ARCore, se ejecutará el cliente de escucha.

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

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);
}

Por último, agregue el siguiente método handleTap(), que agrupará todo. Creará una esfera y la colocará en la ubicación tocada. Esta esfera inicialmente será negra, ya que this.recommendedSessionProgress está establecido en cero en este momento. Este valor se ajustará más adelante.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

Vuelva a implementar la aplicación en el dispositivo para validarla una vez más. Esta vez, puede desplazar el dispositivo para que ARCore empiece a reconocer su entorno. Luego, toque la pantalla para crear la esfera negra y colocarla en la superficie que haya elegido.

Conexión de un anclaje espacial local de Azure

Modifique Gradle Scripts\build.gradle (Module: app) para incluir la siguiente entrada. Este fragmento de código de ejemplo tiene como destino la versión 2.10.2 del SDK de Azure Spatial Anchors. Tenga en cuenta que la versión 2.7.0 del SDK es la versión mínima admitida y que la referencia a cualquier versión más reciente de Azure Spatial Anchors también debería funcionar. Se recomienda usar la versión más reciente del SDK de Azure Spatial Anchors. Aquí puede encontrar las notas de la versión del SDK.

dependencies {
    ...
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_jni:[2.10.2]'
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_java:[2.10.2]'
    ...
}

Si el destino es el SDK 2.10.0 o posterior de Azure Spatial Anchors, incluya la siguiente entrada en la sección de repositorios del archivo settings.gradle del proyecto. Esto incluirá la dirección URL de la fuente de paquetes de Maven en la que se hospedan los paquetes Android de Azure Spatial Anchors para el SDK 2.10.0 o posterior:

dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven {
            url 'https://pkgs.dev.azure.com/aipmr/MixedReality-Unity-Packages/_packaging/Maven-packages/maven/v1'
        }
        ...
    }
}

Haga clic con el botón derecho en app\java\<PackageName>->New->Java Class (Nuevo > Clase Java). Establezca el nombre en MyFirstApp y seleccione la clase. Se creará un archivo llamado MyFirstApp.java. Agréguele el siguiente elemento importado:

import com.microsoft.CloudServices;

Defina android.app.Application como su superclase.

public class MyFirstApp extends android.app.Application {...

Luego, agregue el siguiente código a la nueva clase MyFirstApp, lo que garantizará que Azure Spatial Anchors se inicializa con el contexto de la aplicación.

    @Override
    public void onCreate() {
        super.onCreate();
        CloudServices.initialize(this);
    }

Ahora, modifique app\manifests\AndroidManifest.xml para incluir la siguiente entrada en el nodo <application> raíz. Este código enlazará la clase Application que creó en la aplicación.

    <application
        android:name=".MyFirstApp"
        ...
    </application>

En app\java\<PackageName>\MainActivity, agregue los siguientes elementos importados a:

import android.view.MotionEvent;
import android.util.Log;

import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.Scene;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

Luego, agregue las siguientes variables de miembro a la clase MainActivity:

private float recommendedSessionProgress = 0f;

private ArSceneView sceneView;
private CloudSpatialAnchorSession cloudSession;
private boolean sessionInitialized = false;

A continuación, vamos a agregar el siguiente método initializeSession() a la clase mainActivity. Una vez que se le llama, garantiza que se crea una sesión de Azure Spatial Anchors y que se inicializa correctamente durante el inicio de la aplicación. Este código se asegura de que la sesión de sceneView pasada a la sesión de ASA a través de la llamada a cloudSession.setSession no sea NULL al tener una devolución anticipada.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;
}

Puesto que initializeSession() podría realizar una devolución anticipada si la sesión de sceneView aún no está configurada (es decir, si sceneView.getSession() es NULL), agregamos una llamada onUpdate para asegurarnos de que la sesión ASA se inicializa una vez creada la sesión de sceneView.

private void scene_OnUpdate(FrameTime frameTime) {
    if (!sessionInitialized) {
        //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
        initializeSession();
    }
}

Ahora, vamos a enlazar el método initializeSession() y scene_OnUpdate(...) a su método onCreate(). Además, nos aseguraremos de que los fotogramas de la fuente de la cámara se envían al SDK de Azure Spatial Anchors para su procesamiento.

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

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);

    this.sceneView = arFragment.getArSceneView();
    Scene scene = sceneView.getScene();
    scene.addOnUpdateListener(frameTime -> {
        if (this.cloudSession != null) {
            this.cloudSession.processFrame(sceneView.getArFrame());
        }
    });
    scene.addOnUpdateListener(this::scene_OnUpdate);
    initializeSession();
}

Por último, agregue el código siguiente al método handleTap(). Conectará un anclaje espacial local de Azure a la esfera negra que vamos a colocar en el mundo real.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

Vuelva a implementar la aplicación. Desplace el dispositivo, toque la pantalla y coloque una esfera negra. Sin embargo, esta vez se creará el código y se conectará un anclaje espacial local de Azure a la esfera.

Antes de continuar,tiene que crear una cuenta de Azure Spatial Anchors, para obtener el identificador, la clave y el dominio de la cuenta, si no los tiene aún. En la sección siguiente se explica cómo obtenerlos.

Creación de un recurso de Spatial Anchors

Vaya a Azure Portal.

En el menú izquierdo, seleccione Crear un recurso.

Use el cuadro de búsqueda para buscar Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Seleccione Spatial Anchors y, después, seleccione Crear.

En el panel Cuenta de Spatial Anchors, haga lo siguiente:

  • Escriba un nombre de recurso único con caracteres alfanuméricos normales.

  • Seleccione la suscripción a la que desea asociar el recurso.

  • Cree un grupo de recursos, para lo que debe seleccionar Crear nuevo. Asígnele el nombre myResourceGroup y luego seleccione Aceptar.

    Un grupo de recursos es un contenedor lógico en el que los recursos de Azure, como aplicaciones web, bases de datos y cuentas de almacenamiento, se implementen y administren. Por ejemplo, más adelante puede elegir eliminar todo el grupo de recursos en un solo paso.

  • Seleccione la ubicación (región) en la que desea colocar el recurso.

  • Seleccione Crear para empezar a crear el recurso.

Screenshot of the Spatial Anchors pane for creating a resource.

Una vez creado el recurso, Azure Portal muestra que se ha completado la implementación.

Screenshot showing that the resource deployment is complete.

Haga clic en Go to resource (Ir al recurso). Ahora puede ver las propiedades del recurso.

Copie el valor de Id. de cuenta del recurso en un editor de texto para usarlo más adelante.

Screenshot of the resource properties pane.

Copie también el valor de Account Domain (Dominio de cuenta) del recurso en un editor de texto para usarlo más adelante.

Screenshot showing the resource's account domain value.

En Configuración, seleccione Clave de acceso. Copie el valor de Clave principal, Clave de cuenta, en un editor de texto para su uso posterior.

Screenshot of the Keys pane for the account.

Carga del anclaje local en la nube

Una vez que tenga el identificador, la clave y el domino de la cuenta de Azure Spatial Anchors, podemos volver a app\java\<PackageName>\MainActivity y agregarle los siguientes elementos importados:

import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

Luego, agregue las siguientes variables de miembro a la clase MainActivity:

private boolean sessionInitialized = false;

private String anchorId = null;
private boolean scanningForUpload = false;
private final Object syncSessionProgress = new Object();
private ExecutorService executorService = Executors.newSingleThreadExecutor();

Ahora, agregue el código siguiente al método initializeSession(). En primer lugar, este código permitirá que la aplicación supervise el progreso que hace el SDK de Azure Spatial Anchors cuando recopila los fotogramas de la fuente de la cámara. A medida que lo haga, el color de la esfera empezará a pasar del negro original a un color gris. Luego, pasará a ser blanco una vez que se hayan recopilado suficientes fotogramas para enviar el anclaje a la nube. En segundo lugar, este código proporcionará las credenciales necesarias para comunicarse con el back-end de nube. Aquí es donde se configurará la aplicación para que utilice el identificador, la clave y el dominio de la cuenta. Los copió en un editor cuando configuró el recurso de Spatial Anchors.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

A continuación, agregue el siguiente método uploadCloudAnchorAsync() a la clase mainActivity. Una vez que se le llame, este método esperará de forma asincrónica hasta que se recopilen suficientes fotogramas del dispositivo. En cuanto eso suceda, el color de la esfera cambiará a amarillo y, después, comenzará la carga del anclaje espacial local de Azure en la nube. Una vez finalizada la carga, el código devolverá un identificador del anclaje.

private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
    synchronized (this.syncSessionProgress) {
        this.scanningForUpload = true;
    }


    return CompletableFuture.runAsync(() -> {
        try {
            float currentSessionProgress;
            do {
                synchronized (this.syncSessionProgress) {
                    currentSessionProgress = this.recommendedSessionProgress;
                }
                if (currentSessionProgress < 1.0) {
                    Thread.sleep(500);
                }
            }
            while (currentSessionProgress < 1.0);

            synchronized (this.syncSessionProgress) {
                this.scanningForUpload = false;
            }
            runOnUiThread(() -> {
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                        .thenAccept(yellowMaterial -> {
                            this.nodeRenderable.setMaterial(yellowMaterial);
                        });
            });

            this.cloudSession.createAnchorAsync(anchor).get();
        } catch (InterruptedException | ExecutionException e) {
            Log.e("ASAError", e.toString());
            throw new RuntimeException(e);
        }
    }, executorService).thenApply(ignore -> anchor.getIdentifier());
}

Por último, vamos a enlazar todos los elementos. En el método handleTap(), agregue la siguiente línea. Esta línea invocará el método uploadCloudAnchorAsync() en cuanto se cree la esfera. Una vez que el método realice la devolución, el código siguiente realizará una actualización final a la esfera y su color cambiará a azul.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

Vuelva a implementar la aplicación. Desplace el dispositivo, toque la pantalla y coloque su esfera. No obstante, esta vez el color de la esfera cambiará de negro a blanco a medida que se recopilan los fotogramas de la cámara. Una vez que tengamos suficientes fotogramas, la esfera se pondrá amarilla y se iniciará la carga en la nube. Asegúrese de que el teléfono está conectado a Internet. Cuando finalice la carga, la esfera pasará a ser azul. Si lo desea, también puede supervisar la ventana Logcat en Android Studio para ver los mensajes de registro que envía la aplicación. Algunos ejemplos de mensajes que se registrarían son el progreso de la sesión durante la captura de fotogramas y el identificador de delimitador que devuelve la nube una vez completada la carga.

Nota

Si no ve cambiar el valor de recommendedSessionProgress (en los registros de depuración denominados Session progress), asegúrese de que mueve y gira el teléfono alrededor de la esfera que ha colocado.

Búsqueda del anclaje espacial de la nube

Una vez que el anclaje se carga en la nube, ya es posible volver a intentar localizarlo. En primer lugar, vamos a agregar los elementos importados al código.

import java.util.concurrent.Executors;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;

Luego, vamos a agregar el siguiente código al método handleTap(). Este código hará lo siguiente:

  • Quitar la esfera azul existente de la pantalla.
  • Volver a inicializar la sesión de Azure Spatial Anchors. Esta acción garantizará que el anclaje que vamos a buscar proviene de la nube, en lugar del anclaje local que hemos creado.
  • Emita una consulta para el anclaje que se ha cargado en la nube.
protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    if (this.anchorId != null) {
        this.anchorNode.getAnchor().detach();
        this.anchorNode.setParent(null);
        this.anchorNode = null;
        initializeSession();
        AnchorLocateCriteria criteria = new AnchorLocateCriteria();
        criteria.setIdentifiers(new String[]{this.anchorId});
        cloudSession.createWatcher(criteria);
        return;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

Ahora, vamos a enlazar el código que se invocará cuando se busca el anclaje que se consulta. En el método initializeSession(), agregue el siguiente código. Este fragmento de código creará una esfera verde y la colocará una vez que se encuentre el anclaje espacial de la nube. También volverá a habilitar la opción de tocar la pantalla, con el fin de que pueda repetir el escenario completo una vez más: crear otro anclaje local, cargarlo y volver a buscarlo.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.addAnchorLocatedListener(args -> {
        if (args.getStatus() == LocateAnchorStatus.Located) {
            runOnUiThread(() -> {
                this.anchorNode = new AnchorNode();
                this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                        .thenAccept(greenMaterial -> {
                            this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                            this.anchorNode.setRenderable(nodeRenderable);
                            this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                            this.anchorId = null;
                            synchronized (this.syncTaps) {
                                this.tapExecuted = false;
                            }
                        });
            });
        }
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

Eso es todo. Vuelva a implementar la aplicación una última vez para probar el escenario completo. Desplace el dispositivo y coloque la esfera negra. Luego, siga moviendo el dispositivo para capturar los fotogramas de la fuente de la cámara hasta que la esfera pase a ser amarilla. Se cargará el anclaje local y la esfera volverá a ser azul. Por último, toque la pantalla una vez más, con el fin de quitar el anclaje local y, después, consultaremos su homólogo en la nube. Siga desplazando el dispositivo hasta que se encuentre el anclaje espacial en la nube. Debe aparecer una esfera verde en la ubicación correcta, y puede volver a aclarar y repetir todo el escenario.

Todo junto

Así es como debería verse el archivo de clase MainActivity completo después de unir los distintos elementos. Puede usarlo como referencia para compararlo con su propio archivo, con el fin de detectar si es posible que queden diferencias.

package com.example.myfirstapp;

import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;

import androidx.appcompat.app.AppCompatActivity;

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    private boolean tapExecuted = false;
    private final Object syncTaps = new Object();
    private ArFragment arFragment;
    private AnchorNode anchorNode;
    private Renderable nodeRenderable = null;
    private float recommendedSessionProgress = 0f;

    private ArSceneView sceneView;
    private CloudSpatialAnchorSession cloudSession;
    private boolean sessionInitialized = false;

    private String anchorId = null;
    private boolean scanningForUpload = false;
    private final Object syncSessionProgress = new Object();
    private ExecutorService executorService = Executors.newSingleThreadExecutor();

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

        this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
        this.arFragment.setOnTapArPlaneListener(this::handleTap);

        this.sceneView = arFragment.getArSceneView();
        Scene scene = sceneView.getScene();
        scene.addOnUpdateListener(frameTime -> {
            if (this.cloudSession != null) {
                this.cloudSession.processFrame(sceneView.getArFrame());
            }
        });
        scene.addOnUpdateListener(this::scene_OnUpdate);
        initializeSession();
    }

    // <scene_OnUpdate>
    private void scene_OnUpdate(FrameTime frameTime) {
        if (!sessionInitialized) {
            //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
            initializeSession();
        }
    }
    // </scene_OnUpdate>

    // <initializeSession>
    private void initializeSession() {
        if (sceneView.getSession() == null) {
            //Early return if the ARCore Session is still being set up
            return;
        }

        if (this.cloudSession != null) {
            this.cloudSession.close();
        }
        this.cloudSession = new CloudSpatialAnchorSession();
        this.cloudSession.setSession(sceneView.getSession());
        this.cloudSession.setLogLevel(SessionLogLevel.Information);
        this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
        this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

        sessionInitialized = true;

        this.cloudSession.addSessionUpdatedListener(args -> {
            synchronized (this.syncSessionProgress) {
                this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
                Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
                if (!this.scanningForUpload) {
                    return;
                }
            }

            runOnUiThread(() -> {
                synchronized (this.syncSessionProgress) {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress))
                            .thenAccept(material -> {
                                this.nodeRenderable.setMaterial(material);
                            });
                }
            });
        });

        this.cloudSession.addAnchorLocatedListener(args -> {
            if (args.getStatus() == LocateAnchorStatus.Located) {
                runOnUiThread(() -> {
                    this.anchorNode = new AnchorNode();
                    this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                            .thenAccept(greenMaterial -> {
                                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                                this.anchorNode.setRenderable(nodeRenderable);
                                this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                                this.anchorId = null;
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            }
        });

        this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
        this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
        this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
        this.cloudSession.start();
    }
    // </initializeSession>

    // <handleTap>
    protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
        synchronized (this.syncTaps) {
            if (this.tapExecuted) {
                return;
            }

            this.tapExecuted = true;
        }

        if (this.anchorId != null) {
            this.anchorNode.getAnchor().detach();
            this.anchorNode.setParent(null);
            this.anchorNode = null;
            initializeSession();
            AnchorLocateCriteria criteria = new AnchorLocateCriteria();
            criteria.setIdentifiers(new String[]{this.anchorId});
            cloudSession.createWatcher(criteria);
            return;
        }

        this.anchorNode = new AnchorNode();
        this.anchorNode.setAnchor(hitResult.createAnchor());
        CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
        cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

        MaterialFactory.makeOpaqueWithColor(this, new Color(
                this.recommendedSessionProgress,
                this.recommendedSessionProgress,
                this.recommendedSessionProgress))
                .thenAccept(material -> {
                    this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                    this.anchorNode.setRenderable(nodeRenderable);
                    this.anchorNode.setParent(arFragment.getArSceneView().getScene());
                });


        uploadCloudAnchorAsync(cloudAnchor)
                .thenAccept(id -> {
                    this.anchorId = id;
                    Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                    runOnUiThread(() -> {
                        MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                                .thenAccept(blueMaterial -> {
                                    this.nodeRenderable.setMaterial(blueMaterial);
                                    synchronized (this.syncTaps) {
                                        this.tapExecuted = false;
                                    }
                                });
                    });
                });
    }
    // </handleTap>

    // <uploadCloudAnchorAsync>
    private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
        synchronized (this.syncSessionProgress) {
            this.scanningForUpload = true;
        }


        return CompletableFuture.runAsync(() -> {
            try {
                float currentSessionProgress;
                do {
                    synchronized (this.syncSessionProgress) {
                        currentSessionProgress = this.recommendedSessionProgress;
                    }
                    if (currentSessionProgress < 1.0) {
                        Thread.sleep(500);
                    }
                }
                while (currentSessionProgress < 1.0);

                synchronized (this.syncSessionProgress) {
                    this.scanningForUpload = false;
                }
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                            .thenAccept(yellowMaterial -> {
                                this.nodeRenderable.setMaterial(yellowMaterial);
                            });
                });

                this.cloudSession.createAnchorAsync(anchor).get();
            } catch (InterruptedException | ExecutionException e) {
                Log.e("ASAError", e.toString());
                throw new RuntimeException(e);
            }
        }, executorService).thenApply(ignore -> anchor.getIdentifier());
    }
    // </uploadCloudAnchorAsync>

}

Pasos siguientes

En este tutorial ha visto cómo crear una aplicación Android que integra la funcionalidad ARCore con Azure Spatial Anchors. Para más información acerca de la biblioteca de Azure Spatial Anchors, consulte nuestra guía sobre cómo crear y localizar los delimitadores.