Skip to content

Add manifest feed parsing for SDK/JDK download resolution#280

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-manifest-feed-parsing
Open

Add manifest feed parsing for SDK/JDK download resolution#280
Copilot wants to merge 5 commits intomainfrom
copilot/add-manifest-feed-parsing

Conversation

Copy link
Contributor

Copilot AI commented Feb 23, 2026

Adds infrastructure to resolve SDK/JDK download URLs and SHA-1 checksums from Xamarin Android Manifest Feed and Google SDK Repository without hardcoding. Enables manifest-driven downloads with offline/CI caching for downstream tools.

Components

  • Data models: PackageInfo, ArchiveInfo, and JdkPackageInfo represent packages with platform-specific archives (URL, checksum, size, OS, arch)
  • ManifestFeed: Core API with LoadAsync(), LoadFromFile(), CacheAsync(), ResolvePackage(), GetJdkVersions(), GetJdkPackages(), GetJdkPackagesByVendor(), GetJdkPackagesByMajorVersion(), and VerifyChecksum()
  • GoogleRepositoryParser: Parses repository2-3.xml with namespace-aware XML handling
  • XamarinManifestParser: Parses Xamarin Android Manifest Feed JSON with flexible property mapping
  • PlatformUtils: Shared utility class for OS/architecture detection and normalization

Enhanced Models

PackageInfo enhancements:

  • PackageType property for package categorization (cmdline-tools, platform-tools, etc.)
  • Obsolete flag
  • GetBestArchiveForCurrentSystem() helper method

ArchiveInfo enhancements:

  • Checksum and ChecksumType properties (supporting both SHA-1 and SHA-256)
  • HostBits property (32 or 64)
  • IsValidForCurrentSystem() helper method

New JdkPackageInfo class:

  • VendorId and VendorDisplay for vendor identification (Microsoft, Eclipse Adoptium, Azul, Oracle, Amazon Corretto)
  • Preview flag for early-access releases
  • GetMajorVersion() helper method
  • ParseVendorFromPath() for automatic vendor detection

Usage

// Load and resolve platform-specific package
var manifest = await ManifestFeed.LoadAsync(
    "https://dl.google.com/android/repository/repository2-3.xml");

var package = manifest.ResolvePackage("cmdline-tools;20.0", "linux", "x86_64");
// package.Archives[0].Url - fully qualified download URL
// package.Archives[0].Checksum - checksum for verification
// package.Archives[0].ChecksumType - "sha1" or "sha256"

// Get best archive for current system
var bestArchive = package.GetBestArchiveForCurrentSystem();

// JDK-specific queries with vendor filtering
var jdkPackages = manifest.GetJdkPackages();
var microsoftJdks = manifest.GetJdkPackagesByVendor("microsoft");
var jdk17s = manifest.GetJdkPackagesByMajorVersion(17);

// Offline/CI support
await manifest.CacheAsync("/path/to/cache.xml");
var cached = ManifestFeed.LoadFromFile("/path/to/cache.xml");

// Verify downloads
bool valid = ManifestFeed.VerifyChecksum(downloadPath, package.Archives[0].Checksum);

Dependencies

Adds System.Text.Json 8.0.5 for JSON parsing in netstandard2.0 (patched version for CVE-2024-43485, CVE-2024-30105).

Downstream enablement

