diff --git a/Assets/Scripts/AssetsDownloader.cs b/Assets/Scripts/AssetsDownloader.cs index ae41256..7cf44bc 100644 --- a/Assets/Scripts/AssetsDownloader.cs +++ b/Assets/Scripts/AssetsDownloader.cs @@ -12,478 +12,550 @@ using UnityEngine.SceneManagement; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace QuestAppLauncher { - public class InstallAPK { - private bool installApp(string apkPath) { - bool success = true; - //GameObject.Find("TextDebug").GetComponent().text = "Installing App"; +namespace QuestAppLauncher +{ + public class InstallAPK + { + private bool installApp(string apkPath) +{ + bool success = true; + //GameObject.Find("TextDebug").GetComponent().text = "Installing App"; - try { + try + { //Get Activity then Context AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); - AndroidJavaObject currentActivity = unityPlayer.GetStatic < AndroidJavaObject > ("currentActivity"); - AndroidJavaObject unityContext = currentActivity.Call < AndroidJavaObject > ("getApplicationContext"); + AndroidJavaObject currentActivity = unityPlayer.GetStatic("currentActivity"); + AndroidJavaObject unityContext = currentActivity.Call("getApplicationContext"); //Get the package Name - string packageName = unityContext.Call < string > ("getPackageName"); + string packageName = unityContext.Call("getPackageName"); string authority = packageName + ".fileprovider"; AndroidJavaClass intentObj = new AndroidJavaClass("android.content.Intent"); - string ACTION_VIEW = intentObj.GetStatic < string > ("ACTION_VIEW"); + string ACTION_VIEW = intentObj.GetStatic("ACTION_VIEW"); AndroidJavaObject intent = new AndroidJavaObject("android.content.Intent", ACTION_VIEW); - int FLAG_ACTIVITY_NEW_TASK = intentObj.GetStatic < int > ("FLAG_ACTIVITY_NEW_TASK"); - int FLAG_GRANT_READ_URI_PERMISSION = intentObj.GetStatic < int > ("FLAG_GRANT_READ_URI_PERMISSION"); + + int FLAG_ACTIVITY_NEW_TASK = intentObj.GetStatic("FLAG_ACTIVITY_NEW_TASK"); + int FLAG_GRANT_READ_URI_PERMISSION = intentObj.GetStatic("FLAG_GRANT_READ_URI_PERMISSION"); //File fileObj = new File(String pathname); AndroidJavaObject fileObj = new AndroidJavaObject("java.io.File", apkPath); //FileProvider object that will be used to call it static function AndroidJavaClass fileProvider = new AndroidJavaClass("android.support.v4.content.FileProvider"); //getUriForFile(Context context, String authority, File file) - AndroidJavaObject uri = fileProvider.CallStatic < AndroidJavaObject > ("getUriForFile", unityContext, authority, fileObj); + AndroidJavaObject uri = fileProvider.CallStatic("getUriForFile", unityContext, authority, fileObj); - intent.Call < AndroidJavaObject > ("setDataAndType", uri, "application/vnd.android.package-archive"); - intent.Call < AndroidJavaObject > ("addFlags", FLAG_ACTIVITY_NEW_TASK); - intent.Call < AndroidJavaObject > ("addFlags", FLAG_GRANT_READ_URI_PERMISSION); + intent.Call("setDataAndType", uri, "application/vnd.android.package-archive"); + intent.Call("addFlags", FLAG_ACTIVITY_NEW_TASK); + intent.Call("addFlags", FLAG_GRANT_READ_URI_PERMISSION); currentActivity.Call("startActivity", intent); //GameObject.Find("TextDebug").GetComponent().text = "Success"; - } catch (System.Exception e) { + } + catch (System.Exception e) + { //GameObject.Find("TextDebug").GetComponent().text = "Error: " + e.Message; success = false; - } - - return success; - } - } - /// - /// Downloads assets (app icons packs and names files) from configured repos. - /// - public class AssetsDownloader { - // Download cache folder that contains downloaded files - const string DownloadCacheFolder = "download_cache"; - - // Manifest file to track what we've downloaded - const string DownloadManifestFile = "download_manifest.json"; - - // Temporary filename for download - const string TempDownloadFileExtention = ".tmp_download"; - - // GitHub API url - const string GithubApiUrl = @ "http://api.github.com/"; - - // APK Remote config url - const string APKCUrl = @ "http://vrdevices.hooverhigh.ml/sys/oculus-apk.json"; - - // If Github account is a org set this to true - public bool isorg = true; - - public string GithubUrl = ""; - - // Rate limit in minutes - const int RateLimitInMins = 5; - - // Used for mutual exclusion when loading assets - static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); - - /// - /// Class that tracks each file downloaded. - /// Used to determine whether an asset has been updated since last download. - /// - [Serializable] - public class AssetInfo { - // Repo uri - public string RepoUri; - - // Asset's url - public string Url; - - // Asset's updated at timestamp - public string UpdatedAt; - - // Asset's tag name - public string TagName; } - /// - /// Mapping of asset name to asset info above. This is persisted in the download manifest. - /// - [Serializable] - public class AssetsManifest { - // File name -> release metadata - public Dictionary < string, AssetInfo > Metadata = new Dictionary < string, AssetInfo > (); - - // Last updated timestamp - public DateTime LastUpdated; - } - - /// - /// Helper method to download assets asynchronously. After completion, scene is automatically reloaded - /// if new assets have been downloaded. - /// - /// Current config - /// Download progress interface - used to indicate download progress - /// - public static async Task DownloadAssetsAsync(Config config, IDownloadProgress downloadProgress = null, bool forceCheck = false) { - // Start background thread - Task.Run(async () => { - // Mutual exclusion while loading assets - await AssetsDownloader.semaphoreSlim.WaitAsync(); - - // Attach / detatch JNI. Required for any calls into JNI from background threads. - AndroidJNI.AttachCurrentThread(); - - try { - // Download assets from repos. - AssetsDownloader assetsDownloader = new AssetsDownloader(); - return await assetsDownloader.DownloadFromReposAsync(config, downloadProgress, forceCheck); - } finally { - AssetsDownloader.semaphoreSlim.Release(); - AndroidJNI.DetachCurrentThread(); - } - }).ContinueWith((downloadedAssets) => { - if (downloadedAssets.Result) { - // We downloaded new assets, so re-load the scene - Debug.Log("Downloaded new assets. Re-populating panel"); - SceneManager.LoadSceneAsync(SceneManager.GetActiveScene().name); - } - }); - } - - /// - /// Asynchronously download assets from configured repos. - /// - /// Current config - /// Download progress interface - /// - private async Task < bool > DownloadFromReposAsync(Config config, IDownloadProgress downloadProgress = null, bool forceCheck = false) { - if (null == config.downloadRepos) { - // No repos configured, so return - return false; - } - - // Load the download manifest. This is used to compare if we're up-to-date or not. - AssetsManifest manifest = LoadManifest(); - if (null == manifest) { - manifest = new AssetsManifest(); - } - - // Rate limit update checks to one per couple of minutes, to avoid GitHub's rate limit - if (!forceCheck && DateTime.Now.Subtract(manifest.LastUpdated).TotalMinutes < RateLimitInMins) { - Debug.LogFormat("Exceeded rate limit of {0} mins - last checked for update on {1}", RateLimitInMins, manifest.LastUpdated); - return false; - } - - // Mark that we've just checked for updates & update manifest - manifest.LastUpdated = DateTime.Now; - SaveManifest(manifest); - - if (null != downloadProgress) { - downloadProgress.OnCheckingForUpdates(); - } - - // Download the assets metadata - used to determine whether we are up-to-date or not - var assetsInfo = await DownloadAssetsMetadata(config, manifest, downloadProgress, forceCheck); - if (assetsInfo.Count == 0) { - // No updates have been found, so return - Debug.Log("No updates found"); - if (null != downloadProgress) { - downloadProgress.OnNoUpdatesAvailable(); - } - - return false; - } - - // Download the assets - var downloadedAssets = await DownloadFromReposInternalAsync(manifest, assetsInfo, downloadProgress); - if (null != downloadProgress) { - downloadProgress.OnUpdateFinish(); - } - - return downloadedAssets; - } - - /// - /// Downloads assets metadata. This is used to determine whether our assets are up-to-date. - /// - /// Current config - /// Download progress interface - /// - private async Task < Dictionary < string, AssetInfo >> DownloadAssetsMetadata( - Config config, AssetsManifest manifest, IDownloadProgress downloadProgress = null, bool forceCheck = false) { - // Get asset info from repos - var assetsInfo = new Dictionary < string, - AssetInfo > (StringComparer.OrdinalIgnoreCase); - - // Get the set of repo URIs (removing any duplicates) - var configRepos = new HashSet < string > (); - foreach(var item in config.downloadRepos) { - if (null == item.type || !string.Equals(item.type, Config.DownloadRepo_Type_GitHub, StringComparison.OrdinalIgnoreCase) || !string.Equals(item.type, Config.DownloadRepo_Type_PrivUrl, StringComparison.OrdinalIgnoreCase)) { - // For now, we only support GitHub repos, or our private update url - continue; - } - - configRepos.Add(item.repoUri); - } - - var reposLoaded = new HashSet < string > (); - foreach(var repoUri in configRepos) { - // Get assets from the GitHub repo - var repoLoaded = await GetAssetsInfoFromGithubRepoAsync(repoUri, assetsInfo, downloadProgress); - if (repoLoaded) { - reposLoaded.Add(repoUri); - } - } - - Debug.LogFormat("Assets info contains {0} entries", assetsInfo.Count); - - // Enumerate asset metadata in our download manifest - var deletedAssets = new HashSet < string > (); - foreach(var entry in manifest.Metadata) { - if (assetsInfo.ContainsKey(entry.Key)) { - if (string.Equals(entry.Value.UpdatedAt, assetsInfo[entry.Key].UpdatedAt, StringComparison.OrdinalIgnoreCase) && - File.Exists(Path.Combine(GetOrCreateDownloadPath(), entry.Key))) { - // Matched on file name and updated-at timestamp, so we're up to date. Skip this one. - Debug.LogFormat("Asset {0} is already up to date ({1})", entry.Key, entry.Value.UpdatedAt); - assetsInfo.Remove(entry.Key); - } - } else if (reposLoaded.Contains(entry.Value.RepoUri)) { - // The repo no longer has the cached file, so nuke it to keep things in sync - var filePath = Path.Combine(GetOrCreateDownloadPath(), entry.Key); - if (File.Exists(filePath)) { - Debug.LogFormat("Deleting cached file that no longer exists on server: {0} @ {1}", filePath, entry.Value.RepoUri); - File.Delete(filePath); - } - deletedAssets.Add(entry.Key); - } - } - - // Remove old cached files - foreach(var entry in deletedAssets) { - manifest.Metadata.Remove(entry); - } - - if (deletedAssets.Count > 0) { - // Persist manifest - SaveManifest(manifest); - } - - return assetsInfo; - } - - /// - /// Download assets from repos. The download manifest is also updated. - /// - /// Download manifest - /// Assets to download - /// Download progress interface - /// - private async Task < bool > DownloadFromReposInternalAsync( - AssetsManifest manifest, Dictionary < string, AssetInfo > assetsInfo, IDownloadProgress downloadProgress = null) { - var downloadedAsset = false; - - foreach(var entry in assetsInfo) { - // Download asset - var success = await DownloadAssetFromGitHubRepoAsync(entry.Key, entry.Value, downloadProgress); - if (success) { - // Update manifest - manifest.Metadata[entry.Key] = entry.Value; - downloadedAsset = true; - } - } - - if (downloadedAsset) { - // Persist manifest - SaveManifest(manifest); - } - - return downloadedAsset; - } - - /// - /// Download asset metadata from given uri. The assets info parameter is populate with the metadata. - /// - /// The repo uri to download - /// Assets info mapping that is populate by this function - /// Download progress interface - /// - private async Task < bool > GetAssetsInfoFromGithubRepoAsync(string repoUri, - Dictionary < string, AssetInfo > assetsInfo, IDownloadProgress downloadProgress = null) { - if (isorg == true) { - GithubUrl = GithubApiUrl + "orgs/"; - } else { - GithubUrl = GithubApiUrl; - } - var requestUrl = GithubUrl + repoUri; - Debug.LogFormat("Reading assets from {0}", requestUrl); - - try { - // Request asset url - using(var req = new UnityWebRequest(requestUrl)) { - req.downloadHandler = new DownloadHandlerBuffer(); - await req.SendWebRequest(); - if (req.isNetworkError || req.isHttpError) { - // Error reading asset metadata, so return. - Debug.LogFormat("Error reading asset info: {0}", req.error); - - var responseHeaders = req.GetResponseHeaders(); - if (null != responseHeaders && responseHeaders.ContainsKey("X-RateLimit-Remaining") && - responseHeaders["X-RateLimit-Remaining"] == "0") { - // Github request limit reached. Display a friendly error message. - Debug.LogFormat("Request limit reached"); - - if (null != downloadProgress) { - downloadProgress.OnError(string.Format("Error updating: Request Limit Reached - try again later. ({1})", - req.error, requestUrl)); - } - } else { - if (null != downloadProgress) { - downloadProgress.OnError(string.Format("Error updating: {0} ({1})", - req.error, requestUrl)); - } - } - return false; - } - - // Parse the returned asset metadata - var jObject = JObject.Parse(req.downloadHandler.text); - var tagName = jObject["tag_name"].Value < string > (); - - foreach(var property in jObject["assets"]) { - var updatedAt = property["updated_at"].Value < string > (); - var url = property["url"].Value < string > (); - var name = property["name"].Value < string > (); - - // For now, simply accept any iconpack*.zip and appnames*.txt/json. - if ((name.StartsWith("iconpack") && name.EndsWith(".zip")) || - (name.StartsWith("appnames") && name.EndsWith(".txt")) || - (name.StartsWith("appnames") && name.EndsWith(".json"))) { - assetsInfo[name] = new AssetInfo { - RepoUri = repoUri, Url = url, UpdatedAt = updatedAt, TagName = tagName - }; - } - } - } - } catch (Exception e) { - Debug.LogFormat("Exception reading asset info: {0} ", e.Message); - if (null != downloadProgress) { - downloadProgress.OnError(string.Format("Error updating: {0} ({1})", - e.Message, requestUrl)); - } - - return false; - } - - return true; - } - - /// - /// Download a single asset (file) from repo - /// - /// Name of the asset (file) to download - /// Asset info (metadata) - /// Download progress interface - /// - private async Task < bool > DownloadAssetFromGitHubRepoAsync(string name, AssetInfo assetInfo, - IDownloadProgress downloadProgress) { - var filePath = Path.Combine(GetOrCreateDownloadPath(), name); - var tempFilePath = filePath + TempDownloadFileExtention; - Debug.LogFormat("Downloading asset {0} from {1}", filePath, assetInfo.Url); - - try { - // Request asset url - using(var req = new UnityWebRequest(assetInfo.Url)) { - req.SetRequestHeader("Accept", "application/octet-stream"); - if (null != downloadProgress) { - downloadProgress.OnDownloadStart(name); - } - var downloadHandler = new DownloadHandlerFileWithProgress(tempFilePath, downloadProgress.OnDownloadProgress); - downloadHandler.removeFileOnAbort = true; - req.downloadHandler = downloadHandler; - await req.SendWebRequest(); - - if (req.isNetworkError || req.isHttpError) { - // Error reading asset metadata, so return. - Debug.LogFormat("Error downloading asset: {0}", req.error); - if (null != downloadProgress) { - downloadProgress.OnError(string.Format("Error updating: {0} ({1})", - req.error, assetInfo.Url)); - } - - if (File.Exists(tempFilePath)) { - File.Delete(tempFilePath); - } - - return false; - } - - // Rename temp file to desination file to ensure that we are not downloading - // an error body message into the destination file. - if (File.Exists(filePath)) { - File.Delete(filePath); - } - - File.Move(tempFilePath, filePath); - - if (null != downloadProgress) { - downloadProgress.OnDownloadFinish(); - } - - return true; - } - } catch (Exception e) { - Debug.LogFormat("Exception downloading asset: {0} ", e.Message); - if (null != downloadProgress) { - downloadProgress.OnError(string.Format("Error updating: {0} ({1})", - e.Message, assetInfo.Url)); - } - } - - return false; - } - - /// - /// Load download manifest - /// - /// Download manifest - static private AssetsManifest LoadManifest() { - var manifestPath = Path.Combine(GetOrCreateDownloadPath(), DownloadManifestFile); - if (!File.Exists(manifestPath)) { - return null; - } - - Debug.Log("Found manifest: " + manifestPath); - var jsonManifest = File.ReadAllText(manifestPath); - - try { - AssetsManifest manifest = new AssetsManifest(); - return JsonConvert.DeserializeObject < AssetsManifest > (jsonManifest); - } catch (Exception e) { - Debug.Log(string.Format("Failed to read manifest: {0}", e.Message)); - } - - return null; - } - - /// - /// Persist download manifest - /// - /// Download manifest to persist - static private void SaveManifest(AssetsManifest manifest) { - var manifestPath = Path.Combine(GetOrCreateDownloadPath(), DownloadManifestFile); - Debug.Log("Saving manifest: " + manifestPath); - - try { - File.WriteAllText(manifestPath, JsonConvert.SerializeObject(manifest, Formatting.Indented)); - } catch (Exception e) { - Debug.Log(string.Format("Failed to write manifest: {0}", e.Message)); - } - } - - static public string GetOrCreateDownloadPath() { - string path = Path.Combine(UnityEngine.Application.persistentDataPath, DownloadCacheFolder); - Directory.CreateDirectory(path); - return path; - } - } + return success; +} + } + /// + /// Downloads assets (app icons packs and names files) from configured repos. + /// + public class AssetsDownloader + { + // Download cache folder that contains downloaded files + const string DownloadCacheFolder = "download_cache"; + + // Manifest file to track what we've downloaded + const string DownloadManifestFile = "download_manifest.json"; + + // Temporary filename for download + const string TempDownloadFileExtention = ".tmp_download"; + + // GitHub API url + const string GithubApiUrl = @"http://api.github.com/"; + + // If Github account is a org set this to true + public bool isorg = true; + + public string GithubUrl = ""; + + // Rate limit in minutes + const int RateLimitInMins = 5; + + // Used for mutual exclusion when loading assets + static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); + + /// + /// Class that tracks each file downloaded. + /// Used to determine whether an asset has been updated since last download. + /// + [Serializable] + public class AssetInfo + { + // Repo uri + public string RepoUri; + + // Asset's url + public string Url; + + // Asset's updated at timestamp + public string UpdatedAt; + + // Asset's tag name + public string TagName; + } + + /// + /// Mapping of asset name to asset info above. This is persisted in the download manifest. + /// + [Serializable] + public class AssetsManifest + { + // File name -> release metadata + public Dictionary Metadata = new Dictionary(); + + // Last updated timestamp + public DateTime LastUpdated; + } + + /// + /// Helper method to download assets asynchronously. After completion, scene is automatically reloaded + /// if new assets have been downloaded. + /// + /// Current config + /// Download progress interface - used to indicate download progress + /// + public static async Task DownloadAssetsAsync(Config config, IDownloadProgress downloadProgress = null, bool forceCheck = false) + { + // Start background thread + Task.Run(async () => + { + // Mutual exclusion while loading assets + await AssetsDownloader.semaphoreSlim.WaitAsync(); + + // Attach / detatch JNI. Required for any calls into JNI from background threads. + AndroidJNI.AttachCurrentThread(); + + try + { + // Download assets from repos. + AssetsDownloader assetsDownloader = new AssetsDownloader(); + return await assetsDownloader.DownloadFromReposAsync(config, downloadProgress, forceCheck); + } + finally + { + AssetsDownloader.semaphoreSlim.Release(); + AndroidJNI.DetachCurrentThread(); + } + }).ContinueWith((downloadedAssets) => + { + if (downloadedAssets.Result) + { + // We downloaded new assets, so re-load the scene + Debug.Log("Downloaded new assets. Re-populating panel"); + SceneManager.LoadSceneAsync(SceneManager.GetActiveScene().name); + } + }); + } + + /// + /// Asynchronously download assets from configured repos. + /// + /// Current config + /// Download progress interface + /// + private async Task DownloadFromReposAsync(Config config, IDownloadProgress downloadProgress = null, bool forceCheck = false) + { + if (null == config.downloadRepos) + { + // No repos configured, so return + return false; + } + + // Load the download manifest. This is used to compare if we're up-to-date or not. + AssetsManifest manifest = LoadManifest(); + if (null == manifest) + { + manifest = new AssetsManifest(); + } + + // Rate limit update checks to one per couple of minutes, to avoid GitHub's rate limit + if (!forceCheck && DateTime.Now.Subtract(manifest.LastUpdated).TotalMinutes < RateLimitInMins) + { + Debug.LogFormat("Exceeded rate limit of {0} mins - last checked for update on {1}", RateLimitInMins, manifest.LastUpdated); + return false; + } + + // Mark that we've just checked for updates & update manifest + manifest.LastUpdated = DateTime.Now; + SaveManifest(manifest); + + if (null != downloadProgress) + { + downloadProgress.OnCheckingForUpdates(); + } + + // Download the assets metadata - used to determine whether we are up-to-date or not + var assetsInfo = await DownloadAssetsMetadata(config, manifest, downloadProgress, forceCheck); + if (assetsInfo.Count == 0) + { + // No updates have been found, so return + Debug.Log("No updates found"); + if (null != downloadProgress) + { + downloadProgress.OnNoUpdatesAvailable(); + } + + return false; + } + + // Download the assets + var downloadedAssets = await DownloadFromReposInternalAsync(manifest, assetsInfo, downloadProgress); + if (null != downloadProgress) + { + downloadProgress.OnUpdateFinish(); + } + + return downloadedAssets; + } + + /// + /// Downloads assets metadata. This is used to determine whether our assets are up-to-date. + /// + /// Current config + /// Download progress interface + /// + private async Task> DownloadAssetsMetadata( + Config config, AssetsManifest manifest, IDownloadProgress downloadProgress = null, bool forceCheck = false) + { + // Get asset info from repos + var assetsInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Get the set of repo URIs (removing any duplicates) + var configRepos = new HashSet(); + foreach (var item in config.downloadRepos) + { + if (null == item.type || !string.Equals(item.type, Config.DownloadRepo_Type_GitHub, StringComparison.OrdinalIgnoreCase) || !string.Equals(item.type, Config.DownloadRepo_Type_PrivUrl, StringComparison.OrdinalIgnoreCase)) + { + // For now, we only support GitHub repos, or our private update url + continue; + } + + configRepos.Add(item.repoUri); + } + + var reposLoaded = new HashSet(); + foreach (var repoUri in configRepos) + { + // Get assets from the GitHub repo + var repoLoaded = await GetAssetsInfoFromGithubRepoAsync(repoUri, assetsInfo, downloadProgress); + if (repoLoaded) + { + reposLoaded.Add(repoUri); + } + } + + Debug.LogFormat("Assets info contains {0} entries", assetsInfo.Count); + + // Enumerate asset metadata in our download manifest + var deletedAssets = new HashSet(); + foreach (var entry in manifest.Metadata) + { + if (assetsInfo.ContainsKey(entry.Key)) + { + if (string.Equals(entry.Value.UpdatedAt, assetsInfo[entry.Key].UpdatedAt, StringComparison.OrdinalIgnoreCase) && + File.Exists(Path.Combine(GetOrCreateDownloadPath(), entry.Key))) + { + // Matched on file name and updated-at timestamp, so we're up to date. Skip this one. + Debug.LogFormat("Asset {0} is already up to date ({1})", entry.Key, entry.Value.UpdatedAt); + assetsInfo.Remove(entry.Key); + } + } + else if (reposLoaded.Contains(entry.Value.RepoUri)) + { + // The repo no longer has the cached file, so nuke it to keep things in sync + var filePath = Path.Combine(GetOrCreateDownloadPath(), entry.Key); + if (File.Exists(filePath)) + { + Debug.LogFormat("Deleting cached file that no longer exists on server: {0} @ {1}", filePath, entry.Value.RepoUri); + File.Delete(filePath); + } + deletedAssets.Add(entry.Key); + } + } + + // Remove old cached files + foreach (var entry in deletedAssets) + { + manifest.Metadata.Remove(entry); + } + + if (deletedAssets.Count > 0) + { + // Persist manifest + SaveManifest(manifest); + } + + return assetsInfo; + } + + /// + /// Download assets from repos. The download manifest is also updated. + /// + /// Download manifest + /// Assets to download + /// Download progress interface + /// + private async Task DownloadFromReposInternalAsync( + AssetsManifest manifest, Dictionary assetsInfo, IDownloadProgress downloadProgress = null) + { + var downloadedAsset = false; + + foreach (var entry in assetsInfo) + { + // Download asset + var success = await DownloadAssetFromGitHubRepoAsync(entry.Key, entry.Value, downloadProgress); + if (success) + { + // Update manifest + manifest.Metadata[entry.Key] = entry.Value; + downloadedAsset = true; + } + } + + if (downloadedAsset) + { + // Persist manifest + SaveManifest(manifest); + } + + return downloadedAsset; + } + + /// + /// Download asset metadata from given uri. The assets info parameter is populate with the metadata. + /// + /// The repo uri to download + /// Assets info mapping that is populate by this function + /// Download progress interface + /// + private async Task GetAssetsInfoFromGithubRepoAsync(string repoUri, + Dictionary assetsInfo, IDownloadProgress downloadProgress = null) + { + if (isorg == true) { + GithubUrl = GithubApiUrl + "orgs/"; + } else { + GithubUrl = GithubApiUrl; + } + var requestUrl = GithubUrl + repoUri; + Debug.LogFormat("Reading assets from {0}", requestUrl); + + try + { + // Request asset url + using (var req = new UnityWebRequest(requestUrl)) + { + req.downloadHandler = new DownloadHandlerBuffer(); + await req.SendWebRequest(); + if (req.isNetworkError || req.isHttpError) + { + // Error reading asset metadata, so return. + Debug.LogFormat("Error reading asset info: {0}", req.error); + + var responseHeaders = req.GetResponseHeaders(); + if (null != responseHeaders && responseHeaders.ContainsKey("X-RateLimit-Remaining") && + responseHeaders["X-RateLimit-Remaining"] == "0") + { + // Github request limit reached. Display a friendly error message. + Debug.LogFormat("Request limit reached"); + + if (null != downloadProgress) + { + downloadProgress.OnError(string.Format("Error updating: Request Limit Reached - try again later. ({1})", + req.error, requestUrl)); + } + } + else + { + if (null != downloadProgress) + { + downloadProgress.OnError(string.Format("Error updating: {0} ({1})", + req.error, requestUrl)); + } + } + return false; + } + + // Parse the returned asset metadata + var jObject = JObject.Parse(req.downloadHandler.text); + var tagName = jObject["tag_name"].Value(); + + foreach (var property in jObject["assets"]) + { + var updatedAt = property["updated_at"].Value(); + var url = property["url"].Value(); + var name = property["name"].Value(); + + // For now, simply accept any iconpack*.zip and appnames*.txt/json. + if ((name.StartsWith("iconpack") && name.EndsWith(".zip")) || + (name.StartsWith("appnames") && name.EndsWith(".txt")) || + (name.StartsWith("appnames") && name.EndsWith(".json"))) + { + assetsInfo[name] = new AssetInfo { RepoUri = repoUri, Url = url, UpdatedAt = updatedAt, TagName = tagName }; + } + } + } + } + catch (Exception e) + { + Debug.LogFormat("Exception reading asset info: {0} ", e.Message); + if (null != downloadProgress) + { + downloadProgress.OnError(string.Format("Error updating: {0} ({1})", + e.Message, requestUrl)); + } + + return false; + } + + return true; + } + + /// + /// Download a single asset (file) from repo + /// + /// Name of the asset (file) to download + /// Asset info (metadata) + /// Download progress interface + /// + private async Task DownloadAssetFromGitHubRepoAsync(string name, AssetInfo assetInfo, + IDownloadProgress downloadProgress) + { + var filePath = Path.Combine(GetOrCreateDownloadPath(), name); + var tempFilePath = filePath + TempDownloadFileExtention; + Debug.LogFormat("Downloading asset {0} from {1}", filePath, assetInfo.Url); + + try + { + // Request asset url + using (var req = new UnityWebRequest(assetInfo.Url)) + { + req.SetRequestHeader("Accept", "application/octet-stream"); + if (null != downloadProgress) + { + downloadProgress.OnDownloadStart(name); + } + var downloadHandler = new DownloadHandlerFileWithProgress(tempFilePath, downloadProgress.OnDownloadProgress); + downloadHandler.removeFileOnAbort = true; + req.downloadHandler = downloadHandler; + await req.SendWebRequest(); + + if (req.isNetworkError || req.isHttpError) + { + // Error reading asset metadata, so return. + Debug.LogFormat("Error downloading asset: {0}", req.error); + if (null != downloadProgress) + { + downloadProgress.OnError(string.Format("Error updating: {0} ({1})", + req.error, assetInfo.Url)); + } + + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + + return false; + } + + // Rename temp file to desination file to ensure that we are not downloading + // an error body message into the destination file. + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + File.Move(tempFilePath, filePath); + + if (null != downloadProgress) + { + downloadProgress.OnDownloadFinish(); + } + + return true; + } + } + catch (Exception e) + { + Debug.LogFormat("Exception downloading asset: {0} ", e.Message); + if (null != downloadProgress) + { + downloadProgress.OnError(string.Format("Error updating: {0} ({1})", + e.Message, assetInfo.Url)); + } + } + + return false; + } + + /// + /// Load download manifest + /// + /// Download manifest + static private AssetsManifest LoadManifest() + { + var manifestPath = Path.Combine(GetOrCreateDownloadPath(), DownloadManifestFile); + if (!File.Exists(manifestPath)) + { + return null; + } + + Debug.Log("Found manifest: " + manifestPath); + var jsonManifest = File.ReadAllText(manifestPath); + + try + { + AssetsManifest manifest = new AssetsManifest(); + return JsonConvert.DeserializeObject(jsonManifest); + } + catch (Exception e) + { + Debug.Log(string.Format("Failed to read manifest: {0}", e.Message)); + } + + return null; + } + + /// + /// Persist download manifest + /// + /// Download manifest to persist + static private void SaveManifest(AssetsManifest manifest) + { + var manifestPath = Path.Combine(GetOrCreateDownloadPath(), DownloadManifestFile); + Debug.Log("Saving manifest: " + manifestPath); + + try + { + File.WriteAllText(manifestPath, JsonConvert.SerializeObject(manifest, Formatting.Indented)); + } + catch (Exception e) + { + Debug.Log(string.Format("Failed to write manifest: {0}", e.Message)); + } + } + + static public string GetOrCreateDownloadPath() + { + string path = Path.Combine(UnityEngine.Application.persistentDataPath, DownloadCacheFolder); + Directory.CreateDirectory(path); + return path; + } + } }