Skip to content

Make Health Connect records assignable to IRecord#1464

Merged
jonathanpeppers merged 3 commits into
mainfrom
copilot/fix-health-connect-record-assignability
Jun 9, 2026
Merged

Make Health Connect records assignable to IRecord#1464
jonathanpeppers merged 3 commits into
mainfrom
copilot/fix-health-connect-record-assignability

Conversation

Copilot AI commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Concrete Xamarin.AndroidX.Health.Connect.ConnectClient record types (SleepSessionRecord, WeightRecord, HeartRateRecord, RestingHeartRateRecord, …) could not be passed as AndroidX.Health.Connect.Client.Records.IRecord, blocking shared helpers written against the common record abstraction.

Root cause

In upstream AndroidX, the intermediate Kotlin interfaces are @PublishedApi internal:

@PublishedApi internal interface IntervalRecord : Record { ... }
@PublishedApi internal interface InstantaneousRecord : Record { ... }
@PublishedApi internal interface SeriesRecord<out T : Any> : IntervalRecord { ... }

class-parse therefore correctly emits an empty visibility="" for them, the binding generator drops them, and that strips the implements chain (SleepSessionRecord → IntervalRecord → Record) from every concrete record — leaving the records with no IRecord.

Forcing <attr ... name="visibility">public</attr> on the dropped interfaces (the original approach) is rejected by metadata-verify (build/cake/validations.cake):

Preventing exposing/surfacing interfaces with default package accessibility as public

That guard is correct: it stops us from republishing types upstream explicitly marked internal.

Fix

Inject implements Record directly onto every concrete record that originally implemented one of the three internal intermediaries, mirroring the pattern in source/GPS.Metadata.Common.xml (ReflectedParcelable/Parcelable):

<add-node path="/api/package[@name='androidx.health.connect.client.records']/class[implements[@name='androidx.health.connect.client.records.IntervalRecord'] or implements[@name='androidx.health.connect.client.records.InstantaneousRecord'] or implements[@name='androidx.health.connect.client.records.SeriesRecord']]">
  <implements name="androidx.health.connect.client.records.Record" name-generic-aware="androidx.health.connect.client.records.Record"></implements>
</add-node>

This keeps the public IRecord chain on every concrete record without surfacing the internal intermediaries, so metadata-verify passes and the upstream @PublishedApi internal contract is honored.

The fix DOES add API — even though PublicAPI.txt has no additions

This is the important part to understand when reviewing the diff. The actual API surface change is in the generated class declarations, not in PublicAPI.Unshipped.txt:

// Before this PR (generated C#)
public sealed partial class WeightRecord         : Java.Lang.Object { }
public sealed partial class SleepSessionRecord   : Java.Lang.Object { }
public sealed partial class HeartRateRecord      : Java.Lang.Object { }

// After this PR (generated C#)  ← contracts genuinely added
public sealed partial class WeightRecord         : Java.Lang.Object, IRecord { }
public sealed partial class SleepSessionRecord   : Java.Lang.Object, IRecord { }
public sealed partial class HeartRateRecord      : Java.Lang.Object, IRecord { }

