void StopProjectDiscovery()
    {
        if (m_ProjectManagerInternal != null)
        {
            m_ProjectManagerInternal.Cancel();

            m_ProjectManagerInternal.onProjectsRefreshBegin -= OnProjectRefreshBegin;
            m_ProjectManagerInternal.onProjectsRefreshEnd   -= OnProjectRefreshEnd;

            m_ProjectManagerInternal = null;
        }
    }
    async Task DownloadManifestDiff(IPlayerClient client, SyncManifest oldManifest, SyncManifest newManifest,
                                    UnityProject project, string sourceId, PlayerStorage storage)
    {
        List <ManifestEntry> entries;

        if (oldManifest == null)
        {
            var content = newManifest.Content;
            entries = content.Values.ToList();
        }
        else
        {
            ParseManifest(oldManifest.Content, newManifest.Content, out var modified, out var deleted);
            entries = modified.ToList();

            // TODO Handle deleted models
        }

        var destinationFolder = storage.GetSourceProjectFolder(project, sourceId);
        var downloadFolder    = Path.Combine(destinationFolder, k_TemporaryFolder);

        var progress = new ProjectManagerInternal.DownloadProgress();

        progress.SetTotal(entries.Count);

        var tasks = entries.Select(entry => ProjectManagerInternal.DownloadAndStore(client, sourceId, entry, newManifest, downloadFolder, progress)).ToList();

        // Don't forget the manifest itself
        tasks.Add(ProjectManagerInternal.RunFileIOOperation(() =>
        {
            newManifest.EditorSave(downloadFolder);
            return(Task.CompletedTask);
        }));


        // Wait for all download to finish
        var task = Task.WhenAll(tasks);

        while (!task.IsCompleted)
        {
            lock (m_ProgressLock)
            {
                m_Progress = progress.percent;
            }

            await Task.Delay(200);
        }

        // TODO Handle errors in the DownloadProgress

        // Backward compatibility with local viewer cache that have SyncPrefab as a file.
        var prefabPath = SyncInstance.GetPrefabPath(downloadFolder);

        if (prefabPath == null)
        {
            var prefabName = sourceId + SyncPrefab.Extension;
            var syncPrefab = SyncInstance.GenerateSyncPrefabFromManifest(prefabName, downloadFolder, newManifest);
            var fullPath   = Path.Combine(downloadFolder, prefabName);

            PlayerFile.Save(syncPrefab, fullPath);
        }

        // Delete SyncInstances since they cannot be imported
        var instancesFolder = Path.Combine(downloadFolder, "instances");

        if (Directory.Exists(instancesFolder))
        {
            Directory.Delete(instancesFolder, true);
        }

        // Move all content from temporary download folder to the final destination
        MoveDirectory(downloadFolder, destinationFolder);
    }
    void StartProjectDiscovery()
    {
        if (m_RefreshProjectsCoroutine != null)
        {
            EditorCoroutineUtility.StopCoroutine(m_RefreshProjectsCoroutine);
        }

        if (m_ProjectManagerInternal == null)
        {
            var fullImportFolder = Path.Combine(Application.dataPath, m_ImportFolder);
            m_ProjectManagerInternal = new ProjectManagerInternal(fullImportFolder, false, true);

            m_ProjectDownloader = new ReflectProjectDownloader(fullImportFolder);

            m_ProjectDownloader.onProgressChanged += f =>
            {
                m_TaskProgress = f;

                if (m_TaskProgress < 1.0f)
                {
                    m_TaskInProgressName = "Downloading";
                }
                else
                {
                    m_TaskInProgressName = null;
                    m_TaskProgress       = 0.0f;
                    AssetDatabase.Refresh();
                }
            };
            m_ProjectDownloader.onError += exception =>
            {
                var msg = exception is RpcException rpcException ? rpcException.Status.Detail : exception.ToString();
                Debug.LogError(msg);
            };

            m_ProjectManagerInternal.progressChanged += (f, s) =>
            {
                m_TaskProgress       = f;
                m_TaskInProgressName = s;
            };

            m_ProjectManagerInternal.onError += exception =>
            {
                if (m_IsFetchingProjects)
                {
                    m_RefreshProjectsException = exception;
                }

                var msg = exception is RpcException rpcException ? rpcException.Status.Detail : exception.Message;
                Debug.LogError(msg);
            };

            m_ProjectManagerInternal.taskCompleted += () =>
            {
                m_TaskInProgressName = null;
                m_TaskProgress       = 0.0f;
                AssetDatabase.Refresh();
            };

            m_TaskInProgressName = null;
            m_TaskProgress       = 0.0f;

            m_ProjectManagerInternal.onProjectsRefreshBegin += OnProjectRefreshBegin;
            m_ProjectManagerInternal.onProjectsRefreshEnd   += OnProjectRefreshEnd;
        }

        m_RefreshProjectsCoroutine = EditorCoroutineUtility.StartCoroutine(m_ProjectManagerInternal.RefreshProjectListCoroutine(), this);
    }