Merge "Experimental Deduplication read API" into androidx-main
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/ExperimentalDeduplicationApi.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/ExperimentalDeduplicationApi.kt
new file mode 100644
index 0000000..6dbb08b
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/ExperimentalDeduplicationApi.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 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.connect.client
+
+import androidx.annotation.RestrictTo
+
+@RequiresOptIn(
+ message = "This is an experimental HC Dedupe-read API and is likely to change in the future."
+)
+@Retention(AnnotationRetention.BINARY)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public annotation class ExperimentalDeduplicationApi
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index 75be7bf..5127a59 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -20,6 +20,7 @@
import android.os.RemoteException
import android.os.TransactionTooLargeException
import androidx.annotation.VisibleForTesting
+import androidx.health.connect.client.ExperimentalDeduplicationApi
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.HealthConnectClient.Companion.HEALTH_CONNECT_CLIENT_TAG
import androidx.health.connect.client.HealthConnectFeatures
@@ -50,6 +51,7 @@
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.request.ChangesTokenRequest
import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.request.ReadRecordsRequest.Companion.DEDUPLICATION_STRATEGY_DISABLED
import androidx.health.connect.client.response.ChangesResponse
import androidx.health.connect.client.response.InsertRecordsResponse
import androidx.health.connect.client.response.ReadRecordResponse
@@ -214,9 +216,13 @@
return toChangesResponse(proto)
}
+ @OptIn(ExperimentalDeduplicationApi::class)
override suspend fun <T : Record> readRecords(
request: ReadRecordsRequest<T>,
): ReadRecordsResponse<T> {
+ if (request.deduplicateStrategy != DEDUPLICATION_STRATEGY_DISABLED) {
+ TODO("Not yet implemented")
+ }
val proto = wrapRemoteException {
delegate.readDataRange(toReadDataRangeRequestProto(request)).await()
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
index bbe062e..13cbe90 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
@@ -31,6 +31,7 @@
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.os.asOutcomeReceiver
+import androidx.health.connect.client.ExperimentalDeduplicationApi
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.PermissionController
@@ -60,6 +61,7 @@
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.request.ChangesTokenRequest
import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.request.ReadRecordsRequest.Companion.DEDUPLICATION_STRATEGY_DISABLED
import androidx.health.connect.client.response.ChangesResponse
import androidx.health.connect.client.response.InsertRecordsResponse
import androidx.health.connect.client.response.ReadRecordResponse
@@ -190,10 +192,14 @@
return ReadRecordResponse(response.records[0].toSdkRecord() as T)
}
+ @OptIn(ExperimentalDeduplicationApi::class)
@Suppress("UNCHECKED_CAST") // Safe to cast as the type should match
override suspend fun <T : Record> readRecords(
request: ReadRecordsRequest<T>
): ReadRecordsResponse<T> {
+ if (request.deduplicateStrategy != DEDUPLICATION_STRATEGY_DISABLED) {
+ TODO("Not yet implemented")
+ }
val response = wrapPlatformException {
suspendCancellableCoroutine { continuation ->
healthConnectManager.readRecords(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
index c52e70d..b1e84d8 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
@@ -15,7 +15,9 @@
*/
package androidx.health.connect.client.request
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
+import androidx.health.connect.client.ExperimentalDeduplicationApi
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.metadata.DataOrigin
import androidx.health.connect.client.time.TimeRangeFilter
@@ -47,9 +49,9 @@
* filters.
*
* Returned collection will contain a
- * [androidx.health.data.client.response.ReadRecordsResponse.pageToken] if number of records exceeds
- * [pageSize]. Use this if you expect an unbound number of records within specified time ranges.
- * Stops at any time once desired amount of records are processed.
+ * [androidx.health.connect.client.response.ReadRecordsResponse.pageToken] if number of records
+ * exceeds [pageSize]. Use this if you expect an unbound number of records within specified time
+ * ranges. Stops at any time once desired amount of records are processed.
*
* @param T type of [Record], such as `Steps`.
* @param recordType Which type of [Record] to read, such as `Steps::class`.
@@ -65,7 +67,10 @@
* @see androidx.health.connect.client.response.ReadRecordsResponse
* @see androidx.health.connect.client.HealthConnectClient.readRecords
*/
-public class ReadRecordsRequest<T : Record>(
+public class ReadRecordsRequest<T : Record>
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@ExperimentalDeduplicationApi
+constructor(
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val recordType: KClass<T>,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val timeRangeFilter: TimeRangeFilter,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -73,7 +78,28 @@
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val ascendingOrder: Boolean = true,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val pageSize: Int = 1000,
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val pageToken: String? = null,
+ @DeduplicationStrategy
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ val deduplicateStrategy: Int = DEDUPLICATION_STRATEGY_ENABLED_DEFAULT,
) {
+ @OptIn(ExperimentalDeduplicationApi::class)
+ constructor(
+ recordType: KClass<T>,
+ timeRangeFilter: TimeRangeFilter,
+ dataOriginFilter: Set<DataOrigin> = emptySet(),
+ ascendingOrder: Boolean = true,
+ pageSize: Int = 1000,
+ pageToken: String? = null
+ ) : this(
+ recordType = recordType,
+ timeRangeFilter = timeRangeFilter,
+ dataOriginFilter = dataOriginFilter,
+ ascendingOrder = ascendingOrder,
+ pageSize = pageSize,
+ pageToken = pageToken,
+ deduplicateStrategy = DEDUPLICATION_STRATEGY_DISABLED,
+ )
+
init {
require(pageSize > 0) { "pageSize must be positive." }
}
@@ -90,6 +116,7 @@
if (ascendingOrder != other.ascendingOrder) return false
if (pageSize != other.pageSize) return false
if (pageToken != other.pageToken) return false
+ if (deduplicateStrategy != other.deduplicateStrategy) return false
return true
}
@@ -101,6 +128,59 @@
result = 31 * result + ascendingOrder.hashCode()
result = 31 * result + pageSize
result = 31 * result + (pageToken?.hashCode() ?: 0)
+ result = 31 * result + (deduplicateStrategy.hashCode())
return result
}
+
+ /**
+ * Available strategies used to handle duplicate records in the
+ * [androidx.health.connect.client.response.ReadRecordsResponse.records].
+ */
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ value =
+ [
+ DEDUPLICATION_STRATEGY_DISABLED,
+ DEDUPLICATION_STRATEGY_ENABLED_DEFAULT,
+ DEDUPLICATION_STRATEGY_ENABLED_PRIORITIZE_CALLING_APP,
+ ]
+ )
+ @OptIn(ExperimentalDeduplicationApi::class)
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ annotation class DeduplicationStrategy
+
+ @ExperimentalDeduplicationApi
+ internal companion object {
+ /**
+ * No deduplication handled. Returns all raw data.
+ *
+ * This is the default option.
+ */
+ @ExperimentalDeduplicationApi
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val DEDUPLICATION_STRATEGY_DISABLED = 0
+
+ /**
+ * Uses the default deduplication strategy recommended by Health Connect. This may change
+ * over time, it's not guaranteed the strategy remains the same over different updates.
+ *
+ * <p>Currently this is {@code DEDUPLICATION_STRATEGY_ENABLED_DEDUPE_ALL}. To stick to a
+ * specified strategy over updates, set the desired strategy directly.
+ */
+ @ExperimentalDeduplicationApi
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val DEDUPLICATION_STRATEGY_ENABLED_DEFAULT = 1
+
+ /**
+ * Strips all duplicate records in the database and returns a fully deduplicated list
+ * available via {@link ReadRecordsResponse#getRecords}. If duplications are detected, the
+ * record belongs to the calling app will be the winner.
+ *
+ * <p>Only records of session types like {@link ExerciseSessionRecord} and {@link
+ * SleepSessionRecord} are affected. It's no-op for other record types.
+ */
+ @ExperimentalDeduplicationApi
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ const val DEDUPLICATION_STRATEGY_ENABLED_PRIORITIZE_CALLING_APP = 2
+ }
}
diff --git a/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt
index 5c7fab4..614d56b 100644
--- a/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt
+++ b/health/connect/connect-testing/src/main/java/androidx/health/connect/client/testing/FakeHealthConnectClient.kt
@@ -255,6 +255,9 @@
public override suspend fun <T : Record> readRecords(
request: ReadRecordsRequest<T>
): ReadRecordsResponse<T> {
+ if (request.deduplicateStrategy != DEDUPLICATION_STRATEGY_DISABLED) {
+ TODO("Not yet implemented")
+ }
// Stubs
overrides.readRecords?.throwOrContinue(null)
@@ -439,6 +442,14 @@
public companion object {
/** Default package name used in [FakeHealthConnectClient]. */
public const val DEFAULT_PACKAGE_NAME: String = "androidx.health.connect.test"
+
+ /**
+ * Default dedupe strategy constant. This is a workaround to bypass b/358308051
+ *
+ * It should be removed (and reference the one in [ReadRecordsRequest.Companion]) when the
+ * dedupe API is ready to publish.
+ */
+ private const val DEDUPLICATION_STRATEGY_DISABLED = 0
}
}