Esercitazione: Istruzioni dettagliate per la creazione di una nuova app Android usando Ancoraggi nello spazio di Azure

Questa esercitazione illustra come creare una nuova app Android che integra funzionalità ARCore con Ancoraggi nello spazio di Azure.

Prerequisiti

Per completare questa esercitazione, accertarsi di avere:

Introduzione

Avviare Android Studio. Nella finestra iniziale di Android Studio selezionare Start a new Android Studio project (Avvia un nuovo progetto di Android Studio).

  1. Selezionare File->Nuovo progetto.
  2. Nella finestra Phone and Tablet (Telefono e tablet) della finestra Create New Project (Crea nuovo progetto) scegliere Empty Activity (Attività vuota) e fare clic su Avanti.
  3. Nella finestra Nuovo progetto - Attività vuota modificare i valori seguenti:
    • Modificare Nome, Nome del pacchetto e Percorso di salvataggio con i valori desiderati
    • Impostare Lingua su Java
    • Impostare Livello API minimo su API 26: Android 8.0 (Oreo)
    • Lasciare inalterate le altre opzioni
    • Fare clic su Fine.
  4. Verrà eseguito il programma di installazione del componente. Dopo un certo tempo di elaborazione, Android Studio aprirà l'IDE.

Android Studio - Nuovo progetto

Prova pratica

Per testare la nuova app, connettere il dispositivo abilitato per lo sviluppo al computer di sviluppo con un cavo USB. In alto a destra di Android Studio selezionare il dispositivo connesso e fare clic sull'icona Esegui 'app'. Android Studio installa l'app nel dispositivo connesso e la avvia. Verrà ora visualizzato "Hello World!" nell'app in esecuzione nel dispositivo. Fare clic su Esegui->Interrompi "app". Android Studio - Run

Integrazione di ARCore

ARCore è la piattaforma di Google per lo sviluppo di esperienze di realtà aumentata, che consente al dispositivo di tenere traccia della propria posizione mentre si muove e inizia a riconoscere il mondo reale.

Modificare app\manifests\AndroidManifest.xml per includere le voci seguenti nel nodo <manifest> radice. Questo frammento di codice esegue alcune operazioni:

  • Consente all'app di accedere alla fotocamera del dispositivo.
  • Assicura inoltre che l'app sia visibile in Google Play Store solo per i dispositivi che supportano ARCore.
  • Configura Google Play Store per scaricare e installare ARCore, se non è già installato, quando viene installata l'app.
<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>

Modificare Gradle Scripts\build.gradle (Module: app) per includere la voce seguente. Questo codice assicura che l'app sia destinata ad ARCore versione 1.25. Dopo aver apportato questa modifica, è possibile che venga visualizzata una notifica di Gradle che chiede di eseguire la sincronizzazione. Fare clic su Sync now (Sincronizza ora).

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

Integrazione di Sceneform

Sceneform semplifica il rendering di scene 3D realistiche nelle app di realtà aumentata, senza la necessità di competenze in OpenGL.

Modificare Gradle Scripts\build.gradle (Module: app) per includere le voci seguenti. Questo codice consente all'app di usare i costrutti del linguaggio Java 8, richiesti da Sceneform. Garantisce inoltre che l'app sia destinata alla Sceneform versione 1.15. Dopo aver apportato questa modifica, è possibile che venga visualizzata una notifica di Gradle che chiede di eseguire la sincronizzazione. Fare clic su Sync now (Sincronizza ora).

android {
    ...

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

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

Aprire app\res\layout\activity_main.xml e sostituire l'elemento <TextView ... /> Hello Word con l'elemento ArFragment seguente. Questo codice causa la visualizzazione del feed della fotocamera sullo schermo, consentendo ad ARCore di tenere traccia della posizione del dispositivo mentre si muove.

<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

Per visualizzare il codice XML non elaborato dell'attività principale, fare clic sul pulsante "Codice" o "Dividi" in alto a destra di Android Studio.

Ridistribuire l'app nel dispositivo per verificarla ancora una volta. Questa volta, dovrebbero essere chieste le autorizzazioni per la fotocamera. Dopo l'approvazione, sullo schermo dovrebbe essere visualizzato il feed della fotocamera.

Posizionare un oggetto nel mondo reale

Creare e posizionare un oggetto con l'app. Prima di tutto, aggiungere le istruzioni import seguenti in 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;

Quindi aggiungere le variabili membro seguenti nella classe 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;

Aggiungere poi il codice seguente nel metodo onCreate() di app\java\<PackageName>\MainActivity. Questo codice assocerà un listener, denominato handleTap(), che rileverà il tocco dell'utente sullo schermo del dispositivo. Se il tocco avviene su una superficie del mondo reale che è già stata riconosciuta dal rilevamento di ARCore, il listener verrà eseguito.

@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);
}

