Last active
June 10, 2025 00:22
-
-
Save ababilinski/4b47d805c4796fec929105afc58709ec to your computer and use it in GitHub Desktop.
Utilities for reading Android Intent extras in Unity and mapping them directly onto C# data classes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ──────────────────────────────────────────────────────────────────────────────── | |
| // Description: Utilities for reading Android Intent extras in Unity and mapping | |
| // them directly onto C# data classes with strong typing. | |
| // ──────────────────────────────────────────────────────────────────────────────── | |
| #nullable enable | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Globalization; | |
| using System.Reflection; | |
| using UnityEngine; | |
| namespace Common | |
| { | |
| /// <summary> | |
| /// Decorates a property to indicate which <c>Intent</c> extra key supplies its | |
| /// value. If omitted, the property name itself is used as the key. | |
| /// </summary> | |
| [AttributeUsage(AttributeTargets.Property)] | |
| public sealed class IntentKeyAttribute : Attribute | |
| { | |
| public string Key { get; } | |
| public IntentKeyAttribute(string key) => Key = key; | |
| } | |
| /// <summary> | |
| /// Low-level API that talks directly to the Android <c>Intent</c>. | |
| /// </summary> | |
| public static class AndroidIntentExtras | |
| { | |
| // ──────────────────────────────────────────────────────────────────── | |
| // PRIVATE HELPERS | |
| // ──────────────────────────────────────────────────────────────────── | |
| private static AndroidJavaObject GetIntent() | |
| { | |
| var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); | |
| var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); | |
| return activity.Call<AndroidJavaObject>("getIntent"); | |
| } | |
| private static string? JavaClassName(AndroidJavaObject obj) => | |
| obj.Call<AndroidJavaObject>("getClass")?.Call<string>("getName"); | |
| // ──────────────────────────────────────────────────────────────────── | |
| // SNAPSHOT – Dictionary<string,object> | |
| // ──────────────────────────────────────────────────────────────────── | |
| public static Dictionary<string, object> GetIntentExtras() | |
| { | |
| var dict = new Dictionary<string, object>(); | |
| #if UNITY_EDITOR | |
| return dict; | |
| #endif | |
| using var intent = GetIntent(); | |
| using var bundle = intent.Call<AndroidJavaObject>("getExtras"); | |
| if (bundle == null) return dict; | |
| using var keySet = bundle.Call<AndroidJavaObject>("keySet"); | |
| foreach (string key in keySet.Call<string[]>("toArray")) | |
| { | |
| using var obj = bundle.Call<AndroidJavaObject>("get", key); | |
| if (obj == null) continue; | |
| switch (JavaClassName(obj)) | |
| { | |
| case "java.lang.String": dict[key] = obj.Call<string>("toString"); break; | |
| case "java.lang.Boolean": dict[key] = obj.Call<bool>("booleanValue"); break; | |
| case "java.lang.Integer": dict[key] = obj.Call<int>("intValue"); break; | |
| case "java.lang.Long": dict[key] = obj.Call<long>("longValue"); break; | |
| case "java.lang.Float": dict[key] = obj.Call<float>("floatValue"); break; | |
| default: dict[key] = obj.Call<string>("toString"); break; | |
| } | |
| } | |
| return dict; | |
| } | |
| // ──────────────────────────────────────────────────────────────────── | |
| // GENERIC TYPED GETTER - CORRECTED VERSION | |
| // ──────────────────────────────────────────────────────────────────── | |
| public static bool TryGet<T>(string key, out T? value) | |
| { | |
| value = default; | |
| #if UNITY_EDITOR | |
| return false; | |
| #endif | |
| using var intent = GetIntent(); | |
| if (!intent.Call<bool>("hasExtra", key)) return false; | |
| // Always use Bundle to get the actual type, then convert appropriately | |
| using var bundle = intent.Call<AndroidJavaObject>("getExtras"); | |
| if (bundle == null) return false; | |
| using var obj = bundle.Call<AndroidJavaObject>("get", key); | |
| if (obj == null) return false; | |
| var javaClassName = JavaClassName(obj); | |
| object? raw = null; | |
| try | |
| { | |
| // First, get the value based on its actual Java type | |
| switch (javaClassName) | |
| { | |
| case "java.lang.String": | |
| raw = obj.Call<string>("toString"); | |
| break; | |
| case "java.lang.Boolean": | |
| raw = obj.Call<bool>("booleanValue"); | |
| break; | |
| case "java.lang.Integer": | |
| raw = obj.Call<int>("intValue"); | |
| break; | |
| case "java.lang.Long": | |
| raw = obj.Call<long>("longValue"); | |
| break; | |
| case "java.lang.Float": | |
| raw = obj.Call<float>("floatValue"); | |
| break; | |
| case "java.lang.Double": | |
| raw = obj.Call<double>("doubleValue"); | |
| break; | |
| default: | |
| // Unknown type - get as string | |
| raw = obj.Call<string>("toString"); | |
| break; | |
| } | |
| // If we have a type mismatch, try to convert | |
| if (raw != null && raw.GetType() != typeof(T)) | |
| { | |
| // Special handling for bool conversions from string | |
| if (typeof(T) == typeof(bool) && raw is string str) | |
| { | |
| if (bool.TryParse(str, out var b)) | |
| raw = b; | |
| else if (str is "1" or "yes" or "YES" or "true" or "TRUE") | |
| raw = true; | |
| else if (str is "0" or "no" or "NO" or "false" or "FALSE") | |
| raw = false; | |
| else if (string.IsNullOrEmpty(str)) | |
| raw = true; // presence flag | |
| else | |
| return false; // Can't convert | |
| } | |
| // Special handling for numeric conversions | |
| else if (raw is string strNum) | |
| { | |
| if (typeof(T) == typeof(int) && int.TryParse(strNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var i)) | |
| raw = i; | |
| else if (typeof(T) == typeof(long) && long.TryParse(strNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var l)) | |
| raw = l; | |
| else if (typeof(T) == typeof(float) && float.TryParse(strNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var f)) | |
| raw = f; | |
| else if (typeof(T) == typeof(double) && double.TryParse(strNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) | |
| raw = d; | |
| else | |
| { | |
| // Generic conversion attempt | |
| try | |
| { | |
| raw = Convert.ChangeType(strNum, typeof(T), CultureInfo.InvariantCulture); | |
| } | |
| catch | |
| { | |
| return false; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // Try generic conversion | |
| try | |
| { | |
| raw = Convert.ChangeType(raw, typeof(T), CultureInfo.InvariantCulture); | |
| } | |
| catch | |
| { | |
| return false; | |
| } | |
| } | |
| } | |
| if (raw is T cast) | |
| { | |
| value = cast; | |
| return true; | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Debug.LogError($"Failed to get extra '{key}' as {typeof(T).Name}: {ex.Message}"); | |
| } | |
| return false; | |
| } | |
| // ──────────────────────────────────────────────────────────────────── | |
| // PRESENCE-ONLY FLAG | |
| // ──────────────────────────────────────────────────────────────────── | |
| public static bool HasExtra(string key) | |
| { | |
| #if UNITY_EDITOR | |
| return false; | |
| #endif | |
| using var intent = GetIntent(); | |
| return intent.Call<bool>("hasExtra", key); | |
| } | |
| // ──────────────────────────────────────────────────────────────────── | |
| // BACK-COMPAT WRAPPERS | |
| // ──────────────────────────────────────────────────────────────────── | |
| public static bool TryGetString(string key, out string value) => | |
| TryGet<string>(key, out value); | |
| public static bool HasIntent(string key) => HasExtra(key); | |
| // ──────────────────────────────────────────────────────────────────── | |
| // "WERE WE LAUNCHED WITH *ANY* ARGUMENTS?" | |
| // ──────────────────────────────────────────────────────────────────── | |
| public static bool HasAnyExtras() | |
| { | |
| #if UNITY_EDITOR | |
| return false; | |
| #endif | |
| using var intent = GetIntent(); | |
| using var bundle = intent.Call<AndroidJavaObject>("getExtras"); | |
| if (bundle == null) return false; | |
| int count = bundle.Call<int>("size"); | |
| return count > 0; | |
| } | |
| } | |
| // ════════════════════════════════════════════════════════════════════════════ | |
| // High-level reflection mapper | |
| // ════════════════════════════════════════════════════════════════════════════ | |
| public static class AndroidIntentMapper | |
| { | |
| public static T FromIntentExtras<T>() where T : new() | |
| { | |
| T instance = new T(); | |
| var props = typeof(T).GetProperties( | |
| BindingFlags.Instance | | |
| BindingFlags.Public | | |
| BindingFlags.NonPublic); | |
| foreach (PropertyInfo p in props) | |
| { | |
| if (!p.CanWrite) continue; | |
| // ── 1. Resolve key ────────────────────────────────────────── | |
| var attr = p.GetCustomAttribute<IntentKeyAttribute>(); | |
| string key = attr?.Key ?? p.Name; | |
| // ── 2. Try to get the value (the improved TryGet handles all cases) | |
| if (TryGetExact(p.PropertyType, key, out object? exact)) | |
| { | |
| p.SetValue(instance, exact); | |
| } | |
| } | |
| return instance; | |
| } | |
| // ──────────────────────────────────────────────────────────────────── | |
| // REFLECTION HELPER – dynamic generic call | |
| // ──────────────────────────────────────────────────────────────────── | |
| private static bool TryGetExact(Type t, string key, out object? val) | |
| { | |
| var m = typeof(AndroidIntentExtras) | |
| .GetMethod(nameof(AndroidIntentExtras.TryGet))! | |
| .MakeGenericMethod(t); | |
| object[] args = { key, null! }; | |
| bool ok = (bool)m.Invoke(null, args)!; | |
| val = args[1]; | |
| return ok; | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
am startSnippetstringgetStringExtra→ orToString()fallback-e/--es--es username "alice"boolgetBooleanExtra→ smart-parse ("true","1", empty)--ez--ez isPremiumUser true—or—--ez isPremiumUserintgetIntExtra→int.Parsefallback--ei--ei age 42longgetLongExtra→long.Parsefallback--el--el score 9876543210floatgetFloatExtra→float.Parsefallback--ef--ef ratio 3.14doubledouble.Parse)--es--es price "99.95"enum(any)Enum.TryParse(case-insensitive)--es--es role "Admin"DateTime--es/--el--es expires "2025-12-31T23:59:59Z"--el expires 1767225599Convert.ChangeTypefallback--es--es misc "value"¹ Short form flags (
-e) are valid only for strings; all others require the long form.