Health: Ensure unregistering non-registered callback is a no-op.

This brings the implemention in line with the documentation and
intention.

Bug: 318429136
Bug: 318430531
Test: Unit tests passing.
Change-Id: I0f24916aa7af16d0bfb6ff5ab9e45a8c214aaf41
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
index 8016126..3cd2656 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
@@ -171,14 +171,14 @@
             executor)
     }
 
+    @Suppress("UNCHECKED_CAST")
     override fun clearUpdateCallbackAsync(
         callback: ExerciseUpdateCallback
     ): ListenableFuture<Void> {
+        // Cast is unfortunately required as there is no non-null Void in Kotlin.
         val listenerStub =
             ExerciseUpdateListenerStub.ExerciseUpdateListenerCache.INSTANCE.remove(callback)
-                ?: return Futures.immediateFailedFuture(
-                    IllegalArgumentException("Given listener was not added.")
-                )
+                ?: return Futures.immediateFuture(null) as ListenableFuture<Void>
         return unregisterListener(listenerStub.listenerKey) { service, resultFuture ->
             service.clearUpdateListener(packageName, listenerStub, StatusCallback(resultFuture))
         }
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt
index 13bf8b2..a9fd73c 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedMeasureClient.kt
@@ -89,15 +89,15 @@
             executor)
     }
 
+    @Suppress("UNCHECKED_CAST")
     override fun unregisterMeasureCallbackAsync(
         dataType: DeltaDataType<*, *>,
         callback: MeasureCallback
     ): ListenableFuture<Void> {
+        // Cast is unfortunately required as there is no non-null Void in Kotlin.
         val callbackStub =
             MeasureCallbackCache.INSTANCE.remove(dataType, callback)
-                ?: return Futures.immediateFailedFuture(
-                    IllegalArgumentException("Given callback was not registered.")
-                )
+                ?: return Futures.immediateFuture(null) as ListenableFuture<Void>
         val request = MeasureUnregistrationRequest(context.packageName, dataType)
         return unregisterListener(callbackStub.listenerKey) { service, resultFuture ->
             service.unregisterCallback(request, callbackStub, StatusCallback(resultFuture))
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
index f463bc8..a6950d4 100644
--- 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
@@ -590,7 +590,6 @@
         Shadows.shadowOf(Looper.getMainLooper()).idle()
         statesList += (service.listener == null)
         val deferred = async {
-
             client.clearUpdateCallback(callback)
             statesList += (service.listener == null)
         }
@@ -602,6 +601,17 @@
 
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
     @Test
+    fun clearUpdateCallback_nothingRegistered_noOp() = runTest {
+        val deferred = async {
+            client.clearUpdateCallback(callback)
+        }
+        advanceMainLooperIdle()
+
+        Truth.assertThat(deferred.await()).isNull()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
     fun addGoalToActiveExerciseShouldBeInvoked() = runTest {
         val startExercise = async {
             val exerciseConfig = ExerciseConfig(
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
index 3b7411d..58b1b20 100644
--- 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
@@ -114,21 +114,13 @@
 
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
     @Test
-    fun unregisterCallbackSynchronously_throwsIllegalArgumentException() = runTest {
-        var isExceptionCaught = false
-
+    fun unregisterCallbackSynchronously_callbackNotRegistered_success() = runTest {
         val deferred = async {
-            try {
-                client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
-            } catch (e: IllegalArgumentException) {
-                isExceptionCaught = true
-            }
+            client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
         }
         advanceMainLooperIdle()
-        deferred.await()
-        cleanup = false // Not registered
 
-        Truth.assertThat(isExceptionCaught).isTrue()
+        Truth.assertThat(deferred.await()).isNull()
     }
 
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
index d0afedb..5a483e59 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
@@ -114,6 +114,14 @@
     }
 
     @Test
+    fun clearUpdateCallbackAsync_callbackNotRegistered_noOp() {
+        val resultFuture = client.clearUpdateCallbackAsync(callback)
+        shadowOf(getMainLooper()).idle()
+
+        assertThat(resultFuture.get()).isNull()
+    }
+
+    @Test
     fun dataTypeInAvailabilityCallbackShouldMatchRequested_justSampleType_startExercise() {
         val exerciseConfig = ExerciseConfig(
             ExerciseType.WALKING,
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 a8a7128..6d996a9 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
@@ -115,6 +115,15 @@
     }
 
     @Test
+        fun unregisterCallback_callbackNotRegistered_noOp() {
+            val resultFuture = client.unregisterMeasureCallbackAsync(HEART_RATE_BPM, callback)
+            shadowOf(Looper.getMainLooper()).idle()
+
+            assertThat(fakeService.unregisterEvents).isEmpty()
+            assertThat(resultFuture.get()).isNull()
+        }
+
+    @Test
     fun dataPointsReachAppCallback() {
         val event = MeasureCallbackEvent.createDataPointsUpdateEvent(
             DataPointsResponse(