Infine, aggiungere il metodo handleTap() seguente, che collegherà tutti gli elementi tra loro. Viene creata una sfera, che viene posizionata nel punto toccato. Inizialmente la sfera sarà nera, perché this.recommendedSessionProgress è impostato su zero per il momento. Questo valore verrà modificato in seguito.

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());
            });
}

Ridistribuire l'app nel dispositivo per verificarla ancora una volta. Questa volta, è possibile muovere il dispositivo per fare in modo che ARCore inizi a riconoscere l'ambiente. Quindi, toccare lo schermo per creare e posizionare la sfera nera sopra la superficie scelta.

Collegare un ancoraggio nello spazio di Azure locale

Modificare Gradle Scripts\build.gradle (Module: app) per includere la voce seguente. Questo frammento di codice di esempio è destinato all'SDK di Ancoraggi nello spazio di Azure versione 2.10.2. Si noti che la versione 2.7.0 dell'SDK è attualmente la versione minima supportata, per cui il riferimento a una versione più recente di Ancoraggi nello spazio di Azure dovrebbe funzionare ugualmente. È consigliabile usare la versione più recente dell’SDK di Ancoraggi nello spazio di Azure. Le note sulla versione dell’SDK sono disponibili qui.

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

Se si usa l’SDK di Ancoraggi nello spazio di Azure 2.10.0 o versione successiva, includere la voce seguente nella sezione repository del file settings.gradle del progetto. Ciò include l'URL del feed di pacchetti Maven che ospita i pacchetti Android di Ancoraggi nello spazio di Azure per l’SDK 2.10.0 o versione successiva:

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

Fare clic con il pulsante destro del mouse su app\java\<PackageName>->Nuova->Classe Java. Impostare Nome su MyFirstApp e selezionare Classe. Verrà creato un file denominato MyFirstApp.java. Aggiungere l'istruzione import seguente:

import com.microsoft.CloudServices;

Definire android.app.Application come superclasse.

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

Aggiungere quindi il codice seguente all'interno della nuova classe MyFirstApp, che assicura l'inizializzazione di Ancoraggi nello spazio di Azure con il contesto dell'applicazione.

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

Modificare ora app\manifests\AndroidManifest.xml per includere la voce seguente nel nodo <application> radice. Questo codice associa la classe Application creata nell'app.

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

Tornare in app\java\<PackageName>\MainActivity e aggiungere le istruzioni import seguenti:

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;

Quindi aggiungere le variabili membro seguenti nella classe MainActivity:

private float recommendedSessionProgress = 0f;

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

Aggiungere poi il metodo initializeSession() seguente nella classe mainActivity. Quando viene chiamato, questo metodo assicura che durante l'avvio dell'app venga creata e inizializzata correttamente una sessione di Ancoraggi nello spazio di Azure. Questo codice garantisce che la sessione di sceneview passata alla sessione ASA tramite la chiamata cloudSession.setSession non sia null, avendo una restituzione anticipata.

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;
}

Poiché initializeSession() potrebbe restituire un risultato anticipato se la sessione sceneView non è ancora configurata, ad esempio, se sceneView.getSession() è null, aggiungere una chiamata onUpdate per assicurarsi che la sessione ASA venga inizializzata una volta creata la sessione 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();
    }
}

Associare quindi il metodo initializeSession() e scene_OnUpdate(...) al metodo onCreate(). Assicurarsi inoltre che i fotogrammi provenienti dal feed della fotocamera vengano inviati all'SDK di Ancoraggi nello spazio di Azure per l'elaborazione.