Prerequisite for JDK installation (#270) and SDK bootstrap (#271) features.

Original prompt

This section details on the original issue you should resolve

<issue_title>Add manifest feed parsing for SDK/JDK download resolution (move from android-platform-support)</issue_title>
<issue_description>## Summary

Add manifest feed parsing to Xamarin.Android.Tools.AndroidSdk so that consumers can resolve SDK/JDK download URLs and checksums from the Xamarin Android Manifest Feed and Google's repository without hardcoding URLs.

Background

The android-platform-support (internal) repository has XamarinRepository and GoogleV2Repository classes that parse:

  • Xamarin Android Manifest Feed (https://aka.ms/AndroidManifestFeed/d{version}) — JDK archives, platform-tools, command-line tools, emulator with per-platform/architecture URLs and SHA-1 checksums
  • Google SDK Repository (https://dl.google.com/android/repository/repository2-3.xml) — SDK platforms, build-tools, system images, add-ons with SHA-1 checksums

It also includes LocalManifestProvider for offline/CI manifest caching.

This is a prerequisite for the JDK installation (#270) and SDK bootstrap (#271) features — both need manifest feed parsing to resolve download URLs and verify checksums.

Proposed API Surface

  • ManifestFeed.LoadAsync(feedUrl) — download and parse a manifest feed
  • ManifestFeed.ResolvePackage(packageId, os, arch) — get download URL + SHA-1 for a specific package
  • ManifestFeed.GetJdkVersions() — list available JDK versions with download info
  • ManifestFeed.Cache(localPath) — cache manifest for offline use

Key Requirements

  • No hardcoded download URLs — all URLs come from manifest feeds
  • SHA-1 checksum verification — every download must be verified against manifest checksums
  • Manifest caching — support offline/CI scenarios with cached manifests
  • Cross-platform — resolve correct URLs for Windows/macOS/Linux and x64/aarch64

Consumer

Related

Comments on the Issue (you are @copilot in this section)

@rmarinho ## Priority Note

This issue is a critical prerequisite for:

Current state in MAUI DevTools work

I've implemented sdkmanager/avdmanager/adb/emulator wrappers (see #277, #279, #278), but they all assume the tools already exist. Without manifest feed parsing, we cannot:

  1. Bootstrap an SDK from scratch (download cmdline-tools)
  2. Install JDK automatically (download OpenJDK)
  3. Verify downloads with SHA-1 checksums

Reference implementation

The android-platform-support repo has working manifest parsing:

  • XamarinRepository — parses Xamarin Android Manifest Feed
  • GoogleV2Repository — parses Google SDK repository XML
  • LocalManifestProvider — caching for offline/CI

Suggested implementation order

  1. This issue (Add manifest feed parsing for SDK/JDK download resolution (move from android-platform-support) #273) — manifest feed parsing
  2. Add SDK bootstrap and sdkmanager wrapper (move from android-platform-support) #271 — complete SDK bootstrap using manifest
  3. Add JDK installation support (move from android-platform-support) #270 — JDK installation using manifest
  4. Add SDK license acceptance support (move from android-platform-support) #272 — complete license management

Would be happy to help implement this if there's alignment on the approach.</comment_new>


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI changed the title [WIP] Add manifest feed parsing for SDK/JDK download resolution Add manifest feed parsing for SDK/JDK download resolution Feb 23, 2026
Copilot AI requested a review from rmarinho February 23, 2026 17:07
@rmarinho
Copy link
Member

rmarinho commented Feb 23, 2026

Suggestions from parallel work

Thanks for implementing the manifest feed parsing! I've been working on related tooling and have a few observations:

1. Our Manifest Package Models Include More Metadata

We include additional fields that are useful for consumers:

public class ManifestPackage
{
    public string Path { get; set; }          // Package identifier
    public string DisplayName { get; set; }   // Human-readable name
    public string Revision { get; set; }      // Version
    public string PackageType { get; set; }   // cmdline-tools, platform-tools, etc.
    public bool Obsolete { get; set; }
    public string? LicenseId { get; set; }
    public List<ManifestArchive> Archives { get; set; }
}

public class ManifestArchive  
{
    public Uri Url { get; set; }
    public string Checksum { get; set; }
    public string? ChecksumType { get; set; }  // Important for SHA-256 vs SHA-1
    public string HostOS { get; set; }
    public string? HostArch { get; set; }
    public uint HostBits { get; set; }
    
    // Helper methods
    public bool IsValidForCurrentSystem();
    public ManifestArchive? GetBestArchiveForCurrentSystem();
}

2. JDK-Specific Package Model

For JDK packages, we have additional vendor info:

public class JdkManifestPackage
{
    public string VendorId { get; set; }      // e.g., ""microsoft""
    public string VendorDisplay { get; set; } // e.g., ""Microsoft""
    public bool Preview { get; set; }
    public int GetMajorVersion();             // Helper to extract major version
}

These are used by the JDK installer to filter and select appropriate versions.

Would love to collaborate on a unified approach!

@copilot can you implement these changes

@rmarinho rmarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
Copy link
Member

@copilot you look ash the comment above? I tagged you to work on that.

Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

Suggestions from parallel work

Thanks for implementing the manifest feed parsing! I've been working on related tooling and have a few observations:
...

Implemented the enhanced models you suggested in commits 2ff0e9a and a29ff82:

PackageInfo enhancements:

  • Added PackageType and Obsolete properties
  • Added GetBestArchiveForCurrentSystem() helper method

ArchiveInfo enhancements:

  • Added Checksum and ChecksumType properties (supporting both SHA-1 and SHA-256)
  • Added HostBits property
  • Added IsValidForCurrentSystem() helper method

New JdkPackageInfo class:

  • VendorId and VendorDisplay properties
  • Preview flag for early-access releases
  • GetMajorVersion() helper method
  • ParseVendorFromPath() for automatic vendor detection (Microsoft, Eclipse Adoptium, Azul, Oracle, Amazon Corretto)

ManifestFeed API additions:

  • GetJdkPackages() - returns JDK packages with vendor info
  • GetJdkPackagesByVendor(vendorId) - filter by vendor
  • GetJdkPackagesByMajorVersion(majorVersion) - filter by JDK version

Also extracted platform utilities to shared PlatformUtils class for DRY compliance.

@rmarinho rmarinho marked this pull request as ready for review February 24, 2026 14:25
Copilot AI review requested due to automatic review settings February 24, 2026 14:25
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot do we need to use this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this dependency is required. The project targets netstandard2.0 which doesn't include System.Text.Json. The XamarinManifestParser uses JsonDocument and JsonElement from this package to parse JSON manifest feeds. Version 8.0.5 is used specifically because it includes patches for CVE-2024-43485 and CVE-2024-30105.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive manifest feed parsing infrastructure to enable SDK/JDK download resolution from Xamarin Android Manifest Feed and Google SDK Repository without hardcoding URLs. The implementation includes data models for packages and archives, parsers for both XML (Google) and JSON (Xamarin) formats, platform utilities, and extensive test coverage.

Changes:

  • Adds System.Text.Json 8.0.5 dependency for JSON parsing with CVE patches
  • Implements ManifestFeed API with LoadAsync(), LoadFromFile(), CacheAsync(), ResolvePackage(), and JDK-specific query methods
  • Creates PackageInfo, ArchiveInfo, and JdkPackageInfo models with platform/architecture filtering capabilities
  • Provides GoogleRepositoryParser and XamarinManifestParser for parsing both manifest formats
  • Includes PlatformUtils for OS/architecture detection and normalization

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
Xamarin.Android.Tools.AndroidSdk.csproj Adds System.Text.Json 8.0.5 package reference
ManifestFeed/ManifestFeed.cs Core API for loading, caching, and querying manifest feeds
ManifestFeed/PackageInfo.cs Data models for packages, archives, and JDK packages with helper methods
ManifestFeed/GoogleRepositoryParser.cs XML parser for Google SDK Repository format with namespace handling
ManifestFeed/XamarinManifestParser.cs JSON parser for Xamarin Android Manifest Feed with flexible property mapping
ManifestFeed/PlatformUtils.cs Shared utilities for OS/architecture detection and normalization
tests/.../ManifestFeedTests.cs Comprehensive unit tests for all components

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +81
// Return first archive if no match (may be cross-platform)
return Archives.FirstOrDefault (a => string.IsNullOrEmpty (a.HostOs));
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic in GetBestArchiveForCurrentSystem returns the first cross-platform archive (where HostOs is null/empty) if no OS-specific match is found. However, this could return an archive with a non-matching HostOs if there are no cross-platform archives in the list. Consider returning null instead if no valid match is found, or add a final fallback that returns the first archive only if there are truly cross-platform archives available.

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +144
public async Task CacheAsync (string localPath, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty (FeedUrl))
throw new InvalidOperationException ("Cannot cache a manifest that was not loaded from a URL.");

logger (TraceLevel.Info, $"Caching manifest feed to: {localPath}");

try {
var directory = Path.GetDirectoryName (localPath);
if (!string.IsNullOrEmpty (directory) && !Directory.Exists (directory)) {
Directory.CreateDirectory (directory);
}

var response = await httpClient.GetAsync (FeedUrl, cancellationToken).ConfigureAwait (false);
response.EnsureSuccessStatusCode ();

var content = await response.Content.ReadAsStringAsync ().ConfigureAwait (false);
File.WriteAllText (localPath, content);

logger (TraceLevel.Info, "Manifest feed cached successfully.");
} catch (Exception ex) {
logger (TraceLevel.Error, $"Failed to cache manifest feed: {ex.Message}");
throw;
}
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no test coverage for the CacheAsync method. Given that this is a public API method with potential failure modes (file path vs URL validation, network errors, directory creation), it should have test coverage. Consider adding tests for CacheAsync including success scenarios and error cases.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
const string CommonNamespace = "http://schemas.android.com/repository/android/common/02";
const string GenericNamespace = "http://schemas.android.com/repository/android/generic/02";
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CommonNamespace and GenericNamespace constants are defined but never used. These namespace constants may be needed for parsing other elements in the Google SDK Repository format, or they should be removed if not needed. Consider removing them if they're not required, or add a comment explaining why they're declared for future use.

Suggested change
const string CommonNamespace = "http://schemas.android.com/repository/android/common/02";
const string GenericNamespace = "http://schemas.android.com/repository/android/generic/02";

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +204
public static JdkPackageInfo FromPackageInfo (PackageInfo package)
{
return new JdkPackageInfo {
Path = package.Path,
DisplayName = package.DisplayName,
Version = package.Version,
Archives = package.Archives,
License = package.License,
Description = package.Description,
PackageType = package.PackageType,
Obsolete = package.Obsolete
};
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FromPackageInfo method performs a shallow copy of the Archives list, meaning both the original PackageInfo and the new JdkPackageInfo will share the same List instance and ArchiveInfo objects. Modifications to the Archives list in one will affect the other. Consider creating a new List and copying the ArchiveInfo instances, or document this behavior clearly if sharing is intentional.

Copilot uses AI. Check for mistakes.
{
return Packages
.Where (p => p.Path != null && (
p.Path.StartsWith ("jdk", StringComparison.OrdinalIgnoreCase) ||
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering logic in GetJdkVersions and GetJdkPackages is inconsistent. GetJdkVersions uses StartsWith("jdk") which will miss packages like "microsoft-jdk-17", while GetJdkPackages uses IndexOf("jdk") which will match them. This inconsistency means the two methods will return different sets of packages for the same manifest. Consider making the filtering logic consistent between the two methods, likely by using IndexOf in both cases.

Suggested change
p.Path.StartsWith ("jdk", StringComparison.OrdinalIgnoreCase) ||
p.Path.IndexOf ("jdk", StringComparison.OrdinalIgnoreCase) >= 0 ||

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +123
if (string.IsNullOrEmpty (FeedUrl))
throw new InvalidOperationException ("Cannot cache a manifest that was not loaded from a URL.");
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CacheAsync method allows caching even if FeedUrl is a local file path. Since LoadFromFile sets FeedUrl to the file path, calling CacheAsync on a manifest loaded from a local file would attempt to use that file path as a URL in httpClient.GetAsync, which would fail. Consider checking if FeedUrl is a URL (starts with http:// or https://) before allowing caching.

Suggested change
if (string.IsNullOrEmpty (FeedUrl))
throw new InvalidOperationException ("Cannot cache a manifest that was not loaded from a URL.");
if (string.IsNullOrEmpty (FeedUrl) ||
(!FeedUrl.StartsWith ("http://", StringComparison.OrdinalIgnoreCase) &&
!FeedUrl.StartsWith ("https://", StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException ("Cannot cache a manifest that was not loaded from an HTTP or HTTPS URL.");

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +274
if (string.IsNullOrEmpty (expectedSha1))
return false;

try {
using (var sha1 = SHA1.Create ())
using (var stream = File.OpenRead (filePath)) {
var hashBytes = sha1.ComputeHash (stream);
var actualSha1 = BitConverter.ToString (hashBytes).Replace ("-", "").ToLowerInvariant ();
return string.Equals (actualSha1, expectedSha1.ToLowerInvariant (), StringComparison.Ordinal);
}
} catch {
return false;
}
}

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VerifyChecksum method only supports SHA-1 verification (as indicated by the method name parameter and implementation), but ArchiveInfo supports both SHA-1 and SHA-256 checksums via the ChecksumType property. The method should either be enhanced to support both checksum types based on the ChecksumType, or the documentation should clearly state it only supports SHA-1. Consider adding a checksumType parameter or overload to support SHA-256 verification.

Suggested change
if (string.IsNullOrEmpty (expectedSha1))
return false;
try {
using (var sha1 = SHA1.Create ())
using (var stream = File.OpenRead (filePath)) {
var hashBytes = sha1.ComputeHash (stream);
var actualSha1 = BitConverter.ToString (hashBytes).Replace ("-", "").ToLowerInvariant ();
return string.Equals (actualSha1, expectedSha1.ToLowerInvariant (), StringComparison.Ordinal);
}
} catch {
return false;
}
}
// Preserve existing behavior: SHA-1 verification.
return VerifyChecksum (filePath, expectedSha1, "SHA-1");
}
/// <summary>
/// Verifies that a downloaded file matches the expected checksum using the specified algorithm.
/// </summary>
/// <param name="filePath">Path to the downloaded file.</param>
/// <param name="expectedChecksum">Expected checksum in hexadecimal format.</param>
/// <param name="checksumType">
/// The checksum algorithm to use (for example, "SHA-1" or "SHA-256").
/// Comparison is case-insensitive.
/// </param>
/// <returns>True if the checksum matches, false otherwise.</returns>
public static bool VerifyChecksum (string filePath, string expectedChecksum, string checksumType)
{
if (string.IsNullOrEmpty (expectedChecksum) || string.IsNullOrEmpty (checksumType))
return false;
try {
using (var algorithm = CreateHashAlgorithm (checksumType))
{
if (algorithm == null)
return false;
using (var stream = File.OpenRead (filePath)) {
var hashBytes = algorithm.ComputeHash (stream);
var actualChecksum = BitConverter.ToString (hashBytes).Replace ("-", "").ToLowerInvariant ();
var expected = expectedChecksum.Replace ("-", "").ToLowerInvariant ();
return string.Equals (actualChecksum, expected, StringComparison.Ordinal);
}
}
} catch {
return false;
}
}
static HashAlgorithm? CreateHashAlgorithm (string checksumType)
{
switch (checksumType.ToUpperInvariant ()) {
case "SHA1":
case "SHA-1":
return SHA1.Create ();
case "SHA256":
case "SHA-256":
return SHA256.Create ();
default:
return null;
}
}

Copilot uses AI. Check for mistakes.
try {
var response = await httpClient.GetAsync (feedUrl, cancellationToken).ConfigureAwait (false);
response.EnsureSuccessStatusCode ();

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReadAsStringAsync call does not pass the cancellationToken parameter. In .NET Standard 2.0, ReadAsStringAsync does not accept a CancellationToken, but the cancellation token should still be honored by checking for cancellation before this call. Consider checking cancellationToken.ThrowIfCancellationRequested() before calling ReadAsStringAsync, or note that cancellation is only effective for the GetAsync call.

Suggested change
cancellationToken.ThrowIfCancellationRequested ();

Copilot uses AI. Check for mistakes.

var response = await httpClient.GetAsync (FeedUrl, cancellationToken).ConfigureAwait (false);
response.EnsureSuccessStatusCode ();

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReadAsStringAsync call does not pass the cancellationToken parameter. In .NET Standard 2.0, ReadAsStringAsync does not accept a CancellationToken. Consider checking cancellationToken.ThrowIfCancellationRequested() before calling ReadAsStringAsync to respect cancellation requests.

Suggested change
cancellationToken.ThrowIfCancellationRequested ();

Copilot uses AI. Check for mistakes.
Comment on lines +277 to +294
if (string.IsNullOrEmpty (os))
return os;

// Normalize OS names to match manifest conventions
switch (os.ToLowerInvariant ()) {
case "windows":
case "win":
return "windows";
case "macosx":
case "macos":
case "darwin":
case "osx":
return "macosx";
case "linux":
return "linux";
default:
return os.ToLowerInvariant ();
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NormalizeOsName method in ManifestFeed duplicates the same logic in PlatformUtils.NormalizeOsName. This violates the DRY principle and creates maintenance overhead. The ManifestFeed.NormalizeOsName should call PlatformUtils.NormalizeOsName instead of duplicating the logic.

Suggested change
if (string.IsNullOrEmpty (os))
return os;
// Normalize OS names to match manifest conventions
switch (os.ToLowerInvariant ()) {
case "windows":
case "win":
return "windows";
case "macosx":
case "macos":
case "darwin":
case "osx":
return "macosx";
case "linux":
return "linux";
default:
return os.ToLowerInvariant ();
}
return PlatformUtils.NormalizeOsName (os);

Copilot uses AI. Check for mistakes.
Copilot AI and others added 5 commits February 24, 2026 19:08
- Add PackageInfo and ArchiveInfo data models
- Implement ManifestFeed class with LoadAsync, LoadFromFile, and CacheAsync methods
- Add GoogleRepositoryParser for parsing Google SDK repository XML
- Add XamarinManifestParser for parsing Xamarin Android Manifest Feed JSON
- Support package resolution by OS and architecture
- Implement SHA-1 checksum verification
- Add comprehensive unit tests (11/12 passing, 1 ignored for network access)

Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
…elper methods

Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
Use 'is null' and 'is not null' instead of '== null' and '!= null'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the copilot/add-manifest-feed-parsing branch from c832c20 to fda2fad Compare February 24, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add manifest feed parsing for SDK/JDK download resolution (move from android-platform-support)

3 participants