Verified locally on a real build of androidx.health.connect.connect-client.csproj41 concrete record types now declare : IRecord in obj/.../generated/src/*.cs (all 41 listed below).

Why PublicAPI.Unshipped.txt only loses lines: the Roslyn PublicApiAnalyzer (RS0016/RS0017) tracks declared members — types, methods, properties, fields, events, ctors. It does not track base-type or interface-implementation lists on already-declared types. Adding : IRecord to an existing public class is invisible to it (no RS0016 warnings fire on the 41 changed classes, and the build is clean). The 9 lines removed in this PR are the IInstantaneousRecord / IIntervalRecord / ISeriesRecord entries from the previous attempt — those interfaces are no longer generated, and they shouldn't be: they correspond to upstream's @PublishedApi internal types.

Verification (compiles 0 errors, 0 warnings)

This is the exact snippet from the user's reported scenario, built against the freshly-produced Xamarin.AndroidX.Health.Connect.ConnectClient.dll from this PR:

using AndroidX.Health.Connect.Client.Records;

static string GetPackageName(IRecord record) =>
    record.Metadata.DataOrigin.PackageName!;

void Use(WeightRecord w, SleepSessionRecord s, HeartRateRecord h,
         StepsRecord st, BloodPressureRecord bp, PowerRecord p)
{
    _ = GetPackageName(w);   // instantaneous
    _ = GetPackageName(s);   // interval
    _ = GetPackageName(h);   // series
    _ = GetPackageName(st);
    _ = GetPackageName(bp);
    _ = GetPackageName(p);
}

Before this PR every GetPackageName(...) call is a CS1503. After this PR all 6 calls (spanning instantaneous, interval, and series record categories) compile.

All 41 concrete record types now implementing IRecord

ActiveCaloriesBurnedRecord, BasalBodyTemperatureRecord, BasalMetabolicRateRecord, BloodGlucoseRecord, BloodPressureRecord, BodyFatRecord, BodyTemperatureRecord, BodyWaterMassRecord, BoneMassRecord, CervicalMucusRecord, CyclingPedalingCadenceRecord, DistanceRecord, ElevationGainedRecord, ExerciseSessionRecord, FloorsClimbedRecord, HeartRateRecord, HeartRateVariabilityRmssdRecord, HeightRecord, HydrationRecord, IntermenstrualBleedingRecord, LeanBodyMassRecord, MenstruationFlowRecord, MenstruationPeriodRecord, MindfulnessSessionRecord, NutritionRecord, OvulationTestRecord, OxygenSaturationRecord, PlannedExerciseSessionRecord, PowerRecord, RespiratoryRateRecord, RestingHeartRateRecord, SexualActivityRecord, SkinTemperatureRecord, SleepSessionRecord, SpeedRecord, StepsCadenceRecord, StepsRecord, TotalCaloriesBurnedRecord, Vo2MaxRecord, WeightRecord, WheelchairPushesRecord.

Changes

  • Transforms/Metadata.xml: replace the three visibility="public" attrs and the SeriesRecord/getSamples <remove-node> with the single <add-node> above. The getSamples workaround is no longer needed because ISeriesRecord is no longer generated, so the CS0738 collision can't occur.
  • PublicAPI.Unshipped.txt: drop the IInstantaneousRecord / IIntervalRecord / ISeriesRecord entries that will no longer be generated. IRecord itself remains. No additions are needed (see "The fix DOES add API" above).
  • config.json: keep Xamarin.AndroidX.Health.Connect.ConnectClient 1.1.0.2 → 1.1.0.3.
  • .github/copilot-instructions.md: document the @PublishedApi internal pattern and the supported <add-node><implements/></add-node> fix so future changes hit the right approach first.

metadata-verify passes locally and androidx.health.connect.connect-client builds clean.

Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix Xamarin.AndroidX.Health.Connect ConnectClient record assignability Fix Health Connect record classes not assignable to IRecord Jun 8, 2026
Copilot AI requested a review from jonathanpeppers June 8, 2026 18:37
…rfaces public

IntervalRecord, InstantaneousRecord, and SeriesRecord are `@PublishedApi
internal` in upstream Kotlin source. The previous fix forced
visibility="public" on them, which both:

- Tripped `metadata-verify` ("Preventing exposing/surfacing interfaces with
  default package accessibility as public").
- Surfaced AndroidX-internal types in the public binding API surface,
  contradicting upstream's intent.

Replace the visibility attrs (and the SeriesRecord/getSamples remove-node,
which is no longer needed) with a single `<add-node>` that injects
`implements Record` directly onto every concrete record class that originally
implemented one of the three internal intermediaries. This preserves the
`SleepSessionRecord : IRecord` (etc.) chain that callers need without
republishing the internal interfaces.

PublicAPI.Unshipped.txt: drop the IInstantaneousRecord/IIntervalRecord/
ISeriesRecord entries that will no longer be generated; IRecord and the
existing concrete record APIs are retained.

Also document the pattern in .github/copilot-instructions.md so future
changes hit the supported approach first.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers marked this pull request as ready for review June 8, 2026 19:52
Copilot AI review requested due to automatic review settings June 8, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown

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 updates the androidx.health.connect:connect-client binding to restore the expected public interface chain so concrete Health Connect record types can be used where AndroidX.Health.Connect.Client.Records.IRecord is required.

Changes:

  • Injects androidx.health.connect.client.records.Record onto concrete record classes via a metadata <add-node> transform to restore IRecord assignability.
  • Bumps Xamarin.AndroidX.Health.Connect.ConnectClient NuGet revision from 1.1.0.2 to 1.1.0.3.
  • Documents the recommended fix pattern in .github/copilot-instructions.md for cases where Kotlin @PublishedApi internal intermediates are dropped by the generator.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
source/androidx.health.connect/connect-client/Transforms/Metadata.xml Adds an XML transform to re-attach Record to concrete record types when intermediate interfaces are dropped.
config.json Increments the ConnectClient NuGet revision to publish the binding fix.
.github/copilot-instructions.md Adds guidance for handling dropped intermediate interfaces without surfacing internal types.

@jonathanpeppers jonathanpeppers changed the title Fix Health Connect record classes not assignable to IRecord Make Health Connect records assignable to IRecord Jun 8, 2026
@jonathanpeppers jonathanpeppers merged commit ebc9b98 into main Jun 9, 2026
3 checks passed
@jonathanpeppers jonathanpeppers deleted the copilot/fix-health-connect-record-assignability branch June 9, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Xamarin.AndroidX.Health.Connect.ConnectClient: record classes are not assignable to Record/IRecord interface

3 participants