@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();
}

Infine, aggiungere il codice seguente nel metodo handleTap(). Il codice collega un ancoraggio nello spazio di Azure locale alla sfera nera posizionata nel mondo reale.

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());
            });
}

Ridistribuire ancora una volta l'app. Muovere il dispositivo, toccare lo schermo e posizionare una sfera nera. Questa volta, però, il codice crea e collega un ancoraggio nello spazio di Azure locale alla sfera.

Prima di procedere, sarà necessario creare un account di Ancoraggi nello spazio di Azure per ottenere l'identificatore, la chiave e il dominio dell'account, se non si hanno già. Per ottenerli, seguire la sezione seguente.

Creare una risorsa di Ancoraggi nello spazio

Vai al portale di Azure.

Nel riquadro sinistro selezionare Crea una risorsa.

Digitare Ancoraggi nello spazio nella casella di ricerca.

Screenshot che mostra i risultati di una ricerca di ancoraggi nello spazio.

Selezionare Ancoraggi nello spazio, quindi selezionare Crea.

Nel riquadro Account ancoraggi nello spazio procedere come segue:

  • Immettere un nome di risorsa univoco usando i normali caratteri alfanumerici.

  • Selezionare la sottoscrizione a cui collegare la risorsa.

  • Creare un gruppo di risorse selezionando Crea nuovo. Assegnare al gruppo il nome myResourceGroup e quindi selezionare OK.

    Un gruppo di risorse è un contenitore logico in cui vengono distribuite e gestite risorse di Azure come app Web, database e account di archiviazione. Ad esempio, si può scegliere in un secondo momento di eliminare l'intero gruppo di risorse in un unico semplice passaggio.

  • Selezionare un'area in cui inserire la risorsa.

  • Selezionare Crea per iniziare a creare la risorsa.

Screenshot del riquadro Ancoraggi nello spazio per la creazione di una risorsa.

Dopo aver creato la risorsa, il portale di Azure indica che la distribuzione è stata completata.

Screenshot che mostra la distribuzione della risorsa completata.

Selezionare Vai alla risorsa. È ora possibile visualizzare le proprietà della risorsa.

Copiare il valore di ID account della risorsa in un editor di testo per un uso successivo.

Screenshot del riquadro di proprietà della risorsa.

Copiare anche il valore di Dominio account della risorsa in un editor di testo per un uso successivo.

Screenshot che mostra il valore di Dominio account della risorsa.

In Impostazioni, selezionare Chiave di accesso. Copiare i valori di Chiave primaria e Chiave dell'account in un editor di testo per un uso successivo.

Screenshot del riquadro delle chiavi per l'account.

Caricare l'ancoraggio locale nel cloud

Dopo aver ottenuto l'identificatore, la chiave e il dominio dell'account di Ancoraggi nello spazio di Azure, è possibile tornare in app\java\<PackageName>\MainActivity e aggiungervi le istruzioni import seguenti:

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;

Quindi aggiungere le variabili membro seguenti nella classe MainActivity:

private boolean sessionInitialized = false;

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

Aggiungere poi il codice seguente nel metodo initializeSession(). Questo codice consente prima di tutto all'app di monitorare lo stato di avanzamento dell'SDK Ancoraggi nello spazio di Azure mentre raccoglie i fotogrammi dal feed della fotocamera. Durante questo processo, il colore della sfera inizia a cambiare dal nero originale al grigio. Quindi diventerà bianca quando sarà stato raccolto un numero di fotogrammi sufficiente per inviare l'ancoraggio nel cloud. Il codice fornirà poi le credenziali necessarie per comunicare con il back-end del cloud. Ecco dove configurare l'app per usare l'identificatore, la chiave e il dominio dell'account. Questi dati sono stati copiati in un editor di testo durante la configurazione della risorsa Ancoraggi nello spazio.

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();
}

Aggiungere poi il metodo uploadCloudAnchorAsync() seguente nella classe mainActivity. Quando viene chiamato, questo metodo aspetta in modo asincrono finché non viene raccolto un numero di fotogrammi sufficiente dal dispositivo. Non appena ciò avviene, trasforma il colore della sfera in giallo e quindi avvia il caricamento dell'ancoraggio nello spazio di Azure locale nel cloud. Al termine del caricamento, il codice restituisce un identificatore di ancoraggio.

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());
}

