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/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..14884f7 --- /dev/null +++ b/Assets/Scripts/AppProcessor.cs @@ -0,0 +1,414 @@ +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 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"; + + // 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(StringComparer.OrdinalIgnoreCase); + var excludedPackageNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + 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); + } + + // Process appname*.txt files, sorted by name + foreach (var appNameOverrideFilePath in Directory.GetFiles( + persistentDataPath, AppNameOverrideFileSearch).OrderBy(f => f)) + { + ProcessAppNameOverrideFile(apps, appNameOverrideFilePath); + } + + // Extract icon packs + ExtractIconPacks(currentActivity); + + // Process extracted icons + ProcessExtractedIcons(apps); + + // Process any individual icons + var iconOverridePath = persistentDataPath; + if (Directory.Exists(persistentDataPath)) + { + 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; + 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 c913c9d..67a8151 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; @@ -16,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 bool IsOverride; - } - - // 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; @@ -64,20 +45,9 @@ 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"; - 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 - void Start() + async void Start() { // Set high texture resolution scale to minimize aliasing XRSettings.eyeTextureResolutionScale = 2.0f; @@ -86,7 +56,7 @@ namespace QuestAppLauncher Core.AsyncInitialize(); // Populate the grid - StartPopulate(); + await PopulateAsync(); } void Update() @@ -97,74 +67,150 @@ namespace QuestAppLauncher #region Private Functions /// - /// Static method for launching an Android app + /// Populate the grid from installed apps /// - /// - static public void LaunchApp(string packageId) + /// + private async Task PopulateAsync() { - using (AndroidJavaClass up = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) - using (AndroidJavaObject ca = up.GetStatic("currentActivity")) - using (AndroidJavaObject packageManager = ca.Call("getPackageManager")) + // Load configuration + Config config = new Config(); + ConfigPersistence.LoadConfig(config); + + // Process apps in background + var apps = await Task.Run(() => { - AndroidJavaObject launchIntent = null; + AndroidJNI.AttachCurrentThread(); 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)); + return AppProcessor.ProcessApps(config); } finally { - if (null != launchIntent) - { - launchIntent.Dispose(); - } + AndroidJNI.DetachCurrentThread(); + } + }); + + // Populate the panel content + await PopulatePanelContentAsync(config, apps); + } + + private async Task PopulatePanelContentAsync( + Config config, + Dictionary apps) + { + // Set up tabs + var topTabs = new List(); + var leftTabs = new List(); + var rightTabs = new List(); + + // Set auto tabs + if (config.autoCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase)) + { + topTabs.AddRange(AppProcessor.Auto_Tabs); + } + else if (config.autoCategory.Equals(Config.Category_Left, StringComparison.OrdinalIgnoreCase)) + { + leftTabs.AddRange(AppProcessor.Auto_Tabs); + } + else if (config.autoCategory.Equals(Config.Category_Right, StringComparison.OrdinalIgnoreCase)) + { + rightTabs.AddRange(AppProcessor.Auto_Tabs); + } + + // Set custom tabs, sorted alphabetically + var customTabs = apps.Where(x => null != x.Value.Tab1Name).Select(x => x.Value.Tab1Name).Union(apps + .Where(x => null != x.Value.Tab2Name).Select(x => x.Value.Tab2Name)) + .Distinct(StringComparer.CurrentCultureIgnoreCase).ToList(); + customTabs.Sort(); + + if (config.customCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase)) + { + topTabs.AddRange(customTabs); + } + else if (config.customCategory.Equals(Config.Category_Left, StringComparison.OrdinalIgnoreCase)) + { + leftTabs.AddRange(customTabs); + } + else if (config.customCategory.Equals(Config.Category_Right, StringComparison.OrdinalIgnoreCase)) + { + rightTabs.AddRange(customTabs); + } + + // Add the "all" top tab + topTabs.Add(AppProcessor.Tab_All); + + // Process the tab containers + var gridContents = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ProcessTabContainer(config, topTabs, this.topTabContainer, this.topTabContainerContent, true, gridContents); + ProcessTabContainer(config, leftTabs, this.leftTabContainer, this.leftTabContainerContent, false, gridContents); + ProcessTabContainer(config, rightTabs, this.rightTabContainer, this.rightTabContainerContent, false, gridContents); + + // Set panel size, use any grid content for reference (since they are all the same size) + if (gridContents.Count > 0) + { + var size = SetGridSize(gridContents.First().Value, config.gridSize.rows, config.gridSize.cols); + + // Adjust tab sizes + ResizeTabContent(this.topTabContainer.transform, size, topTabs.Count, true); + ResizeTabContent(this.leftTabContainer.transform, size, leftTabs.Count, false); + ResizeTabContent(this.rightTabContainer.transform, size, rightTabs.Count, false); + } + + // Populate grid with app information (name & icon) + // Sort by app name + foreach (var app in apps.OrderBy(key => key.Value.AppName)) + { + // Add to all tab + await AddCellToGridAsync(app.Value, gridContents[AppProcessor.Tab_All].transform); + + // Add to auto (built-in) tabs + if (gridContents.ContainsKey(app.Value.AutoTabName)) + { + await AddCellToGridAsync(app.Value, gridContents[app.Value.AutoTabName].transform); + } + + // Add to tab1 + if (null != app.Value.Tab1Name && gridContents.ContainsKey(app.Value.Tab1Name)) + { + await AddCellToGridAsync(app.Value, gridContents[app.Value.Tab1Name].transform); + } + + // Add to tab2 + if (null != app.Value.Tab2Name && gridContents.ContainsKey(app.Value.Tab2Name)) + { + await AddCellToGridAsync(app.Value, gridContents[app.Value.Tab2Name].transform); } } } - /// - /// Static method to add a package name to the excludedFile - /// - /// - static public void AddAppToExcludedFile(string packageName) + private void ResizeTabContent(Transform transform, Vector2 size, int childCount, bool isHorizontal) { - var persistentDataPath = UnityEngine.Application.persistentDataPath; - var excludedPackageNamesFilePath = Path.Combine(persistentDataPath, ExcludedPackagesFile); + var rect = transform.GetComponent(); - using (StreamWriter writer = File.AppendText(excludedPackageNamesFilePath)) + // Resize the transform + Vector2 newSize; + if (isHorizontal) { - writer.WriteLine(packageName); - Debug.Log($"Added package {packageName} to the excluded file {excludedPackageNamesFilePath}"); + newSize = new Vector2(size.x, rect.rect.height); } - } - - /// - /// 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)) + else { - File.Delete(excludedPackageNamesFilePath); - return true; + newSize = new Vector2(rect.rect.width, size.y); } - return false; + rect.sizeDelta = newSize; + + // Resize box collider + var boxCollider = transform.GetComponent(); + boxCollider.size = new Vector3(newSize.x, newSize.y, (float)0.05); + + // Refresh tab prev / next buttons + transform.gameObject.GetComponent().RefreshScrollContent(childCount); } - public Vector2 SetGridSize(GameObject gridContent, int rows, int cols) + private Vector2 SetGridSize(GameObject gridContent, int rows, int cols) { // Make sure grid size have sane value cols = Math.Min(cols, 10); @@ -206,330 +252,6 @@ 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() - { - var persistentDataPath = UnityEngine.Application.persistentDataPath; - Debug.Log("Persistent data path: " + persistentDataPath); - - // Load configuration - Config config = new Config(); - ConfigPersistence.LoadConfig(config); - - 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"); - 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); - yield return null; - } - - // 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, - IsOverride = true - }; - } - } - else - { - 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)) - { - var entry = Path.GetFileNameWithoutExtension(iconFileName); - if (apps.ContainsKey(entry)) - { - Debug.Log("Found file: " + iconFileName); - iconOverrides[entry] = Path.Combine(iconOverridePath, iconFileName); - } - - yield return null; - } - } - - yield return null; - - // Populate the panel content - yield return PopulatePanelContent(currentActivity, config, apps, iconOverrides); - } - } - - private IEnumerator PopulatePanelContent( - AndroidJavaObject currentActivity, - Config config, - Dictionary apps, - Dictionary iconOverrides) - { - // Set up tabs - var topTabs = new List(); - var leftTabs = new List(); - var rightTabs = new List(); - - // Set auto tabs - if (config.autoCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase)) - { - topTabs.AddRange(Auto_Tabs); - } - else if (config.autoCategory.Equals(Config.Category_Left, StringComparison.OrdinalIgnoreCase)) - { - leftTabs.AddRange(Auto_Tabs); - } - else if (config.autoCategory.Equals(Config.Category_Right, StringComparison.OrdinalIgnoreCase)) - { - rightTabs.AddRange(Auto_Tabs); - } - - // Set custom tabs, sorted alphabetically - var customTabs = apps.Where(x => null != x.Value.Tab1Name).Select(x => x.Value.Tab1Name).Union(apps - .Where(x => null != x.Value.Tab2Name).Select(x => x.Value.Tab2Name)) - .Distinct(StringComparer.CurrentCultureIgnoreCase).ToList(); - customTabs.Sort(); - - if (config.customCategory.Equals(Config.Category_Top, StringComparison.OrdinalIgnoreCase)) - { - topTabs.AddRange(customTabs); - } - else if (config.customCategory.Equals(Config.Category_Left, StringComparison.OrdinalIgnoreCase)) - { - leftTabs.AddRange(customTabs); - } - else if (config.customCategory.Equals(Config.Category_Right, StringComparison.OrdinalIgnoreCase)) - { - rightTabs.AddRange(customTabs); - } - - // Add the "all" top tab - topTabs.Add(Tab_All); - - // Process the tab containers - var gridContents = new Dictionary(StringComparer.OrdinalIgnoreCase); - - ProcessTabContainer(config, topTabs, this.topTabContainer, this.topTabContainerContent, true, gridContents); - ProcessTabContainer(config, leftTabs, this.leftTabContainer, this.leftTabContainerContent, false, gridContents); - ProcessTabContainer(config, rightTabs, this.rightTabContainer, this.rightTabContainerContent, false, gridContents); - - // Set panel size, use any grid content for reference (since they are all the same size) - if (gridContents.Count > 0) - { - var size = SetGridSize(gridContents.First().Value, config.gridSize.rows, config.gridSize.cols); - - // Adjust tab sizes - ResizeTabContent(this.topTabContainer.transform, size, topTabs.Count, true); - ResizeTabContent(this.leftTabContainer.transform, size, leftTabs.Count, false); - ResizeTabContent(this.rightTabContainer.transform, size, rightTabs.Count, false); - } - - // Populate grid with app information (name & icon) - // Sort by app name - 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); - - // Add to auto (built-in) tabs - if (gridContents.ContainsKey(app.Value.AutoTabName)) - { - yield return AddCellToGrid(app.Value, gridContents[app.Value.AutoTabName].transform, iconOverrides, currentActivity); - } - - // 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); - } - - // 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); - } - } - } - - private void ResizeTabContent(Transform transform, Vector2 size, int childCount, bool isHorizontal) - { - var rect = transform.GetComponent(); - - // Resize the transform - Vector2 newSize; - if (isHorizontal) - { - newSize = new Vector2(size.x, rect.rect.height); - } - else - { - newSize = new Vector2(rect.rect.width, size.y); - } - - rect.sizeDelta = newSize; - - // Resize box collider - var boxCollider = transform.GetComponent(); - boxCollider.size = new Vector3(newSize.x, newSize.y, (float)0.05); - - // Refresh tab prev / next buttons - transform.gameObject.GetComponent().RefreshScrollContent(childCount); - } - private void ProcessTabContainer(Config config, List tabs, GameObject tabContainer, GameObject tabContainerContent, bool setFirstTabActive, Dictionary gridContents) { @@ -562,10 +284,23 @@ namespace QuestAppLauncher } } - private IEnumerator AddCellToGrid(ProcessedApp app, Transform transform, - Dictionary iconOverrides, - AndroidJavaObject currentActivity) + private async Task AddCellToGridAsync(ProcessedApp app, Transform transform) { + // Get app icon in background + var bytesIcon = await Task.Run(() => + { + AndroidJNI.AttachCurrentThread(); + + try + { + return AppProcessor.GetAppIcon(app.IconPath, app.Index); + } + finally + { + AndroidJNI.DetachCurrentThread(); + } + }); + // Create new instances of our app info prefabCell var newObj = (GameObject)Instantiate(this.prefabCell, transform); @@ -574,44 +309,21 @@ namespace QuestAppLauncher appEntry.packageId = app.PackageName; appEntry.appName = app.AppName; - // Get app icon - byte[] bytesIcon = null; - bool useApkIcon = true; - if (iconOverrides.ContainsKey(app.PackageName)) - { - // Use overridden icon - try - { - bytesIcon = File.ReadAllBytes(iconOverrides[app.PackageName]); - useApkIcon = false; - } - catch (Exception e) - { - // Fall back to using the apk icon - Debug.Log(string.Format("Error reading app icon from file [{0}]: {1}", iconOverrides[app.PackageName], e.Message)); - } - } - - 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/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(); } } 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