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