[go: nahoru, domu]

Introduced suspend functions for async APIs

Introduced suspend functions as an alternative to the existing async
APIs to make the curent APIs more kotlin friendly.

Relnote: "Introduced suspend functions for async APIs making them more kotlin friendly"
Test: ./gradlew :health:health-services-client:test
Bug: 248307039

Change-Id: Iadea42d6c5a4a7ba510e6400f374c13574c2abfd
diff --git a/health/health-services-client/api/1.0.0-beta02.txt b/health/health-services-client/api/1.0.0-beta02.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/1.0.0-beta02.txt
+++ b/health/health-services-client/api/1.0.0-beta02.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/restricted_1.0.0-beta02.txt b/health/health-services-client/api/restricted_1.0.0-beta02.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta02.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta02.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/build.gradle b/health/health-services-client/build.gradle
index 138d521..1e6d812 100644
--- a/health/health-services-client/build.gradle
+++ b/health/health-services-client/build.gradle
@@ -25,10 +25,12 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    implementation(libs.kotlinCoroutinesCore)
     api("androidx.annotation:annotation:1.1.0")
     implementation(libs.guavaListenableFuture)
     implementation(libs.guavaAndroid)
     implementation("androidx.core:core-ktx:1.7.0")
+    implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     implementation(libs.protobufLite)
 
     testImplementation(libs.junit)
@@ -36,6 +38,9 @@
     testImplementation(libs.robolectric)
     testImplementation(libs.testCore)
     testImplementation(libs.truth)
+    testImplementation(libs.kotlinTest)
+    testImplementation(libs.kotlinCoroutinesTest)
+    testImplementation(libs.kotlinTest)
 }
 
 android {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index 59e76bd..fd572f3 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -52,7 +52,8 @@
      * aggregation will occur until the exercise is started.
      *
      * If an app is actively preparing and another app starts tracking an active exercise then the
-     * preparing app should expect to receive an [ExerciseUpdate] with [ExerciseState.TERMINATED]
+     * preparing app should expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along
+     * with the reason [ExerciseEndReason.AUTO_END_SUPERSEDED] to the [ExerciseUpdateCallback]
      * indicating that their session has been superseded and ended. At that point no additional
      * updates to availability or data will be sent until the app calls prepareExercise again.
      *
@@ -69,15 +70,17 @@
      *
      * Since Health Services only allows a single active exercise at a time, this will terminate any
      * active exercise currently in progress before starting the new one. If this occurs, clients
-     * can expect to receive an [ExerciseUpdate] with [ExerciseState.TERMINATED], indicating that
-     * their exercise has been superseded and that no additional updates will be sent. Clients can
-     * use [getCurrentExerciseInfoAsync] (described below) to check if they or another app has an
-     * active exercise in-progress.
+     * can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along with the reason
+     * [ExerciseEndReason.AUTO_END_SUPERSEDED] to the [ExerciseUpdateCallback] indicating that their
+     * exercise has been superseded and that no additional updates will be sent. Clients can use
+     * [getCurrentExerciseInfoAsync] (described below) to check if they or another app has an active
+     * exercise in-progress.
      *
      * If the client fails to maintain a live [ExerciseUpdateCallback] for at least five minutes
      * during the duration of the exercise, Health Services can decide to terminate the exercise. If
-     * this occurs, clients can expect to receive an [ExerciseUpdate] with
-     * [ExerciseState.AUTO_ENDED] indicating that their exercise has been automatically ended due to
+     * this occurs, clients can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED]
+     * along with the reason [ExerciseEndReason.AUTO_END_MISSING_LISTENER] to the
+     * [ExerciseUpdateCallback] indicating that their exercise has been automatically ended due to
      * the lack of callback.
      *
      * Clients should only request [ExerciseType]s, [DataType]s, goals, and auto-pause enabled that
@@ -193,7 +196,8 @@
      * deliver them as soon as the callback is registered again. If the client fails to maintain a
      * live [ExerciseUpdateCallback] for at least five minutes during the duration of the exercise
      * Health Services can decide to terminate the exercise automatically. If this occurs, clients
