// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Collections; using UnityEngine; using HoloToolkit.Unity; #if UNITY_WSA && !UNITY_EDITOR using System.Collections.Generic; #if UNITY_2017_2_OR_NEWER using UnityEngine.XR.WSA; using UnityEngine.XR.WSA.Persistence; using UnityEngine.XR.WSA.Sharing; #else using UnityEngine.VR.WSA; using UnityEngine.VR.WSA.Persistence; using UnityEngine.VR.WSA.Sharing; #endif #endif namespace HoloToolkit.Sharing.Tests { /// <summary> /// Manages creating anchors and sharing the anchors with other clients. /// </summary> public class ImportExportAnchorManager : Singleton<ImportExportAnchorManager> { /// <summary> /// Enum to track the progress through establishing a shared coordinate system. /// </summary> private enum ImportExportState { // Overall states Start, Failed, Ready, RoomApiInitializing, RoomApiInitialized, AnchorEstablished, // AnchorStore states AnchorStore_Initializing, // Anchor creation values InitialAnchorRequired, CreatingInitialAnchor, ReadyToExportInitialAnchor, UploadingInitialAnchor, // Anchor values DataRequested, DataReady, Importing } private ImportExportState currentState = ImportExportState.Start; public string StateName { get { return currentState.ToString(); } } public bool AnchorEstablished { get { return currentState == ImportExportState.AnchorEstablished; } } /// <summary> /// The Room Id of the current room. /// </summary> public long RoomID { get { return roomID; } set { if (currentRoom == null) { roomID = value; } } } private static bool ShouldLocalUserCreateRoom { get { if (SharingStage.Instance == null || SharingStage.Instance.SessionUsersTracker == null) { return false; } long localUserId; using (User localUser = SharingStage.Instance.Manager.GetLocalUser()) { localUserId = localUser.GetID(); } for (int i = 0; i < SharingStage.Instance.SessionUsersTracker.CurrentUsers.Count; i++) { if (SharingStage.Instance.SessionUsersTracker.CurrentUsers[i].GetID() < localUserId) { return false; } } return true; } } /// <summary> /// Called once the anchor has fully uploaded /// </summary> public event Action<bool> AnchorUploaded; #if UNITY_WSA && !UNITY_EDITOR /// <summary> /// Called when the anchor has been loaded /// </summary> public event Action AnchorLoaded; /// <summary> /// WorldAnchorTransferBatch is the primary object in serializing/deserializing anchors. /// <remarks>Only available on device.</remarks> /// </summary> private WorldAnchorTransferBatch sharedAnchorInterface; /// <summary> /// Keeps track of locally stored anchors. /// <remarks>Only available on device.</remarks> /// </summary> private WorldAnchorStore anchorStore; /// <summary> /// Keeps track of the name of the anchor we are exporting. /// </summary> private string exportingAnchorName; /// <summary> /// The datablob of the anchor. /// </summary> private List<byte> exportingAnchorBytes = new List<byte>(); /// <summary> /// Sometimes we'll see a really small anchor blob get generated. /// These tend to not work, so we have a minimum trustworthy size. /// </summary> private const uint MinTrustworthySerializedAnchorDataSize = 100000; #endif /// <summary> /// Keeps track of stored anchor data blob. /// </summary> private byte[] rawAnchorData; /// <summary> /// Keeps track of if the sharing service is ready. /// We need the sharing service to be ready so we can /// upload and download data for sharing anchors. /// </summary> private bool sharingServiceReady; /// <summary> /// The room manager API for the sharing service. /// </summary> private RoomManager roomManager; /// <summary> /// Keeps track of the current room we are connected to. Anchors /// are kept in rooms. /// </summary> private Room currentRoom; /// <summary> /// Some room ID for indicating which room we are in. /// </summary> private long roomID = 8675309; /// <summary> /// Indicates if the room should kept around even after all users leave /// </summary> public bool KeepRoomAlive; /// <summary> /// Room name to join /// </summary> public string RoomName = "DefaultRoom"; /// <summary> /// Debug text for displaying information. /// </summary> public TextMesh AnchorDebugText; /// <summary> /// Provides updates when anchor data is uploaded/downloaded. /// </summary> private RoomManagerAdapter roomManagerListener; #region Unity APIs protected override void Awake() { base.Awake(); #if UNITY_WSA && !UNITY_EDITOR // We need to get our local anchor store started up. currentState = ImportExportState.AnchorStore_Initializing; WorldAnchorStore.GetAsync(AnchorStoreReady); #else currentState = ImportExportState.Ready; #endif } private void Start() { // SharingStage should be valid at this point, but we may not be connected. if (SharingStage.Instance.IsConnected) { Connected(); } else { SharingStage.Instance.SharingManagerConnected += Connected; } } private void Update() { switch (currentState) { // If the local anchor store is initialized. case ImportExportState.Ready: if (sharingServiceReady) { StartCoroutine(InitRoomApi()); } break; case ImportExportState.RoomApiInitialized: StartAnchorProcess(); break; #if UNITY_WSA && !UNITY_EDITOR case ImportExportState.DataReady: // DataReady is set when the anchor download completes. currentState = ImportExportState.Importing; WorldAnchorTransferBatch.ImportAsync(rawAnchorData, ImportComplete); break; case ImportExportState.InitialAnchorRequired: currentState = ImportExportState.CreatingInitialAnchor; CreateAnchorLocally(); break; case ImportExportState.ReadyToExportInitialAnchor: // We've created an anchor locally and it is ready to export. currentState = ImportExportState.UploadingInitialAnchor; Export(); break; #endif } } protected override void OnDestroy() { if (SharingStage.Instance != null) { if (SharingStage.Instance.SessionsTracker != null) { SharingStage.Instance.SessionsTracker.CurrentUserJoined -= CurrentUserJoinedSession; SharingStage.Instance.SessionsTracker.CurrentUserLeft -= CurrentUserLeftSession; } } if (roomManagerListener != null) { roomManagerListener.AnchorsChangedEvent -= RoomManagerCallbacks_AnchorsChanged; roomManagerListener.AnchorsDownloadedEvent -= RoomManagerListener_AnchorsDownloaded; roomManagerListener.AnchorUploadedEvent -= RoomManagerListener_AnchorUploaded; if (roomManager != null) { roomManager.RemoveListener(roomManagerListener); } roomManagerListener.Dispose(); roomManagerListener = null; } if (roomManager != null) { roomManager.Dispose(); roomManager = null; } base.OnDestroy(); } #endregion #region Event Callbacks /// <summary> /// Called when the sharing stage connects to a server. /// </summary> /// <param name="sender">Sender.</param> /// <param name="e">Events Arguments.</param> private void Connected(object sender = null, EventArgs e = null) { SharingStage.Instance.SharingManagerConnected -= Connected; if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Starting..."); } if (AnchorDebugText != null) { AnchorDebugText.text += "\nConnected to Server"; } // Setup the room manager callbacks. roomManager = SharingStage.Instance.Manager.GetRoomManager(); roomManagerListener = new RoomManagerAdapter(); roomManagerListener.AnchorsChangedEvent += RoomManagerCallbacks_AnchorsChanged; roomManagerListener.AnchorsDownloadedEvent += RoomManagerListener_AnchorsDownloaded; roomManagerListener.AnchorUploadedEvent += RoomManagerListener_AnchorUploaded; roomManager.AddListener(roomManagerListener); // We will register for session joined and left to indicate when the sharing service // is ready for us to make room related requests. SharingStage.Instance.SessionsTracker.CurrentUserJoined += CurrentUserJoinedSession; SharingStage.Instance.SessionsTracker.CurrentUserLeft += CurrentUserLeftSession; } /// <summary> /// Called when anchor upload operations complete. /// </summary> private void RoomManagerListener_AnchorUploaded(bool successful, XString failureReason) { if (successful) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Successfully uploaded anchor"); } if (AnchorDebugText != null) { AnchorDebugText.text += "\nSuccessfully uploaded anchor"; } currentState = ImportExportState.AnchorEstablished; } else { if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\n Upload failed " + failureReason); } Debug.LogError("Anchor Manager: Upload failed " + failureReason); currentState = ImportExportState.Failed; } if (AnchorUploaded != null) { AnchorUploaded(successful); } } /// <summary> /// Called when anchor download operations complete. /// </summary> private void RoomManagerListener_AnchorsDownloaded(bool successful, AnchorDownloadRequest request, XString failureReason) { // If we downloaded anchor data successfully we should import the data. if (successful) { int dataSize = request.GetDataSize(); if (SharingStage.Instance.ShowDetailedLogs) { Debug.LogFormat("Anchor Manager: Anchor size: {0} bytes.", dataSize.ToString()); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nAnchor size: {0} bytes.", dataSize.ToString()); } rawAnchorData = new byte[dataSize]; request.GetData(rawAnchorData, dataSize); currentState = ImportExportState.DataReady; } else { if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nAnchor DL failed " + failureReason); } // If we failed, we can ask for the data again. Debug.LogWarning("Anchor Manager: Anchor DL failed " + failureReason); #if UNITY_WSA && !UNITY_EDITOR MakeAnchorDataRequest(); #endif } } /// <summary> /// Called when the anchors have changed in the room. /// </summary> /// <param name="room">The room where the anchors have changed.</param> private void RoomManagerCallbacks_AnchorsChanged(Room room) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.LogFormat("Anchor Manager: Anchors in room {0} changed", room.GetName()); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nAnchors in room {0} changed", room.GetName()); } // if we're currently in the room where the anchors changed if (currentRoom == room) { ResetState(); } } /// <summary> /// Called when the user joins a session. /// In this case, we are using this event is used to signal that the sharing service is ready to make room-related requests. /// </summary> /// <param name="session">Session joined.</param> private void CurrentUserJoinedSession(Session session) { if (SharingStage.Instance.Manager.GetLocalUser().IsValid()) { sharingServiceReady = true; } else { Debug.LogWarning("Unable to get local user on session joined"); } } /// <summary> /// Called when the user leaves a session. /// This event is used to signal that the sharing service must stop making room-related requests. /// </summary> /// <param name="session">Session left.</param> private void CurrentUserLeftSession(Session session) { sharingServiceReady = false; // Reset the state so that we join a new room when we eventually rejoin a session ResetState(); } #endregion /// <summary> /// Resets the Anchor Manager state. /// </summary> private void ResetState() { #if UNITY_WSA && !UNITY_EDITOR if (anchorStore != null) { currentState = ImportExportState.Ready; } else { currentState = ImportExportState.AnchorStore_Initializing; } #else currentState = ImportExportState.Ready; #endif } /// <summary> /// Initializes the room API. /// </summary> private IEnumerator InitRoomApi() { currentState = ImportExportState.RoomApiInitializing; // First check if there is a current room currentRoom = roomManager.GetCurrentRoom(); while (currentRoom == null) { // If we have a room, we'll join the first room we see. // If we are the user with the lowest user ID, we will create the room. // Otherwise we will wait for the room to be created. yield return new WaitForEndOfFrame(); if (roomManager.GetRoomCount() == 0) { if (ShouldLocalUserCreateRoom) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Creating room " + RoomName); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nCreating room " + RoomName); } // To keep anchors alive even if all users have left the session ... // Pass in true instead of false in CreateRoom. currentRoom = roomManager.CreateRoom(new XString(RoomName), roomID, KeepRoomAlive); } } else { // Look through the existing rooms and join the one that matches the room name provided. int roomCount = roomManager.GetRoomCount(); for (int i = 0; i < roomCount; i++) { Room room = roomManager.GetRoom(i); if (room.GetName().GetString().Equals(RoomName, StringComparison.OrdinalIgnoreCase)) { currentRoom = room; roomManager.JoinRoom(currentRoom); if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Joining room " + room.GetName().GetString()); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nJoining room " + room.GetName().GetString()); } break; } } if (currentRoom == null) { // Couldn't locate a matching room, just join the first one. currentRoom = roomManager.GetRoom(0); roomManager.JoinRoom(currentRoom); RoomName = currentRoom.GetName().GetString(); } currentState = ImportExportState.RoomApiInitialized; } yield return new WaitForEndOfFrame(); } if (currentRoom.GetAnchorCount() == 0) { #if UNITY_WSA && !UNITY_EDITOR // If the room has no anchors, request the initial anchor currentState = ImportExportState.InitialAnchorRequired; #else currentState = ImportExportState.RoomApiInitialized; #endif } else { // Room already has anchors currentState = ImportExportState.RoomApiInitialized; } if (SharingStage.Instance.ShowDetailedLogs) { Debug.LogFormat("Anchor Manager: In room {0} with ID {1}", roomManager.GetCurrentRoom().GetName().GetString(), roomManager.GetCurrentRoom().GetID().ToString()); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nIn room {0} with ID {1}", roomManager.GetCurrentRoom().GetName().GetString(), roomManager.GetCurrentRoom().GetID().ToString()); } yield return null; } /// <summary> /// Kicks off the process of creating the shared space. /// </summary> private void StartAnchorProcess() { // First, are there any anchors in this room? int anchorCount = currentRoom.GetAnchorCount(); if (SharingStage.Instance.ShowDetailedLogs) { Debug.LogFormat("Anchor Manager: {0} anchors found.", anchorCount.ToString()); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\n{0} anchors found.", anchorCount.ToString()); } #if UNITY_WSA && !UNITY_EDITOR // If there are anchors, we should attach to the first one. if (anchorCount > 0) { // Extract the name of the anchor. XString storedAnchorString = currentRoom.GetAnchorName(0); string storedAnchorName = storedAnchorString.GetString(); // Attempt to attach to the anchor in our local anchor store. if (AttachToCachedAnchor(storedAnchorName) == false) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Starting room anchor download of " + storedAnchorString); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nStarting room anchor download of " + storedAnchorString); } // If we cannot find the anchor by name, we will need the full data blob. MakeAnchorDataRequest(); } } #else currentState = ImportExportState.AnchorEstablished; if (AnchorDebugText != null) { AnchorDebugText.text += anchorCount > 0 ? "\n" + currentRoom.GetAnchorName(0).ToString() : "\nNo Anchors Found"; } #endif } #region WSA Specific Methods #if UNITY_WSA && !UNITY_EDITOR /// <summary> /// Kicks off getting the datablob required to import the shared anchor. /// When finished downloading, the RoomManager will raise RoomManagerListener_AnchorsDownloaded. /// </summary> private void MakeAnchorDataRequest() { if (roomManager.DownloadAnchor(currentRoom, currentRoom.GetAnchorName(0))) { currentState = ImportExportState.DataRequested; } else { Debug.LogError("Anchor Manager: Couldn't make the download request."); if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nCouldn't make the download request."); } currentState = ImportExportState.Failed; } } /// <summary> /// Called when the local anchor store is ready. /// </summary> /// <param name="store"></param> private void AnchorStoreReady(WorldAnchorStore store) { anchorStore = store; if (!KeepRoomAlive) { anchorStore.Clear(); } currentState = ImportExportState.Ready; } /// <summary> /// Starts establishing a new anchor. /// </summary> private void CreateAnchorLocally() { WorldAnchor anchor = this.EnsureComponent<WorldAnchor>(); if (anchor.isLocated) { currentState = ImportExportState.ReadyToExportInitialAnchor; } else { anchor.OnTrackingChanged += Anchor_OnTrackingChanged_InitialAnchor; } } /// <summary> /// Callback to trigger when an anchor has been 'found'. /// </summary> private void Anchor_OnTrackingChanged_InitialAnchor(WorldAnchor self, bool located) { if (located) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Found anchor, ready to export"); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nFound anchor, ready to export"); } currentState = ImportExportState.ReadyToExportInitialAnchor; } else { Debug.LogError("Anchor Manager: Failed to locate local anchor!"); if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nFailed to locate local anchor!"); } currentState = ImportExportState.Failed; } self.OnTrackingChanged -= Anchor_OnTrackingChanged_InitialAnchor; } /// <summary> /// Attempts to attach to an anchor by anchorName in the local store. /// </summary> /// <returns>True if it attached, false if it could not attach</returns> private bool AttachToCachedAnchor(string anchorName) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.LogFormat("Anchor Manager: Looking for cached anchor {0}...", anchorName); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nLooking for cached anchor {0}...", anchorName); } string[] ids = anchorStore.GetAllIds(); for (int index = 0; index < ids.Length; index++) { if (ids[index] == anchorName) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.LogFormat("Anchor Manager: Attempting to load cached anchor {0}...", anchorName); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nAttempting to load cached anchor {0}...", anchorName); } WorldAnchor anchor = anchorStore.Load(ids[index], gameObject); if (anchor.isLocated) { AnchorLoadComplete(); } else { if (AnchorDebugText != null) { AnchorDebugText.text += "\n"+anchorName; } anchor.OnTrackingChanged += ImportExportAnchorManager_OnTrackingChanged_Attaching; currentState = ImportExportState.AnchorEstablished; } return true; } } // Didn't find the anchor, so we'll download from room. return false; } /// <summary> /// Called when tracking changes for a 'cached' anchor. /// </summary> /// <param name="self"></param> /// <param name="located"></param> private void ImportExportAnchorManager_OnTrackingChanged_Attaching(WorldAnchor self, bool located) { if (located) { AnchorLoadComplete(); } else { Debug.LogWarning("Anchor Manager: Failed to find local anchor from cache."); if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nFailed to find local anchor from cache."); } MakeAnchorDataRequest(); } self.OnTrackingChanged -= ImportExportAnchorManager_OnTrackingChanged_Attaching; } /// <summary> /// Called when a remote anchor has been deserialized. /// </summary> /// <param name="status"></param> /// <param name="anchorBatch"></param> private void ImportComplete(SerializationCompletionReason status, WorldAnchorTransferBatch anchorBatch) { if (status == SerializationCompletionReason.Succeeded) { if (anchorBatch.GetAllIds().Length > 0) { string first = anchorBatch.GetAllIds()[0]; if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Successfully imported anchor " + first); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nSuccessfully imported anchor " + first); } WorldAnchor anchor = anchorBatch.LockObject(first, gameObject); anchorStore.Save(first, anchor); } AnchorLoadComplete(); } else { Debug.LogError("Anchor Manager: Import failed"); if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nImport failed"); } currentState = ImportExportState.DataReady; } } private void AnchorLoadComplete() { if (AnchorLoaded != null) { AnchorLoaded(); } currentState = ImportExportState.AnchorEstablished; } /// <summary> /// Exports the currently created anchor. /// </summary> private void Export() { WorldAnchor anchor = this.GetComponent<WorldAnchor>(); string guidString = Guid.NewGuid().ToString(); exportingAnchorName = guidString; // Save the anchor to our local anchor store. if (anchor != null && anchorStore.Save(exportingAnchorName, anchor)) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Exporting anchor " + exportingAnchorName); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nExporting anchor {0}", exportingAnchorName); } sharedAnchorInterface = new WorldAnchorTransferBatch(); sharedAnchorInterface.AddWorldAnchor(guidString, anchor); WorldAnchorTransferBatch.ExportAsync(sharedAnchorInterface, WriteBuffer, ExportComplete); } else { Debug.LogWarning("Anchor Manager: Failed to export anchor, trying again..."); if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nFailed to export anchor, trying again..."); } currentState = ImportExportState.InitialAnchorRequired; } } /// <summary> /// Called by the WorldAnchorTransferBatch as anchor data is available. /// </summary> /// <param name="data"></param> private void WriteBuffer(byte[] data) { exportingAnchorBytes.AddRange(data); } /// <summary> /// Called by the WorldAnchorTransferBatch when anchor exporting is complete. /// </summary> /// <param name="status"></param> private void ExportComplete(SerializationCompletionReason status) { if (status == SerializationCompletionReason.Succeeded && exportingAnchorBytes.Count > MinTrustworthySerializedAnchorDataSize) { if (SharingStage.Instance.ShowDetailedLogs) { Debug.Log("Anchor Manager: Uploading anchor: " + exportingAnchorName); } if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nUploading anchor: " + exportingAnchorName); } roomManager.UploadAnchor( currentRoom, new XString(exportingAnchorName), exportingAnchorBytes.ToArray(), exportingAnchorBytes.Count); } else { Debug.LogWarning("Anchor Manager: Failed to upload anchor, trying again..."); if (AnchorDebugText != null) { AnchorDebugText.text += string.Format("\nFailed to upload anchor, trying again..."); } currentState = ImportExportState.InitialAnchorRequired; } } #endif // UNITY_WSA #endregion // WSA Specific Methods } }