Infine, associare tutti gli elementi tra loro. Aggiungere il codice seguente nel metodo handleTap(). Il codice richiama il metodo uploadCloudAnchorAsync() non appena verrà creata la sfera. Quando il metodo restituisce il risultato, il codice seguente esegue l'aggiornamento finale della sfera, cambiandone il colore in blu.

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;
                                }
                            });
                });
            });
}

Ridistribuire ancora una volta l'app. Muovere il dispositivo, toccare lo schermo e posizionare la sfera. Questa volta, però, il colore della sfera cambia da nero a bianco, mentre vengono raccolti i fotogrammi dalla fotocamera. Una volta ottenuto un numero sufficiente di fotogrammi, la sfera diventerà gialla e verrà avviato il caricamento del cloud. Assicurarsi che il telefono sia connesso a Internet. Al termine del caricamento, la sfera diventerà blu. Facoltativamente, è possibile monitorare la finestra Logcat in Android Studio per visualizzare i messaggi di log inviati dall'app. Alcuni esempi di messaggi che verrebbero registrati includono lo stato di avanzamento della sessione durante l'acquisizione dei frame e l'identificatore di ancoraggio restituito dal cloud al termine del caricamento.

Nota

Se non viene visualizzato il valore di recommendedSessionProgress nei log di debug indicati come Session progress, assicurarsi di spostare e ruotare il telefono attorno alla sfera posizionata.

Individuare l'ancoraggio nello spazio nel cloud

Dopo aver caricato l'ancoraggio nel cloud, si è pronti per provare a individuarlo di nuovo. Prima di tutto, aggiungere le istruzioni import seguenti nel codice.

import java.util.concurrent.Executors;

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

Quindi, aggiungere il codice seguente nel metodo handleTap(). Con questo codice sarà possibile:

  • Rimuovere l'attuale sfera blu dallo schermo.
  • Inizializzare di nuovo la sessione di Ancoraggi nello spazio di Azure. Questa operazione assicura che l'ancoraggio da individuare proviene dal cloud e non è l'ancoraggio locale creato.
  • Eseguire una query per trovare l'ancoraggio caricato nel cloud.
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;
                                }
                            });
                });
            });
}

Associare ora il codice che verrà richiamato quando verrà individuato l'ancoraggio tramite la query. Aggiungere il codice seguente nel metodo initializeSession(). Questo frammento di codice crea e posiziona una sfera verde quando verrà individuato l'ancoraggio nello spazio nel cloud. Consente inoltre di toccare di nuovo lo schermo, quindi sarà possibile ripetere ancora una volta l'intero scenario, ossia creare un altro ancoraggio locale, caricarlo e individuarlo.

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();
}

Ecco fatto! Ridistribuire l'app un'ultima volta per provare l'intero scenario completato. Muovere il dispositivo e posizionare la sfera nera. Quindi continuare a muovere il dispositivo per acquisire i fotogrammi della fotocamera finché la sfera non diventa gialla. L'ancoraggio locale verrà caricato e la sfera diventerà blu. Infine, toccare ancora una volta lo schermo in modo da rimuovere l'ancoraggio locale ed eseguire una query per trovare la controparte nel cloud. Continuare a muovere il dispositivo finché non viene individuato l'ancoraggio nello spazio nel cloud. Dovrebbe comparire una sfera verde nella posizione corretta ed è possibile pulire e ripetere di nuovo l'intero scenario.

Riunire tutti gli elementi

Ecco come dovrebbe apparire il file di classe MainActivity completo dopo aver riunito tutti i diversi elementi. È possibile usarlo come riferimento da confrontare con il proprio file e verificare se sono state lasciate differenze.

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>

}

Passaggi successivi

Questa esercitazione ha illustrato come creare una nuova app Android che integra funzionalità ARCore con Ancoraggi nello spazio di Azure. Per altre informazioni sulla libreria di Ancoraggi nello spazio di Azure, passare alla guida che illustra come creare e individuare ancoraggi.