## Cloud anchors Un'ulteriore funzionalità messa a disposizione da ARCore sono le *Cloud Anchors* che ci permette di salvare su un server remoto le ancore a cui sono agganciati i nodi. Grazie a questa funzionalità è possibile salvare un'esperienza di realtà aumentata per un uso futuro[^futuro] o per condividerla con altri utenti. In questo progetto verrà mostrato come sia possibile posizionare, tramite il device A, un vaso di fiori su una superficie piana, e vedere la stessa scena sul dispositivo B. ![Messaggio di avvenuto upload sul server](figures/ca1.png){#ca1 width=225px height=400px} ![Schermata di ripristino di un'ancora](figures/ca2.png){#ca2 width=225px height=400px} ### Configurazioni iniziali Per poter sfruttare le cloud anchors è necessario richiedere un API KEY sul sito di Google \url{https://console.cloud.google.com/apis/library/arcorecloudanchor.googleapis.com}. Una volta ottenuta la chiave è necessario dichiararla nell'Android Manifest mediante il seguente codice xml. ```xml ``` Inoltre per tenere traccia dello stato dell'applicazione si è definita una classe enumerativa con cinque possibili valori. - `NONE`: non è presente alcuno oggetto nella scena né se ne sta recuperando uno dal server. - `HOSTING`: si sta caricando l'ancora sul server. - `HOSTED`: l'ancora è stata caricata sul server. - `RESOLVING`: si sta recuperando l'ancora dal server. - `RESOLVED`: l'ancora è stata recuperata del server. ### Attivazione delle cloud anchors Le cloud anchors di default sono disattivate e la loro attivazione può avvenire in due modi. - **Attivazione manuale**: Con questa soluzione lo sviluppatore si occupa di creare una nuova configurazione della sessione di ARCore in cui le cloud anchors sono attivate e andare a sostituire questa nuova configurazione a quella di default. - **Estensione dell'`ArFragment`**: Viene creata una nuova classe che estende `ArFragment` in cui le cloud anchors sono attivate. Sebbene la prima soluzione possa sembrare più immediata, nasconde una grande insidia. Infatti sarà compito dello sviluppatore andare a sovrascrivere i vari metodi che gestiscono il ciclo di vita dell'activity affinché non vengano ripristinate le impostazioni iniziali. Mentre con il secondo metodo sarà Sceneform a gestire il ciclo di vita al posto nostro. Per questo motivo è stata creata la classe `CloudArFragment` in cui è stata sovrascritta la funzione `getSessionConfiguration` in modo da attivare le cloud anchors. ```kotlin class CloudArFragment: ArFragment(){ override fun getSessionConfiguration( session: Session? ): Config { val config = super.getSessionConfiguration(session) config.cloudAnchorMode = Config .CloudAnchorMode.ENABLED return config } } ``` Inoltre bisogna modificare anche il file di layout affinché non utilizzi più l'`ArFragment`, ma il `CloudArFragment`. ```xml ``` ### Cloud Anchor Helper Quando viene caricata un'ancora sul server viene associata ad essa un valore alfanumerico che ci permette di identificarla univocamente. Dato che il codice risulta essere molto lungo e quindi difficile da ricordare e ricopiare, si è scelto di appoggiarsi al servizio *firestore * di Firebase[@firebase:Firebase:2019] per creare una relazione uno a uno tra l'UUID e uno *short code* intero. Queste operazioni avvengono tramite la classe `CloudAnchorHelper` che fornisce due metodi principali `getShortCode` e `getCloudAnchorId`. ```kotlin fun getShortCode(cloudAnchorId: String): Int { fireStoreDb.collection(COLLECTION) .document(DOCUMENT) .set( mapOf(Pair(nextCode.toString(), cloudAnchorId)), SetOptions.merge() ) uploadNextCode(nextCode+1) return nextCode++ } ``` ```kotlin fun getCloudAnchorId( shortCode: Int, onSuccess: (String) -> Unit ) { fireStoreDb.collection(COLLECTION) .document(DOCUMENT) .get() .addOnSuccessListener { val uuid=it.data.get(shortCode.toString()) as String onSuccess(uuid) } } ``` Il primo metodo riceve in ingresso l'UUID dell'ancora e lo aggiunge al database di Firebase usando come chiave un numero intero che viene restituito al chiamante. Mentre il secondo metodo, dato il codice intero, recupera l'identificativo dell'ancora e svolge su di esso le operazioni specificate nella *lambda expression* `onSuccess`. ### Aggiunta del modello L'aggiunta del modello avviene attraverso la funzione `addModel`, che opera in modo simile a quanto visto fin'ora, con l'unica differenza che l'aggiunta è consentita solo se l'applicazione si trova nello stato `NONE`. Inoltre la creazione dell'ancora è delegata al metodo `hostCloudAnchors` che si occupa anche dell'upload di quest'ultima sul server. ```kotlin private fun addModel( hitResult: HitResult, plane: Plane, motionEvent: MotionEvent ) { if (cloudAnchorState != CloudAnchorState.NONE) return cloudAnchor = arFragment.arSceneView.session .hostCloudAnchor(hitResult.createAnchor()) cloudAnchorState = CloudAnchorState.HOSTING buildRenderable(this, Uri.parse("model.sfb")) { addTransformableNodeToScene( arFragment, cloudAnchor ?: return@buildRenderable, it ) } } ``` ### Check Hosting Il metodo `checkCloudAnchor` viene eseguito ogni qual volta viene aggiornata la scena e, in base allo stato dell'applicazione vengono eseguite determinate operazioni. ```kotlin private fun checkCloudAnchor(frameTime: FrameTime) { if (cloudAnchorState != CloudAnchorState.HOSTING && cloudAnchorState != CloudAnchorState.RESOLVING ) return val cloudState=cloudAnchor?.cloudAnchorState?:return if (cloudState.isError) { toastError() cloudAnchorState = CloudAnchorState.NONE return } if (cloudState != Anchor.CloudAnchorState.SUCCESS) return if (cloudAnchorState == CloudAnchorState.HOSTING) checkHosting() else checkResolving() } ``` Nel caso specifico in cui il processo di caricamento sia stato completato con successo viene eseguita la funzione `checkHosting` che si occupa di notificare all'utente il codice numerico associato all'ancora (vedi fig. \ref{ca1}) e di cambiare lo stato dell'applicazione da `HOSTING` a `HOSTED`. ```kotlin private fun checkHosting() { val cAnchor = cloudAnchor ?: return val shortCode = cloudAnchorsHelper .getShortCode(cAnchor.cloudAnchorId) Toast .makeText( this, "Anchor hosted with code $shortCode", Toast.LENGTH_LONG ) .show() cloudAnchorState = CloudAnchorState.HOSTED } ``` ### Resolving dell'ancora L'utente può ripristinare un'ancora premendo sul pulsante *resolve*. Il listener associato a questo evento è racchiuso nella funzione `onResolve` che a sua volta mostra all'utente un dialog in cui può inserire il codice dell'ancora da ripristinare (vedi fig. \ref{ca2}). ```kotlin fun onResolveClick(view: View) { if (cloudAnchor != null) return val dialog = ResolveDialogFragment() dialog.setOkListener(this::onResolveOkPressed) dialog.show(supportFragmentManager, "Resolve") } ``` Alla conferma dell'inserimento, da parte dell'utente, viene eseguito il metodo `onResolveOkPressed` che converte lo *short code* nell'UUID dell'ancora e da questo ripristina il nodo nella scena. ```kotlin private fun onResolveOkPressed(dialogValue: String) { val shortCode = dialogValue.toInt() cloudAnchorsHelper.getCloudAnchorId(shortCode) { cloudAnchor = arFragment.arSceneView.session .resolveCloudAnchor(it) buildRenderable(this, Uri.parse("model.sfb")) { val anchor = cloudAnchor ?: return@buildRenderable addTransformableNodeToScene(arFragment, anchor, it) cloudAnchorState = CloudAnchorState.RESOLVING } } } ``` [^futuro]: Il ripristino non può essere troppo dilazionato nel tempo in quanto le ancore vengono conservate sul server per massimo ventiquattro ore.