-     * can expect to receive an [ExerciseUpdate] with [ExerciseState.AUTO_ENDED] indicating that
+     * can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along with the reason
+     * [ExerciseEndReason.AUTO_END_MISSING_LISTENER] to the [ExerciseUpdateCallback] indicating that
      * their exercise has been automatically ended due to the lack of callback.
      *
      * Calls to the callback will be executed on the main application thread. To control where to
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
new file mode 100644
index 0000000..2e54b70
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.services.client
+
+import androidx.concurrent.futures.await
+import androidx.health.services.client.data.DataPoint
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.ExerciseCapabilities
+import androidx.health.services.client.data.ExerciseConfig
+import androidx.health.services.client.data.ExerciseEndReason
+import androidx.health.services.client.data.ExerciseGoal
+import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseState
+import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseUpdate
+import androidx.health.services.client.data.WarmUpConfig
+
+/**
+ * Prepares for a new exercise.
+ *
+ * Once called, Health Services will warmup the sensors based on the [ExerciseType] and
+ * requested [DataType]s.
+ *
+ * If the calling app already has an active exercise in progress or if it does not have the
+ * required permissions, then this call throws [android.os.RemoteException]. If another app owns
+ * the active exercise then this call will succeed.
+ *
+ * Sensors available for warmup are GPS [DataType.LOCATION] and HeartRate
+ * [DataType.HEART_RATE_BPM]. Other [DataType]s requested for warmup based on exercise
+ * capabilities will be a no-op for the prepare stage.
+ *
+ * The DataType availability can be obtained through the
+ * [ExerciseUpdateCallback.onAvailabilityChanged] callback. [ExerciseUpdate]s with the supported
+ * DataType [DataPoint] will also be returned in the [ExerciseState.PREPARING] state, though no
+ * aggregation will occur until the exercise is started.
+ *
+ * If an app is actively preparing and another app starts tracking an active exercise then the
+ * preparing app should expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along with
+ * the reason [ExerciseEndReason.AUTO_END_SUPERSEDED] to the [ExerciseUpdateCallback] indicating
+ * that their session has been superseded and ended. At that point no additional updates to
+ * availability or data will be sent until the app calls prepareExercise again.
+ *
+ * @param configuration the [WarmUpConfig] containing the desired exercise and data types
+ * @throws [android.os.RemoteException] if the calling app already has an active exercise in
+ * progress or if it does not have the required permissions or if Health Service fails to process
+ * the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.prepareExercise(configuration: WarmUpConfig) =
+    prepareExerciseAsync(configuration).await()
+
+/**
+ * Starts a new exercise.
+ *
+ * Once started, Health Services will begin collecting data associated with the exercise.
+ *
+ * Since Health Services only allows a single active exercise at a time, this will terminate any
+ * active exercise currently in progress before starting the new one. If this occurs, clients
+ * can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED], indicating that
+ * their exercise has been superseded and that no additional updates will be sent. Clients can
+ * use [getCurrentExerciseInfo] (described below) to check if they or another app has an
+ * active exercise in-progress.
+ *
+ * If the client fails to maintain a live [ExerciseUpdateCallback] for at least five minutes
+ * during the duration of the exercise, Health Services can decide to terminate the exercise. If
+ * this occurs, clients can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along
+ * with the reason [ExerciseEndReason.AUTO_END_MISSING_LISTENER] to the [ExerciseUpdateCallback]
+ * indicating that their exercise has been automatically ended due to the lack of callback.
+ *
+ * Clients should only request [ExerciseType]s, [DataType]s, goals, and auto-pause enabled that
+ * matches the [ExerciseCapabilities] returned by [getCapabilities] since Health Services
+ * will reject requests asking for unsupported configurations.
+ *
+ * @param configuration the [ExerciseConfig] describing this exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call or if it does
+ * not have the required permissions
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.startExercise(configuration: ExerciseConfig) =
+    startExerciseAsync(configuration).await()
+
+/**
+ * Pauses the current exercise, if it is currently started.
+ *
+ * Before transitioning to [ExerciseState.USER_PAUSED], Health Services will flush and return
+ * the sensor data. While the exercise is paused, active time and cumulative metrics such as
+ * distance will not accumulate. Instantaneous measurements such as speed and heart rate will
+ * continue to update if requested in the [ExerciseConfig].
+ *
+ * Note that GPS and other sensors may be stopped when the exercise is paused in order to
+ * conserve battery. This may happen immediately, or after some time. (The exact behavior is
+ * hardware dependent.) Should this happen, access will automatically resume when the exercise
+ * is resumed.
+ *
+ * If the exercise is already paused then this method has no effect. If the exercise has ended
+ * then [android.os.RemoteException] is thrown.
+ *
+ * @throws [android.os.RemoteException] if the exercise has ended or if Health Service fails to
+ * process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.pauseExercise() = pauseExerciseAsync().await()
+
+/**
+ * Resumes the current exercise, if it is currently paused.
+ *
+ * Once resumed active time and cumulative metrics such as distance will resume accumulating.
+ *
+ * If the exercise has been started but is not currently paused this method has no effect. If
+ * the exercise has ended then [android.os.RemoteException] is thrown.
+ *
+ * @throws [android.os.RemoteException] if the exercise has ended or if Health Service fails to
+ * process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.resumeExercise() = resumeExerciseAsync().await()
+
+/**
+ * Ends the current exercise, if it has been started.
+ *
+ * Health Services will flush and then shut down the active sensors and return an
+ * [ExerciseUpdate] with [ExerciseState.ENDED] along with the reason
+ * [ExerciseEndReason.USER_END] to the [ExerciseUpdateCallback]. If the exercise has already
+ * ended then this call fails with a [android.os.RemoteException].
+ *
+ * No additional metrics will be produced for the exercise and any on device persisted data
+ * about the exercise will be deleted after the summary has been sent back.
+ *
+ * @throws [android.os.RemoteException] if exercise has already ended or if Health Service fails to
+ * process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.endExercise() = endExerciseAsync().await()
+
+/**
+ * Flushes the sensors for the active exercise. This call should be used sparingly and will be
+ * subject to throttling by Health Services.
+ *
+ * @throws [android.os.RemoteException] if the Health Service fails to process the request
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.flush() = flushAsync().await()
+
+/**
+ * Ends the current lap, calls [ExerciseUpdateCallback.onLapSummaryReceived] with data spanning
+ * the marked lap and starts a new lap. If the exercise supports laps this method can be called
+ * at any point after an exercise has been started and before it has been ended regardless of
+ * the exercise status.
+ *
+ * The metrics in the lap summary will start from either the start time of the exercise or the
+ * last time a lap was marked to the time this method is being called.
+ *
+ * @throws [android.os.RemoteException] If there's no exercise being tracked or if Health Service
+ * fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.markLap() = markLapAsync().await()
+
+/**
+ * Returns the current [ExerciseInfo].
+ *
+ * This can be used by clients to determine if they or another app already owns an active
+ * exercise being tracked by Health Services. For example, if an app is killed and it learns it
+ * owns the active exercise it can register a new [ExerciseUpdateCallback] and pick tracking up
+ * from where it left off.
+ *
+ * @return a [ExerciseInfo] that contains information about the current exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.getCurrentExerciseInfo() = getCurrentExerciseInfoAsync().await()
+
+/**
+ * Clears the callback set using [ExerciseClient.setUpdateCallback].
+ *
+ * If this callback is not already registered then this will be a no-op.
+ *
+ * @param callback the [ExerciseUpdateCallback] to clear
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@Suppress("ExecutorRegistration")
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.clearUpdateCallback(callback: ExerciseUpdateCallback) =
+    clearUpdateCallbackAsync(callback).await()
+
+/**
+ * Adds an [ExerciseGoal] for an active exercise.
+ *
+ * Goals apply to only active exercises owned by the client, and will be invalidated once the
+ * exercise is complete.
+ *
+ * @param exerciseGoal the [ExerciseGoal] to add to this exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.addGoalToActiveExercise(exerciseGoal: ExerciseGoal<*>) =
+    addGoalToActiveExerciseAsync(exerciseGoal).await()
+
+/**
+ * Removes an exercise goal for an active exercise.
+ *
+ * Takes into account equivalent milestones (i.e. milestones which are not equal but are
+ * different representation of a common milestone. e.g. milestone A for every 2kms, currently at
+ * threshold of 10kms, and milestone B for every 2kms, currently at threshold of 8kms).
+ *
+ * @param exerciseGoal the [ExerciseGoal] to remove from this exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.removeGoalFromActiveExercise(
+    exerciseGoal: ExerciseGoal<*>
+) = removeGoalFromActiveExerciseAsync(exerciseGoal).await()
+
+/**
+ * Enables or disables auto pause/resume for the current exercise.
+ *
+ * @param enabled a boolean to indicate if should be enabled or disabled
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.overrideAutoPauseAndResumeForActiveExercise(
+    enabled: Boolean
+) = overrideAutoPauseAndResumeForActiveExerciseAsync(enabled).await()
+
+/**
+ * Returns the [ExerciseCapabilities] of this client for the device.
+ *
+ * This can be used to determine what [ExerciseType]s and [DataType]s this device supports.
+ * Clients should use the capabilities to inform their requests since Health Services will
+ * typically reject requests made for [DataType]s or features (such as auto-pause) which are not
+ * enabled for the rejected [ExerciseType].
+ *
+ * @return a the [ExerciseCapabilities] for this device
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/MeasureClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureClientExtension.kt
new file mode 100644
index 0000000..951ccd3
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureClientExtension.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.services.client
+
+import androidx.concurrent.futures.await
+import androidx.health.services.client.data.DeltaDataType
+import androidx.health.services.client.data.MeasureCapabilities
+
+/**
+ * Unregisters the given [MeasureCallback] for updates of the given [DeltaDataType].
+ *
+ * @param dataType the [DeltaDataType] that needs to be unregistered
+ * @param callback the [MeasureCallback] which was used in registration
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@Suppress("PairedRegistration")
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun MeasureClient.unregisterMeasureCallback(
+    dataType: DeltaDataType<*, *>,
+    callback: MeasureCallback
+) = unregisterMeasureCallbackAsync(dataType, callback).await()
+
+/**
+ * Returns the [MeasureCapabilities] of this client for the device.
+ *
+ * This can be used to determine what [DeltaDataType]s this device supports for live
+ * measurement. Clients should use the capabilities to inform their requests since Health
+ * Services will typically reject requests made for [DeltaDataType]s which are not enabled for
+ * measurement.
+ *
+ * @return a [MeasureCapabilities] for this device
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun MeasureClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveMonitoringClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveMonitoringClientExtension.kt
new file mode 100644
index 0000000..78973c6
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveMonitoringClientExtension.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.services.client
+
+import androidx.concurrent.futures.await
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.PassiveListenerConfig
+import androidx.health.services.client.data.PassiveMonitoringCapabilities
+
+/**
+ * Subscribes for updates to be periodically delivered to the app.
+ *
+ * Data updates will be batched and delivered from the point of initial registration and will
+ * continue to be delivered until the [DataType] is unregistered, either by explicitly calling
+ * [clearPassiveListenerService] or by registering again without that [DataType]
+ * included in the request. Higher frequency updates are available through [ExerciseClient] or
+ * [MeasureClient]. Any requested goal, user activity, or health event updates will not be
+ * batched.
+ *
+ * Health Services will automatically bind to the provided [PassiveListenerService] to send the
+ * update. Clients are responsible for defining the service in their app manifest. They should
+ * also require the `com.google.android.wearable.healthservices.permission.PASSIVE_DATA_BINDING`
+ * permission in their app manifest service definition in order to ensure that Health Services
+ * is the source of the binding.
+ *
+ * This registration is unique per subscribing app. Subsequent registrations will replace the
+ * previous registration, if one had been made. The client is responsible for ensuring that
+ * their requested [PassiveListenerConfig] is supported on this device by checking the
+ * [PassiveMonitoringCapabilities]. The returned future will fail if the request is not
+ * supported on the current device or the client does not have the required permissions for the
+ * request.
+ *
+ * @param service the [PassiveListenerService] to bind to
+ * @param config the [PassiveListenerConfig] from the client
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.setPassiveListenerService(
+    service: Class<out PassiveListenerService>,
+    config: PassiveListenerConfig
+) = setPassiveListenerServiceAsync(service, config).await()
+
+/**
+ * Unregisters the subscription made by [setPassiveListenerService].
+ *
+ * Data will not be delivered after this call so if clients care about any pending batched data
+ * they should call flush before unregistering.
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.clearPassiveListenerService() =
+    clearPassiveListenerServiceAsync().await()
+
+/**
+ * Unregisters the subscription made by [PassiveMonitoringClient.setPassiveListenerCallback].
+ *
+ * Data will not be delivered after this call so if clients care about any pending batched data
+ * they should call flush before unregistering.
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.clearPassiveListenerCallback() =
+    clearPassiveListenerCallbackAsync().await()
+
+/**
+ * Flushes the sensors for the registered [DataType]s.
+ *
+ * If no listener has been registered by this client, this will be a no-op. This call should be
+ * used sparingly and will be subject to throttling by Health Services.
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.flush() = flushAsync().await()
+
+/**
+ * Returns the [PassiveMonitoringCapabilities] of this client for this device.
+ *
+ * This can be used to determine what [DataType]s this device supports for passive monitoring
+ * and goals. Clients should use the capabilities to inform their requests since Health Services
+ * will typically reject requests made for [DataType]s which are not supported.
+ *
+ * @return a [PassiveMonitoringCapabilities] for this device
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
index b23d8bd..c2db7c4 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
@@ -38,6 +38,6 @@
 
     @Throws(RemoteException::class)
     override fun onFailure(message: String) {
-        resultFuture.setException(Exception(message))
+        resultFuture.setException(RemoteException(message))
     }
 }
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
index 874eec1..0d71261 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
@@ -39,6 +39,6 @@
     @Throws(RemoteException::class)
     @CallSuper
     override fun onFailure(msg: String) {
-        resultFuture.setException(Exception(msg))
+        resultFuture.setException(RemoteException(msg))
     }
 }
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
new file mode 100644
index 0000000..8d2e322
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
@@ -0,0 +1,881 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Looper
+import android.os.RemoteException
+import androidx.health.services.client.data.Availability
+import androidx.health.services.client.data.ComparisonType
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.DataTypeAvailability
+import androidx.health.services.client.data.DataTypeCondition
+import androidx.health.services.client.data.ExerciseCapabilities
+import androidx.health.services.client.data.ExerciseConfig
+import androidx.health.services.client.data.ExerciseGoal
+import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseLapSummary
+import androidx.health.services.client.data.ExerciseTrackedStatus
+import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseTypeCapabilities
+import androidx.health.services.client.data.ExerciseUpdate
+import androidx.health.services.client.data.WarmUpConfig
+import androidx.health.services.client.impl.IExerciseApiService
+import androidx.health.services.client.impl.IExerciseUpdateListener
+import androidx.health.services.client.impl.IpcConstants
+import androidx.health.services.client.impl.ServiceBackedExerciseClient
+import androidx.health.services.client.impl.event.ExerciseUpdateListenerEvent
+import androidx.health.services.client.impl.internal.IExerciseInfoCallback
+import androidx.health.services.client.impl.internal.IStatusCallback
+import androidx.health.services.client.impl.ipc.ClientConfiguration
+import androidx.health.services.client.impl.ipc.internal.ConnectionManager
+import androidx.health.services.client.impl.request.AutoPauseAndResumeConfigRequest
+import androidx.health.services.client.impl.request.CapabilitiesRequest
+import androidx.health.services.client.impl.request.ExerciseGoalRequest
+import androidx.health.services.client.impl.request.FlushRequest
+import androidx.health.services.client.impl.request.PrepareExerciseRequest
+import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.response.AvailabilityResponse
+import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse
+import androidx.health.services.client.impl.response.ExerciseInfoResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableSet
+import com.google.common.truth.Truth
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class ExerciseClientTest {
+
+    private lateinit var client: ServiceBackedExerciseClient
+    private lateinit var service: FakeServiceStub
+    private val callback = FakeExerciseUpdateCallback()
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun TestScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        client =
+            ServiceBackedExerciseClient(context, ConnectionManager(context, context.mainLooper))
+        service = FakeServiceStub()
+
+        val packageName = CLIENT_CONFIGURATION.servicePackageName
+        val action = CLIENT_CONFIGURATION.bindAction
+        Shadows.shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
+            Intent().setPackage(packageName).setAction(action),
+            ComponentName(packageName, CLIENT),
+            service
+        )
+    }
+
+    @After
+    fun tearDown() {
+        client.clearUpdateCallbackAsync(callback)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_prepareExerciseSynchronously() = runTest {
+        launch {
+            val warmUpConfig = WarmUpConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.prepareExercise(warmUpConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_prepareExerciseSynchronously_ThrowsException() =
+        runTest {
+            launch {
+                val warmUpConfig = WarmUpConfig(
+                    ExerciseType.WALKING,
+                    setOf(DataType.HEART_RATE_BPM),
+                )
+                var exception: Exception? = null
+                client.setUpdateCallback(callback)
+                // Mocking the calling app already has an active exercise in
+                // progress or if it does not have the required permissions
+                service.throwException = true
+
+                try {
+                    client.prepareExercise(warmUpConfig)
+                } catch (e: RemoteException) {
+                    exception = e
+                }
+
+                Truth.assertThat(exception).isNotNull()
+                Truth.assertThat(exception).isInstanceOf(RemoteException::class.java)
+            }
+            advanceMainLooperIdle()
+        }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_startExerciseSynchronously() = runTest {
+        launch {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.startExercise(exerciseConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justStatsType_startExerciseSynchronously() = runTest {
+        launch {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM_STATS),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                // Currently the proto form of HEART_RATE_BPM and HEART_RATE_BPM_STATS is identical.
+                // The APK doesn't know about _STATS, so pass the sample type to mimic that
+                // behavior.
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.startExercise(exerciseConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM_STATS, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_statsAndSample_startExerciseSynchronously() = runTest {
+        launch {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM, DataType.HEART_RATE_BPM_STATS),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                // Currently the proto form of HEART_RATE_BPM and HEART_RATE_BPM_STATS is identical.
+                // The APK doesn't know about _STATS, so pass the sample type to mimic that
+                // behavior.
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.startExercise(exerciseConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            // When both the sample type and stat type are requested, both should be notified
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM_STATS, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_pauseExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val pauseExercise = async {
+
+            client.pauseExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        pauseExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.PAUSED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_resumeExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val pauseExercise = async {
+
+            client.pauseExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        pauseExercise.await()
+        val resumeExercise = async {
+
+            client.resumeExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        resumeExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.PAUSED,
+            TestExerciseStates.RESUMED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_endExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val endExercise = async {
+
+            client.endExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        endExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.ENDED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_endPausedExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val pauseExercise = async {
+
+            client.pauseExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        pauseExercise.await()
+        val endExercise = async {
+
+            client.endExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        endExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.PAUSED,
+            TestExerciseStates.ENDED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun flushSynchronously() = runTest {
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val flushExercise = async {
+
+            client.flush()
+        }
+        advanceMainLooperIdle()
+        flushExercise.await()
+
+        Truth.assertThat(service.registerFlushRequests).hasSize(1)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun markLapSynchronously() = runTest {
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val markLap = async {
+
+            client.markLap()
+        }
+        advanceMainLooperIdle()
+        markLap.await()
+
+        Truth.assertThat(service.laps).isEqualTo(1)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCurrentExerciseInfoSynchronously() = runTest {
+        lateinit var exerciseInfo: ExerciseInfo
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val currentExerciseInfoDeferred = async {
+            exerciseInfo = client.getCurrentExerciseInfo()
+        }
+        advanceMainLooperIdle()
+        currentExerciseInfoDeferred.await()
+
+        Truth.assertThat(exerciseInfo.exerciseType).isEqualTo(ExerciseType.WALKING)
+        Truth.assertThat(exerciseInfo.exerciseTrackedStatus)
+            .isEqualTo(ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCurrentExerciseInfoSynchronously_cancelled() = runTest {
+        var isCancellationException = false
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val currentExerciseInfoDeferred = async {
+            client.getCurrentExerciseInfo()
+        }
+        val cancelDeferred = async {
+            currentExerciseInfoDeferred.cancel()
+        }
+        try {
+            currentExerciseInfoDeferred.await()
+        } catch (e: CancellationException) {
+            isCancellationException = true
+        }
+        cancelDeferred.await()
+
+        Truth.assertThat(isCancellationException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCurrentExerciseInfoSynchronously_exception() = runTest {
+        var isException = false
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val currentExerciseInfoDeferred = async {
+            service.throwException = true
+            try {
+                client.getCurrentExerciseInfo()
+            } catch (e: RemoteException) {
+                isException = true
+            }
+        }
+        advanceMainLooperIdle()
+        currentExerciseInfoDeferred.await()
+
+        Truth.assertThat(isException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun clearUpdateCallbackShouldBeInvoked() = runTest {
+        val statesList = mutableListOf<Boolean>()
+
+        client.setUpdateCallback(callback)
+        Shadows.shadowOf(Looper.getMainLooper()).idle()
+        statesList += (service.listener == null)
+        val deferred = async {
+
+            client.clearUpdateCallback(callback)
+            statesList += (service.listener == null)
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(statesList).containsExactly(false, true)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun addGoalToActiveExerciseShouldBeInvoked() = runTest {
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false,
+                exerciseGoals = listOf(
+                    ExerciseGoal.createOneTimeGoal(
+                        DataTypeCondition(
+                            DataType.DISTANCE_TOTAL, 50.0,
+                            ComparisonType.GREATER_THAN
+                        )
+                    ),
+                    ExerciseGoal.createOneTimeGoal(
+                        DataTypeCondition(
+                            DataType.DISTANCE_TOTAL, 150.0,
+                            ComparisonType.GREATER_THAN
+                        )
+                    ),
+                )
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val addGoalDeferred = async {
+            val proto = ExerciseGoal.createOneTimeGoal(
+                DataTypeCondition(DataType.HEART_RATE_BPM_STATS, 145.0, ComparisonType.GREATER_THAN)
+            ).proto
+            val goal = ExerciseGoal.fromProto(proto)
+
+            client.addGoalToActiveExercise(goal)
+        }
+        advanceMainLooperIdle()
+        addGoalDeferred.await()
+
+        Truth.assertThat(service.goals).hasSize(3)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun removeGoalFromActiveExerciseShouldBeInvoked() = runTest {
+        val goal1 = ExerciseGoal.createOneTimeGoal(
+            DataTypeCondition(
+                DataType.DISTANCE_TOTAL, 50.0,
+                ComparisonType.GREATER_THAN
+            )
+        )
+        val goal2 = ExerciseGoal.createOneTimeGoal(
+            DataTypeCondition(
+                DataType.DISTANCE_TOTAL, 150.0,
+                ComparisonType.GREATER_THAN
+            )
+        )
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false,
+                exerciseGoals = listOf(
+                    goal1,
+                    goal2,
+                )
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val removeGoalDeferred = async {
+            client.removeGoalFromActiveExercise(goal1)
+        }
+        advanceMainLooperIdle()
+        removeGoalDeferred.await()
+
+        Truth.assertThat(service.goals).hasSize(1)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously() = runTest {
+        lateinit var passiveMonitoringCapabilities: ExerciseCapabilities
+        val deferred = async {
+            passiveMonitoringCapabilities = client.getCapabilities()
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(service.registerGetCapabilitiesRequests).hasSize(1)
+        Truth.assertThat(passiveMonitoringCapabilities).isNotNull()
+        Truth.assertThat(service.getTestCapabilities().toString())
+            .isEqualTo(passiveMonitoringCapabilities.toString())
+    }
+
+    class FakeExerciseUpdateCallback : ExerciseUpdateCallback {
+        val availabilities = mutableMapOf<DataType<*, *>, Availability>()
+        val registrationFailureThrowables = mutableListOf<Throwable>()
+        var >
+        var >
+        var update: ExerciseUpdate? = null
+
+        override fun onRegistered() {
+            onRegisteredCalls++
+        }
+
+        override fun onRegistrationFailed(throwable: Throwable) {
+            onRegistrationFailedCalls++
+            registrationFailureThrowables.add(throwable)
+        }
+
+        override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
+            this@FakeExerciseUpdateCallback.update = update
+        }
+
+        override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {}
+
+        override fun onAvailabilityChanged(dataType: DataType<*, *>, availability: Availability) {
+            availabilities[dataType] = availability
+        }
+    }
+
+    class FakeServiceStub : IExerciseApiService.Stub() {
+
+        var listener: IExerciseUpdateListener? = null
+        val registerFlushRequests = mutableListOf<FlushRequest>()
+        var statusCallbackAction: (IStatusCallback?) -> Unit = { it!!.onSuccess() }
+        var testExerciseStates = TestExerciseStates.UNKNOWN
+        var laps = 0
+        var exerciseConfig: ExerciseConfig? = null
+        override fun getApiVersion(): Int = 12
+        val goals = mutableListOf<ExerciseGoal<*>>()
+        var throwException = false
+        val registerGetCapabilitiesRequests = mutableListOf<CapabilitiesRequest>()
+
+        override fun prepareExercise(
+            prepareExerciseRequest: PrepareExerciseRequest?,
+            statusCallback: IStatusCallback
+        ) {
+            if (throwException) {
+                statusCallback.onFailure("Remote Exception")
+            } else {
+                statusCallbackAction.invoke(statusCallback)
+            }
+        }
+
+        override fun startExercise(
+            startExerciseRequest: StartExerciseRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            exerciseConfig = startExerciseRequest?.exerciseConfig
+            exerciseConfig?.exerciseGoals?.let { goals.addAll(it) }
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.STARTED
+        }
+
+        override fun pauseExercise(packageName: String?, statusCallback: IStatusCallback?) {
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.PAUSED
+        }
+
+        override fun resumeExercise(packageName: String?, statusCallback: IStatusCallback?) {
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.RESUMED
+        }
+
+        override fun endExercise(packageName: String?, statusCallback: IStatusCallback?) {
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.ENDED
+        }
+
+        override fun markLap(packageName: String?, statusCallback: IStatusCallback?) {
+            laps++
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun getCurrentExerciseInfo(
+            packageName: String?,
+            exerciseInfoCallback: IExerciseInfoCallback?
+        ) {
+            if (throwException) {
+                exerciseInfoCallback?.onFailure("Remote Exception")
+            }
+            if (exerciseConfig == null) {
+                exerciseInfoCallback?.onExerciseInfo(
+                    ExerciseInfoResponse(
+                        ExerciseInfo(
+                            ExerciseTrackedStatus.UNKNOWN,
+                            ExerciseType.UNKNOWN
+                        )
+                    )
+                )
+            } else {
+                exerciseInfoCallback?.onExerciseInfo(
+                    ExerciseInfoResponse(
+                        ExerciseInfo(
+                            ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS,
+                            exerciseConfig!!.exerciseType
+                        )
+                    )
+                )
+            }
+        }
+
+        override fun setUpdateListener(
+            packageName: String?,
+            listener: IExerciseUpdateListener?,
+            statusCallback: IStatusCallback?
+        ) {
+            this.listener = listener
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun clearUpdateListener(
+            packageName: String?,
+            listener: IExerciseUpdateListener?,
+            statusCallback: IStatusCallback?
+        ) {
+            if (this.listener == listener)
+                this.listener = null
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun addGoalToActiveExercise(
+            request: ExerciseGoalRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            if (request != null) {
+                goals.add(request.exerciseGoal)
+            }
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun removeGoalFromActiveExercise(
+            request: ExerciseGoalRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            if (request != null) {
+                goals.remove(request.exerciseGoal)
+            }
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun overrideAutoPauseAndResumeForActiveExercise(
+            request: AutoPauseAndResumeConfigRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            throw NotImplementedError()
+        }
+
+        override fun getCapabilities(request: CapabilitiesRequest): ExerciseCapabilitiesResponse {
+            if (throwException) {
+                throw RemoteException("Remote Exception")
+            }
+            registerGetCapabilitiesRequests.add(request)
+            val capabilities = getTestCapabilities()
+            return ExerciseCapabilitiesResponse(capabilities)
+        }
+
+        fun getTestCapabilities(): ExerciseCapabilities {
+            val exerciseTypeToCapabilitiesMapping =
+                ImmutableMap.of(
+                    ExerciseType.WALKING, ExerciseTypeCapabilities( /* supportedDataTypes= */
+                        ImmutableSet.of(DataType.STEPS),
+                        ImmutableMap.of(
+                            DataType.STEPS_TOTAL,
+                            ImmutableSet.of(ComparisonType.GREATER_THAN)
+                        ),
+                        ImmutableMap.of(
+                            DataType.STEPS_TOTAL,
+                            ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+                        ), /* supportsAutoPauseAndResume= */
+                        false
+                    ),
+                    ExerciseType.RUNNING, ExerciseTypeCapabilities(
+                        ImmutableSet.of(DataType.HEART_RATE_BPM, DataType.SPEED),
+                        ImmutableMap.of(
+                            DataType.HEART_RATE_BPM_STATS,
+                            ImmutableSet.of(ComparisonType.GREATER_THAN, ComparisonType.LESS_THAN),
+                            DataType.SPEED_STATS,
+                            ImmutableSet.of(ComparisonType.LESS_THAN)
+                        ),
+                        ImmutableMap.of(
+                            DataType.HEART_RATE_BPM_STATS,
+                            ImmutableSet.of(ComparisonType.GREATER_THAN_OR_EQUAL),
+                            DataType.SPEED_STATS,
+                            ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+                        ), /* supportsAutoPauseAndResume= */
+                        true
+                    ),
+                    ExerciseType.SWIMMING_POOL, ExerciseTypeCapabilities( /* supportedDataTypes= */
+                        ImmutableSet.of(), /* supportedGoals= */
+                        ImmutableMap.of(), /* supportedMilestones= */
+                        ImmutableMap.of(), /* supportsAutoPauseAndResume= */
+                        true
+                    )
+                )
+
+            return ExerciseCapabilities(exerciseTypeToCapabilitiesMapping)
+        }
+
+        override fun flushExercise(request: FlushRequest, statusCallback: IStatusCallback?) {
+            registerFlushRequests += request
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        fun setException() {
+            throwException = true
+        }
+    }
+
+    enum class TestExerciseStates {
+        UNKNOWN,
+        PREPARED,
+        STARTED,
+        PAUSED,
+        RESUMED,
+        ENDED
+    }
+
+    internal companion object {
+        internal const val CLIENT = "HealthServicesExerciseClient"
+        internal val CLIENT_CONFIGURATION =
+            ClientConfiguration(
+                CLIENT,
+                IpcConstants.SERVICE_PACKAGE_NAME,
+                IpcConstants.EXERCISE_API_BIND_ACTION
+            )
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/MeasureClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/MeasureClientTest.kt
new file mode 100644
index 0000000..b15bbca
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/MeasureClientTest.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Looper
+import android.os.RemoteException
+import androidx.health.services.client.data.Availability
+import androidx.health.services.client.data.DataPointContainer
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.DeltaDataType
+import androidx.health.services.client.data.MeasureCapabilities
+import androidx.health.services.client.impl.IMeasureApiService
+import androidx.health.services.client.impl.IMeasureCallback
+import androidx.health.services.client.impl.IpcConstants
+import androidx.health.services.client.impl.ServiceBackedMeasureClient
+import androidx.health.services.client.impl.internal.IStatusCallback
+import androidx.health.services.client.impl.ipc.ClientConfiguration
+import androidx.health.services.client.impl.ipc.internal.ConnectionManager
+import androidx.health.services.client.impl.request.CapabilitiesRequest
+import androidx.health.services.client.impl.request.MeasureRegistrationRequest
+import androidx.health.services.client.impl.request.MeasureUnregistrationRequest
+import androidx.health.services.client.impl.response.MeasureCapabilitiesResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class MeasureClientTest {
+
+    private lateinit var callback: FakeCallback
+    private lateinit var client: ServiceBackedMeasureClient
+    private lateinit var service: FakeServiceStub
+    private var cleanup: Boolean = false
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun TestScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+    private fun CoroutineScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        callback = FakeCallback()
+        client =
+            ServiceBackedMeasureClient(context, ConnectionManager(context, context.mainLooper))
+        service = FakeServiceStub()
+
+        val packageName = CLIENT_CONFIGURATION.servicePackageName
+        val action = CLIENT_CONFIGURATION.bindAction
+        Shadows.shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
+            Intent().setPackage(packageName).setAction(action),
+            ComponentName(packageName, CLIENT),
+            service
+        )
+        cleanup = true
+    }
+
+    @After
+    fun tearDown() {
+        if (!cleanup)
+            return
+        runBlocking {
+            launch { client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback) }
+            advanceMainLooperIdle()
+        }
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun unregisterCallbackReachesServiceSynchronously() = runTest {
+        val deferred = async {
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            cleanup = false // Already unregistered
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(service.unregisterEvents).hasSize(1)
+        Truth.assertThat(service.unregisterEvents[0].request.dataType)
+            .isEqualTo(DataType.HEART_RATE_BPM)
+        Truth.assertThat(service.unregisterEvents[0].request.packageName)
+            .isEqualTo("androidx.health.services.client.test")
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun unregisterCallbackSynchronously_throwsIllegalArgumentException() = runTest {
+        var isExceptionCaught = false
+
+        val deferred = async {
+            try {
+                client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            } catch (e: IllegalArgumentException) {
+                isExceptionCaught = true
+            }
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+        cleanup = false // Not registered
+
+        Truth.assertThat(isExceptionCaught).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun capabilitiesReturnsCorrectValueSynchronously() = runTest {
+        lateinit var capabilities: MeasureCapabilities
+        val deferred = async {
+            service.supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            capabilities = client.getCapabilities()
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(capabilities.supportedDataTypesMeasure).hasSize(1)
+        Truth.assertThat(capabilities.supportedDataTypesMeasure)
+            .containsExactly(DataType.HEART_RATE_BPM)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun capabilitiesReturnsCorrectValue_cancelSuccessfully() = runTest {
+        var isCancellationException = false
+        val deferred = async {
+            service.supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            client.getCapabilities()
+        }
+        val cancelCoroutine = async { deferred.cancel() }
+        try {
+            deferred.await()
+        } catch (e: CancellationException) {
+            isCancellationException = true
+        }
+        cancelCoroutine.await()
+
+        Truth.assertThat(isCancellationException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun capabilitiesReturnsCorrectValue_catchException() = runTest {
+        var isRemoteException = false
+        val deferred = async {
+            service.supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            service.setException()
+            try {
+                client.getCapabilities()
+            } catch (e: RemoteException) {
+                isRemoteException = true
+            }
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(isRemoteException).isTrue()
+    }
+
+    class FakeCallback : MeasureCallback {
+        data class AvailabilityChangeEvent(
+            val dataType: DataType<*, *>,
+            val availability: Availability
+        )
+
+        data class DataReceivedEvent(val data: DataPointContainer)
+
+        val availabilityChangeEvents = mutableListOf<AvailabilityChangeEvent>()
+        val dataReceivedEvents = mutableListOf<DataReceivedEvent>()
+        var >
+        var registrationFailureThrowables = mutableListOf<Throwable>()
+
+        override fun onRegistered() {
+            onRegisteredInvocationCount++
+        }
+
+        override fun onRegistrationFailed(throwable: Throwable) {
+            registrationFailureThrowables += throwable
+        }
+
+        override fun onAvailabilityChanged(
+            dataType: DeltaDataType<*, *>,
+            availability: Availability
+        ) {
+            availabilityChangeEvents += AvailabilityChangeEvent(dataType, availability)
+        }
+
+        override fun onDataReceived(data: DataPointContainer) {
+            dataReceivedEvents += DataReceivedEvent(data)
+        }
+    }
+
+    class FakeServiceStub : IMeasureApiService.Stub() {
+        private var throwExcepotion = false
+
+        class RegisterEvent(
+            val request: MeasureRegistrationRequest,
+            val callback: IMeasureCallback,
+            val statusCallback: IStatusCallback
+        )
+
+        class UnregisterEvent(
+            val request: MeasureUnregistrationRequest,
+            val callback: IMeasureCallback,
+            val statusCallback: IStatusCallback
+        )
+
+        var statusCallbackAction: (IStatusCallback) -> Unit = { it.onSuccess() }
+        var supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+
+        val registerEvents = mutableListOf<RegisterEvent>()
+        val unregisterEvents = mutableListOf<UnregisterEvent>()
+
+        override fun getApiVersion() = 42
+
+        override fun registerCallback(
+            request: MeasureRegistrationRequest,
+            callback: IMeasureCallback,
+            statusCallback: IStatusCallback
+        ) {
+            registerEvents += RegisterEvent(request, callback, statusCallback)
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun unregisterCallback(
+            request: MeasureUnregistrationRequest,
+            callback: IMeasureCallback,
+            statusCallback: IStatusCallback
+        ) {
+            unregisterEvents += UnregisterEvent(request, callback, statusCallback)
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun getCapabilities(request: CapabilitiesRequest): MeasureCapabilitiesResponse {
+            if (throwExcepotion) {
+                throw RemoteException("Remote Exception")
+            }
+            return MeasureCapabilitiesResponse(MeasureCapabilities(supportedDataTypes))
+        }
+
+        fun setException() {
+            throwExcepotion = true
+        }
+    }
+
+    internal companion object {
+        internal const val CLIENT = "HealthServicesMeasureClient"
+        internal val CLIENT_CONFIGURATION =
+            ClientConfiguration(
+                CLIENT,
+                IpcConstants.SERVICE_PACKAGE_NAME,
+                IpcConstants.MEASURE_API_BIND_ACTION
+            )
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/PassiveMonitoringClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveMonitoringClientTest.kt
new file mode 100644
index 0000000..1a4a39d
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveMonitoringClientTest.kt
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Looper
+import android.os.RemoteException
+import androidx.health.services.client.data.DataPointContainer
+import androidx.health.services.client.data.DataType.Companion.CALORIES_DAILY
+import androidx.health.services.client.data.DataType.Companion.CALORIES_TOTAL
+import androidx.health.services.client.data.DataType.Companion.DISTANCE
+import androidx.health.services.client.data.DataType.Companion.STEPS
+import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
+import androidx.health.services.client.data.HealthEvent
+import androidx.health.services.client.data.PassiveGoal
+import androidx.health.services.client.data.PassiveListenerConfig
+import androidx.health.services.client.data.PassiveMonitoringCapabilities
+import androidx.health.services.client.data.UserActivityInfo
+import androidx.health.services.client.data.UserActivityState
+import androidx.health.services.client.impl.IPassiveListenerCallback
+import androidx.health.services.client.impl.IPassiveMonitoringApiService
+import androidx.health.services.client.impl.ServiceBackedPassiveMonitoringClient
+import androidx.health.services.client.impl.internal.IStatusCallback
+import androidx.health.services.client.impl.ipc.internal.ConnectionManager
+import androidx.health.services.client.impl.request.CapabilitiesRequest
+import androidx.health.services.client.impl.request.FlushRequest
+import androidx.health.services.client.impl.request.PassiveListenerCallbackRegistrationRequest
+import androidx.health.services.client.impl.request.PassiveListenerServiceRegistrationRequest
+import androidx.health.services.client.impl.response.PassiveMonitoringCapabilitiesResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class PassiveMonitoringClientTest {
+
+    private lateinit var client: PassiveMonitoringClient
+    private lateinit var service: FakeServiceStub
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun TestScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    private fun CoroutineScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        client = ServiceBackedPassiveMonitoringClient(
+            context, ConnectionManager(context, context.mainLooper)
+        )
+        service = FakeServiceStub()
+
+        val packageName =
+            ServiceBackedPassiveMonitoringClient.CLIENT_CONFIGURATION.servicePackageName
+        val action = ServiceBackedPassiveMonitoringClient.CLIENT_CONFIGURATION.bindAction
+        Shadows.shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
+            Intent().setPackage(packageName).setAction(action),
+            ComponentName(packageName, ServiceBackedPassiveMonitoringClient.CLIENT),
+            service
+        )
+    }
+
+    @After
+    fun tearDown() {
+        runBlocking {
+            launch { client.clearPassiveListenerCallback() }
+            advanceMainLooperIdle()
+            launch { client.clearPassiveListenerService() }
+            advanceMainLooperIdle()
+        }
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun registersPassiveListenerServiceSynchronously() = runTest {
+        launch {
+            val config = PassiveListenerConfig(
+                dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+                shouldUserActivityInfoBeRequested = true,
+                dailyGoals = setOf(),
+                healthEventTypes = setOf()
+            )
+
+            client.setPassiveListenerService(FakeListenerService::class.java, config)
+            val request = service.registerServiceRequests[0]
+
+            Truth.assertThat(service.registerServiceRequests).hasSize(1)
+            Truth.assertThat(request.passiveListenerConfig.dataTypes).containsExactly(
+                STEPS_DAILY, CALORIES_DAILY
+            )
+            Truth.assertThat(request.passiveListenerConfig.shouldUserActivityInfoBeRequested)
+                .isTrue()
+            Truth.assertThat(request.packageName).isEqualTo("androidx.health.services.client.test")
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun flushSynchronously() = runTest {
+        launch {
+            val config = PassiveListenerConfig(
+                dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+                shouldUserActivityInfoBeRequested = true,
+                dailyGoals = setOf(),
+                healthEventTypes = setOf()
+            )
+            val callback = FakeCallback()
+            client.setPassiveListenerCallback(config, callback)
+
+            client.flush()
+
+            Truth.assertThat(service.registerFlushRequests).hasSize(1)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously() = runTest {
+        launch {
+            val config = PassiveListenerConfig(
+                dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+                shouldUserActivityInfoBeRequested = true,
+                dailyGoals = setOf(),
+                healthEventTypes = setOf()
+            )
+            val callback = FakeCallback()
+            client.setPassiveListenerCallback(config, callback)
+
+            val passiveMonitoringCapabilities = client.getCapabilities()
+
+            Truth.assertThat(service.registerGetCapabilitiesRequests).hasSize(1)
+            Truth.assertThat(passiveMonitoringCapabilities).isNotNull()
+            Truth.assertThat(service.getTestCapabilities().toString())
+                .isEqualTo(passiveMonitoringCapabilities.toString())
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously_cancelled() = runTest {
+        val config = PassiveListenerConfig(
+            dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+            shouldUserActivityInfoBeRequested = true,
+            dailyGoals = setOf(),
+            healthEventTypes = setOf()
+        )
+        val callback = FakeCallback()
+        client.setPassiveListenerCallback(config, callback)
+        var isCancellationException = false
+
+        val deferred = async {
+            client.getCapabilities()
+        }
+        val cancellationDeferred = async {
+            deferred.cancel(CancellationException())
+        }
+        try {
+            deferred.await()
+        } catch (e: CancellationException) {
+            isCancellationException = true
+        }
+        cancellationDeferred.await()
+
+        Truth.assertThat(isCancellationException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously_Exception() = runTest {
+        val config = PassiveListenerConfig(
+            dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+            shouldUserActivityInfoBeRequested = true,
+            dailyGoals = setOf(),
+            healthEventTypes = setOf()
+        )
+        val callback = FakeCallback()
+        client.setPassiveListenerCallback(config, callback)
+        var isExceptionCaught = false
+        val deferred = async {
+            service.setException()
+            try {
+
+                client.getCapabilities()
+            } catch (e: RemoteException) {
+                isExceptionCaught = true
+            }
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(isExceptionCaught).isTrue()
+    }
+
+    class FakeListenerService : PassiveListenerService()
+
+    internal class FakeCallback : PassiveListenerCallback {
+        private var >
+        private val >
+        private val dataPointsReceived = mutableListOf<DataPointContainer>()
+        private val userActivityInfosReceived = mutableListOf<UserActivityInfo>()
+        private val completedGoals = mutableListOf<PassiveGoal>()
+        private val healthEventsReceived = mutableListOf<HealthEvent>()
+        var >
+
+        override fun onRegistered() {
+            onRegisteredCalls++
+        }
+
+        override fun onRegistrationFailed(throwable: Throwable) {
+            onRegistrationFailedThrowables += throwable
+        }
+
+        override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
+            dataPointsReceived += dataPoints
+        }
+
+        override fun onUserActivityInfoReceived(info: UserActivityInfo) {
+            userActivityInfosReceived += info
+        }
+
+        override fun onGoalCompleted(goal: PassiveGoal) {
+            completedGoals += goal
+        }
+
+        override fun onHealthEventReceived(event: HealthEvent) {
+            healthEventsReceived += event
+        }
+
+        override fun onPermissionLost() {
+            onPermissionLostCalls++
+        }
+    }
+
+    internal class FakeServiceStub : IPassiveMonitoringApiService.Stub() {
+        @JvmField
+        var apiVersion = 42
+
+        private var statusCallbackAction: (IStatusCallback?) -> Unit = { it!!.onSuccess() }
+        val registerServiceRequests = mutableListOf<PassiveListenerServiceRegistrationRequest>()
+        private val registerCallbackRequests =
+            mutableListOf<PassiveListenerCallbackRegistrationRequest>()
+        val registerFlushRequests = mutableListOf<FlushRequest>()
+        val registerGetCapabilitiesRequests = mutableListOf<CapabilitiesRequest>()
+        private val registeredCallbacks = mutableListOf<IPassiveListenerCallback>()
+        private val unregisterServicePackageNames = mutableListOf<String>()
+        private val unregisterCallbackPackageNames = mutableListOf<String>()
+        private var throwExcepotion = false
+
+        override fun getApiVersion() = 42
+
+        override fun getCapabilities(
+            request: CapabilitiesRequest
+        ): PassiveMonitoringCapabilitiesResponse {
+            if (throwExcepotion) {
+                throw RemoteException("Remote Exception")
+            }
+            registerGetCapabilitiesRequests.add(request)
+            val capabilities = getTestCapabilities()
+            return PassiveMonitoringCapabilitiesResponse(capabilities)
+        }
+
+        override fun flush(request: FlushRequest, statusCallback: IStatusCallback?) {
+            registerFlushRequests.add(request)
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun registerPassiveListenerService(
+            request: PassiveListenerServiceRegistrationRequest,
+            statusCallback: IStatusCallback
+        ) {
+            registerServiceRequests += request
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun registerPassiveListenerCallback(
+            request: PassiveListenerCallbackRegistrationRequest,
+            callback: IPassiveListenerCallback,
+            statusCallback: IStatusCallback
+        ) {
+            registerCallbackRequests += request
+            registeredCallbacks += callback
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun unregisterPassiveListenerService(
+            packageName: String,
+            statusCallback: IStatusCallback
+        ) {
+            unregisterServicePackageNames += packageName
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun unregisterPassiveListenerCallback(
+            packageName: String,
+            statusCallback: IStatusCallback
+        ) {
+            unregisterCallbackPackageNames += packageName
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        fun getTestCapabilities(): PassiveMonitoringCapabilities {
+            return PassiveMonitoringCapabilities(
+                supportedDataTypesPassiveMonitoring = setOf(STEPS, DISTANCE),
+                supportedDataTypesPassiveGoals = setOf(CALORIES_TOTAL),
+                supportedHealthEventTypes = setOf(HealthEvent.Type.FALL_DETECTED),
+                supportedUserActivityStates = setOf(UserActivityState.USER_ACTIVITY_PASSIVE)
+            )
+        }
+
+        fun setException() {
+            throwExcepotion = true
+        }
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt
index a86c098..f42c82c 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt
@@ -76,6 +76,7 @@
     @After
     fun tearDown() {
         client.unregisterMeasureCallbackAsync(HEART_RATE_BPM, callback)
+        shadowOf(Looper.getMainLooper()).idle()
     }
 
     @Test