From 4fa3de6dec48d2994aa95540ebab1728dafec9b6 Mon Sep 17 00:00:00 2001 From: tverona1 Date: Thu, 8 Aug 2019 23:17:37 -0700 Subject: [PATCH 1/3] Async processing of apps - Async processing of apps - Async load of icon image --- Assets/Scripts/GridPopulation.cs | 172 +++++++++++++++----------- ProjectSettings/ProjectSettings.asset | 5 +- 2 files changed, 100 insertions(+), 77 deletions(-) diff --git a/Assets/Scripts/GridPopulation.cs b/Assets/Scripts/GridPopulation.cs index c913c9d..a6240fd 100644 --- a/Assets/Scripts/GridPopulation.cs +++ b/Assets/Scripts/GridPopulation.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using UnityEngine; using UnityEngine.UI; using UnityEngine.XR; @@ -24,7 +25,7 @@ namespace QuestAppLauncher public string AutoTabName; public string Tab1Name; public string Tab2Name; - public bool IsOverride; + public string IconPath; } // File name of app name overrides @@ -64,9 +65,6 @@ namespace QuestAppLauncher // Tab prefab public GameObject prefabTab; - // Reference to executing populate routine - private Coroutine populateCoroutine; - // Built-in tab names private const string Tab_Quest = "Quest"; private const string Tab_Go = "Go/Gear"; @@ -77,7 +75,7 @@ namespace QuestAppLauncher #region MonoBehaviour handler - void Start() + async void Start() { // Set high texture resolution scale to minimize aliasing XRSettings.eyeTextureResolutionScale = 2.0f; @@ -86,7 +84,7 @@ namespace QuestAppLauncher Core.AsyncInitialize(); // Populate the grid - StartPopulate(); + await PopulateAsync(); } void Update() @@ -206,36 +204,47 @@ namespace QuestAppLauncher return size; } - public void StartPopulate() - { - // Ensure we only exeucte on populate routine at a time - if (null != this.populateCoroutine) - { - StopCoroutine(this.populateCoroutine); - } - - this.populateCoroutine = this.StartCoroutine(Populate()); - } - /// /// Populate the grid from installed apps /// /// - private IEnumerator Populate() + private async Task PopulateAsync() { - var persistentDataPath = UnityEngine.Application.persistentDataPath; - Debug.Log("Persistent data path: " + persistentDataPath); - // Load configuration Config config = new Config(); ConfigPersistence.LoadConfig(config); + // Process apps in background + var apps = await Task.Run(() => + { + AndroidJNI.AttachCurrentThread(); + + try + { + return ProcessApps(config); + } + finally + { + AndroidJNI.DetachCurrentThread(); + } + }); + + // Populate the panel content + await PopulatePanelContentAsync(config, apps); + } + + private Dictionary ProcessApps(Config config) + { + var persistentDataPath = UnityEngine.Application.persistentDataPath; + Debug.Log("Persistent data path: " + persistentDataPath); + + // Dictionary to hold package name -> app index, app name + var apps = new Dictionary(); + var excludedPackageNames = new HashSet(); + using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) { - // Dictionary to hold package name -> app index, app name - var apps = new Dictionary(); - var excludedPackageNames = new HashSet(); // Get # of installed apps int numApps = currentActivity.Call("getSize"); @@ -296,7 +305,6 @@ namespace QuestAppLauncher apps.Add(packageName, new ProcessedApp { PackageName = packageName, Index = i, AutoTabName = tabName, AppName = appName }); Debug.LogFormat("[{0}] package: {1}, name: {2}, auto tab: {3}", i, packageName, appName, tabName); - yield return null; } // Override app names, if any @@ -376,7 +384,6 @@ namespace QuestAppLauncher AutoTabName = autoTabName ?? apps[entry[0]].AutoTabName, Tab1Name = tab1 ?? apps[entry[0]].Tab1Name, Tab2Name = tab2 ?? apps[entry[0]].Tab2Name, - IsOverride = true }; } } @@ -385,12 +392,9 @@ namespace QuestAppLauncher Debug.Log("Did not find: " + appNameOverrideFilePath); } - yield return null; - // Load list of app icon overrides // This is a list of jpg images stored as packageName.jpg. var iconOverridePath = persistentDataPath; - var iconOverrides = new Dictionary(); if (Directory.Exists(iconOverridePath)) { foreach (var iconFileName in Directory.GetFiles(iconOverridePath, IconOverrideExtSearch)) @@ -398,26 +402,22 @@ namespace QuestAppLauncher var entry = Path.GetFileNameWithoutExtension(iconFileName); if (apps.ContainsKey(entry)) { - Debug.Log("Found file: " + iconFileName); - iconOverrides[entry] = Path.Combine(iconOverridePath, iconFileName); - } + Debug.Log("Found icon override: " + iconFileName); - yield return null; + ProcessedApp newProcessedApp = apps[entry]; + newProcessedApp.IconPath = Path.Combine(iconOverridePath, iconFileName); + apps[entry] = newProcessedApp; + } } } - - yield return null; - - // Populate the panel content - yield return PopulatePanelContent(currentActivity, config, apps, iconOverrides); } + + return apps; } - private IEnumerator PopulatePanelContent( - AndroidJavaObject currentActivity, + private async Task PopulatePanelContentAsync( Config config, - Dictionary apps, - Dictionary iconOverrides) + Dictionary apps) { // Set up tabs var topTabs = new List(); @@ -483,24 +483,24 @@ namespace QuestAppLauncher foreach (var app in apps.OrderBy(key => key.Value.AppName)) { // Add to all tab - yield return AddCellToGrid(app.Value, gridContents[Tab_All].transform, iconOverrides, currentActivity); + await AddCellToGridAsync(app.Value, gridContents[Tab_All].transform); // Add to auto (built-in) tabs if (gridContents.ContainsKey(app.Value.AutoTabName)) { - yield return AddCellToGrid(app.Value, gridContents[app.Value.AutoTabName].transform, iconOverrides, currentActivity); + await AddCellToGridAsync(app.Value, gridContents[app.Value.AutoTabName].transform); } // Add to tab1 if (null != app.Value.Tab1Name && gridContents.ContainsKey(app.Value.Tab1Name)) { - yield return AddCellToGrid(app.Value, gridContents[app.Value.Tab1Name].transform, iconOverrides, currentActivity); + await AddCellToGridAsync(app.Value, gridContents[app.Value.Tab1Name].transform); } // Add to tab2 if (null != app.Value.Tab2Name && gridContents.ContainsKey(app.Value.Tab2Name)) { - yield return AddCellToGrid(app.Value, gridContents[app.Value.Tab2Name].transform, iconOverrides, currentActivity); + await AddCellToGridAsync(app.Value, gridContents[app.Value.Tab2Name].transform); } } } @@ -562,9 +562,39 @@ namespace QuestAppLauncher } } - private IEnumerator AddCellToGrid(ProcessedApp app, Transform transform, - Dictionary iconOverrides, - AndroidJavaObject currentActivity) + private byte[] GetAppIcon(string iconPath, int appIndex) + { + byte[] bytesIcon = null; + bool useApkIcon = true; + if (null != iconPath) + { + // Use overridden icon + try + { + bytesIcon = File.ReadAllBytes(iconPath); + useApkIcon = false; + } + catch (Exception e) + { + // Fall back to using the apk icon + Debug.Log(string.Format("Error reading app icon from file [{0}]: {1}", iconPath, e.Message)); + } + } + + if (useApkIcon) + { + // Use built-in icon from the apk + using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) + { + bytesIcon = (byte[])(Array)currentActivity.Call("getIcon", appIndex); + } + } + + return bytesIcon; + } + + private async Task AddCellToGridAsync(ProcessedApp app, Transform transform) { // Create new instances of our app info prefabCell var newObj = (GameObject)Instantiate(this.prefabCell, transform); @@ -574,44 +604,36 @@ namespace QuestAppLauncher appEntry.packageId = app.PackageName; appEntry.appName = app.AppName; - // Get app icon - byte[] bytesIcon = null; - bool useApkIcon = true; - if (iconOverrides.ContainsKey(app.PackageName)) + // Get app icon in background + var bytesIcon = await Task.Run(() => { - // Use overridden icon + AndroidJNI.AttachCurrentThread(); + try { - bytesIcon = File.ReadAllBytes(iconOverrides[app.PackageName]); - useApkIcon = false; + return GetAppIcon(app.IconPath, app.Index); } - catch (Exception e) + finally { - // Fall back to using the apk icon - Debug.Log(string.Format("Error reading app icon from file [{0}]: {1}", iconOverrides[app.PackageName], e.Message)); + AndroidJNI.DetachCurrentThread(); } - } - - if (useApkIcon) - { - // Use built-in icon from the apk - bytesIcon = (byte[])(Array)currentActivity.Call("getIcon", app.Index); - } + }); // Set the icon image - var image = newObj.transform.Find("AppIcon").GetComponentInChildren(); - var texture = new Texture2D(2, 2, TextureFormat.RGB24, false); - texture.filterMode = FilterMode.Trilinear; - texture.anisoLevel = 16; - texture.LoadImage(bytesIcon); - var rect = new Rect(0, 0, texture.width, texture.height); - image.sprite = Sprite.Create(texture, rect, new Vector2(0.5f, 0.5f)); + if (null != bytesIcon) + { + var image = newObj.transform.Find("AppIcon").GetComponentInChildren(); + var texture = new Texture2D(2, 2, TextureFormat.RGB24, false); + texture.filterMode = FilterMode.Trilinear; + texture.anisoLevel = 16; + texture.LoadImage(bytesIcon); + var rect = new Rect(0, 0, texture.width, texture.height); + image.sprite = Sprite.Create(texture, rect, new Vector2(0.5f, 0.5f)); + } // Set app name in text var text = newObj.transform.Find("AppName").GetComponentInChildren(); text.text = app.AppName; - - yield return null; } } #endregion diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index a5271c8..8c1c6d7 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -120,7 +120,7 @@ PlayerSettings: 16:10: 1 16:9: 1 Others: 1 - bundleVersion: 0.5 + bundleVersion: 0.6 preloadedAssets: [] metroInputSource: 0 wsaTransparentSwapchain: 0 @@ -658,7 +658,8 @@ PlayerSettings: scriptingRuntimeVersion: 1 gcIncremental: 0 gcWBarrierValidation: 0 - apiCompatibilityLevelPerPlatform: {} + apiCompatibilityLevelPerPlatform: + Android: 3 m_RenderingPath: 1 m_MobileRenderingPath: 1 metroPackageName: Template_3D From 18dbf110c9c79aa1b94c246af393dcca53a2d047 Mon Sep 17 00:00:00 2001 From: tverona1 Date: Thu, 8 Aug 2019 23:52:36 -0700 Subject: [PATCH 2/3] Refactored code a bit - Refactored GridPopulation class into two classes: GridPopulation handles populating the grid UI and AppProcessor handles processing the app entries. --- Assets/Resources/OVRBuildConfig.asset | 2 +- Assets/Scripts/AppProcessor.cs | 322 +++++++++++++++++++ Assets/Scripts/AppProcessor.cs.meta | 11 + Assets/Scripts/GridPopulation.cs | 424 ++++---------------------- Assets/Scripts/OnInteraction.cs | 4 +- Assets/Scripts/SettingsHandler.cs | 2 +- 6 files changed, 394 insertions(+), 371 deletions(-) create mode 100644 Assets/Scripts/AppProcessor.cs create mode 100644 Assets/Scripts/AppProcessor.cs.meta diff --git a/Assets/Resources/OVRBuildConfig.asset b/Assets/Resources/OVRBuildConfig.asset index f2d36f8..cf5cd6c 100644 --- a/Assets/Resources/OVRBuildConfig.asset +++ b/Assets/Resources/OVRBuildConfig.asset @@ -12,6 +12,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 20553fac56ec59645857c0732b787431, type: 3} m_Name: OVRBuildConfig m_EditorClassIdentifier: - androidSDKPath: C:\Users\tvero\AppData\Local\Android\Sdk + androidSDKPath: gradlePath: jdkPath: diff --git a/Assets/Scripts/AppProcessor.cs b/Assets/Scripts/AppProcessor.cs new file mode 100644 index 0000000..1d48b24 --- /dev/null +++ b/Assets/Scripts/AppProcessor.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace QuestAppLauncher +{ + public class ProcessedApp + { + public int Index; + public string PackageName; + public string AppName; + public string AutoTabName; + public string Tab1Name; + public string Tab2Name; + public string IconPath; + } + + public class AppProcessor + { + // File name of app name overrides + const string AppNameOverrideFile = "appnames.txt"; + + // File name of excluded package names + const string ExcludedPackagesFile = "excludedpackages.txt"; + + // Extension search for icon overrides + const string IconOverrideExtSearch = "*.jpg"; + + // Built-in tab names + public const string Tab_Quest = "Quest"; + public const string Tab_Go = "Go/Gear"; + public const string Tab_2D = "2D"; + public const string Tab_All = "All"; + + public static readonly string[] Auto_Tabs = { Tab_Quest, Tab_Go, Tab_2D }; + + public static Dictionary ProcessApps(Config config) + { + var persistentDataPath = UnityEngine.Application.persistentDataPath; + Debug.Log("Persistent data path: " + persistentDataPath); + + // Dictionary to hold package name -> app index, app name + var apps = new Dictionary(); + var excludedPackageNames = new HashSet(); + + using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) + { + + // Get # of installed apps + int numApps = currentActivity.Call("getSize"); + Debug.Log("# installed apps: " + numApps); + + // Add current package name to excludedPackageNames to filter it out + excludedPackageNames.Add(currentActivity.Call("getPackageName")); + + // This is a file containing packageNames that will be excluded + var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); + if (File.Exists(excludedPackageNamesFilePath)) + { + Debug.Log("Found file: " + excludedPackageNamesFilePath); + string[] excludedPackages = File.ReadAllLines(excludedPackageNamesFilePath); + foreach (string excludedPackage in excludedPackages) + { + if (!string.IsNullOrEmpty(excludedPackage)) + { + excludedPackageNames.Add(excludedPackage); + } + } + } + + // Get installed package and app names + for (int i = 0; i < numApps; i++) + { + var packageName = currentActivity.Call("getPackageName", i); + var appName = currentActivity.Call("getAppName", i); + + if (excludedPackageNames.Contains(packageName)) + { + // Skip excluded package + Debug.LogFormat("Skipping Excluded [{0}] Package: {1}, name: {2}", i, packageName, appName); + continue; + } + + // Determine app type (Quest, Go or 2D) + string tabName; + if (currentActivity.Call("is2DApp", i)) + { + if (!config.show2D) + { + // Skip 2D apps + Debug.LogFormat("Skipping 2D [{0}] Package: {1}, name: {2}", i, packageName, appName); + continue; + } + + tabName = Tab_2D; + } + else if (currentActivity.Call("isQuestApp", i)) + { + tabName = Tab_Quest; + } + else + { + tabName = Tab_Go; + } + + apps.Add(packageName, new ProcessedApp { PackageName = packageName, Index = i, AutoTabName = tabName, AppName = appName }); + Debug.LogFormat("[{0}] package: {1}, name: {2}, auto tab: {3}", i, packageName, appName, tabName); + } + + // Override app names, if any + // This is just a file with comma-separated packageName,appName[,category1[, category2]] + // Category1 and category2 are optional categories (tabs). + var appNameOverrideFilePath = Path.Combine(persistentDataPath, AppNameOverrideFile); + if (File.Exists(appNameOverrideFilePath)) + { + Debug.Log("Found file: " + appNameOverrideFilePath); + string[] lines = File.ReadAllLines(appNameOverrideFilePath); + foreach (string line in lines) + { + line.Trim(); + + if (line.StartsWith("#")) + { + // Skip comments + continue; + } + + // Parse line + var entry = line.Split(','); + if (entry.Length < 2) + { + // We expect at least two entries + continue; + } + + var packageName = entry[0]; + var appName = entry[1]; + + if (!apps.ContainsKey(packageName)) + { + // App is not installed, so skip + continue; + } + + // Get the custom tab names, if any + string autoTabName = null; + var tab1 = entry.Length > 2 ? entry[2] : null; + var tab2 = entry.Length > 3 ? entry[3] : null; + + if (tab1 != null && tab1.Length == 0) + { + tab1 = null; + } + + if (tab2 != null && tab2.Length == 0) + { + tab2 = null; + } + + if (tab1 != null && tab2 != null && tab1.Equals(tab2, StringComparison.OrdinalIgnoreCase)) + { + tab2 = null; + } + + // Override auto tabe name if custom name matches built-in tab name + if (tab1 != null && Auto_Tabs.Contains(tab1, StringComparer.OrdinalIgnoreCase)) + { + autoTabName = tab1; + tab1 = null; + } + + if (tab2 != null && Auto_Tabs.Contains(tab2, StringComparer.OrdinalIgnoreCase)) + { + autoTabName = tab2; + tab2 = null; + } + + // Update entry + apps[packageName] = new ProcessedApp + { + PackageName = apps[entry[0]].PackageName, + Index = apps[entry[0]].Index, + AppName = appName, + AutoTabName = autoTabName ?? apps[entry[0]].AutoTabName, + Tab1Name = tab1 ?? apps[entry[0]].Tab1Name, + Tab2Name = tab2 ?? apps[entry[0]].Tab2Name, + }; + } + } + else + { + Debug.Log("Did not find: " + appNameOverrideFilePath); + } + + // Load list of app icon overrides + // This is a list of jpg images stored as packageName.jpg. + var iconOverridePath = persistentDataPath; + if (Directory.Exists(iconOverridePath)) + { + foreach (var iconFileName in Directory.GetFiles(iconOverridePath, IconOverrideExtSearch)) + { + var entry = Path.GetFileNameWithoutExtension(iconFileName); + if (apps.ContainsKey(entry)) + { + Debug.Log("Found icon override: " + iconFileName); + + ProcessedApp newProcessedApp = apps[entry]; + newProcessedApp.IconPath = Path.Combine(iconOverridePath, iconFileName); + apps[entry] = newProcessedApp; + } + } + } + } + + return apps; + } + + public static byte[] GetAppIcon(string iconPath, int appIndex) + { + byte[] bytesIcon = null; + bool useApkIcon = true; + if (null != iconPath) + { + // Use overridden icon + try + { + bytesIcon = File.ReadAllBytes(iconPath); + useApkIcon = false; + } + catch (Exception e) + { + // Fall back to using the apk icon + Debug.Log(string.Format("Error reading app icon from file [{0}]: {1}", iconPath, e.Message)); + } + } + + if (useApkIcon) + { + // Use built-in icon from the apk + using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) + { + bytesIcon = (byte[])(Array)currentActivity.Call("getIcon", appIndex); + } + } + + return bytesIcon; + } + + /// + /// Static method to add a package name to the excludedFile + /// + /// + static public void AddAppToExcludedFile(string packageName) + { + var persistentDataPath = UnityEngine.Application.persistentDataPath; + var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); + + using (StreamWriter writer = File.AppendText(excludedPackageNamesFilePath)) + { + writer.WriteLine(packageName); + Debug.Log($"Added package {packageName} to the excluded file {excludedPackageNamesFilePath}"); + } + } + + /// + /// Static method to delete the excludedFile + /// + /// true if file exists + static public bool DeleteExcludedAppsFile() + { + var persistentDataPath = UnityEngine.Application.persistentDataPath; + var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); + + if (File.Exists(excludedPackageNamesFilePath)) + { + File.Delete(excludedPackageNamesFilePath); + return true; + } + + return false; + } + + /// + /// Static method for launching an Android app + /// + /// + static public void LaunchApp(string packageId) + { + using (AndroidJavaClass up = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (AndroidJavaObject ca = up.GetStatic("currentActivity")) + using (AndroidJavaObject packageManager = ca.Call("getPackageManager")) + { + AndroidJavaObject launchIntent = null; + + try + { + launchIntent = packageManager.Call("getLaunchIntentForPackage", packageId); + ca.Call("startActivity", launchIntent); + + // Quest doesn't like multiple VR apps running simultaneously. Kill ourselves. + UnityEngine.Application.Quit(); + } + catch (System.Exception e) + { + Debug.Log(string.Format("Failed to launch app {0}: {1}", packageId, e.Message)); + } + finally + { + if (null != launchIntent) + { + launchIntent.Dispose(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/AppProcessor.cs.meta b/Assets/Scripts/AppProcessor.cs.meta new file mode 100644 index 0000000..09cc077 --- /dev/null +++ b/Assets/Scripts/AppProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a4082e0949808345ac8377500fff8ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/GridPopulation.cs b/Assets/Scripts/GridPopulation.cs index a6240fd..67a8151 100644 --- a/Assets/Scripts/GridPopulation.cs +++ b/Assets/Scripts/GridPopulation.cs @@ -17,26 +17,6 @@ namespace QuestAppLauncher /// public class GridPopulation : MonoBehaviour { - class ProcessedApp - { - public int Index; - public string PackageName; - public string AppName; - public string AutoTabName; - public string Tab1Name; - public string Tab2Name; - public string IconPath; - } - - // File name of app name overrides - const string AppNameOverrideFile = "appnames.txt"; - - // File name of excluded package names - const string ExcludedPackagesFile = "excludedpackages.txt"; - - // Extension search for icon overrides - const string IconOverrideExtSearch = "*.jpg"; - // Grid container game object public GameObject panelContainer; @@ -65,14 +45,6 @@ namespace QuestAppLauncher // Tab prefab public GameObject prefabTab; - // Built-in tab names - private const string Tab_Quest = "Quest"; - private const string Tab_Go = "Go/Gear"; - private const string Tab_2D = "2D"; - private const string Tab_All = "All"; - - private static readonly string[] Auto_Tabs = { Tab_Quest, Tab_Go, Tab_2D }; - #region MonoBehaviour handler async void Start() @@ -94,116 +66,6 @@ namespace QuestAppLauncher #region Private Functions - /// - /// Static method for launching an Android app - /// - /// - static public void LaunchApp(string packageId) - { - using (AndroidJavaClass up = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - using (AndroidJavaObject ca = up.GetStatic("currentActivity")) - using (AndroidJavaObject packageManager = ca.Call("getPackageManager")) - { - AndroidJavaObject launchIntent = null; - - try - { - launchIntent = packageManager.Call("getLaunchIntentForPackage", packageId); - ca.Call("startActivity", launchIntent); - - // Quest doesn't like multiple VR apps running simultaneously. Kill ourselves. - UnityEngine.Application.Quit(); - } - catch (System.Exception e) - { - Debug.Log(string.Format("Failed to launch app {0}: {1}", packageId, e.Message)); - } - finally - { - if (null != launchIntent) - { - launchIntent.Dispose(); - } - } - } - } - - /// - /// Static method to add a package name to the excludedFile - /// - /// - static public void AddAppToExcludedFile(string packageName) - { - var persistentDataPath = UnityEngine.Application.persistentDataPath; - var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); - - using (StreamWriter writer = File.AppendText(excludedPackageNamesFilePath)) - { - writer.WriteLine(packageName); - Debug.Log($"Added package {packageName} to the excluded file {excludedPackageNamesFilePath}"); - } - } - - /// - /// Static method to delete the excludedFile - /// - /// true if file exists - static public bool DeleteExcludedAppsFile() - { - var persistentDataPath = UnityEngine.Application.persistentDataPath; - var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); - - if (File.Exists(excludedPackageNamesFilePath)) - { - File.Delete(excludedPackageNamesFilePath); - return true; - } - - return false; - } - - public Vector2 SetGridSize(GameObject gridContent, int rows, int cols) - { - // Make sure grid size have sane value - cols = Math.Min(cols, 10); - cols = Math.Max(cols, 1); - rows = Math.Min(rows, 10); - rows = Math.Max(rows, 1); - - // Get cell size, spacing & padding from the grid layout - var gridLayoutGroup = gridContent.GetComponent(); - var cellHeight = gridLayoutGroup.cellSize.y; - var cellWidth = gridLayoutGroup.cellSize.x; - var paddingX = gridLayoutGroup.padding.horizontal; - var paddingY = gridLayoutGroup.padding.vertical; - var spaceX = gridLayoutGroup.spacing.x; - var spaceY = gridLayoutGroup.spacing.y; - - // Width = horizontal padding + # cols * cell width + (# cols - 1) * horizontal spacing - var width = paddingX + cols * cellWidth + (cols - 1) * spaceX; - - // Height = vertical padding + # rows * cell height + (# rows - 1) * veritcal spacing + a bit more to show there are more elements - var height = paddingY + rows * cellHeight + (rows - 1) * spaceY + 120; - - Debug.Log(string.Format("Setting grid size to {0} x {1} cells", cols, rows)); - Debug.Log(string.Format("Grid size calculated width x height: {0} x {1}", width, height)); - - // Adjust grid container rect transform - var size = new Vector2(width, height); - - var gridTransform = this.panelContainer.GetComponent(); - gridTransform.sizeDelta = size; - - // Adjust grid container Y position to maintain constant height. - // TODO: Figure out a way to adjust UI to avoid this calculation in code - var gridPosition = new Vector3(gridTransform.anchoredPosition3D.x, - (float)((gridTransform.rect.height - 2000) / 2.0), - gridTransform.anchoredPosition3D.z); - gridTransform.anchoredPosition3D = gridPosition; - - return size; - } - /// /// Populate the grid from installed apps /// @@ -221,7 +83,7 @@ namespace QuestAppLauncher try { - return ProcessApps(config); + return AppProcessor.ProcessApps(config); } finally { @@ -233,188 +95,6 @@ namespace QuestAppLauncher await PopulatePanelContentAsync(config, apps); } - private Dictionary ProcessApps(Config config) - { - var persistentDataPath = UnityEngine.Application.persistentDataPath; - Debug.Log("Persistent data path: " + persistentDataPath); - - // Dictionary to hold package name -> app index, app name - var apps = new Dictionary(); - var excludedPackageNames = new HashSet(); - - using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) - { - - // Get # of installed apps - int numApps = currentActivity.Call("getSize"); - Debug.Log("# installed apps: " + numApps); - - // Add current package name to excludedPackageNames to filter it out - excludedPackageNames.Add(currentActivity.Call("getPackageName")); - - // This is a file containing packageNames that will be excluded - var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); - if (File.Exists(excludedPackageNamesFilePath)) - { - Debug.Log("Found file: " + excludedPackageNamesFilePath); - string[] excludedPackages = File.ReadAllLines(excludedPackageNamesFilePath); - foreach (string excludedPackage in excludedPackages) - { - if (!string.IsNullOrEmpty(excludedPackage)) - { - excludedPackageNames.Add(excludedPackage); - } - } - } - - // Get installed package and app names - for (int i = 0; i < numApps; i++) - { - var packageName = currentActivity.Call("getPackageName", i); - var appName = currentActivity.Call("getAppName", i); - - if (excludedPackageNames.Contains(packageName)) - { - // Skip excluded package - Debug.LogFormat("Skipping Excluded [{0}] Package: {1}, name: {2}", i, packageName, appName); - continue; - } - - // Determine app type (Quest, Go or 2D) - string tabName; - if (currentActivity.Call("is2DApp", i)) - { - if (!config.show2D) - { - // Skip 2D apps - Debug.LogFormat("Skipping 2D [{0}] Package: {1}, name: {2}", i, packageName, appName); - continue; - } - - tabName = Tab_2D; - } - else if (currentActivity.Call("isQuestApp", i)) - { - tabName = Tab_Quest; - } - else - { - tabName = Tab_Go; - } - - apps.Add(packageName, new ProcessedApp { PackageName = packageName, Index = i, AutoTabName = tabName, AppName = appName }); - Debug.LogFormat("[{0}] package: {1}, name: {2}, auto tab: {3}", i, packageName, appName, tabName); - } - - // Override app names, if any - // This is just a file with comma-separated packageName,appName[,category1[, category2]] - // Category1 and category2 are optional categories (tabs). - var appNameOverrideFilePath = Path.Combine(persistentDataPath, AppNameOverrideFile); - if (File.Exists(appNameOverrideFilePath)) - { - Debug.Log("Found file: " + appNameOverrideFilePath); - string[] lines = File.ReadAllLines(appNameOverrideFilePath); - foreach (string line in lines) - { - line.Trim(); - - if (line.StartsWith("#")) - { - // Skip comments - continue; - } - - // Parse line - var entry = line.Split(','); - if (entry.Length < 2) - { - // We expect at least two entries - continue; - } - - var packageName = entry[0]; - var appName = entry[1]; - - if (!apps.ContainsKey(packageName)) - { - // App is not installed, so skip - continue; - } - - // Get the custom tab names, if any - string autoTabName = null; - var tab1 = entry.Length > 2 ? entry[2] : null; - var tab2 = entry.Length > 3 ? entry[3] : null; - - if (tab1 != null && tab1.Length == 0) - { - tab1 = null; - } - - if (tab2 != null && tab2.Length == 0) - { - tab2 = null; - } - - if (tab1 != null && tab2 != null && tab1.Equals(tab2, StringComparison.OrdinalIgnoreCase)) - { - tab2 = null; - } - - // Override auto tabe name if custom name matches built-in tab name - if (tab1 != null && Auto_Tabs.Contains(tab1, StringComparer.OrdinalIgnoreCase)) - { - autoTabName = tab1; - tab1 = null; - } - - if (tab2 != null && Auto_Tabs.Contains(tab2, StringComparer.OrdinalIgnoreCase)) - { - autoTabName = tab2; - tab2 = null; - } - - // Update entry - apps[packageName] = new ProcessedApp - { - PackageName = apps[entry[0]].PackageName, - Index = apps[entry[0]].Index, - AppName = appName, - AutoTabName = autoTabName ?? apps[entry[0]].AutoTabName, - Tab1Name = tab1 ?? apps[entry[0]].Tab1Name, - Tab2Name = tab2 ?? apps[entry[0]].Tab2Name, - }; - } - } - else - { - Debug.Log("Did not find: " + appNameOverrideFilePath); - } - - // Load list of app icon overrides - // This is a list of jpg images stored as packageName.jpg. - var iconOverridePath = persistentDataPath; - if (Directory.Exists(iconOverridePath)) - { - foreach (var iconFileName in Directory.GetFiles(iconOverridePath, IconOverrideExtSearch)) - { - var entry = Path.GetFileNameWithoutExtension(iconFileName); - if (apps.ContainsKey(entry)) - { - Debug.Log("Found icon override: " + iconFileName); - - ProcessedApp newProcessedApp = apps[entry]; - newProcessedApp.IconPath = Path.Combine(iconOverridePath, iconFileName); - apps[entry] = newProcessedApp; - } - } - } - } - - return apps; - } - private async Task PopulatePanelContentAsync( Config config, Dictionary apps) @@ -427,15 +107,15 @@ namespace QuestAppLauncher // Set auto tabs if (config.autoCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase)) { - topTabs.AddRange(Auto_Tabs); + topTabs.AddRange(AppProcessor.Auto_Tabs); } else if (config.autoCategory.Equals(Config.Category_Left, StringComparison.OrdinalIgnoreCase)) { - leftTabs.AddRange(Auto_Tabs); + leftTabs.AddRange(AppProcessor.Auto_Tabs); } else if (config.autoCategory.Equals(Config.Category_Right, StringComparison.OrdinalIgnoreCase)) { - rightTabs.AddRange(Auto_Tabs); + rightTabs.AddRange(AppProcessor.Auto_Tabs); } // Set custom tabs, sorted alphabetically @@ -458,7 +138,7 @@ namespace QuestAppLauncher } // Add the "all" top tab - topTabs.Add(Tab_All); + topTabs.Add(AppProcessor.Tab_All); // Process the tab containers var gridContents = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -483,7 +163,7 @@ namespace QuestAppLauncher foreach (var app in apps.OrderBy(key => key.Value.AppName)) { // Add to all tab - await AddCellToGridAsync(app.Value, gridContents[Tab_All].transform); + await AddCellToGridAsync(app.Value, gridContents[AppProcessor.Tab_All].transform); // Add to auto (built-in) tabs if (gridContents.ContainsKey(app.Value.AutoTabName)) @@ -530,6 +210,48 @@ namespace QuestAppLauncher transform.gameObject.GetComponent().RefreshScrollContent(childCount); } + private Vector2 SetGridSize(GameObject gridContent, int rows, int cols) + { + // Make sure grid size have sane value + cols = Math.Min(cols, 10); + cols = Math.Max(cols, 1); + rows = Math.Min(rows, 10); + rows = Math.Max(rows, 1); + + // Get cell size, spacing & padding from the grid layout + var gridLayoutGroup = gridContent.GetComponent(); + var cellHeight = gridLayoutGroup.cellSize.y; + var cellWidth = gridLayoutGroup.cellSize.x; + var paddingX = gridLayoutGroup.padding.horizontal; + var paddingY = gridLayoutGroup.padding.vertical; + var spaceX = gridLayoutGroup.spacing.x; + var spaceY = gridLayoutGroup.spacing.y; + + // Width = horizontal padding + # cols * cell width + (# cols - 1) * horizontal spacing + var width = paddingX + cols * cellWidth + (cols - 1) * spaceX; + + // Height = vertical padding + # rows * cell height + (# rows - 1) * veritcal spacing + a bit more to show there are more elements + var height = paddingY + rows * cellHeight + (rows - 1) * spaceY + 120; + + Debug.Log(string.Format("Setting grid size to {0} x {1} cells", cols, rows)); + Debug.Log(string.Format("Grid size calculated width x height: {0} x {1}", width, height)); + + // Adjust grid container rect transform + var size = new Vector2(width, height); + + var gridTransform = this.panelContainer.GetComponent(); + gridTransform.sizeDelta = size; + + // Adjust grid container Y position to maintain constant height. + // TODO: Figure out a way to adjust UI to avoid this calculation in code + var gridPosition = new Vector3(gridTransform.anchoredPosition3D.x, + (float)((gridTransform.rect.height - 2000) / 2.0), + gridTransform.anchoredPosition3D.z); + gridTransform.anchoredPosition3D = gridPosition; + + return size; + } + private void ProcessTabContainer(Config config, List tabs, GameObject tabContainer, GameObject tabContainerContent, bool setFirstTabActive, Dictionary gridContents) { @@ -562,48 +284,8 @@ namespace QuestAppLauncher } } - private byte[] GetAppIcon(string iconPath, int appIndex) - { - byte[] bytesIcon = null; - bool useApkIcon = true; - if (null != iconPath) - { - // Use overridden icon - try - { - bytesIcon = File.ReadAllBytes(iconPath); - useApkIcon = false; - } - catch (Exception e) - { - // Fall back to using the apk icon - Debug.Log(string.Format("Error reading app icon from file [{0}]: {1}", iconPath, e.Message)); - } - } - - if (useApkIcon) - { - // Use built-in icon from the apk - using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) - { - bytesIcon = (byte[])(Array)currentActivity.Call("getIcon", appIndex); - } - } - - return bytesIcon; - } - private async Task AddCellToGridAsync(ProcessedApp app, Transform transform) { - // Create new instances of our app info prefabCell - var newObj = (GameObject)Instantiate(this.prefabCell, transform); - - // Set app entry info - var appEntry = newObj.GetComponent("AppEntry") as AppEntry; - appEntry.packageId = app.PackageName; - appEntry.appName = app.AppName; - // Get app icon in background var bytesIcon = await Task.Run(() => { @@ -611,7 +293,7 @@ namespace QuestAppLauncher try { - return GetAppIcon(app.IconPath, app.Index); + return AppProcessor.GetAppIcon(app.IconPath, app.Index); } finally { @@ -619,6 +301,14 @@ namespace QuestAppLauncher } }); + // Create new instances of our app info prefabCell + var newObj = (GameObject)Instantiate(this.prefabCell, transform); + + // Set app entry info + var appEntry = newObj.GetComponent("AppEntry") as AppEntry; + appEntry.packageId = app.PackageName; + appEntry.appName = app.AppName; + // Set the icon image if (null != bytesIcon) { diff --git a/Assets/Scripts/OnInteraction.cs b/Assets/Scripts/OnInteraction.cs index 28469fb..5fee2e4 100644 --- a/Assets/Scripts/OnInteraction.cs +++ b/Assets/Scripts/OnInteraction.cs @@ -39,7 +39,7 @@ namespace QuestAppLauncher { // Launch app Debug.Log("Launching: " + appEntry.appName + " (package id: " + appEntry.packageId + ")"); - QuestAppLauncher.GridPopulation.LaunchApp(appEntry.packageId); + AppProcessor.LaunchApp(appEntry.packageId); } } @@ -50,7 +50,7 @@ namespace QuestAppLauncher { // Add package name to excluded file Debug.Log("Hiding: " + appEntry.appName + " (package id: " + appEntry.packageId + ")"); - QuestAppLauncher.GridPopulation.AddAppToExcludedFile(appEntry.packageId); + AppProcessor.AddAppToExcludedFile(appEntry.packageId); // Remove ourselves from the gridview Destroy(t.gameObject); diff --git a/Assets/Scripts/SettingsHandler.cs b/Assets/Scripts/SettingsHandler.cs index 5e050ed..aded5de 100644 --- a/Assets/Scripts/SettingsHandler.cs +++ b/Assets/Scripts/SettingsHandler.cs @@ -116,7 +116,7 @@ namespace QuestAppLauncher if (!this.deletedHiddenAppsFile) { - this.deletedHiddenAppsFile = QuestAppLauncher.GridPopulation.DeleteExcludedAppsFile(); + this.deletedHiddenAppsFile = AppProcessor.DeleteExcludedAppsFile(); } } From bf732321ee0d9b0ae4d96abab5051d29de923f87 Mon Sep 17 00:00:00 2001 From: tverona1 Date: Fri, 9 Aug 2019 23:38:17 -0700 Subject: [PATCH 3/3] Support for zipped icon packs - Added support for zipped icon pack files (iconpack*.zip) - Added support for multiple appnames*.txt files --- Assets/Plugins/Android/AppInfo.java | 51 ++++- Assets/Scripts/AppProcessor.cs | 292 ++++++++++++++++++---------- 2 files changed, 242 insertions(+), 101 deletions(-) diff --git a/Assets/Plugins/Android/AppInfo.java b/Assets/Plugins/Android/AppInfo.java index 7f58372..e19c8f2 100644 --- a/Assets/Plugins/Android/AppInfo.java +++ b/Assets/Plugins/Android/AppInfo.java @@ -7,11 +7,18 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageInfo; import android.content.pm.FeatureInfo; import android.content.pm.ApplicationInfo; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import android.os.Bundle; import android.util.Log; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; -import java.io.ByteArrayOutputStream; import java.util.List; import java.util.LinkedList; @@ -88,4 +95,46 @@ public class AppInfo extends UnityPlayerActivity { byte[] byteArray = stream.toByteArray(); return byteArray; } + + public static void unzip(String zipFileName, String targetPath) { + + File outDir = new File(targetPath); + + // Create target path if not exist + createDirIfNotExist(outDir); + + byte[] buffer = new byte[8192]; + try (FileInputStream fis = new FileInputStream(zipFileName); + BufferedInputStream bis = new BufferedInputStream(fis); + ZipInputStream stream = new ZipInputStream(bis)) { + + ZipEntry entry = null; + while ((entry = stream.getNextEntry()) != null) { + + File filePath = new File(outDir, entry.getName()); + Log.v(TAG, "Unzipping " + filePath); + + if (entry.isDirectory()) { + // Create dir if required while unzipping + createDirIfNotExist(filePath); + } else { + try (FileOutputStream fos = new FileOutputStream(filePath); + BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) { + int len; + while ((len = stream.read(buffer)) > 0) { + bos.write(buffer, 0, len); + } + } + } + } + } catch (Exception e) { + System.out.println(e); + } + } + + private static void createDirIfNotExist(File path) { + if (!path.exists()) { + path.mkdirs(); + } + } } \ No newline at end of file diff --git a/Assets/Scripts/AppProcessor.cs b/Assets/Scripts/AppProcessor.cs index 1d48b24..14884f7 100644 --- a/Assets/Scripts/AppProcessor.cs +++ b/Assets/Scripts/AppProcessor.cs @@ -21,11 +21,17 @@ namespace QuestAppLauncher public class AppProcessor { // File name of app name overrides - const string AppNameOverrideFile = "appnames.txt"; + const string AppNameOverrideFileSearch = "appnames*.txt"; // File name of excluded package names const string ExcludedPackagesFile = "excludedpackages.txt"; + // Icon pack search string + const string IconPackSearch = "iconpack*.zip"; + + // Icon pack extraction dir + const string IconPackExtractionDir = "cache"; + // Extension search for icon overrides const string IconOverrideExtSearch = "*.jpg"; @@ -43,8 +49,8 @@ namespace QuestAppLauncher Debug.Log("Persistent data path: " + persistentDataPath); // Dictionary to hold package name -> app index, app name - var apps = new Dictionary(); - var excludedPackageNames = new HashSet(); + var apps = new Dictionary(StringComparer.OrdinalIgnoreCase); + var excludedPackageNames = new HashSet(StringComparer.OrdinalIgnoreCase); using (AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (AndroidJavaObject currentActivity = unity.GetStatic("currentActivity")) @@ -111,114 +117,200 @@ namespace QuestAppLauncher Debug.LogFormat("[{0}] package: {1}, name: {2}, auto tab: {3}", i, packageName, appName, tabName); } - // Override app names, if any - // This is just a file with comma-separated packageName,appName[,category1[, category2]] - // Category1 and category2 are optional categories (tabs). - var appNameOverrideFilePath = Path.Combine(persistentDataPath, AppNameOverrideFile); - if (File.Exists(appNameOverrideFilePath)) + // Process appname*.txt files, sorted by name + foreach (var appNameOverrideFilePath in Directory.GetFiles( + persistentDataPath, AppNameOverrideFileSearch).OrderBy(f => f)) { - Debug.Log("Found file: " + appNameOverrideFilePath); - string[] lines = File.ReadAllLines(appNameOverrideFilePath); - foreach (string line in lines) - { - line.Trim(); - - if (line.StartsWith("#")) - { - // Skip comments - continue; - } - - // Parse line - var entry = line.Split(','); - if (entry.Length < 2) - { - // We expect at least two entries - continue; - } - - var packageName = entry[0]; - var appName = entry[1]; - - if (!apps.ContainsKey(packageName)) - { - // App is not installed, so skip - continue; - } - - // Get the custom tab names, if any - string autoTabName = null; - var tab1 = entry.Length > 2 ? entry[2] : null; - var tab2 = entry.Length > 3 ? entry[3] : null; - - if (tab1 != null && tab1.Length == 0) - { - tab1 = null; - } - - if (tab2 != null && tab2.Length == 0) - { - tab2 = null; - } - - if (tab1 != null && tab2 != null && tab1.Equals(tab2, StringComparison.OrdinalIgnoreCase)) - { - tab2 = null; - } - - // Override auto tabe name if custom name matches built-in tab name - if (tab1 != null && Auto_Tabs.Contains(tab1, StringComparer.OrdinalIgnoreCase)) - { - autoTabName = tab1; - tab1 = null; - } - - if (tab2 != null && Auto_Tabs.Contains(tab2, StringComparer.OrdinalIgnoreCase)) - { - autoTabName = tab2; - tab2 = null; - } - - // Update entry - apps[packageName] = new ProcessedApp - { - PackageName = apps[entry[0]].PackageName, - Index = apps[entry[0]].Index, - AppName = appName, - AutoTabName = autoTabName ?? apps[entry[0]].AutoTabName, - Tab1Name = tab1 ?? apps[entry[0]].Tab1Name, - Tab2Name = tab2 ?? apps[entry[0]].Tab2Name, - }; - } - } - else - { - Debug.Log("Did not find: " + appNameOverrideFilePath); + ProcessAppNameOverrideFile(apps, appNameOverrideFilePath); } - // Load list of app icon overrides - // This is a list of jpg images stored as packageName.jpg. + // Extract icon packs + ExtractIconPacks(currentActivity); + + // Process extracted icons + ProcessExtractedIcons(apps); + + // Process any individual icons var iconOverridePath = persistentDataPath; - if (Directory.Exists(iconOverridePath)) + if (Directory.Exists(persistentDataPath)) { - foreach (var iconFileName in Directory.GetFiles(iconOverridePath, IconOverrideExtSearch)) - { - var entry = Path.GetFileNameWithoutExtension(iconFileName); - if (apps.ContainsKey(entry)) - { - Debug.Log("Found icon override: " + iconFileName); - - ProcessedApp newProcessedApp = apps[entry]; - newProcessedApp.IconPath = Path.Combine(iconOverridePath, iconFileName); - apps[entry] = newProcessedApp; - } - } + ProcessIconsInPath(apps, persistentDataPath); } } return apps; } + private static void ProcessAppNameOverrideFile(Dictionary apps, string appNameOverrideFilePath) + { + // Override app names, if any + // This is just a file with comma-separated packageName,appName[,category1[, category2]] + // Category1 and category2 are optional categories (tabs). + Debug.Log("Found file: " + appNameOverrideFilePath); + + string[] lines = File.ReadAllLines(appNameOverrideFilePath); + foreach (string line in lines) + { + line.Trim(); + + if (line.StartsWith("#")) + { + // Skip comments + continue; + } + + // Parse line + var entry = line.Split(','); + if (entry.Length < 2) + { + // We expect at least two entries + continue; + } + + var packageName = entry[0]; + var appName = entry[1]; + + if (!apps.ContainsKey(packageName)) + { + // App is not installed, so skip + continue; + } + + // Get the custom tab names, if any + string autoTabName = null; + var tab1 = entry.Length > 2 ? entry[2] : null; + var tab2 = entry.Length > 3 ? entry[3] : null; + + if (tab1 != null && tab1.Length == 0) + { + tab1 = null; + } + + if (tab2 != null && tab2.Length == 0) + { + tab2 = null; + } + + if (tab1 != null && tab2 != null && tab1.Equals(tab2, StringComparison.OrdinalIgnoreCase)) + { + tab2 = null; + } + + // Override auto tabe name if custom name matches built-in tab name + if (tab1 != null && Auto_Tabs.Contains(tab1, StringComparer.OrdinalIgnoreCase)) + { + autoTabName = tab1; + tab1 = null; + } + + if (tab2 != null && Auto_Tabs.Contains(tab2, StringComparer.OrdinalIgnoreCase)) + { + autoTabName = tab2; + tab2 = null; + } + + // Update entry + apps[packageName] = new ProcessedApp + { + PackageName = apps[entry[0]].PackageName, + Index = apps[entry[0]].Index, + AppName = appName, + AutoTabName = autoTabName ?? apps[entry[0]].AutoTabName, + Tab1Name = tab1 ?? apps[entry[0]].Tab1Name, + Tab2Name = tab2 ?? apps[entry[0]].Tab2Name, + }; + } + } + + private static void ExtractIconPacks(AndroidJavaObject currentActivity) + { + var iconPackDestinationFolders = new Dictionary(StringComparer.OrdinalIgnoreCase); + var iconPacksPath = UnityEngine.Application.persistentDataPath; + + if (!Directory.Exists(iconPacksPath)) + { + return; + } + + // Full path of extraction dir + var extractionDirPath = Path.Combine(iconPacksPath, IconPackExtractionDir); + Debug.LogFormat("Extraction dir: {0}", extractionDirPath); + + // Enumerate all iconpack *.zip files, sorted by name + foreach (var iconPackFilePath in Directory.GetFiles(iconPacksPath, IconPackSearch).OrderBy(f => f)) + { + var iconPackFileName = Path.GetFileName(iconPackFilePath); + + // Get file modified date + var modifiedTime = File.GetLastWriteTime(iconPackFilePath); + + // Construct destination folder name w/ modified time + var destinationFolderName = iconPackFileName + "_" + modifiedTime.ToString("yyyy-dd-MM--HH-mm-ss"); + + iconPackDestinationFolders.Add(destinationFolderName, iconPackFileName); + } + + // Enumerate all folders under destination path + if (Directory.Exists(extractionDirPath)) + { + var dirs = Directory.GetDirectories(extractionDirPath); + foreach (var dirPath in dirs) + { + var dir = new DirectoryInfo(dirPath).Name; + if (iconPackDestinationFolders.ContainsKey(dir)) + { + // Remove matching entry - this means that we've already extracted and matched on modified time + iconPackDestinationFolders.Remove(dir); + } + else + { + // Delete any folder that is not in the icon pack target destination path + Directory.Delete(dirPath, true); + } + } + } + + // Unzip icon packs + foreach (var iconPack in iconPackDestinationFolders) + { + currentActivity.CallStatic("unzip", Path.Combine(iconPacksPath, iconPack.Value), + Path.Combine(extractionDirPath, iconPack.Key)); + } + } + + private static void ProcessExtractedIcons(Dictionary apps) + { + // Full path of extraction dir + var extractionDirPath = Path.Combine(UnityEngine.Application.persistentDataPath, IconPackExtractionDir); + + if (Directory.Exists(extractionDirPath)) + { + // Enumerate extracted icon packs, sorted alphabetically + var dirs = Directory.GetDirectories(extractionDirPath).OrderBy(f => f); + foreach (var dir in dirs) + { + ProcessIconsInPath(apps, dir); + } + } + } + + private static void ProcessIconsInPath(Dictionary apps, string path) + { + foreach (var iconFilePath in Directory.GetFiles(path, IconOverrideExtSearch)) + { + // This is a list of jpg images stored as packageName.jpg. + var entry = Path.GetFileNameWithoutExtension(iconFilePath); + if (apps.ContainsKey(entry)) + { + Debug.Log("Found icon override: " + iconFilePath); + + ProcessedApp newProcessedApp = apps[entry]; + newProcessedApp.IconPath = iconFilePath; + apps[entry] = newProcessedApp; + } + } + } + public static byte[] GetAppIcon(string iconPath, int appIndex) { byte[] bytesIcon = null;