diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
new file mode 100644
index 00000000000..2928bdd6b80
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+static class NullableExtensions
+{
+ // The static methods in System.String are not NRT annotated in netstandard2.0,
+ // so we need to add our own extension methods to make them nullable aware.
+ public static bool IsNullOrEmpty ([NotNullWhen (false)] this string? str)
+ {
+ return string.IsNullOrEmpty (str);
+ }
+
+ public static bool IsNullOrWhiteSpace ([NotNullWhen (false)] this string? str)
+ {
+ return string.IsNullOrWhiteSpace (str);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
new file mode 100644
index 00000000000..17c686a7a31
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions,
+/// all subsequent lookups are O(1) dictionary lookups.
+///
+sealed class AssemblyIndex : IDisposable
+{
+ readonly PEReader peReader;
+ internal readonly CustomAttributeTypeProvider customAttributeTypeProvider;
+
+ public MetadataReader Reader { get; }
+ public string AssemblyName { get; }
+ public string FilePath { get; }
+
+ ///
+ /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle.
+ ///
+ public Dictionary TypesByFullName { get; } = new (StringComparer.Ordinal);
+
+ ///
+ /// Cached [Register] attribute data per type.
+ ///
+ public Dictionary RegisterInfoByType { get; } = new ();
+
+ ///
+ /// All custom attribute data per type, pre-parsed for the attributes we care about.
+ ///
+ public Dictionary AttributesByType { get; } = new ();
+
+ AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath)
+ {
+ this.peReader = peReader;
+ this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader);
+ Reader = reader;
+ AssemblyName = assemblyName;
+ FilePath = filePath;
+ }
+
+ public static AssemblyIndex Create (string filePath)
+ {
+ var peReader = new PEReader (File.OpenRead (filePath));
+ var reader = peReader.GetMetadataReader ();
+ var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
+ var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
+ index.Build ();
+ return index;
+ }
+
+ void Build ()
+ {
+ foreach (var typeHandle in Reader.TypeDefinitions) {
+ var typeDef = Reader.GetTypeDefinition (typeHandle);
+
+ var fullName = GetFullName (typeDef, Reader);
+ if (fullName.Length == 0) {
+ continue;
+ }
+
+ TypesByFullName [fullName] = typeHandle;
+
+ var (registerInfo, attrInfo) = ParseAttributes (typeDef);
+
+ if (attrInfo is not null) {
+ AttributesByType [typeHandle] = attrInfo;
+ }
+
+ if (registerInfo is not null) {
+ RegisterInfoByType [typeHandle] = registerInfo;
+ }
+ }
+ }
+
+ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reader)
+ {
+ var name = reader.GetString (typeDef.Name);
+ var ns = reader.GetString (typeDef.Namespace);
+
+ if (typeDef.IsNested) {
+ var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ());
+ var parentName = GetFullName (declaringType, reader);
+ return $"{parentName}+{name}";
+ }
+
+ if (ns.Length == 0) {
+ return name;
+ }
+
+ return $"{ns}.{name}";
+ }
+
+ (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef)
+ {
+ RegisterInfo? registerInfo = null;
+ TypeAttributeInfo? attrInfo = null;
+
+ foreach (var caHandle in typeDef.GetCustomAttributes ()) {
+ var ca = Reader.GetCustomAttribute (caHandle);
+ var attrName = GetCustomAttributeName (ca, Reader);
+
+ if (attrName is null) {
+ continue;
+ }
+
+ if (attrName == "RegisterAttribute") {
+ registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider);
+ } else if (attrName == "ExportAttribute") {
+ // [Export] methods are not handled yet and supporting them will be implemented later
+ } else if (IsKnownComponentAttribute (attrName)) {
+ attrInfo ??= CreateTypeAttributeInfo (attrName);
+ var componentName = TryGetNameProperty (ca);
+ if (componentName is not null) {
+ attrInfo.JniName = componentName.Replace ('.', '/');
+ }
+ if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) {
+ applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent");
+ applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity");
+ }
+ }
+ }
+
+ return (registerInfo, attrInfo);
+ }
+
+ static TypeAttributeInfo CreateTypeAttributeInfo (string attrName)
+ {
+ return attrName switch {
+ "ActivityAttribute" => new ActivityAttributeInfo (),
+ "ServiceAttribute" => new ServiceAttributeInfo (),
+ "BroadcastReceiverAttribute" => new BroadcastReceiverAttributeInfo (),
+ "ContentProviderAttribute" => new ContentProviderAttributeInfo (),
+ "ApplicationAttribute" => new ApplicationAttributeInfo (),
+ "InstrumentationAttribute" => new InstrumentationAttributeInfo (),
+ _ => throw new ArgumentException ($"Unknown component attribute: {attrName}"),
+ };
+ }
+
+ static bool IsKnownComponentAttribute (string attrName)
+ {
+ return attrName == "ActivityAttribute"
+ || attrName == "ServiceAttribute"
+ || attrName == "BroadcastReceiverAttribute"
+ || attrName == "ContentProviderAttribute"
+ || attrName == "ApplicationAttribute"
+ || attrName == "InstrumentationAttribute";
+ }
+
+ internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader)
+ {
+ if (ca.Constructor.Kind == HandleKind.MemberReference) {
+ var memberRef = reader.GetMemberReference ((MemberReferenceHandle)ca.Constructor);
+ if (memberRef.Parent.Kind == HandleKind.TypeReference) {
+ var typeRef = reader.GetTypeReference ((TypeReferenceHandle)memberRef.Parent);
+ return reader.GetString (typeRef.Name);
+ }
+ } else if (ca.Constructor.Kind == HandleKind.MethodDefinition) {
+ var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor);
+ var declaringType = reader.GetTypeDefinition (methodDef.GetDeclaringType ());
+ return reader.GetString (declaringType.Name);
+ }
+ return null;
+ }
+
+ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider)
+ {
+ var value = ca.DecodeValue (provider);
+
+ string jniName = "";
+ string? signature = null;
+ string? connector = null;
+ bool doNotGenerateAcw = false;
+
+ if (value.FixedArguments.Length > 0) {
+ jniName = (string?)value.FixedArguments [0].Value ?? "";
+ }
+ if (value.FixedArguments.Length > 1) {
+ signature = (string?)value.FixedArguments [1].Value;
+ }
+ if (value.FixedArguments.Length > 2) {
+ connector = (string?)value.FixedArguments [2].Value;
+ }
+
+ if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) {
+ doNotGenerateAcw = doNotGenerateAcwValue;
+ }
+
+ return new RegisterInfo {
+ JniName = jniName,
+ Signature = signature,
+ Connector = connector,
+ DoNotGenerateAcw = doNotGenerateAcw,
+ };
+ }
+
+ string? TryGetTypeProperty (CustomAttribute ca, string propertyName)
+ {
+ var value = ca.DecodeValue (customAttributeTypeProvider);
+ var typeName = TryGetNamedStringArgument (value, propertyName);
+ if (!typeName.IsNullOrEmpty ()) {
+ return typeName;
+ }
+ return null;
+ }
+
+ string? TryGetNameProperty (CustomAttribute ca)
+ {
+ var value = ca.DecodeValue (customAttributeTypeProvider);
+
+ // Check named arguments first (e.g., [Activity(Name = "...")])
+ var name = TryGetNamedStringArgument (value, "Name");
+ if (!name.IsNullOrEmpty ()) {
+ return name;
+ }
+
+ // Fall back to first constructor argument (e.g., [CustomJniName("...")])
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !ctorName.IsNullOrEmpty ()) {
+ return ctorName;
+ }
+
+ return null;
+ }
+
+ static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue)
+ {
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == argumentName && named.Value is bool boolValue) {
+ argumentValue = boolValue;
+ return true;
+ }
+ }
+
+ argumentValue = false;
+ return false;
+ }
+
+ static string? TryGetNamedStringArgument (CustomAttributeValue value, string argumentName)
+ {
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == argumentName && named.Value is string stringValue) {
+ return stringValue;
+ }
+ }
+
+ return null;
+ }
+
+ public void Dispose ()
+ {
+ peReader.Dispose ();
+ }
+}
+
+///
+/// Parsed [Register] attribute data for a type or method.
+///
+sealed record RegisterInfo
+{
+ public required string JniName { get; init; }
+ public string? Signature { get; init; }
+ public string? Connector { get; init; }
+ public bool DoNotGenerateAcw { get; init; }
+}
+
+///
+/// Parsed [Export] attribute data for a method.
+///
+sealed record ExportInfo
+{
+ public IReadOnlyList? ThrownNames { get; init; }
+ public string? SuperArgumentsString { get; init; }
+}
+
+abstract class TypeAttributeInfo
+{
+ protected TypeAttributeInfo (string attributeName)
+ {
+ AttributeName = attributeName;
+ }
+
+ public string AttributeName { get; }
+ public string? JniName { get; set; }
+}
+
+sealed class ActivityAttributeInfo : TypeAttributeInfo
+{
+ public ActivityAttributeInfo () : base ("ActivityAttribute")
+ {
+ }
+}
+
+sealed class ServiceAttributeInfo : TypeAttributeInfo
+{
+ public ServiceAttributeInfo () : base ("ServiceAttribute")
+ {
+ }
+}
+
+sealed class BroadcastReceiverAttributeInfo : TypeAttributeInfo
+{
+ public BroadcastReceiverAttributeInfo () : base ("BroadcastReceiverAttribute")
+ {
+ }
+}
+
+sealed class ContentProviderAttributeInfo : TypeAttributeInfo
+{
+ public ContentProviderAttributeInfo () : base ("ContentProviderAttribute")
+ {
+ }
+}
+
+sealed class InstrumentationAttributeInfo : TypeAttributeInfo
+{
+ public InstrumentationAttributeInfo () : base ("InstrumentationAttribute")
+ {
+ }
+}
+
+sealed class ApplicationAttributeInfo : TypeAttributeInfo
+{
+ public ApplicationAttributeInfo () : base ("ApplicationAttribute")
+ {
+ }
+
+ public string? BackupAgent { get; set; }
+ public string? ManageSpaceActivity { get; set; }
+}