Skip to content

Instantly share code, notes, and snippets.

@ababilinski
Last active June 10, 2025 00:22
Show Gist options
  • Select an option

  • Save ababilinski/4b47d805c4796fec929105afc58709ec to your computer and use it in GitHub Desktop.

Select an option

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.
// ────────────────────────────────────────────────────────────────────────────────
// 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;
}
}
}
@ababilinski
Copy link
Author

You can use the utility directly or populate a class using the [IntentKey("some_key")] attribute. For example, if if you have arguments like:

- Username
- Age
- IsPremiumUser

Mapping launch arguments to a class

You can have a class like using the IntentKey attribute. here is an example:

public class LaunchParams
{
    [IntentKey("username")]
    public string Username { get; set; }

    [IntentKey("age")]
    public int Age { get; set; }

    [IntentKey("isPremiumUser")]
    public bool IsPremiumUser { get; set; }
}

How to Map/Deserialize arguments into a class

Then to map the intents to the class you call:

var launchParams = AndroidIntentMapper.FromIntentExtras<LaunchParams>()

Debug.Log(launchParams.Username);
Debug.Log(launchParams.Age);
Debug.Log(launchParams.IsPremiumUser);

How to call unity with launch arguments:

adb shell am start \
-n com.example.project/com.unity3d.player.UnityPlayerGameActivity \
-e username "user123" \
--ei age 30 \
--ez isPremiumUser

@ababilinski
Copy link
Author

C# Property Type Read Path in AndroidIntentMapper Recommended ADB Flag(s)¹ Example am start Snippet
string getStringExtraor ToString() fallback -e / --es --es username "alice"
bool getBooleanExtra → smart-parse ("true", "1", empty) --ez --ez isPremiumUser true —or—
--ez isPremiumUser
int getIntExtraint.Parse fallback --ei --ei age 42
long getLongExtralong.Parse fallback --el --el score 9876543210
float getFloatExtrafloat.Parse fallback --ef --ef ratio 3.14
double via string parse (double.Parse) --es --es price "99.95"
enum (any) string parseEnum.TryParse (case-insensitive) --es --es role "Admin"
DateTime string/long parse → ISO-8601 or epoch seconds --es / --el --es expires "2025-12-31T23:59:59Z"
--el expires 1767225599
any other convertible type Convert.ChangeType fallback --es --es misc "value"

¹ Short form flags (-e) are valid only for strings; all others require the long form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment