?>>,
+) {
+ constructor(
+ name: String,
+ creator: Parcelable.Creator,
+ ) : this(
+ metadataKey = ParcelableUtils.metadataKey(getStringKey(name), creator),
+ contextKey = Context.key("REQ-${getStringKey(name)}"),
+ responseHeaderContextKey = Context.key("RESH-${getStringKey(name)}"),
+ responseTrailerContextKey = Context.key("REST-${getStringKey(name)}"),
+ )
+
+ companion object {
+ /**
+ * Returns a string key for the parcelable. All metadata and context keys should be derived from
+ * this string key.
+ *
+ * NEVER CHANGE this key format, it is used by clients and servers to identify the parcelable.
+ */
+ private fun getStringKey(name: String) = "$name-bin"
+ }
+}
+
+/** Key for sending/receiving a single parcelable over gRPC. */
+class SingleParcelableKey
(val name: String, val creator: Parcelable.Creator
) :
+ ParcelableKey
(name, creator) {}
+
+/**
+ * Key for sending/receiving list of parcelables (repeated values) over gRPC.
+ *
+ * NOTE: Under the hood, both SimpleParcelableKey and RepeatedParcelableKey have the same
+ * implementation, since gRPC metadata transport natively supports repeated values for the same key,
+ * we just offer two separate types for easier use by clients.
+ */
+class RepeatedParcelableKey
(val name: String, val creator: Parcelable.Creator
) :
+ ParcelableKey
(name, creator) {}
diff --git a/src/com/google/android/as/oss/delegatedui/utils/ParcelableOverMetadataServerInterceptor.kt b/src/com/google/android/as/oss/delegatedui/utils/ParcelableOverMetadataServerInterceptor.kt
new file mode 100644
index 00000000..fba3d2a2
--- /dev/null
+++ b/src/com/google/android/as/oss/delegatedui/utils/ParcelableOverMetadataServerInterceptor.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.delegatedui.utils
+
+import android.os.Parcelable
+import io.grpc.Context
+import io.grpc.Contexts
+import io.grpc.ForwardingServerCall
+import io.grpc.Metadata
+import io.grpc.ServerCall
+import io.grpc.ServerCallHandler
+import io.grpc.ServerInterceptor
+import io.grpc.Status
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * Grpc server interceptor that copies the request payload provided in the metadata to grpc context
+ * and does the same for response payload using response headers.
+ */
+class ParcelableOverMetadataServerInterceptor(private vararg val parcelableKeys: ParcelableKey<*>) :
+ ServerInterceptor {
+ override fun interceptCall(
+ call: ServerCall,
+ requestHeaders: Metadata,
+ next: ServerCallHandler,
+ ): ServerCall.Listener {
+ var context = Context.current().withMetadataPayload(requestHeaders)
+ context = context.initializeResponseKeys()
+ return Contexts.interceptCall(
+ context,
+ object : ForwardingServerCall.SimpleForwardingServerCall(call) {
+ private val sendHeadersHasBeenCalled: AtomicBoolean = AtomicBoolean(false)
+
+ override fun sendHeaders(responseHeaders: Metadata) {
+ responseHeaders.putContextPayload(context, headers = true)
+ sendHeadersHasBeenCalled.set(true)
+
+ super.sendHeaders(responseHeaders)
+ }
+
+ override fun close(status: Status, responseTrailers: Metadata) {
+ // Check for engineering mistake of attaching headers after response has been sent.
+ if (sendHeadersHasBeenCalled.get() && context.containsResponseHeadersPayload()) {
+ val statusOverride: Status =
+ Status.INTERNAL.withDescription(
+ "Parcelable response headers can be populated only before the first response."
+ )
+ super.close(statusOverride, responseTrailers)
+ } else {
+ responseTrailers.putContextPayload(context, headers = false)
+ super.close(status, responseTrailers)
+ }
+ }
+ },
+ requestHeaders,
+ next,
+ )
+ }
+
+ /**
+ * For each of provided [parcelableKeys] copies metadata payload into grpc context with defined
+ * request key (if metadata object is provided) and initializes holder (atomic reference) in the
+ * context with response key for the possible response payload.
+ */
+ private fun Context.withMetadataPayload(metadata: Metadata): Context {
+ var context = this
+ for (key in parcelableKeys) context = context.withMetadataPayload(metadata, key)
+ return context
+ }
+
+ private fun Context.withMetadataPayload(
+ metadata: Metadata,
+ key: ParcelableKey
,
+ ): Context {
+ var context = this
+ val valuesList = metadata.getAll(key.metadataKey)?.map { it as P } ?: emptyList()
+ if (valuesList.isNotEmpty()) {
+ context = context.withValue(key.contextKey, valuesList)
+ }
+ return context
+ }
+
+ /**
+ * For each of provided [parcelableKeys] initializes holder (atomic reference) in the context with
+ * response key for the possible response payload.
+ */
+ private fun Context.initializeResponseKeys(): Context {
+ var context = this
+ for (key in parcelableKeys) context = context.initializeResponseKey(key)
+ return context
+ }
+
+ private fun
Context.initializeResponseKey(key: ParcelableKey
): Context =
+ withValue(key.responseHeaderContextKey, AtomicReference?>())
+ .withValue(key.responseTrailerContextKey, AtomicReference?>())
+
+ /**
+ * For each of provided [parcelableKeys] copies payload (if populated) from grpc context using
+ * response key into response metadata (headers).
+ */
+ private fun Metadata.putContextPayload(context: Context, headers: Boolean) {
+ for (key in parcelableKeys) putContextPayload(context, key, headers)
+ }
+
+ private fun Metadata.putContextPayload(
+ context: Context,
+ key: ParcelableKey
,
+ headers: Boolean,
+ ) {
+ val responseKey: Context.Key?>> =
+ if (headers) key.responseHeaderContextKey else key.responseTrailerContextKey
+ val values: List? = responseKey.get(context).getAndSet(null)
+ if (values != null) {
+ for (item in values) {
+ put(key.metadataKey, item)
+ }
+ }
+ }
+
+ /** Sanity check for response headers being populated after response message has been sent. */
+ private fun Context.containsResponseHeadersPayload(): Boolean {
+ for (key in parcelableKeys) {
+ if (key.responseHeaderContextKey.get(/* context= */ this).get() != null) {
+ return true
+ }
+ }
+
+ return false
+ }
+}
diff --git a/src/com/google/android/as/oss/delegatedui/utils/ParcelableOverRpcUtils.kt b/src/com/google/android/as/oss/delegatedui/utils/ParcelableOverRpcUtils.kt
new file mode 100644
index 00000000..fc039852
--- /dev/null
+++ b/src/com/google/android/as/oss/delegatedui/utils/ParcelableOverRpcUtils.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.delegatedui.utils
+
+import android.os.Parcelable
+import io.grpc.BindableService
+import io.grpc.Context
+import io.grpc.Metadata
+import io.grpc.stub.AbstractStub
+import io.grpc.stub.MetadataUtils
+import java.util.concurrent.atomic.AtomicReference
+
+object ParcelableOverRpcUtils {
+
+ /** Helper to be called on the client for sending single parcelables over request headers. */
+ fun , P : Parcelable> AbstractStub.sendParcelableInRequest(
+ key: SingleParcelableKey,
+ value: P,
+ ): Stub {
+ val metadata = Metadata().apply { put(key.metadataKey, value) }
+ return withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata))
+ }
+
+ /** Helper to be called on the client for sending repeated parcelables over request headers. */
+ fun , P : Parcelable> AbstractStub.sendParcelablesInRequest(
+ key: RepeatedParcelableKey,
+ values: List
,
+ ): Stub {
+ val metadata = Metadata().apply { for (value in values) put(key.metadataKey, value) }
+ return withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata))
+ }
+
+ /** Helper to be called on the client for receiving single parcelables over response headers. */
+ fun , P : Parcelable> AbstractStub.receiveParcelableFromResponse(
+ key: SingleParcelableKey,
+ delegate: ParcelableOverRpcDelegate
,
+ ): Stub {
+ val headerCapture = AtomicReference()
+ delegate.setValue { headerCapture.get()?.get(key.metadataKey) }
+ return withInterceptors(
+ MetadataUtils.newCaptureMetadataInterceptor(headerCapture, AtomicReference())
+ )
+ }
+
+ /** Helper to be called on the client for receiving repeated parcelables over response headers. */
+ fun , P : Parcelable> AbstractStub.receiveParcelablesFromResponse(
+ key: RepeatedParcelableKey,
+ delegate: ParcelableOverRpcDelegate>,
+ ): Stub {
+ val headerCapture = AtomicReference()
+ delegate.setValue {
+ headerCapture.get()?.let { capturedMetadata ->
+ val allValues = capturedMetadata.getAll(key.metadataKey)
+ allValues?.map { it as P }
+ }
+ }
+ return withInterceptors(
+ MetadataUtils.newCaptureMetadataInterceptor(headerCapture, AtomicReference())
+ )
+ }
+
+ /**
+ * Helper to be called on the server for receiving single parcelables over request headers, throws
+ * an exception if not found.
+ */
+ @Suppress("UnusedReceiverParameter")
+ fun BindableService.receiveParcelableFromRequest(
+ key: SingleParcelableKey
,
+ context: Context = Context.current(),
+ ): P = receiveParcelableOrNullFromRequest(key, context) as P
+
+ /**
+ * Helper to be called on the server for receiving single parcelables over request headers,
+ * returns null if not found.
+ */
+ @Suppress("UnusedReceiverParameter")
+ fun
BindableService.receiveParcelableOrNullFromRequest(
+ key: SingleParcelableKey
,
+ context: Context = Context.current(),
+ ): P? = key.contextKey.get(context)?.firstOrNull()
+
+ /**
+ * Helper to be called on the server for receiving repeated parcelables over request headers,
+ * throws an exception if not found.
+ */
+ @Suppress("UnusedReceiverParameter")
+ fun
BindableService.receiveParcelablesFromRequest(
+ key: RepeatedParcelableKey
,
+ context: Context = Context.current(),
+ ): List
= receiveParcelablesOrNullFromRequest(key, context) as List
+
+ /**
+ * Helper to be called on the server for receiving repeated parcelables over request headers,
+ * returns null if not found.
+ */
+ @Suppress("UnusedReceiverParameter")
+ fun
BindableService.receiveParcelablesOrNullFromRequest(
+ key: RepeatedParcelableKey
,
+ context: Context = Context.current(),
+ ): List
? = key.contextKey.get(context)
+
+ /** Helper to be called on the server for sending single parcelables over response headers. */
+ @Suppress("UnusedReceiverParameter")
+ fun
BindableService.attachParcelableToResponse(
+ key: SingleParcelableKey
,
+ value: P,
+ context: Context = Context.current(),
+ ) = key.responseHeaderContextKey.get(context).set(listOf(value))
+
+ /** Helper to be called on the server for sending repeated parcelables over response headers. */
+ @Suppress("UnusedReceiverParameter")
+ fun
BindableService.attachParcelablesToResponse(
+ key: RepeatedParcelableKey
,
+ values: List
,
+ context: Context = Context.current(),
+ ) = key.responseHeaderContextKey.get(context).set(values)
+
+ /** Creates an empty value holder for receiving Parcelables over RPC. */
+ fun delegateOf(): ParcelableOverRpcDelegate = ParcelableOverRpcDelegate()
+
+ /** Creates a value holder for receiving Parcelables over RPC. */
+ fun T?.delegateOf(): ParcelableOverRpcDelegate {
+ val value = this@delegateOf
+ return ParcelableOverRpcDelegate().apply { this.setValue { value } }
+ }
+
+ /** Creates an empty value holder for receiving Parcelables over RPC, structured as a list. */
+ fun delegateListOf(): ParcelableOverRpcDelegate> =
+ ParcelableOverRpcDelegate()
+
+ /** Creates a value holder for receiving Parcelables over RPC, structured as a list. */
+ fun List?.delegateListOf(): ParcelableOverRpcDelegate> {
+ val value = this@delegateListOf
+ return ParcelableOverRpcDelegate>().apply { this.setValue { value } }
+ }
+
+ /**
+ * Converts a [ParcelableOverRpcDelegate] value holder to a [ParcelableOverRpcDelegate]
+ * value holder.
+ */
+ fun ParcelableOverRpcDelegate.transform(
+ transform: (T?) -> P?
+ ): ParcelableOverRpcDelegate {
+ val parentDelegate = this
+ return ParcelableOverRpcDelegate
().apply { setValue { transform(parentDelegate.value) } }
+ }
+}
+
+/** Holds some value [T]. */
+class ParcelableOverRpcDelegate internal constructor() {
+ private var provider: () -> T? = { null }
+
+ /**
+ * Returns the value held by the delegate.
+ *
+ * @throws NullPointerException Throws if there is no valid value held by the delegate.
+ */
+ val value: T?
+ get() = provider()
+
+ /** Sets the value held by the delegate. */
+ internal fun setValue(valueProvider: () -> T?) {
+ provider = valueProvider
+ }
+
+ override fun toString(): String {
+ return "ParcelableOverRpcDelegate($value)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ParcelableOverRpcDelegate<*>) return false
+
+ if (value != other.value) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return value?.hashCode() ?: 0
+ }
+}
diff --git a/src/com/google/android/as/oss/delegatedui/utils/ResponseWithParcelables.kt b/src/com/google/android/as/oss/delegatedui/utils/ResponseWithParcelables.kt
new file mode 100644
index 00000000..342de8ce
--- /dev/null
+++ b/src/com/google/android/as/oss/delegatedui/utils/ResponseWithParcelables.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.delegatedui.utils
+
+import android.app.PendingIntent
+import android.app.RemoteAction
+import android.graphics.Bitmap
+import com.google.android.`as`.oss.delegatedui.utils.ParcelableOverRpcUtils.delegateListOf
+import com.google.android.`as`.oss.delegatedui.utils.ParcelableOverRpcUtils.delegateOf
+import com.google.protobuf.MessageLite
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+
+/**
+ * Data class holding the response itself, plus any raw and derived data from Parcelables that may
+ * have been sent over RPC.
+ */
+data class ResponseWithParcelables(
+ val value: Response,
+ val image: ParcelableOverRpcDelegate,
+ val pendingIntentList: ParcelableOverRpcDelegate>,
+ val remoteActionList: ParcelableOverRpcDelegate>,
+) {
+
+ /** Maps a [ResponseWithParcelables] to another by transforming the held response. */
+ fun map(transform: (Response) -> R): ResponseWithParcelables {
+ return transform(value).withParcelablesToReceive(image, pendingIntentList, remoteActionList)
+ }
+}
+
+/** Maps a Deferred to another by transforming the held response. */
+fun Deferred>.map(
+ scope: CoroutineScope?,
+ transform: (T) -> V,
+): Deferred>? {
+ return scope?.async { this@map.await().map(transform) }
+}
+
+/**
+ * Wraps a response with Parcelables. Each Parcelable is passed in as a delegate, which can
+ * initially be empty but filled in by the infra for you to read later.
+ */
+fun T.withParcelablesToReceive(
+ image: ParcelableOverRpcDelegate = delegateOf(),
+ pendingIntentList: ParcelableOverRpcDelegate> = delegateListOf(),
+ remoteActionList: ParcelableOverRpcDelegate> = delegateListOf(),
+): ResponseWithParcelables {
+ return ResponseWithParcelables(
+ value = this,
+ image = image,
+ pendingIntentList = pendingIntentList,
+ remoteActionList = remoteActionList,
+ )
+}
+
+/** Wraps a response with Parcelables. */
+fun T.withParcelablesToSend(
+ image: Bitmap? = null,
+ pendingIntentList: List? = null,
+ remoteActionList: List? = null,
+): ResponseWithParcelables {
+ return ResponseWithParcelables(
+ value = this,
+ image = image.delegateOf(),
+ pendingIntentList = pendingIntentList.delegateListOf(),
+ remoteActionList = remoteActionList.delegateListOf(),
+ )
+}
diff --git a/src/com/google/android/as/oss/delegatedui/utils/SerializableBitmap.kt b/src/com/google/android/as/oss/delegatedui/utils/SerializableBitmap.kt
new file mode 100644
index 00000000..13cf42ad
--- /dev/null
+++ b/src/com/google/android/as/oss/delegatedui/utils/SerializableBitmap.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.delegatedui.utils
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.toBitmap
+import com.google.protobuf.ByteString
+
+/** Allows CUJs to send and receive [Bitmap]s in gRPC responses as a serialized proto field. */
+object SerializableBitmap {
+
+ /** Converts a [Bitmap] to a [ByteString]. */
+ fun Bitmap.serializeToByteString(
+ format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
+ quality: Int = 100,
+ ): ByteString {
+ val output = ByteString.newOutput()
+ compress(format, quality, output)
+ return output.toByteString()
+ }
+
+ /** Converts a [Icon] to a [ByteString]. */
+ fun Icon.serializeToByteString(
+ context: Context,
+ format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
+ quality: Int = 100,
+ ): ByteString? {
+ return runCatching { loadDrawable(context)?.toBitmap()?.serializeToByteString(format, quality) }
+ .getOrNull()
+ }
+
+ /** Converts a [ByteString] to a [Bitmap]. */
+ fun ByteString.deserializeToBitmap(): Bitmap? =
+ runCatching { BitmapFactory.decodeStream(newInput()) }.getOrNull()
+}
diff --git a/src/com/google/android/as/oss/delegatedui/utils/TintableIcon.kt b/src/com/google/android/as/oss/delegatedui/utils/TintableIcon.kt
new file mode 100644
index 00000000..523f541e
--- /dev/null
+++ b/src/com/google/android/as/oss/delegatedui/utils/TintableIcon.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.delegatedui.utils
+
+import android.graphics.Bitmap
+import androidx.compose.foundation.Image
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+
+/** Represents an icon that can be tinted. */
+data class TintableIcon(val bitmap: Bitmap, val tintable: Boolean)
+
+/** Converts a Bitmap to a TintableIcon. */
+fun Bitmap.asTintableIcon(tintable: Boolean): TintableIcon = TintableIcon(this, tintable)
+
+/**
+ * A composable of either an [Icon] or [Image], depending on whether the [TintableIcon] is tintable
+ * or not.
+ */
+@Composable
+fun IconOrImage(
+ icon: TintableIcon,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current,
+ contentDescription: String? = null,
+) {
+ if (icon.tintable) {
+ Icon(
+ bitmap = icon.bitmap.asImageBitmap(),
+ tint = tint,
+ contentDescription = contentDescription,
+ modifier = modifier,
+ )
+ } else {
+ Image(
+ bitmap = icon.bitmap.asImageBitmap(),
+ contentDescription = contentDescription,
+ modifier = modifier,
+ )
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/AndroidManifest.xml b/src/com/google/android/as/oss/feedback/AndroidManifest.xml
new file mode 100644
index 00000000..fcd7f1ac
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/google/android/as/oss/feedback/AndroidManifest_api.xml b/src/com/google/android/as/oss/feedback/AndroidManifest_api.xml
new file mode 100644
index 00000000..08db808a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/AndroidManifest_api.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/com/google/android/as/oss/feedback/BUILD b/src/com/google/android/as/oss/feedback/BUILD
new file mode 100644
index 00000000..ffd22ab1
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/BUILD
@@ -0,0 +1,256 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+package_group(
+ name = "feedback_api_visibility",
+ includes = ["//visibility:public"],
+)
+
+filegroup(
+ name = "manifest",
+ srcs = ["AndroidManifest.xml"],
+)
+
+android_library(
+ name = "feedback",
+ srcs = [
+ "FeedbackApi.kt",
+ ],
+ exports_manifest = True,
+ manifest = "AndroidManifest_api.xml",
+ visibility = [":feedback_api_visibility"],
+ deps = [
+ "//src/com/google/android/as/oss/feedback/proto:entity_feedback_dialog_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto:multi_feedback_dialog_data_kt_proto_lite",
+ "//third_party/java/androidx/appcompat",
+ ],
+)
+
+android_library(
+ name = "prod_modules",
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ tags = [
+ "keep_dep",
+ ],
+ exports = [
+ ":feedback_activity",
+ "//src/com/google/android/as/oss/feedback/serviceclient/serviceconnection:module",
+ ],
+)
+
+android_library(
+ name = "feedback_activity",
+ srcs = [
+ "FeedbackActivity.kt",
+ ],
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ ":feedback",
+ ":feedback_ui_state",
+ ":feedback_view_model",
+ ":multi_entity_feedback_dialog",
+ ":single_entity_feedback_dialog",
+ "//src/com/google/android/as/oss/feedback/proto:entity_feedback_dialog_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto:multi_feedback_dialog_data_kt_proto_lite",
+ "//third_party/java/androidx/activity/compose",
+ "//third_party/java/androidx/appcompat",
+ "//third_party/java/androidx/compose/runtime",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_flogger_google_extensions",
+ ],
+)
+
+android_library(
+ name = "single_entity_feedback_dialog",
+ srcs = [
+ "SingleEntityFeedbackDialog.kt",
+ ],
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ ":feedback_opt_in_content",
+ ":feedback_submission_data",
+ ":feedback_ui_state",
+ ":feedback_view_model",
+ ":view_feedback_data_dialog",
+ "//src/com/google/android/as/oss/delegatedui/service/templates/fonts:flex_fonts",
+ "//src/com/google/android/as/oss/feedback/proto:entity_feedback_dialog_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/serviceclient:service_client",
+ "//third_party/java/androidx/activity/compose",
+ "//third_party/java/androidx/appcompat",
+ "//third_party/java/androidx/compose/foundation",
+ "//third_party/java/androidx/compose/foundation/layout",
+ "//third_party/java/androidx/compose/material/icons_core",
+ "//third_party/java/androidx/compose/material3",
+ "//third_party/java/androidx/compose/runtime",
+ "//third_party/java/androidx/compose/runtime/saveable",
+ "//third_party/java/androidx/compose/ui",
+ "//third_party/java/androidx/compose/ui/text",
+ "//third_party/java/androidx/compose/ui/unit",
+ "//third_party/java/androidx/hilt/navigation_compose",
+ ],
+)
+
+android_library(
+ name = "multi_entity_feedback_dialog",
+ srcs = ["MultiEntityFeedbackDialog.kt"],
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ ":feedback_opt_in_content",
+ ":feedback_submission_data",
+ ":feedback_ui_state",
+ ":feedback_view_model",
+ ":view_feedback_data_dialog",
+ "//src/com/google/android/as/oss/delegatedui/service/templates/fonts:flex_fonts",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto:multi_feedback_dialog_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//third_party/java/androidx/activity/compose",
+ "//third_party/java/androidx/appcompat",
+ "//third_party/java/androidx/compose/foundation",
+ "//third_party/java/androidx/compose/foundation/layout",
+ "//third_party/java/androidx/compose/material/icons_core",
+ "//third_party/java/androidx/compose/material3",
+ "//third_party/java/androidx/compose/runtime",
+ "//third_party/java/androidx/compose/ui",
+ "//third_party/java/androidx/compose/ui/text",
+ "//third_party/java/androidx/compose/ui/unit",
+ "//third_party/java/androidx/hilt/navigation_compose",
+ "//third_party/java/androidx/lifecycle/viewmodel_compose",
+ "//third_party/kotlin/kotlinx_coroutines:kotlinx_coroutines-jvm",
+ ],
+)
+
+android_library(
+ name = "feedback_view_model",
+ srcs = ["FeedbackViewModel.kt"],
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ ":feedback_submission_data",
+ ":feedback_ui_state",
+ "//src/com/google/android/as/oss/common",
+ "//src/com/google/android/as/oss/feedback/gateway:feedback_http_client",
+ "//src/com/google/android/as/oss/feedback/gateway:feedback_http_client_module", # buildcleaner: keep for Feedback Http Client Module
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:pixel_apex_api_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/quartz/serviceclient:quartz_service_client",
+ "//src/com/google/android/as/oss/feedback/quartz/utils:quartz_data_helper",
+ "//src/com/google/android/as/oss/feedback/serviceclient:service_client",
+ "//third_party/java/androidx/activity/compose",
+ "//third_party/java/androidx/appcompat",
+ "//third_party/java/androidx/compose/foundation",
+ "//third_party/java/androidx/compose/foundation/layout",
+ "//third_party/java/androidx/compose/material3",
+ "//third_party/java/androidx/compose/runtime",
+ "//third_party/java/androidx/compose/ui",
+ "//third_party/java/androidx/compose/ui/text",
+ "//third_party/java/androidx/compose/ui/unit",
+ "//third_party/java/androidx/hilt/navigation_compose",
+ "//third_party/java/dagger/hilt:view_model",
+ "//third_party/kotlin/kotlinx_coroutines:kotlinx_coroutines-jvm",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "view_feedback_data_dialog",
+ srcs = ["ViewFeedbackDataDialog.kt"],
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ ":feedback_ui_state",
+ ":feedback_view_model",
+ ":view_feedback_data",
+ "//src/com/google/android/as/oss/feedback/quartz/serviceclient:quartz_service_client",
+ "//src/com/google/android/as/oss/feedback/serviceclient:service_client",
+ "//third_party/java/androidx/activity/compose",
+ "//third_party/java/androidx/appcompat",
+ "//third_party/java/androidx/compose/foundation",
+ "//third_party/java/androidx/compose/foundation/layout",
+ "//third_party/java/androidx/compose/material3",
+ "//third_party/java/androidx/compose/runtime",
+ "//third_party/java/androidx/compose/ui",
+ "//third_party/java/androidx/compose/ui/text",
+ "//third_party/java/androidx/compose/ui/unit",
+ ],
+)
+
+android_library(
+ name = "feedback_opt_in_content",
+ srcs = ["FeedbackOptInContent.kt"],
+ exports_manifest = True,
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ deps = [
+ ":feedback_ui_state",
+ ":feedback_view_model",
+ "//src/com/google/android/as/oss/feedback/quartz/serviceclient:quartz_service_client",
+ "//src/com/google/android/as/oss/feedback/serviceclient:service_client",
+ "//third_party/java/androidx/activity/compose",
+ "//third_party/java/androidx/appcompat",
+ "//third_party/java/androidx/compose/foundation",
+ "//third_party/java/androidx/compose/foundation/layout",
+ "//third_party/java/androidx/compose/material3",
+ "//third_party/java/androidx/compose/runtime",
+ "//third_party/java/androidx/compose/ui",
+ "//third_party/java/androidx/compose/ui/text",
+ "//third_party/java/androidx/compose/ui/unit",
+ ],
+)
+
+android_library(
+ name = "feedback_ui_state",
+ srcs = ["FeedbackUiState.kt"],
+ deps = [
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/quartz/serviceclient:quartz_service_client",
+ "//src/com/google/android/as/oss/feedback/serviceclient:service_client",
+ ],
+)
+
+android_library(
+ name = "feedback_submission_data",
+ srcs = ["FeedbackSubmissionData.kt"],
+ deps = [
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ ],
+)
+
+android_library(
+ name = "view_feedback_data",
+ srcs = ["ViewFeedbackData.kt"],
+ deps = [],
+)
diff --git a/src/com/google/android/as/oss/feedback/FeedbackActivity.kt b/src/com/google/android/as/oss/feedback/FeedbackActivity.kt
new file mode 100644
index 00000000..386e200b
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/FeedbackActivity.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.google.android.`as`.oss.feedback.FeedbackApi.EXTRA_ENTITY_FEEDBACK_DIALOG_DATA_PROTO
+import com.google.android.`as`.oss.feedback.FeedbackApi.EXTRA_MULTI_FEEDBACK_DIALOG_DATA_PROTO
+import com.google.android.`as`.oss.feedback.api.EntityFeedbackDialogData
+import com.google.android.`as`.oss.feedback.api.MultiFeedbackDialogData
+import com.google.common.flogger.GoogleLogger
+import com.google.common.flogger.StackSize
+import dagger.hilt.android.AndroidEntryPoint
+
+/** An activity to host UI components for a user to give feedback on an entity or bundle. */
+@AndroidEntryPoint(ComponentActivity::class)
+class FeedbackActivity : Hilt_FeedbackActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ try {
+ val entityFeedbackDialogData =
+ intent.getByteArrayExtra(EXTRA_ENTITY_FEEDBACK_DIALOG_DATA_PROTO)?.let {
+ EntityFeedbackDialogData.parseFrom(it)
+ }
+ val multiFeedbackDialogData =
+ intent.getByteArrayExtra(EXTRA_MULTI_FEEDBACK_DIALOG_DATA_PROTO)?.let {
+ MultiFeedbackDialogData.parseFrom(it)
+ }
+
+ if (entityFeedbackDialogData == null && multiFeedbackDialogData == null) {
+ logger.atSevere().log("No feedback data request provided. Finishing activity.")
+ finish()
+ return
+ }
+
+ setContent {
+ when {
+ entityFeedbackDialogData != null -> {
+ SingleEntityFeedbackDialog(
+ data = checkNotNull(entityFeedbackDialogData),
+ onFeedbackEvent = { event: FeedbackEvent ->
+ when (event) {
+ FeedbackEvent.SUBMISSION_SUCCESSFUL -> {
+ setResult(Activity.RESULT_OK)
+ }
+ FeedbackEvent.SUBMISSION_FAILED -> {
+ setResult(Activity.RESULT_CANCELED)
+ }
+ }
+ },
+ onDismissRequest = { finish() },
+ )
+ }
+ multiFeedbackDialogData != null -> {
+ MultiEntityFeedbackDialog(data = multiFeedbackDialogData) { finish() }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ logger
+ .atSevere()
+ .withCause(e)
+ .withStackTrace(StackSize.SMALL)
+ .log("FeedbackActivity.onCreate() failed. Finishing activity.")
+ finish()
+ return
+ }
+ }
+
+ companion object {
+ private val logger = GoogleLogger.forEnclosingClass()
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/FeedbackApi.kt b/src/com/google/android/as/oss/feedback/FeedbackApi.kt
new file mode 100644
index 00000000..b8ba9ace
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/FeedbackApi.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
+import android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import com.google.android.`as`.oss.feedback.api.EntityFeedbackDialogData
+import com.google.android.`as`.oss.feedback.api.MultiFeedbackDialogData
+
+/** The public API for showing the FeedbackActivity. */
+object FeedbackApi {
+
+ /** Key for the [EntityFeedbackDialogData] proto extra in the Intent bundle. */
+ internal const val EXTRA_ENTITY_FEEDBACK_DIALOG_DATA_PROTO: String =
+ "entity_feedback_dialog_data_proto"
+ /** Key for the [MultiFeedbackDialogData] proto extra in the Intent bundle. */
+ internal const val EXTRA_MULTI_FEEDBACK_DIALOG_DATA_PROTO: String =
+ "multi_feedback_dialog_data_proto"
+
+ private const val PCS_PKG_NAME: String = "com.google.android.as.oss"
+ private const val FEEDBACK_ACTIVITY_NAME: String =
+ "com.google.android.as.oss.feedback.FeedbackActivity"
+
+ /**
+ * Creates an [Intent] that starts [FeedbackActivity] to allow the user to give feedback on a
+ * single entity.
+ */
+ fun createEntityFeedbackIntent(context: Context, data: EntityFeedbackDialogData): Intent {
+ val intent =
+ Intent(Intent.ACTION_MAIN).apply {
+ setComponent(ComponentName(PCS_PKG_NAME, FEEDBACK_ACTIVITY_NAME))
+ // FLAG_ACTIVITY_NEW_TASK: Needed to start an Activity outside of an Activity context.
+ // FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS: Ensure the Activity launched doesn't show up as a
+ // separate task when a user opens recents.
+ setFlags(
+ FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ )
+ putExtra(EXTRA_ENTITY_FEEDBACK_DIALOG_DATA_PROTO, data.toByteArray())
+ }
+
+ return intent
+ }
+
+ /**
+ * Creates an [Intent] that starts [FeedbackActivity] to allow the user to give feedback on
+ * multiple entities.
+ */
+ fun createMultiFeedbackIntent(context: Context, data: MultiFeedbackDialogData): Intent {
+ val intent =
+ Intent(Intent.ACTION_MAIN).apply {
+ setComponent(ComponentName(PCS_PKG_NAME, FEEDBACK_ACTIVITY_NAME))
+ // FLAG_ACTIVITY_NEW_TASK: Needed to start an Activity outside of an Activity context.
+ // FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS: Ensure the Activity launched doesn't show up as a
+ // separate task when a user opens recents.
+ setFlags(
+ FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ )
+ putExtra(EXTRA_MULTI_FEEDBACK_DIALOG_DATA_PROTO, data.toByteArray())
+ }
+
+ return intent
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/FeedbackOptInContent.kt b/src/com/google/android/as/oss/feedback/FeedbackOptInContent.kt
new file mode 100644
index 00000000..18c32afe
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/FeedbackOptInContent.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FeedbackOptInContent(
+ modifier: Modifier,
+ optInLabel: String,
+ optInLabelLinkPrivacyPolicy: String,
+ optInLabelLinkViewData: String,
+ optInChecked: Boolean,
+ onOptInCheckedChanged: (Boolean) -> Unit,
+ onViewDataClicked: () -> Unit,
+) {
+ ParagraphTheme {
+ Row(modifier = modifier) {
+ Checkbox(
+ modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 16.dp).size(24.dp),
+ checked = optInChecked,
+ onCheckedChange = onOptInCheckedChanged,
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+
+ val optInText = buildAnnotatedString {
+ append(optInLabel)
+
+ // Privacy policy.
+ val policyUrl = "https://policies.google.com/privacy"
+ val privacyPolicyStart = optInLabel.indexOf(optInLabelLinkPrivacyPolicy)
+ val privacyPolicyEnd = privacyPolicyStart + optInLabelLinkPrivacyPolicy.length
+ if (privacyPolicyStart != -1) {
+ addLink(
+ url = LinkAnnotation.Url(policyUrl),
+ start = privacyPolicyStart,
+ end = privacyPolicyEnd,
+ )
+ }
+
+ // View data.
+ val viewDataStart = optInLabel.indexOf(optInLabelLinkViewData)
+ val viewDataEnd = viewDataStart + optInLabelLinkViewData.length
+ if (viewDataStart != -1) {
+ addLink(
+ clickable =
+ LinkAnnotation.Clickable(
+ tag = OPT_IN_LABEL_LINK_VIEW_DATA_TAG,
+ linkInteractionListener = { onViewDataClicked() },
+ ),
+ start = viewDataStart,
+ end = viewDataEnd,
+ )
+ addStyle(
+ style = SpanStyle(fontWeight = FontWeight.Bold),
+ start = viewDataStart,
+ end = viewDataEnd,
+ )
+ }
+ }
+
+ Column {
+ Text(
+ modifier =
+ Modifier.fillMaxWidth().semantics(
+ // Required for screen readers because `optInText` is a complex [AnnotatedString]
+ // which forces the Text composable to be pushed to the back of the traversal order.
+ mergeDescendants = true
+ ) {},
+ text = optInText,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ if (!optInLabel.contains(optInLabelLinkViewData)) { // For backwards compat.
+ Spacer(Modifier.height(8.dp))
+ Text(
+ modifier = Modifier.clickable { onViewDataClicked() },
+ text = optInLabelLinkViewData,
+ style =
+ MaterialTheme.typography.bodySmall.copy(
+ fontWeight = FontWeight.Bold,
+ textDecoration = TextDecoration.Underline,
+ ),
+ color = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ }
+ }
+}
+
+/** [MaterialTheme] without flex font. */
+@Composable
+private fun ParagraphTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme =
+ when {
+ dynamicColor -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> darkColorScheme()
+ else -> lightColorScheme()
+ }
+
+ MaterialTheme(colorScheme = colorScheme, typography = Typography()) { content() }
+}
+
+private const val OPT_IN_LABEL_LINK_VIEW_DATA_TAG = "opt_in_label_link_view_data"
diff --git a/src/com/google/android/as/oss/feedback/FeedbackSubmissionData.kt b/src/com/google/android/as/oss/feedback/FeedbackSubmissionData.kt
new file mode 100644
index 00000000..82527b71
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/FeedbackSubmissionData.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.SpoonCUJ
+
+data class FeedbackSubmissionData(
+ val cuj: SpoonCUJ? = null,
+ val quartzCuj: QuartzCUJ? = null,
+ val selectedEntityContent: String,
+ val ratingSentiment: FeedbackRatingSentiment,
+)
diff --git a/src/com/google/android/as/oss/feedback/FeedbackUiState.kt b/src/com/google/android/as/oss/feedback/FeedbackUiState.kt
new file mode 100644
index 00000000..b1b59afd
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/FeedbackUiState.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment
+import com.google.android.`as`.oss.feedback.api.FeedbackTagData
+import com.google.android.`as`.oss.feedback.quartz.serviceclient.QuartzFeedbackDonationData
+import com.google.android.`as`.oss.feedback.serviceclient.FeedbackDonationData
+import java.util.Collections.emptyMap
+
+/** Ui state for [SingleEntityFeedbackDialog] and [MultiEntityFeedbackDialog]. */
+data class FeedbackUiState(
+ val selectedSentimentMap: Map = emptyMap(),
+ val tagsSelectionMap:
+ Map>> =
+ emptyMap(),
+ val freeFormTextMap: Map = emptyMap(),
+ val optInChecked: Boolean = false,
+ val feedbackDialogMode: FeedbackDialogMode = FeedbackDialogMode.EDITING_FEEDBACK,
+ val feedbackSubmitStatus: FeedbackSubmitState = FeedbackSubmitState.DRAFT,
+ val feedbackDonationData: Result? = null,
+ val quartzFeedbackDonationData: Result? = null,
+)
+
+/** The modes that a feedback dialog can be in. */
+enum class FeedbackDialogMode {
+ EDITING_FEEDBACK,
+ VIEWING_FEEDBACK_DONATION_DATA,
+}
+
+/** The state of feedback submission. */
+enum class FeedbackSubmitState {
+ DRAFT,
+ SUBMIT_PENDING,
+ /** May have resulted in a success or failure. */
+ SUBMIT_FINISHED,
+}
+
+/** One time events. */
+enum class FeedbackEvent {
+ SUBMISSION_SUCCESSFUL,
+ SUBMISSION_FAILED,
+}
+
+/** Token representing the feedback entity. */
+typealias FeedbackEntityContent = String
+
+/** Return values depending on the state of the Result. */
+inline fun Result?.fold(
+ onNull: () -> R,
+ onSuccess: (value: T) -> R,
+ onFailure: (exception: Throwable) -> R,
+): R {
+ return if (this == null) {
+ onNull()
+ } else {
+ fold(onSuccess, onFailure)
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/FeedbackViewModel.kt b/src/com/google/android/as/oss/feedback/FeedbackViewModel.kt
new file mode 100644
index 00000000..7684827e
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/FeedbackViewModel.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.`as`.oss.common.Executors.IO_EXECUTOR
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_DOWN
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_UP
+import com.google.android.`as`.oss.feedback.api.FeedbackTagData
+import com.google.android.`as`.oss.feedback.api.gateway.LogFeedbackV2Request
+import com.google.android.`as`.oss.feedback.api.gateway.NegativeRatingTag
+import com.google.android.`as`.oss.feedback.api.gateway.PositiveRatingTag
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.Rating
+import com.google.android.`as`.oss.feedback.api.gateway.UserDataDonationOption
+import com.google.android.`as`.oss.feedback.api.gateway.feedbackCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.logFeedbackV2Request
+import com.google.android.`as`.oss.feedback.api.gateway.memoryEntity
+import com.google.android.`as`.oss.feedback.api.gateway.runtimeConfig
+import com.google.android.`as`.oss.feedback.api.gateway.spoonFeedbackDataDonation
+import com.google.android.`as`.oss.feedback.api.gateway.userDonation
+import com.google.android.`as`.oss.feedback.gateway.FeedbackHttpClient
+import com.google.android.`as`.oss.feedback.quartz.serviceclient.QuartzFeedbackDataServiceClient
+import com.google.android.`as`.oss.feedback.quartz.utils.QuartzDataHelper
+import com.google.android.`as`.oss.feedback.serviceclient.FeedbackDataServiceClient
+import com.google.android.`as`.oss.feedback.serviceclient.FeedbackDonationData
+import com.google.common.flogger.GoogleLogger
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** View model for [SingleEntityFeedbackDialog] and [MultiEntityFeedbackDialog]. */
+@HiltViewModel
+class FeedbackViewModel
+@Inject
+constructor(
+ private var feedbackDataServiceClient: FeedbackDataServiceClient,
+ private var quartzFeedbackDataServiceClient: QuartzFeedbackDataServiceClient,
+ private var feedbackHttpClient: FeedbackHttpClient,
+ private var quartzDataHelper: QuartzDataHelper,
+) : ViewModel() {
+ private val _uiStateFlow = MutableStateFlow(FeedbackUiState())
+ val uiStateFlow = _uiStateFlow.asStateFlow()
+ private val _events = MutableSharedFlow()
+ val events = _events.asSharedFlow()
+
+ private var loadDonationDataJob: Job? = null
+ private var submitFeedbackJob: Job? = null
+
+ fun updateFreeFormText(entity: FeedbackEntityContent, value: String) {
+ _uiStateFlow.update { it.copy(freeFormTextMap = it.freeFormTextMap + (entity to value)) }
+ }
+
+ fun updateOptInChecked(value: Boolean) {
+ _uiStateFlow.update { it.copy(optInChecked = value) }
+ }
+
+ fun updateSelectedSentiment(entity: FeedbackEntityContent, sentiment: FeedbackRatingSentiment) {
+ _uiStateFlow.update {
+ it.copy(selectedSentimentMap = it.selectedSentimentMap + (entity to sentiment))
+ }
+ }
+
+ fun updateTagSelection(
+ entity: FeedbackEntityContent,
+ sentiment: FeedbackRatingSentiment,
+ tag: FeedbackTagData,
+ value: Boolean,
+ singleSelection: Boolean = false,
+ ) {
+ _uiStateFlow.update {
+ val oldTags = it.tagsSelectionMap[entity].orEmpty()[sentiment].orEmpty().toMutableMap()
+ if (singleSelection) oldTags.clear()
+ val tags: Map = oldTags + (tag to value)
+ val sentiments: Map> =
+ it.tagsSelectionMap[entity].orEmpty() + (sentiment to tags)
+ it.copy(tagsSelectionMap = it.tagsSelectionMap + (entity to sentiments))
+ }
+ }
+
+ fun updateFeedbackDialogMode(value: FeedbackDialogMode) {
+ _uiStateFlow.update { it.copy(feedbackDialogMode = value) }
+ }
+
+ fun loadDonationData(clientSessionId: String, quartzCuj: QuartzCUJ? = null) {
+ loadDonationDataJob?.cancel()
+ loadDonationDataJob =
+ viewModelScope.launch {
+ _uiStateFlow.update { it.copy(feedbackDonationData = null) }
+ val response =
+ feedbackDataServiceClient.getFeedbackDonationData(
+ clientSessionId = clientSessionId,
+ uiElementType = 0,
+ uiElementIndex = 0,
+ )
+ _uiStateFlow.update { it.copy(feedbackDonationData = response) }
+
+ if (quartzCuj != null) {
+ val quartzResponse =
+ quartzFeedbackDataServiceClient.getFeedbackDonationData(
+ clientSessionId = clientSessionId,
+ uiElementType = 0,
+ uiElementIndex = 0,
+ quartzCuj = quartzCuj,
+ )
+ _uiStateFlow.update { it.copy(quartzFeedbackDonationData = quartzResponse) }
+ }
+ }
+ }
+
+ fun submitFeedback(submissionDataList: List) {
+ submitFeedbackJob?.cancel()
+ submitFeedbackJob =
+ viewModelScope.launch {
+ try {
+ _uiStateFlow.update { it.copy(feedbackSubmitStatus = FeedbackSubmitState.SUBMIT_PENDING) }
+
+ loadDonationDataJob?.join() // Wait for the data to load, if needed.
+ val data = uiStateFlow.value.feedbackDonationData?.getOrNull()
+ val quartzData = uiStateFlow.value.quartzFeedbackDonationData?.getOrNull()
+ if (data == null && quartzData == null) {
+ logger
+ .atWarning()
+ .log("FeedbackViewModel#Donation data unexpectedly not available. Skipping.")
+ _events.emit(FeedbackEvent.SUBMISSION_FAILED)
+ return@launch
+ }
+
+ if (submissionDataList.isEmpty()) {
+ logger.atWarning().log("FeedbackViewModel#submissionDataList is empty. Skipping.")
+ _events.emit(FeedbackEvent.SUBMISSION_FAILED)
+ return@launch
+ }
+
+ val successful =
+ submissionDataList
+ .mapNotNull { submissionData ->
+ if (submissionData.quartzCuj != null && quartzData != null) {
+ with(quartzDataHelper) {
+ submissionData.toQuartzFeedbackUploadRequest(quartzData, uiStateFlow.value)
+ }
+ } else if (data != null) {
+ submissionData.toFeedbackUploadRequest(data)
+ } else {
+ logger
+ .atWarning()
+ .log(
+ "No valid donation data (Spoon or Quartz) for submission: %s",
+ submissionData.selectedEntityContent,
+ )
+ null
+ }
+ }
+ .map { request -> request.uploadFeedback() }
+ .all { success -> success }
+ if (successful) {
+ logger.atInfo().log("FeedbackViewModel#submitFeedback successful")
+ }
+ _events.emit(
+ if (successful) FeedbackEvent.SUBMISSION_SUCCESSFUL else FeedbackEvent.SUBMISSION_FAILED
+ )
+ } finally {
+ _uiStateFlow.update {
+ it.copy(feedbackSubmitStatus = FeedbackSubmitState.SUBMIT_FINISHED)
+ }
+ }
+ }
+ }
+
+ private suspend fun LogFeedbackV2Request.uploadFeedback(): Boolean {
+ val request = this
+
+ logger.atInfo().log("FeedbackViewModel#submitFeedback with request: %s", request)
+ val success =
+ withContext(IO_EXECUTOR.asCoroutineDispatcher()) {
+ feedbackHttpClient.uploadFeedback(request)
+ }
+
+ if (!success) {
+ logger.atInfo().log("FeedbackViewModel#submitFeedback failed with request")
+ } else {
+ logger.atInfo().log("FeedbackViewModel#submitFeedback successful with request")
+ }
+
+ return success
+ }
+
+ private fun FeedbackSubmissionData.toFeedbackUploadRequest(
+ data: FeedbackDonationData
+ ): LogFeedbackV2Request? {
+ val submissionData = this
+ return logFeedbackV2Request {
+ this.appId = data.appId
+ this.interactionId = data.interactionId
+ this.feedbackCuj = feedbackCUJ { spoonFeedbackCuj = submissionData.cuj ?: data.cuj }
+ this.rating =
+ when (submissionData.ratingSentiment) {
+ RATING_SENTIMENT_THUMBS_UP -> Rating.THUMB_UP
+ RATING_SENTIMENT_THUMBS_DOWN -> Rating.THUMB_DOWN
+ else -> Rating.RATING_UNSPECIFIED
+ }
+ uiStateFlow.value.tagsSelectionMap[submissionData.selectedEntityContent]
+ ?.get(RATING_SENTIMENT_THUMBS_UP)
+ ?.let { entry ->
+ this.positiveTags += entry.keys.map { PositiveRatingTag.entries[it.ratingTagOrdinal] }
+ }
+ uiStateFlow.value.tagsSelectionMap[submissionData.selectedEntityContent]
+ ?.get(RATING_SENTIMENT_THUMBS_DOWN)
+ ?.let { entry ->
+ this.negativeTags += entry.keys.map { NegativeRatingTag.entries[it.ratingTagOrdinal] }
+ }
+ additionalComment =
+ uiStateFlow.value.freeFormTextMap[submissionData.selectedEntityContent] ?: ""
+ runtimeConfig = runtimeConfig {
+ appBuildType = data.runtimeConfig.appBuildType
+ appVersion = data.runtimeConfig.appVersion
+ modelMetadata = data.runtimeConfig.modelMetadata
+ modelId = data.runtimeConfig.modelId
+ }
+ donationOption =
+ if (uiStateFlow.value.optInChecked) {
+ UserDataDonationOption.OPT_IN
+ } else {
+ UserDataDonationOption.OPT_OUT
+ }
+ userDonation = userDonation {
+ // Only include donation data if user has opted in the consent.
+ if (uiStateFlow.value.optInChecked) {
+ structuredDataDonation = spoonFeedbackDataDonation {
+ triggeringMessages += data.triggeringMessages
+ intentQueries += data.intentQueries
+ modelOutputs += data.modelOutputs
+ memoryEntities +=
+ data.memoryEntities.map {
+ memoryEntity {
+ entityData = it.entityData
+ modelVersion = it.modelVersion
+ }
+ }
+ this.selectedEntityContent = submissionData.selectedEntityContent
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ private val logger = GoogleLogger.forEnclosingClass()
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/MultiEntityFeedbackDialog.kt b/src/com/google/android/as/oss/feedback/MultiEntityFeedbackDialog.kt
new file mode 100644
index 00000000..60f2c9a4
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/MultiEntityFeedbackDialog.kt
@@ -0,0 +1,597 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import android.widget.Toast
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.android.`as`.oss.delegatedui.service.templates.fonts.FlexFontUtils.withFlexFont
+import com.google.android.`as`.oss.feedback.FeedbackDialogMode.EDITING_FEEDBACK
+import com.google.android.`as`.oss.feedback.FeedbackDialogMode.VIEWING_FEEDBACK_DONATION_DATA
+import com.google.android.`as`.oss.feedback.api.FeedbackEntityCommonData
+import com.google.android.`as`.oss.feedback.api.FeedbackEntityData
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingData
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_DOWN
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_UP
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_UNDEFINED
+import com.google.android.`as`.oss.feedback.api.FeedbackTagData
+import com.google.android.`as`.oss.feedback.api.MultiFeedbackDialogData
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import kotlinx.coroutines.launch
+
+@Composable
+fun MultiEntityFeedbackDialog(
+ viewModel: FeedbackViewModel = viewModel(),
+ data: MultiFeedbackDialogData,
+ onDismissRequest: () -> Unit,
+) {
+ val context = LocalContext.current
+ val foundQuartzCuj: QuartzCUJ? =
+ data.feedbackEntitiesList.firstOrNull { it.hasQuartzCuj() }?.quartzCuj
+
+ LaunchedEffect(Unit) {
+ viewModel.loadDonationData(clientSessionId = data.clientSessionId, quartzCuj = foundQuartzCuj)
+ launch {
+ viewModel.events.collect { event ->
+ val message =
+ when (event) {
+ FeedbackEvent.SUBMISSION_SUCCESSFUL -> data.feedbackDialogSentSuccessfullyToast
+ FeedbackEvent.SUBMISSION_FAILED -> data.feedbackDialogSentFailedToast
+ }
+
+ message?.let { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
+ onDismissRequest()
+ }
+ }
+ }
+
+ MainTheme {
+ val uiState by viewModel.uiStateFlow.collectAsState()
+ MultiFeedbackBottomSheet(
+ viewModel = viewModel,
+ data = data,
+ onDismissRequest = onDismissRequest,
+ onSendFeedback = {
+ viewModel.submitFeedback(
+ uiState.selectedSentimentMap.entries
+ .filter { (_, sentiment) ->
+ sentiment in listOf(RATING_SENTIMENT_THUMBS_UP, RATING_SENTIMENT_THUMBS_DOWN)
+ }
+ .map { entry ->
+ val entityData = data.feedbackEntitiesList.first { it.entityContent == entry.key }
+ val spoonCuj = if (entityData.hasCuj()) entityData.cuj else null
+ val quartzCuj = if (entityData.hasQuartzCuj()) entityData.quartzCuj else null
+ FeedbackSubmissionData(
+ cuj = spoonCuj,
+ quartzCuj = quartzCuj,
+ selectedEntityContent = entry.key,
+ ratingSentiment = entry.value,
+ )
+ }
+ )
+ },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun MultiFeedbackBottomSheet(
+ viewModel: FeedbackViewModel,
+ data: MultiFeedbackDialogData,
+ onDismissRequest: () -> Unit,
+ onSendFeedback: () -> Unit,
+) {
+ val uiState by viewModel.uiStateFlow.collectAsState()
+ val donationData = uiState.feedbackDonationData
+ val foundQuartzCuj: QuartzCUJ? =
+ data.feedbackEntitiesList.firstOrNull { it.hasQuartzCuj() }?.quartzCuj
+
+ ModalBottomSheet(
+ onDismissRequest = { onDismissRequest() },
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 16.dp).verticalScroll(rememberScrollState())) {
+
+ // Title and back button
+ Row(
+ modifier = Modifier.fillMaxWidth().heightIn(min = 32.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ when (uiState.feedbackDialogMode) {
+ VIEWING_FEEDBACK_DONATION_DATA -> {
+ IconButton(
+ modifier = Modifier.size(32.dp),
+ onClick = { viewModel.updateFeedbackDialogMode(EDITING_FEEDBACK) },
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription =
+ donationData
+ ?.getOrNull()
+ ?.feedbackUiRenderingData
+ ?.feedbackDialogViewDataBackButtonContentDescription
+ ?: data.dialogCommonData.donationDataFailureBackButtonContentDescription,
+ )
+ }
+ }
+ else -> {
+ Spacer(modifier = Modifier.size(32.dp))
+ }
+ }
+
+ val title =
+ when (uiState.feedbackDialogMode) {
+ EDITING_FEEDBACK -> data.title
+ VIEWING_FEEDBACK_DONATION_DATA ->
+ donationData?.getOrNull()?.feedbackUiRenderingData?.feedbackDialogViewDataTitle
+ ?: data.title
+ }
+ Text(text = title, style = MaterialTheme.typography.headlineSmall)
+
+ Spacer(modifier = Modifier.size(32.dp))
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ SameSizeLayout(
+ modifier = Modifier.fillMaxWidth(),
+ baseKey = EDITING_FEEDBACK,
+ activeKey = uiState.feedbackDialogMode,
+ contents =
+ arrayOf(
+ EDITING_FEEDBACK to
+ {
+ MultiFeedbackEditingContent(
+ data = data,
+ selectedSentimentMap = uiState.selectedSentimentMap,
+ tagsSelectionMap = uiState.tagsSelectionMap,
+ freeFormTextMap = uiState.freeFormTextMap,
+ optInChecked = uiState.optInChecked,
+ onSelectedSentimentChanged = viewModel::updateSelectedSentiment,
+ onTagSelectionChanged = { entityContent, sentiment, tag, selected ->
+ viewModel.updateTagSelection(
+ entity = entityContent,
+ sentiment = sentiment,
+ tag = tag,
+ value = selected,
+ singleSelection = (foundQuartzCuj == QuartzCUJ.QUARTZ_CUJ_KEY_TYPE),
+ )
+ },
+ onFreeFormTextChanged = viewModel::updateFreeFormText,
+ onOptInCheckedChanged = viewModel::updateOptInChecked,
+ onViewDataClicked = {
+ viewModel.updateFeedbackDialogMode(VIEWING_FEEDBACK_DONATION_DATA)
+ },
+ )
+ },
+ VIEWING_FEEDBACK_DONATION_DATA to
+ {
+ EntityFeedbackDataCollectionContent(
+ selectedEntityContents =
+ uiState.selectedSentimentMap.entries
+ .filter { it.value != RATING_SENTIMENT_UNDEFINED }
+ .map { it.key },
+ feedbackDonationDataResult = uiState.feedbackDonationData,
+ quartzFeedbackDonationDataResult = uiState.quartzFeedbackDonationData,
+ onBackPressed = { viewModel.updateFeedbackDialogMode(EDITING_FEEDBACK) },
+ onDismissRequest = onDismissRequest,
+ )
+ },
+ ),
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Button
+ Row(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).align(Alignment.End)) {
+ Button(
+ onClick = {
+ if (uiState.feedbackSubmitStatus != FeedbackSubmitState.SUBMIT_FINISHED) {
+ onSendFeedback()
+ }
+ }
+ ) {
+ SameSizeLayout(
+ contentAlignment = Alignment.Center,
+ baseKey = SUBMIT_LABEL_KEY,
+ activeKey =
+ when (uiState.feedbackSubmitStatus != FeedbackSubmitState.SUBMIT_PENDING) {
+ true -> SUBMIT_LABEL_KEY
+ else -> CIRCULAR_INDICATOR_KEY
+ },
+ contents =
+ arrayOf(
+ SUBMIT_LABEL_KEY to
+ {
+ Text(text = data.buttonLabel, style = MaterialTheme.typography.labelLarge)
+ },
+ CIRCULAR_INDICATOR_KEY to
+ {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = MaterialTheme.colorScheme.onPrimary,
+ strokeWidth = 3.dp,
+ )
+ },
+ ),
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
+
+@Composable
+private fun MultiFeedbackEditingContent(
+ data: MultiFeedbackDialogData,
+ selectedSentimentMap: Map,
+ tagsSelectionMap:
+ Map>>,
+ freeFormTextMap: Map,
+ optInChecked: Boolean,
+ onSelectedSentimentChanged: (FeedbackEntityContent, FeedbackRatingSentiment) -> Unit,
+ onTagSelectionChanged:
+ (FeedbackEntityContent, FeedbackRatingSentiment, FeedbackTagData, Boolean) -> Unit,
+ onFreeFormTextChanged: (FeedbackEntityContent, String) -> Unit,
+ onOptInCheckedChanged: (Boolean) -> Unit,
+ onViewDataClicked: () -> Unit,
+) {
+ Column {
+ Column(
+ modifier = Modifier.clip(RoundedCornerShape(20.dp)),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ for (entity in data.feedbackEntitiesList) {
+ MultiFeedbackEntityEditingContent(
+ commonData = data.feedbackEntityCommonData,
+ data = entity,
+ selectedSentiment =
+ selectedSentimentMap[entity.entityContent] ?: RATING_SENTIMENT_UNDEFINED,
+ tagsSelection = tagsSelectionMap[entity.entityContent].orEmpty(),
+ freeFormText = freeFormTextMap[entity.entityContent].orEmpty(),
+ onSelectedSentimentChanged = { onSelectedSentimentChanged(entity.entityContent, it) },
+ onTagSelectionChanged = { sentiment, tag, selected ->
+ onTagSelectionChanged(entity.entityContent, sentiment, tag, selected)
+ },
+ onFreeFormTextChanged = { onFreeFormTextChanged(entity.entityContent, it) },
+ )
+ }
+ }
+
+ Spacer(Modifier.height(16.dp))
+
+ FeedbackOptInContent(
+ modifier = Modifier.fillMaxWidth().padding(top = 12.dp),
+ optInLabel = data.optInLabel,
+ optInLabelLinkPrivacyPolicy = data.optInLabelLinkPrivacyPolicy,
+ optInLabelLinkViewData = data.optInLabelLinkViewData,
+ optInChecked = optInChecked,
+ onOptInCheckedChanged = onOptInCheckedChanged,
+ onViewDataClicked = onViewDataClicked,
+ )
+
+ Spacer(Modifier.height(16.dp))
+ }
+}
+
+@Composable
+private fun MultiFeedbackEntityEditingContent(
+ commonData: FeedbackEntityCommonData,
+ data: FeedbackEntityData,
+ selectedSentiment: FeedbackRatingSentiment,
+ tagsSelection: Map>,
+ freeFormText: String,
+ onSelectedSentimentChanged: (FeedbackRatingSentiment) -> Unit,
+ onTagSelectionChanged: (FeedbackRatingSentiment, FeedbackTagData, Boolean) -> Unit,
+ onFreeFormTextChanged: (String) -> Unit,
+) {
+ Surface(
+ modifier = Modifier.clip(RoundedCornerShape(4.dp)),
+ color = MaterialTheme.colorScheme.surfaceBright,
+ ) {
+ Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp).animateContentSize()) {
+ EntityHeaderContent(
+ commonData = commonData,
+ data = data,
+ selectedSentiment = selectedSentiment,
+ onSelectedSentimentChanged = onSelectedSentimentChanged,
+ )
+
+ val ratingData =
+ when {
+ selectedSentiment == RATING_SENTIMENT_THUMBS_UP && data.hasPositiveRatingData() -> {
+ data.positiveRatingData
+ }
+ selectedSentiment == RATING_SENTIMENT_THUMBS_DOWN && data.hasNegativeRatingData() -> {
+ data.negativeRatingData
+ }
+ else -> null
+ }
+ EntityBodyContent(
+ data = ratingData,
+ tagsSelection = tagsSelection[selectedSentiment].orEmpty(),
+ freeFormText = freeFormText,
+ onTagSelectionChanged = { tag, selected ->
+ onTagSelectionChanged(selectedSentiment, tag, selected)
+ },
+ onFreeFormTextChanged = onFreeFormTextChanged,
+ )
+ }
+ }
+}
+
+@Composable
+private fun EntityHeaderContent(
+ commonData: FeedbackEntityCommonData,
+ data: FeedbackEntityData,
+ selectedSentiment: FeedbackRatingSentiment,
+ onSelectedSentimentChanged: (FeedbackRatingSentiment) -> Unit,
+) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Column(modifier = Modifier.weight(1f).semantics(mergeDescendants = true) {}) {
+ Text(
+ text = data.title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ if (data.hasLabel()) {
+ Text(
+ text = data.label,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+
+ IconButton(
+ modifier =
+ Modifier.size(48.dp)
+ .clip(CircleShape)
+ .then(
+ when (selectedSentiment) {
+ RATING_SENTIMENT_THUMBS_UP ->
+ Modifier.background(color = MaterialTheme.colorScheme.primary, shape = CircleShape)
+ else -> Modifier
+ }
+ ),
+ onClick = {
+ onSelectedSentimentChanged(
+ when (selectedSentiment) {
+ RATING_SENTIMENT_THUMBS_UP -> RATING_SENTIMENT_UNDEFINED
+ else -> RATING_SENTIMENT_THUMBS_UP
+ }
+ )
+ },
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.gs_thumb_up_filled_vd_theme_24),
+ tint =
+ when (selectedSentiment) {
+ RATING_SENTIMENT_THUMBS_UP -> MaterialTheme.colorScheme.onPrimary
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ contentDescription = commonData.thumbsUpButtonContentDescription,
+ )
+ }
+
+ IconButton(
+ modifier =
+ Modifier.size(48.dp)
+ .clip(CircleShape)
+ .then(
+ when (selectedSentiment) {
+ RATING_SENTIMENT_THUMBS_DOWN ->
+ Modifier.background(color = MaterialTheme.colorScheme.primary, shape = CircleShape)
+ else -> Modifier
+ }
+ ),
+ onClick = {
+ onSelectedSentimentChanged(
+ when (selectedSentiment) {
+ RATING_SENTIMENT_THUMBS_DOWN -> RATING_SENTIMENT_UNDEFINED
+ else -> RATING_SENTIMENT_THUMBS_DOWN
+ }
+ )
+ },
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.gs_thumb_down_filled_vd_theme_24),
+ tint =
+ when (selectedSentiment) {
+ RATING_SENTIMENT_THUMBS_DOWN -> MaterialTheme.colorScheme.onPrimary
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ contentDescription = commonData.thumbsDownButtonContentDescription,
+ )
+ }
+ }
+}
+
+@Composable
+private fun EntityBodyContent(
+ data: FeedbackRatingData?,
+ tagsSelection: Map,
+ freeFormText: String,
+ onTagSelectionChanged: (FeedbackTagData, Boolean) -> Unit,
+ onFreeFormTextChanged: (String) -> Unit,
+) {
+ if (data == null) return
+
+ Column(
+ modifier = Modifier.padding(top = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ if (data.hasHeader()) {
+ MainTheme(flexFont = false) {
+ Text(
+ text = data.header,
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+
+ if (data.tagsCount > 0) {
+ EntityFeedbackTagChips(
+ tags = data.tagsList,
+ tagsSelection = tagsSelection,
+ onTagSelectionChanged = onTagSelectionChanged,
+ )
+ }
+
+ if (data.hasFreeFormHint()) {
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth().semantics { contentDescription = data.freeFormHint },
+ value = freeFormText,
+ onValueChange = onFreeFormTextChanged,
+ textStyle = MaterialTheme.typography.bodyMedium,
+ keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
+ minLines = 2,
+ colors =
+ OutlinedTextFieldDefaults.colors(
+ focusedPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ unfocusedPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ ),
+ placeholder = {
+ Text(
+ modifier = Modifier.clearAndSetSemantics {},
+ text = data.freeFormHint,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun EntityFeedbackTagChips(
+ modifier: Modifier = Modifier,
+ tags: List,
+ tagsSelection: Map,
+ onTagSelectionChanged: (FeedbackTagData, Boolean) -> Unit,
+) {
+ FlowRow(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.spacedBy(space = 8.dp, alignment = Alignment.Start),
+ ) {
+ for (tag in tags) {
+ val selected = tagsSelection[tag] == true
+ FilterChip(
+ selected = selected,
+ onClick = { onTagSelectionChanged(tag, !selected) },
+ leadingIcon = {
+ if (selected) Icon(imageVector = Icons.Filled.Check, contentDescription = null)
+ },
+ label = { Text(text = tag.label) },
+ colors =
+ FilterChipDefaults.filterChipColors(
+ labelColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
+ selectedLabelColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+private fun MainTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ flexFont: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme =
+ when {
+ dynamicColor -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> darkColorScheme()
+ else -> lightColorScheme()
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography().let { if (flexFont) it.withFlexFont() else it },
+ ) {
+ content()
+ }
+}
+
+private const val SUBMIT_LABEL_KEY = "submit_label"
+private const val CIRCULAR_INDICATOR_KEY = "circular_indicator"
diff --git a/src/com/google/android/as/oss/feedback/SingleEntityFeedbackDialog.kt b/src/com/google/android/as/oss/feedback/SingleEntityFeedbackDialog.kt
new file mode 100644
index 00000000..ee31f4d0
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/SingleEntityFeedbackDialog.kt
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import android.widget.Toast
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.android.`as`.oss.delegatedui.service.templates.fonts.FlexFontUtils.withFlexFont
+import com.google.android.`as`.oss.feedback.FeedbackDialogMode.EDITING_FEEDBACK
+import com.google.android.`as`.oss.feedback.FeedbackDialogMode.VIEWING_FEEDBACK_DONATION_DATA
+import com.google.android.`as`.oss.feedback.api.EntityFeedbackDialogData
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingTagSource
+import com.google.android.`as`.oss.feedback.api.FeedbackTagData
+import com.google.android.`as`.oss.feedback.api.gateway.SpoonCUJ
+import com.google.android.`as`.oss.feedback.serviceclient.FeedbackDonationData
+import kotlinx.coroutines.launch
+
+/** Dialog displayed when a user gives feedback for a specific entity. */
+@Composable
+fun SingleEntityFeedbackDialog(
+ viewModel: FeedbackViewModel = viewModel(),
+ data: EntityFeedbackDialogData,
+ onFeedbackEvent: (event: FeedbackEvent) -> Unit,
+ onDismissRequest: () -> Unit,
+) {
+ val context = LocalContext.current
+ val uiState by viewModel.uiStateFlow.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.loadDonationData(clientSessionId = data.clientSessionId)
+ launch {
+ viewModel.events.collect { event: FeedbackEvent ->
+ onFeedbackEvent(event)
+
+ val data = uiState.feedbackDonationData?.getOrNull()?.feedbackUiRenderingData
+ val message =
+ when (event) {
+ FeedbackEvent.SUBMISSION_SUCCESSFUL -> data?.feedbackDialogSentSuccessfullyToast
+ FeedbackEvent.SUBMISSION_FAILED -> data?.feedbackDialogSentFailedToast
+ }
+
+ message?.let { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
+ onDismissRequest()
+ }
+ }
+ }
+
+ MainTheme {
+ EntityFeedbackBottomSheet(
+ viewModel = viewModel,
+ data = data,
+ onDismissRequest = onDismissRequest,
+ onSendFeedback = {
+ viewModel.submitFeedback(
+ listOf(
+ FeedbackSubmissionData(
+ cuj =
+ if (data.ratingSentiment == FeedbackRatingSentiment.RATING_SENTIMENT_UNDEFINED) {
+ SpoonCUJ.SPOON_CUJ_OVERALL_FEEDBACK
+ } else {
+ null
+ },
+ selectedEntityContent = data.entityContent,
+ ratingSentiment = data.ratingSentiment,
+ )
+ )
+ )
+ },
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun EntityFeedbackBottomSheet(
+ viewModel: FeedbackViewModel,
+ data: EntityFeedbackDialogData,
+ onDismissRequest: () -> Unit,
+ onSendFeedback: () -> Unit,
+) {
+ val uiState by viewModel.uiStateFlow.collectAsState()
+
+ ModalBottomSheet(
+ onDismissRequest = { onDismissRequest() },
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ dragHandle = null,
+ ) {
+ Box(
+ modifier =
+ Modifier.fillMaxWidth()
+ .heightIn(min = 168.dp)
+ .padding(top = 16.dp)
+ .verticalScroll(rememberScrollState()),
+ contentAlignment = Alignment.Center,
+ ) {
+ uiState.feedbackDonationData.fold(
+ onNull = {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center).padding(16.dp))
+ },
+ onSuccess = { donationData ->
+ EntityFeedbackContents(
+ uiState = uiState,
+ viewModel = viewModel,
+ donationData = donationData,
+ data = data,
+ onSendFeedback = onSendFeedback,
+ onDismissRequest = onDismissRequest,
+ )
+ },
+ onFailure = { FeedbackDataFailureContent(onDismissRequest = onDismissRequest) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun EntityFeedbackContents(
+ uiState: FeedbackUiState,
+ viewModel: FeedbackViewModel,
+ donationData: FeedbackDonationData,
+ data: EntityFeedbackDialogData,
+ onSendFeedback: () -> Unit,
+ onDismissRequest: () -> Unit,
+) {
+ Column(modifier = Modifier.padding(horizontal = 24.dp)) {
+ // Title and back button
+ Row(
+ modifier = Modifier.fillMaxWidth().heightIn(min = 32.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ when (uiState.feedbackDialogMode) {
+ VIEWING_FEEDBACK_DONATION_DATA -> {
+ // Back button
+ IconButton(
+ modifier = Modifier.size(32.dp),
+ onClick = { viewModel.updateFeedbackDialogMode(EDITING_FEEDBACK) },
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription =
+ donationData.feedbackUiRenderingData
+ .feedbackDialogViewDataBackButtonContentDescription,
+ )
+ }
+ }
+
+ else -> {
+ Spacer(modifier = Modifier.size(32.dp))
+ }
+ }
+
+ // Title
+ val title =
+ when (uiState.feedbackDialogMode) {
+ VIEWING_FEEDBACK_DONATION_DATA ->
+ donationData.feedbackUiRenderingData.feedbackDialogViewDataTitle
+
+ else ->
+ if (data.ratingSentiment == FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_UP) {
+ donationData.feedbackUiRenderingData.feedbackDialogGoodFeedbackTitle
+ } else if (
+ data.ratingSentiment == FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_DOWN
+ ) {
+ donationData.feedbackUiRenderingData.feedbackDialogBadFeedbackTitle
+ } else {
+ donationData.feedbackUiRenderingData.feedbackDialogFallbackTitle
+ }
+ }
+ Text(text = title, style = MaterialTheme.typography.headlineSmall)
+
+ Spacer(modifier = Modifier.size(32.dp))
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+
+ SameSizeLayout(
+ modifier = Modifier.fillMaxWidth(),
+ baseKey = EDITING_FEEDBACK,
+ activeKey = uiState.feedbackDialogMode,
+ contents =
+ arrayOf(
+ EDITING_FEEDBACK to
+ {
+ EntityFeedbackEditingContent(
+ data = data,
+ donationData = donationData,
+ tagsSelection =
+ uiState.tagsSelectionMap[data.entityContent]
+ .orEmpty()[data.ratingSentiment]
+ .orEmpty(),
+ freeFormText = uiState.freeFormTextMap[data.entityContent].orEmpty(),
+ optInChecked = uiState.optInChecked,
+ onFreeFormTextChanged = { viewModel.updateFreeFormText(data.entityContent, it) },
+ onOptInCheckedChanged = { viewModel.updateOptInChecked(it) },
+ onViewDataClicked = {
+ viewModel.updateFeedbackDialogMode(VIEWING_FEEDBACK_DONATION_DATA)
+ },
+ onTagSelectionChanged = { tag, selected ->
+ viewModel.updateTagSelection(
+ data.entityContent,
+ data.ratingSentiment,
+ tag,
+ selected,
+ )
+ },
+ )
+ },
+ VIEWING_FEEDBACK_DONATION_DATA to
+ {
+ EntityFeedbackDataCollectionContent(
+ selectedEntityContents = listOf(data.entityContent),
+ feedbackDonationDataResult = Result.success(donationData),
+ onBackPressed = { viewModel.updateFeedbackDialogMode(EDITING_FEEDBACK) },
+ onDismissRequest = onDismissRequest,
+ )
+ },
+ ),
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Button
+ Row(modifier = Modifier.padding(vertical = 4.dp).align(Alignment.End)) {
+ Button(
+ onClick = {
+ if (uiState.feedbackSubmitStatus != FeedbackSubmitState.SUBMIT_FINISHED) {
+ onSendFeedback()
+ }
+ }
+ ) {
+ SameSizeLayout(
+ contentAlignment = Alignment.Center,
+ baseKey = SUBMIT_LABEL_KEY,
+ activeKey =
+ when (uiState.feedbackSubmitStatus != FeedbackSubmitState.SUBMIT_PENDING) {
+ true -> SUBMIT_LABEL_KEY
+ else -> CIRCULAR_INDICATOR_KEY
+ },
+ contents =
+ arrayOf(
+ SUBMIT_LABEL_KEY to
+ {
+ Text(
+ text = donationData.feedbackUiRenderingData.feedbackDialogButtonLabel,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ },
+ CIRCULAR_INDICATOR_KEY to
+ {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = MaterialTheme.colorScheme.onPrimary,
+ strokeWidth = 3.dp,
+ )
+ },
+ ),
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+}
+
+@Composable
+private fun EntityFeedbackEditingContent(
+ data: EntityFeedbackDialogData,
+ tagsSelection: Map,
+ freeFormText: String,
+ optInChecked: Boolean,
+ onFreeFormTextChanged: (String) -> Unit,
+ onOptInCheckedChanged: ((Boolean) -> Unit),
+ onViewDataClicked: () -> Unit,
+ onTagSelectionChanged: (FeedbackTagData, Boolean) -> Unit,
+ donationData: FeedbackDonationData,
+) {
+ Column {
+ // 8.dp outside, 8.dp baked into the chips, total of 24.dp
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Tag chips
+ if (
+ donationData.feedbackUiRenderingData.feedbackChipsList.isNotEmpty() &&
+ data.ratingSentiment != FeedbackRatingSentiment.RATING_SENTIMENT_UNDEFINED
+ ) {
+ EntityFeedbackTagChips(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ tags =
+ donationData.feedbackUiRenderingData.feedbackChipsList.filter {
+ it.ratingTagSource ==
+ if (data.ratingSentiment == FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_UP) {
+ FeedbackRatingTagSource.RATING_TAG_SOURCE_POSITIVE_RATING_TAG
+ } else {
+ FeedbackRatingTagSource.RATING_TAG_SOURCE_NEGATIVE_RATING_TAG
+ }
+ },
+ tagsSelection = tagsSelection,
+ onTagSelectionChanged = onTagSelectionChanged,
+ )
+ // 8.dp baked into the chips, total of 32.dp
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+
+ MainTheme(flexFont = false) {
+ // Freeform text
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth().heightIn(min = 104.dp),
+ value = freeFormText,
+ onValueChange = onFreeFormTextChanged,
+ textStyle = MaterialTheme.typography.bodyLarge,
+ keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
+ label = { Text(text = donationData.feedbackUiRenderingData.feedbackDialogFreeFormLabel) },
+ placeholder = {
+ Text(text = donationData.feedbackUiRenderingData.feedbackDialogFreeFormHint)
+ },
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Opt-in checkbox and label
+ FeedbackOptInContent(
+ modifier = Modifier.fillMaxWidth(),
+ optInLabel =
+ if (data.ratingSentiment == FeedbackRatingSentiment.RATING_SENTIMENT_UNDEFINED) {
+ donationData.feedbackUiRenderingData.feedbackDialogOptInLabelGenericInfoLabel
+ } else {
+ donationData.feedbackUiRenderingData.feedbackDialogOptInLabel
+ },
+ optInLabelLinkPrivacyPolicy =
+ donationData.feedbackUiRenderingData.feedbackDialogOptInLabelLinkPrivacyPolicy,
+ optInLabelLinkViewData =
+ donationData.feedbackUiRenderingData.feedbackDialogOptInLabelLinkViewData,
+ optInChecked = optInChecked,
+ onOptInCheckedChanged = onOptInCheckedChanged,
+ onViewDataClicked = onViewDataClicked,
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+}
+
+@Composable
+private fun EntityFeedbackTagChips(
+ modifier: Modifier,
+ tags: List,
+ tagsSelection: Map,
+ onTagSelectionChanged: (FeedbackTagData, Boolean) -> Unit,
+) {
+ FlowRow(
+ modifier = modifier,
+ horizontalArrangement =
+ Arrangement.spacedBy(space = 8.dp, alignment = Alignment.CenterHorizontally),
+ ) {
+ for (tag in tags) {
+ val selected = tagsSelection[tag] == true
+ FilterChip(
+ selected = selected,
+ onClick = { onTagSelectionChanged(tag, !selected) },
+ leadingIcon = {
+ if (selected) Icon(imageVector = Icons.Filled.Check, contentDescription = null)
+ },
+ label = { Text(text = tag.label) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun MainTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ flexFont: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme =
+ when {
+ dynamicColor -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> darkColorScheme()
+ else -> lightColorScheme()
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography().let { if (flexFont) it.withFlexFont() else it },
+ ) {
+ content()
+ }
+}
+
+private const val SUBMIT_LABEL_KEY = "submit_label"
+private const val CIRCULAR_INDICATOR_KEY = "circular_indicator"
diff --git a/src/com/google/android/as/oss/fl/brella/api/InAppTrainerOptions.aidl b/src/com/google/android/as/oss/feedback/ViewFeedbackData.kt
similarity index 69%
rename from src/com/google/android/as/oss/fl/brella/api/InAppTrainerOptions.aidl
rename to src/com/google/android/as/oss/feedback/ViewFeedbackData.kt
index 4f658693..2f535a18 100644
--- a/src/com/google/android/as/oss/fl/brella/api/InAppTrainerOptions.aidl
+++ b/src/com/google/android/as/oss/feedback/ViewFeedbackData.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,6 +14,10 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.api;
+package com.google.android.`as`.oss.feedback
-parcelable InAppTrainerOptions;
+/** Interface for feedback data to be displayed in the view feedback data dialog. */
+interface ViewFeedbackData {
+ val viewFeedbackHeader: String?
+ val viewFeedbackBody: String
+}
diff --git a/src/com/google/android/as/oss/feedback/ViewFeedbackDataDialog.kt b/src/com/google/android/as/oss/feedback/ViewFeedbackDataDialog.kt
new file mode 100644
index 00000000..c002418a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/ViewFeedbackDataDialog.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.unit.dp
+import com.google.android.`as`.oss.feedback.quartz.serviceclient.QuartzFeedbackDonationData
+import com.google.android.`as`.oss.feedback.serviceclient.FeedbackDonationData
+
+@Composable
+fun EntityFeedbackDataCollectionContent(
+ selectedEntityContents: List,
+ feedbackDonationDataResult: Result?,
+ quartzFeedbackDonationDataResult: Result? = null,
+ onBackPressed: () -> Unit,
+ onDismissRequest: () -> Unit,
+) {
+ BackHandler(enabled = true, onBack = onBackPressed)
+
+ Column {
+ Spacer(modifier = Modifier.height(8.dp))
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ combinedFeedbackDataResults(feedbackDonationDataResult, quartzFeedbackDonationDataResult)
+ .fold(
+ onNull = { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) },
+ onSuccess = { donationData ->
+ ViewFeedbackDataContent(donationData, selectedEntityContents)
+ },
+ onFailure = { FeedbackDataFailureContent(onDismissRequest = onDismissRequest) },
+ )
+ }
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+}
+
+@Composable
+private fun ViewFeedbackDataContent(
+ viewFeedbackData: ViewFeedbackData,
+ selectedEntityContents: List,
+) {
+ val focusManager = LocalFocusManager.current
+ LaunchedEffect(Unit) { focusManager.clearFocus() }
+
+ Column {
+ // Header
+ if (viewFeedbackData.viewFeedbackHeader != null) {
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = viewFeedbackData.viewFeedbackHeader!!,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+
+ // Data collected text
+ val viewDataPrefix =
+ if (selectedEntityContents.any { it.isNotBlank() }) {
+ "Selected Entity Content:\n -" + selectedEntityContents.joinToString("\n -") + "\n"
+ } else {
+ ""
+ }
+ Text(
+ modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
+ text = viewDataPrefix + viewFeedbackData.viewFeedbackBody,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+}
+
+/**
+ * Custom layout that accepts a mapping from [Key] to Composable contents. This layout measures all
+ * contents at the size of [baseKey], and only shows [activeKey].
+ *
+ * If [activeKey] cannot be found, this will show [baseKey] instead.
+ */
+@Composable
+fun SameSizeLayout(
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.TopStart,
+ baseKey: Key,
+ activeKey: Key,
+ vararg contents: Pair Unit>,
+) {
+ val baseContent = checkNotNull(contents[baseKey])
+ val activeContent = if (activeKey != baseKey) contents[activeKey] else null
+
+ Layout(
+ modifier = modifier,
+ content = { // Always compose contentA, even if it might not be placed.
+ Box(modifier = Modifier.wrapContentSize(), contentAlignment = contentAlignment) {
+ baseContent.invoke(this)
+ }
+ Box(modifier = Modifier.wrapContentSize(), contentAlignment = contentAlignment) {
+ activeContent?.invoke(this)
+ }
+ },
+ ) { measurables, constraints ->
+ // Always measure baseContent
+ val baseContentMeasurable = measurables[0]
+ val baseContentPlaceable =
+ baseContentMeasurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
+ val baseContentWidth = baseContentPlaceable.width
+ val baseContentHeight = baseContentPlaceable.height
+
+ val activeContentPlaceable =
+ if (activeContent != null) {
+ // Measure activeContent if it's different
+ val activeContentMeasurable = measurables[1]
+ activeContentMeasurable.measure(
+ constraints.copy(
+ minWidth = baseContentWidth,
+ maxWidth = baseContentWidth,
+ minHeight = baseContentHeight,
+ maxHeight = baseContentHeight,
+ )
+ )
+ } else {
+ // Else use baseContent
+ baseContentPlaceable
+ }
+
+ layout(baseContentWidth, baseContentHeight) { activeContentPlaceable.placeRelative(0, 0) }
+ }
+}
+
+/** Returns the Value of the first Pair whose Key matches the given [key]. */
+private operator fun Array>.get(key: Key): Value? {
+ return firstOrNull { it.first == key }?.second
+}
+
+@Composable
+fun FeedbackDataFailureContent(onDismissRequest: () -> Unit) {
+ Column(
+ modifier = Modifier.wrapContentSize().padding(horizontal = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = "Can't retrieve data",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Spacer(Modifier.height(16.dp))
+ Button(onClick = { onDismissRequest() }) { Text(text = "Try again later") }
+ }
+}
+
+private data class CombinedFeedbackData(
+ val first: ViewFeedbackData?,
+ val second: ViewFeedbackData?,
+) : ViewFeedbackData {
+
+ override val viewFeedbackHeader: String? = first?.viewFeedbackHeader ?: second?.viewFeedbackHeader
+
+ override val viewFeedbackBody: String =
+ listOfNotNull(first?.viewFeedbackBody, second?.viewFeedbackBody).joinToString("\n")
+}
+
+private fun combinedFeedbackDataResults(
+ first: Result?,
+ second: Result?,
+): Result {
+ return if (first?.isSuccess == true || second?.isSuccess == true) {
+ Result.success(CombinedFeedbackData(first?.getOrNull(), second?.getOrNull()))
+ } else {
+ Result.failure(
+ first?.exceptionOrNull() ?: second?.exceptionOrNull() ?: Exception("No exception")
+ )
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/gateway/AndroidManifest.xml b/src/com/google/android/as/oss/feedback/gateway/AndroidManifest.xml
new file mode 100644
index 00000000..718978fb
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/gateway/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/src/com/google/android/as/oss/feedback/gateway/BUILD b/src/com/google/android/as/oss/feedback/gateway/BUILD
new file mode 100644
index 00000000..fcaf822d
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/gateway/BUILD
@@ -0,0 +1,63 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "feedback_http_client",
+ srcs = ["FeedbackHttpClient.kt"],
+ deps = ["//src/com/google/android/as/oss/feedback/proto/gateway:pixel_apex_api_kt_proto_lite"],
+)
+
+android_library(
+ name = "feedback_http_client_impl",
+ srcs = [
+ "FeedbackHttpClientImpl.kt",
+ ],
+ manifest = "AndroidManifest.xml",
+ deps = [
+ ":feedback_http_client",
+ ":http_client_helper",
+ "//java/com/google/apps/tiktok/inject:qualifiers",
+ "//java/com/google/common/annotations",
+ "//src/com/google/android/as/oss/feedback/messagearmour/utils:message_armour_data_helper",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:message_armour_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:pixel_apex_api_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/quartz/utils:quartz_data_helper",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_squareup_okhttp3_okhttp",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "http_client_helper",
+ srcs = ["FeedbackHttpClientHelper.kt"],
+ deps = ["@maven//:com_google_flogger_google_extensions"],
+)
+
+android_library(
+ name = "feedback_http_client_module",
+ srcs = ["FeedbackHttpClientModule.kt"],
+ deps = [
+ ":feedback_http_client",
+ ":feedback_http_client_impl",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ ],
+)
diff --git a/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClient.kt b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClient.kt
new file mode 100644
index 00000000..debf1983
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClient.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.gateway
+
+import com.google.android.`as`.oss.feedback.api.gateway.LogFeedbackV2Request
+
+/**
+ * Http client API that handles Feedback Https requests to APEX backend.
+ *
+ * Example:
+ * ```
+ * val feedbackRequest = logFeedbackV2Request {
+ * appId = "demo"
+ * interactionId = "test"
+ * feedbackCuj = feedbackCUJ { spoonFeedbackCuj = SpoonCUJ.SPOON_CUJ_SUNDOG_EVENT }
+ * rating = Rating.THUMB_UP
+ * positiveTags += PositiveRatingTag.COMPLETE
+ * positiveTags += PositiveRatingTag.CORRECT
+ * additionalComment = "Demo additional comment"
+ * runtimeConfig = runtimeConfig {
+ * appBuildType = "demo build type"
+ * appVersion = "demo app version"
+ * modelMetadata = "demo model metadata"
+ * modelId = "demo model"
+ * }
+ * donationOption = UserDataDonationOption.OPT_IN
+ * userDonation = userDonation {
+ * structuredDataDonation = spoonFeedbackDataDonation {
+ * triggeringMessages += "triggering message"
+ * triggeringMessages += "triggering message 2"
+ * intentQueries += "intent query"
+ * modelOutputs += "model output 1"
+ * modelOutputs += "model output 2"
+ * modelOutputs += "model output 3"
+ * memoryEntities += memoryEntity {
+ * entityData = "entity data 1"
+ * modelVersion = "model version 1"
+ * }
+ * }
+ * }
+ *
+ * scope.launch {
+ * feedbackHttpClient.uploadFeedback(feedbackRequest)
+ * }
+ * ```
+ */
+interface FeedbackHttpClient {
+ fun uploadFeedback(request: LogFeedbackV2Request): Boolean
+}
diff --git a/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientHelper.kt b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientHelper.kt
new file mode 100644
index 00000000..11aaeb61
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientHelper.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.gateway
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.pm.SigningInfo
+import com.google.common.flogger.GoogleLogger
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+
+/**
+ * Returns SHA1 for the application that is passed in network request.
+ *
+ * @param context the [Context]
+ */
+fun getCertFingerprint(context: Context): String? {
+ return try {
+ getCertFingerprint(
+ context.packageManager
+ .getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES)
+ .signingInfo
+ )
+ } catch (impossible: NameNotFoundException) {
+ GoogleLogger.forEnclosingClass().atSevere().withCause(impossible).log("Package not found.")
+ null // Return null in case of exception
+ }
+}
+
+private fun getCertFingerprint(signingInfo: SigningInfo?): String? {
+ val signatures = signingInfo?.apkContentsSigners
+ if (signatures.isNullOrEmpty()) {
+ return null
+ }
+ return try {
+ val sha1 = MessageDigest.getInstance("SHA1")
+ val digest = sha1.digest(signatures[0].toByteArray())
+ bytesToHex(digest)
+ } catch (impossible: NoSuchAlgorithmException) {
+ GoogleLogger.forEnclosingClass().atSevere().withCause(impossible).log("SHA1 not found.")
+ // Log exception
+ null
+ }
+}
+
+private fun bytesToHex(bytes: ByteArray): String {
+ val hexChars = CharArray(bytes.size * 2)
+ for (j in bytes.indices) {
+ val v = bytes[j].toInt() and 0xFF
+ hexChars[j * 2] = HEX_ARRAY[v ushr 4]
+ hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
+ }
+ return String(hexChars)
+}
+
+private val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
diff --git a/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientImpl.kt b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientImpl.kt
new file mode 100644
index 00000000..16a862de
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientImpl.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.gateway
+
+import android.content.Context
+import com.google.android.`as`.oss.feedback.api.gateway.FeedbackCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.LogFeedbackV2Request
+import com.google.android.`as`.oss.feedback.api.gateway.MemoryEntity
+import com.google.android.`as`.oss.feedback.api.gateway.MessageArmourCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.Rating
+import com.google.android.`as`.oss.feedback.api.gateway.RuntimeConfig
+import com.google.android.`as`.oss.feedback.api.gateway.UserDataDonationOption
+import com.google.android.`as`.oss.feedback.api.gateway.UserDonation
+import com.google.android.`as`.oss.feedback.messagearmour.utils.MessageArmourDataHelper
+import com.google.android.`as`.oss.feedback.quartz.utils.QuartzDataHelper
+import com.google.apps.tiktok.inject.ApplicationContext
+import com.google.common.annotations.VisibleForTesting
+import com.google.common.flogger.GoogleLogger
+import java.io.IOException
+import javax.inject.Inject
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+
+/** Http client implementation that handles Feedback Https requests to APEX backend. */
+class FeedbackHttpClientImpl
+@Inject
+internal constructor(
+ private val quartzDataHelper: QuartzDataHelper,
+ private val messageArmourDataHelper: MessageArmourDataHelper,
+ @ApplicationContext private val context: Context,
+) : FeedbackHttpClient {
+
+ /** Uploads the survey results to server. */
+ override fun uploadFeedback(request: LogFeedbackV2Request): Boolean {
+ val client = OkHttpClient.Builder().build()
+ val bodyJsonString =
+ if (request.feedbackCuj.quartzCuj != QuartzCUJ.QUARTZ_CUJ_UNSPECIFIED) {
+ with(quartzDataHelper) { request.convertToQuartzRequestString() }
+ } else if (
+ request.feedbackCuj.messageArmourCuj != MessageArmourCUJ.MESSAGE_ARMOUR_CUJ_UNSPECIFIED
+ ) {
+ with(messageArmourDataHelper) { request.convertToMessageArmourRequestString() }
+ } else {
+ request.convertToRequestString()
+ }
+
+ // Logging all the feedback data transferring to the server as part of the data verifiability
+ // requirement.
+ logger.atInfo().log("APEX server call with request: %s", bodyJsonString)
+
+ val okRequest =
+ Request.Builder()
+ .url(APEX_SERVICE_URL)
+ .addHeader("Content-Type", JSON_CONTENT_TYPE)
+ .addHeader("X-Android-Cert", getCertFingerprint(context) ?: "")
+ .addHeader("X-Android-Package", context.packageName)
+ .post(bodyJsonString.toRequestBody(JSON_MEDIA_TYPE))
+ .build()
+
+ try {
+ val response = client.newCall(okRequest).execute()
+
+ if (response.isSuccessful) {
+ logger.atInfo().log("APEX server call successful")
+ return true
+ } else {
+ logger.atInfo().log("APEX server call failed with response: %s", response.body?.string())
+ }
+ } catch (e: IOException) {
+ logger.atSevere().log("APEX server call failed with exception: %s", e.stackTraceToString())
+ }
+ return false
+ }
+
+ private companion object {
+ private val logger = GoogleLogger.forEnclosingClass()
+
+ const val JSON_CONTENT_TYPE = "application/json"
+ const val API_KEY = ""
+ const val APEX_SERVICE_URL = ""
+ val JSON_MEDIA_TYPE = "$JSON_CONTENT_TYPE; charset=utf-8".toMediaType()
+ }
+}
+
+/** Converts [LogFeedbackV2Request] to a Json-like string that can be parsed by the APEX service. */
+@VisibleForTesting
+fun LogFeedbackV2Request.convertToRequestString(): String {
+ var finalString: String =
+ "{" +
+ "${quote("appId")}: ${quote(appId)}, " +
+ "${quote("interactionId")}: ${quote(interactionId)}, " +
+ "${quote("donationOption")}: ${quote(donationOption.name)}, " +
+ "${quote("appCujType")}: ${getCujTypeString(feedbackCuj)}, " +
+ "${quote("runtimeConfig")}: ${getRuntimeConfigString(runtimeConfig)}"
+
+ if (rating == Rating.THUMB_UP) {
+ finalString = finalString.plus(", ${quote("positiveTags")}: [")
+ for (i in 0 until positiveTagsList.size) {
+ finalString = finalString.plus(quote(positiveTagsList[i].name))
+ // Add comma between tags.
+ if (i < positiveTagsList.size - 1) finalString = finalString.plus(", ")
+ }
+ finalString = finalString.plus("]")
+ }
+
+ if (rating == Rating.THUMB_DOWN) {
+ finalString = finalString.plus(", ${quote("negativeTags")}: [")
+ for (i in 0 until negativeTagsList.size) {
+ finalString = finalString.plus(quote(negativeTagsList[i].name))
+ // Add comma between tags.
+ if (i < negativeTagsList.size - 1) finalString = finalString.plus(", ")
+ }
+ finalString = finalString.plus("]")
+ }
+
+ if (donationOption == UserDataDonationOption.OPT_IN) {
+ finalString = finalString.plus(getDonationDataString(userDonation))
+ }
+
+ // Feedback rating.
+ finalString =
+ finalString.plus(
+ ", ${quote("feedbackRating")}: {${quote("binaryRating")}: ${quote(rating.name)}}"
+ )
+
+ // Feedback additional comment.
+ finalString = finalString.plus(", ${quote("additionalComment")}: ${quote(additionalComment)}")
+
+ // Add the ending indicator.
+ finalString = finalString.plus("}")
+
+ return finalString
+}
+
+private fun getDonationDataString(userDonation: UserDonation): String {
+ var donationString = ""
+ donationString =
+ donationString.plus(
+ ", ${quote("userDonation")}: " +
+ "{${quote("structuredDataDonation")}: " +
+ "{${quote("pixelSpoonDonation")}: " +
+ "{${quote("triggeringMessages")}: " +
+ buildRepeatedMessages(userDonation.structuredDataDonation.triggeringMessagesList) +
+ ", " +
+ "${quote("intentQueries")}: " +
+ buildRepeatedMessages(userDonation.structuredDataDonation.intentQueriesList) +
+ ", " +
+ "${quote("modelOutputs")}: " +
+ buildRepeatedMessages(userDonation.structuredDataDonation.modelOutputsList) +
+ ", " +
+ "${quote("memoryEntities")}: " +
+ buildMemoryEntities(userDonation.structuredDataDonation.memoryEntitiesList) +
+ ", " +
+ "${quote("selectedEntityContent")}: " +
+ quote(userDonation.structuredDataDonation.selectedEntityContent) +
+ "}}"
+ )
+
+ donationString = donationString.plus("}")
+ return donationString
+}
+
+private fun getRuntimeConfigString(config: RuntimeConfig): String {
+ return "{" +
+ "${quote("appBuildType")}: ${quote(config.appBuildType)}, " +
+ "${quote("appVersion")}: ${quote(config.appVersion)}, " +
+ "${quote("modelMetadata")}: ${quote(config.modelMetadata)}, " +
+ "${quote("modelId")}: ${quote(config.modelId)}" +
+ "}"
+}
+
+private fun buildRepeatedMessages(messages: List): String {
+ return "[${messages.map { quote(it) }.joinToString(", ")}]"
+}
+
+private fun buildMemoryEntities(entities: List): String {
+ return "[${ entities.map {"{${quote("entityData")}: ${quote(it.entityData)}, "+
+ "${quote("modelVersion")}: ${quote(it.modelVersion)}}" }.joinToString(", ")}]"
+}
+
+private fun getCujTypeString(appCujType: FeedbackCUJ): String {
+ return "{${quote("pixelSpoonCujType")}: " +
+ "{${quote("pixelSpoonCuj")}: ${quote(appCujType.spoonFeedbackCuj.name)}}}"
+}
+
+private fun quote(content: Any): String = "\"$content\""
diff --git a/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientModule.kt b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientModule.kt
new file mode 100644
index 00000000..f5459609
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/gateway/FeedbackHttpClientModule.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.gateway
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+/** Module that provides the implementation of SurveyHttpClient. */
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface FeedbackHttpClientModule {
+ @Binds
+ fun providesFeedbackHttpClient(feedbackHttpClientImpl: FeedbackHttpClientImpl): FeedbackHttpClient
+}
diff --git a/src/com/google/android/as/oss/feedback/messagearmour/utils/BUILD b/src/com/google/android/as/oss/feedback/messagearmour/utils/BUILD
new file mode 100644
index 00000000..74487559
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/messagearmour/utils/BUILD
@@ -0,0 +1,32 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "message_armour_data_helper",
+ srcs = ["MessageArmourDataHelper.kt"],
+ deps = [
+ "//google/protobuf:timestamp_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback:feedback_ui_state",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:message_armour_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:pixel_apex_api_kt_proto_lite",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/feedback/messagearmour/utils/MessageArmourDataHelper.kt b/src/com/google/android/as/oss/feedback/messagearmour/utils/MessageArmourDataHelper.kt
new file mode 100644
index 00000000..1ca740af
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/messagearmour/utils/MessageArmourDataHelper.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.messagearmour.utils
+
+import com.google.android.`as`.oss.feedback.api.gateway.FeedbackCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.LogFeedbackV2Request
+import com.google.android.`as`.oss.feedback.api.gateway.MessageArmourCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.RuntimeConfig
+import com.google.android.`as`.oss.feedback.api.gateway.UserDataDonationOption
+import com.google.android.`as`.oss.feedback.api.gateway.UserDonation
+import com.google.common.flogger.GoogleLogger
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Helper class for Message Armour feedback data. */
+@Singleton
+class MessageArmourDataHelper @Inject constructor() {
+
+ /**
+ * Converts [LogFeedbackV2Request] to a Json-like string that can be parsed by the APEX service.
+ */
+ fun LogFeedbackV2Request.convertToMessageArmourRequestString(): String {
+ logger
+ .atInfo()
+ .log(
+ "MessageArmourDataHelper#convertToMessageArmourRequestString interactionId: %s",
+ interactionId,
+ )
+
+ var finalString: String =
+ "{" +
+ "${quote("appId")}: ${quote(appId)}, " +
+ "${quote("interactionId")}: ${quote(interactionId)}, " +
+ "${quote("donationOption")}: ${quote(donationOption.name)}, " +
+ "${quote("appCujType")}: ${getMessageArmourCujTypeString(feedbackCuj)}, " +
+ "${quote("runtimeConfig")}: ${getRuntimeConfigString(runtimeConfig)}"
+
+ if (donationOption == UserDataDonationOption.OPT_IN) {
+ finalString = finalString.plus(getDonationDataString(userDonation, feedbackCuj))
+ }
+
+ finalString =
+ finalString.plus(
+ ", ${quote("feedbackRating")}: {${quote("binaryRating")}: ${quote(rating.name)}}"
+ )
+
+ finalString = finalString.plus("}")
+ return finalString
+ }
+
+ private fun getMessageArmourCujTypeString(appCujType: FeedbackCUJ): String =
+ "{${quote("messageArmourCujType")}: " +
+ "{${quote("messageArmourCuj")}: ${quote(appCujType.messageArmourCuj.name)}}}"
+
+ private fun getRuntimeConfigString(config: RuntimeConfig): String =
+ "{" +
+ "${quote("appVersion")}: ${quote(config.appVersion)}, " +
+ "${quote("modelId")}: ${quote(config.modelId)}" +
+ "}"
+
+ private fun getDonationDataString(userDonation: UserDonation, feedbackCuj: FeedbackCUJ): String {
+ var donationString = ""
+ if (feedbackCuj.messageArmourCuj.equals(MessageArmourCUJ.MESSAGE_ARMOUR_CUJ_SCAM_DETECTION)) {
+ donationString =
+ donationString.plus(
+ ", ${quote("userDonation")}: " +
+ "{${quote("structuredDataDonation")}: " +
+ "{${quote("messageArmourDataDonation")}: " +
+ "{${quote("messageArmourUserDataDonation")}: " +
+ "{${quote("userDonatedMessage")}: ${quote(userDonation.messageArmourDataDonation.messageArmourUserDataDonation.userDonatedMessage)}" +
+ "}}}"
+ )
+ donationString = donationString.plus("}")
+ }
+
+ return donationString
+ }
+
+ private fun quote(content: Any): String {
+ val escapedContent = content.toString().replace("\"", "\\\"")
+ return "\"$escapedContent\""
+ }
+
+ private companion object {
+ val logger = GoogleLogger.forEnclosingClass()
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/BUILD b/src/com/google/android/as/oss/feedback/proto/BUILD
new file mode 100644
index 00000000..b0d5f539
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/BUILD
@@ -0,0 +1,99 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+load("//third_party/protobuf/build_defs:kt_jvm_proto_library.bzl", "kt_jvm_lite_proto_library")
+
+package(
+ default_visibility = [
+ "//visibility:public",
+ ],
+)
+
+proto_library(
+ name = "entity_feedback_dialog_data_proto",
+ srcs = ["entity_feedback_dialog_data.proto"],
+ deps = [
+ "",
+ ":feedback_dialog_common_data_proto",
+ ":feedback_tag_data_proto",
+ ],
+)
+
+java_lite_proto_library(
+ name = "entity_feedback_dialog_data_java_proto_lite",
+ deps = [":entity_feedback_dialog_data_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "entity_feedback_dialog_data_kt_proto_lite",
+ deps = [":entity_feedback_dialog_data_proto"],
+)
+
+proto_library(
+ name = "multi_feedback_dialog_data_proto",
+ srcs = ["multi_feedback_dialog_data.proto"],
+ deps = [
+ "",
+ ":feedback_dialog_common_data_proto",
+ ":feedback_tag_data_proto",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_proto",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_proto",
+ ],
+)
+
+java_lite_proto_library(
+ name = "multi_feedback_dialog_data_java_proto_lite",
+ deps = [":multi_feedback_dialog_data_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "multi_feedback_dialog_data_kt_proto_lite",
+ deps = [":multi_feedback_dialog_data_proto"],
+)
+
+proto_library(
+ name = "feedback_tag_data_proto",
+ srcs = ["feedback_tag_data.proto"],
+ deps = [
+ "",
+ ],
+)
+
+java_lite_proto_library(
+ name = "feedback_tag_data_java_proto_lite",
+ deps = [":feedback_tag_data_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "feedback_tag_data_kt_proto_lite",
+ deps = [":feedback_tag_data_proto"],
+)
+
+proto_library(
+ name = "feedback_dialog_common_data_proto",
+ srcs = ["feedback_dialog_common_data.proto"],
+ deps = [""],
+)
+
+java_lite_proto_library(
+ name = "feedback_dialog_common_data_java_proto_lite",
+ deps = [":feedback_dialog_common_data_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "feedback_dialog_common_data_kt_proto_lite",
+ deps = [":feedback_dialog_common_data_proto"],
+)
diff --git a/src/com/google/android/as/oss/feedback/proto/dataservice/BUILD b/src/com/google/android/as/oss/feedback/proto/dataservice/BUILD
new file mode 100644
index 00000000..5e46edb6
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/dataservice/BUILD
@@ -0,0 +1,66 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@rules_proto_grpc//java:defs.bzl", "java_grpc_library")
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+load("//third_party/protobuf/build_defs:kt_jvm_proto_library.bzl", "kt_jvm_lite_proto_library")
+load("//tools/build_defs/kotlin:rules.bzl", "kt_jvm_grpc_library")
+
+package(
+ default_visibility = [
+ "//visibility:public",
+ ],
+)
+
+proto_library(
+ name = "data_service_proto",
+ srcs = [
+ "data_service.proto",
+ "get_feedback_donation_data.proto",
+ ],
+ has_services = True,
+ deps = [
+ "",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_proto",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:pixel_apex_api_proto",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_proto",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_proto",
+ ],
+)
+
+java_lite_proto_library(
+ name = "data_service_java_proto_lite",
+ deps = [":data_service_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "data_service_kt_proto_lite",
+ deps = [":data_service_proto"],
+)
+
+java_grpc_library(
+ name = "data_service_java_grpc",
+ srcs = [":data_service_proto"],
+ constraints = ["android"],
+ flavor = "lite",
+ deps = [":data_service_java_proto_lite"],
+)
+
+kt_jvm_grpc_library(
+ name = "data_service_kt_grpc",
+ srcs = [":data_service_proto"],
+ flavor = "lite",
+ deps = [":data_service_kt_proto_lite"],
+)
diff --git a/src/com/google/android/as/oss/feedback/proto/dataservice/data_service.proto b/src/com/google/android/as/oss/feedback/proto/dataservice/data_service.proto
new file mode 100644
index 00000000..b6b616a3
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/dataservice/data_service.proto
@@ -0,0 +1,38 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api.dataservice;
+
+import "src/com/google/android/as/oss/feedback/proto/dataservice/get_feedback_donation_data.proto";
+
+option java_package = "com.google.android.as.oss.feedback.api.dataservice";
+option java_multiple_files = true;
+
+// Service to provide data for feedback flow.
+//
+// This service proto is defined inside PCS, but the implementation will be
+// offered by other apps.
+service FeedbackDataService {
+ // Get the feedback donation data from from another app.
+ //
+ // The data will be restructured and sent to APEX service by the Feedback
+ // Gateway. User consent needs to be provided prior to the data egress out
+ // of PCC.
+ rpc GetFeedbackDonationData(GetFeedbackDonationDataRequest)
+ returns (GetFeedbackDonationDataResponse) {
+ option deadline = 5.0;
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/dataservice/get_feedback_donation_data.proto b/src/com/google/android/as/oss/feedback/proto/dataservice/get_feedback_donation_data.proto
new file mode 100644
index 00000000..2b9208cf
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/dataservice/get_feedback_donation_data.proto
@@ -0,0 +1,75 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api.dataservice;
+
+import "src/com/google/android/as/oss/feedback/proto/feedback_tag_data.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/pixel_apex_api.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/quartz_log.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/spoon.proto";
+
+option java_package = "com.google.android.as.oss.feedback.api.dataservice";
+option java_multiple_files = true;
+
+// Request for getting feedback donation data.
+message GetFeedbackDonationDataRequest {
+ // The ID that represents the feedback client session.
+ string client_session_id = 1;
+
+ // The type of the UI element user gives feedback to.
+ int32 ui_element_type = 2;
+
+ // The index of the UI element user gives feedback to.
+ //
+ // This field is optional and only needed when there are multiple UI items and
+ // the selected chip needs to be recorded.
+ int32 ui_element_index = 3;
+
+ // The optional field that represents the cuj of Quartz.
+ feedback.api.gateway.QuartzCUJ quartz_cuj = 4;
+}
+
+// Response for GetFeedbackDonationDataRequest.
+message GetFeedbackDonationDataResponse {
+ feedback.api.gateway.UserDonation donation_data = 1;
+ string app_id = 2;
+ string interaction_id = 3;
+ feedback.api.gateway.RuntimeConfig runtime_config = 4;
+ FeedbackUiRenderingData feedback_ui_rendering_data = 5;
+ feedback.api.gateway.SpoonCUJ cuj = 6;
+ // The optional field that represents the cuj of Quartz.
+ feedback.api.gateway.QuartzCUJ quartz_cuj = 7;
+}
+
+// Next ID: 21
+message FeedbackUiRenderingData {
+ string feedback_dialog_good_feedback_title = 5;
+ string feedback_dialog_bad_feedback_title = 6;
+ string feedback_dialog_fallback_title = 19;
+ string feedback_dialog_free_form_label = 7;
+ string feedback_dialog_free_form_hint = 8;
+ string feedback_dialog_opt_in_label = 9;
+ string feedback_dialog_opt_in_label_generic_info_label = 20;
+ string feedback_dialog_opt_in_label_link_privacy_policy = 18;
+ string feedback_dialog_opt_in_label_link_view_data = 10;
+ string feedback_dialog_button_label = 11;
+ repeated FeedbackTagData feedback_chips = 12;
+ string feedback_dialog_view_data_title = 13;
+ string feedback_dialog_view_data_back_button_content_description = 14;
+ string feedback_dialog_view_data_header = 15;
+ string feedback_dialog_sent_successfully_toast = 16;
+ string feedback_dialog_sent_failed_toast = 17;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/entity_feedback_dialog_data.proto b/src/com/google/android/as/oss/feedback/proto/entity_feedback_dialog_data.proto
new file mode 100644
index 00000000..4ebd31ff
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/entity_feedback_dialog_data.proto
@@ -0,0 +1,39 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api;
+
+import "src/com/google/android/as/oss/feedback/proto/feedback_dialog_common_data.proto";
+import "src/com/google/android/as/oss/feedback/proto/feedback_tag_data.proto";
+
+
+option java_package = "com.google.android.as.oss.feedback.api";
+option java_multiple_files = true;
+
+// The data required to populate the feedback UI for a single entity.
+// Next ID: 5
+message EntityFeedbackDialogData {
+ // The content of the entity, uniquely identifies this entity within the same
+ // session.
+ string entity_content = 1;
+ // The client session ID of the entity.
+ string client_session_id = 2;
+ // The rating sentiment of the feedback.
+ FeedbackRatingSentiment rating_sentiment = 3
+ ;
+ // The common dialog data.
+ FeedbackDialogCommonData dialog_common_data = 4;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/feedback_dialog_common_data.proto b/src/com/google/android/as/oss/feedback/proto/feedback_dialog_common_data.proto
new file mode 100644
index 00000000..6c5dddce
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/feedback_dialog_common_data.proto
@@ -0,0 +1,33 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api;
+
+
+option java_package = "com.google.android.as.oss.feedback.api";
+option java_multiple_files = true;
+
+message FeedbackDialogCommonData {
+ // Label shown when donation data can't be fetched.
+ string donation_data_failure_label = 1
+ ;
+ // Button text shown when donation data can't be fetched.
+ string donation_data_failure_button_text = 2
+ ;
+ // Back button content description when donation data can't be fetched.
+ string donation_data_failure_back_button_content_description = 3
+ ;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/feedback_tag_data.proto b/src/com/google/android/as/oss/feedback/proto/feedback_tag_data.proto
new file mode 100644
index 00000000..c2e11c10
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/feedback_tag_data.proto
@@ -0,0 +1,45 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api;
+
+option java_package = "com.google.android.as.oss.feedback.api";
+option java_multiple_files = true;
+
+// Represents a feedback chip.
+message FeedbackTagData {
+ // The label of the tag.
+ string label = 1;
+ // The rating tag enum referenced by rating_tag_ordinal.
+ FeedbackRatingTagSource rating_tag_source = 2;
+ // The rating tag ordinal of the enum specified by rating_tag_source.
+ int32 rating_tag_ordinal = 3;
+}
+
+// The source enum of the rating tag.
+enum FeedbackRatingTagSource {
+ RATING_TAG_SOURCE_UNDEFINED = 0;
+ RATING_TAG_SOURCE_POSITIVE_RATING_TAG = 1;
+ RATING_TAG_SOURCE_NEGATIVE_RATING_TAG = 2;
+ RATING_TAG_SOURCE_KEY_TYPE_OPTION_TAG = 3;
+}
+
+// The sentiment of a feedback rating.
+enum FeedbackRatingSentiment {
+ RATING_SENTIMENT_UNDEFINED = 0;
+ RATING_SENTIMENT_THUMBS_UP = 1;
+ RATING_SENTIMENT_THUMBS_DOWN = 2;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/gateway/BUILD b/src/com/google/android/as/oss/feedback/proto/gateway/BUILD
new file mode 100644
index 00000000..3eb0e915
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/gateway/BUILD
@@ -0,0 +1,91 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+load("//third_party/protobuf/build_defs:kt_jvm_proto_library.bzl", "kt_jvm_lite_proto_library")
+
+package(default_visibility = ["//visibility:public"])
+
+proto_library(
+ name = "spoon_proto",
+ srcs = ["spoon.proto"],
+ deps = [""],
+)
+
+java_lite_proto_library(
+ name = "spoon_java_proto_lite",
+ deps = [":spoon_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "spoon_kt_proto_lite",
+ deps = [":spoon_proto"],
+)
+
+proto_library(
+ name = "quartz_log_proto",
+ srcs = ["quartz_log.proto"],
+ deps = [
+ "",
+ "@com_google_protobuf//:timestamp_proto",
+ ],
+)
+
+kt_jvm_lite_proto_library(
+ name = "quartz_log_kt_proto_lite",
+ deps = [":quartz_log_proto"],
+)
+
+java_lite_proto_library(
+ name = "quartz_log_java_proto_lite",
+ deps = [":quartz_log_proto"],
+)
+
+proto_library(
+ name = "message_armour_proto",
+ srcs = ["message_armour.proto"],
+ deps = [""],
+)
+
+java_lite_proto_library(
+ name = "message_armour_java_proto_lite",
+ deps = [":message_armour_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "message_armour_kt_proto_lite",
+ deps = [":message_armour_proto"],
+)
+
+proto_library(
+ name = "pixel_apex_api_proto",
+ srcs = ["pixel_apex_api.proto"],
+ deps = [
+ "",
+ ":message_armour_proto",
+ ":quartz_log_proto",
+ ":spoon_proto",
+ ],
+)
+
+java_lite_proto_library(
+ name = "pixel_apex_api_java_proto_lite",
+ deps = [":pixel_apex_api_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "pixel_apex_api_kt_proto_lite",
+ deps = [":pixel_apex_api_proto"],
+)
diff --git a/src/com/google/android/as/oss/feedback/proto/gateway/message_armour.proto b/src/com/google/android/as/oss/feedback/proto/gateway/message_armour.proto
new file mode 100644
index 00000000..aefdb53f
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/gateway/message_armour.proto
@@ -0,0 +1,49 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api.gateway;
+
+
+option java_multiple_files = true;
+option java_package = "com.google.android.as.oss.feedback.api.gateway";
+
+// This enum defines the specific CUJs within Message Armour.
+enum MessageArmourCUJ {
+ // Default value. Should not be used explicitly.
+ MESSAGE_ARMOUR_CUJ_UNSPECIFIED = 0;
+
+ // CUJ for user-initiated donations to improve the Message Armour model's
+ // scam detection capabilities.
+ MESSAGE_ARMOUR_CUJ_SCAM_DETECTION = 1;
+}
+
+// This message allows to store specific data related to the Message Armour CUJ,
+// such as user data donation.
+message MessageArmourDataDonation {
+ // This oneof allows to store specific data donation based on the Message
+ // Armour CUJ.
+ oneof cuj_specific_data_donation {
+ // User data related to a message donation.
+ MessageArmourUserDataDonation message_armour_user_data_donation = 1;
+ }
+}
+
+// This message contains user data related to a message donation.
+message MessageArmourUserDataDonation {
+ // The user-donated message.
+ // This field contains the raw message text that the user donated.
+ string user_donated_message = 1;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/gateway/pixel_apex_api.proto b/src/com/google/android/as/oss/feedback/proto/gateway/pixel_apex_api.proto
new file mode 100644
index 00000000..1db0bdb5
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/gateway/pixel_apex_api.proto
@@ -0,0 +1,178 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api.gateway;
+
+import "src/com/google/android/as/oss/feedback/proto/gateway/message_armour.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/quartz_log.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/spoon.proto";
+
+
+option features.field_presence = IMPLICIT;
+option java_api_version = 2;
+option java_multiple_files = true;
+option java_package = "com.google.android.as.oss.feedback.api.gateway";
+
+message LogFeedbackV2Request {
+ // The app id of the feedback.
+ string app_id = 1;
+
+ // The interaction id of the feedback.
+ string interaction_id = 2;
+
+ // The type of the CUJ for clients.
+ FeedbackCUJ feedback_cuj = 3;
+
+ // The runtime config of the app.
+ RuntimeConfig runtime_config = 4;
+
+ // The rating of the feedback.
+ Rating rating = 5;
+
+ // The additional comment of the feedback.
+ string additional_comment = 6;
+
+ // The positive tags of the feedback.
+ repeated PositiveRatingTag positive_tags = 7;
+
+ // The negative tags of the feedback.
+ repeated NegativeRatingTag negative_tags = 8;
+
+ // The donation option of the feedback.
+ UserDataDonationOption donation_option = 9;
+
+ // The user donation of the feedback.
+ UserDonation user_donation = 10;
+
+ // The structured user input sent with the feedback and the data will be
+ // stored in the storage regardless of the user's donation option.
+ StructuredUserInput structured_user_input = 11;
+}
+
+message BlobDonation {
+ // Blob data donation, for example, screenshots of what user currently sees.
+ bytes donation_blob = 1;
+}
+
+// The runtime config of the app.
+message RuntimeConfig {
+ // The build type of the app.
+ string app_build_type = 1;
+
+ // The version of the app.
+ string app_version = 2;
+
+ // The model metadata of the app.
+ // This is a general field for clients to populate.
+ string model_metadata = 3;
+
+ // The id of the model.
+ string model_id = 4;
+}
+
+enum Rating {
+ RATING_UNSPECIFIED = 0;
+ THUMB_UP = 1;
+ THUMB_DOWN = 2;
+}
+
+enum UserDataDonationOption {
+ USER_DATA_DONATION_OPTION_UNSPECIFIED = 0;
+ OPT_IN = 1;
+ OPT_OUT = 2;
+}
+
+// Each client extends this proto to define their own structured user input.
+// This proto is used to store the user input for the online feedback and the
+// data included in this proto will be stored in the storage regardless of the
+// user's donation option.
+message StructuredUserInput {
+ oneof user_input {
+ // This is used to corresponds to the notification category.
+ KeyTypeOptionTag key_type_option = 1
+ ;
+ }
+}
+
+// The user data donation for the Apex API service. Structured proto data
+// donation, storage proto defined by each app.
+message UserDonation {
+ oneof data_donation {
+ SpoonFeedbackDataDonation structured_data_donation = 1
+ ;
+ QuartzDataDonation quartz_data_donation = 3;
+ MessageArmourDataDonation message_armour_data_donation = 4;
+ }
+
+ BlobDonation blob_donation = 2;
+}
+
+// The CUJ for the feedback defined by each app.
+message FeedbackCUJ {
+ SpoonCUJ spoon_feedback_cuj = 1;
+
+ BlobDonation blob_donation = 2;
+
+ QuartzCUJ quartz_cuj = 3;
+
+ MessageArmourCUJ message_armour_cuj = 4
+ ;
+}
+
+enum PositiveRatingTag {
+ POSITIVE_RATING_TAG_UNSPECIFIED = 0;
+ CORRECT = 1;
+ POSITIVE_TAG_OTHER = 2;
+ EASY_TO_UNDERSTAND = 3;
+ COMPLETE = 4;
+ USEFUL_ACTIONS = 5;
+ CORRECT_RESPONSE = 6;
+ CORRECT_SCREENSHOT = 7;
+ HIGH_QUALITY = 8;
+ OTHER = 9;
+}
+
+enum NegativeRatingTag {
+ NEGATIVE_RATING_TAG_UNSPECIFIED = 0;
+ INCORRECT = 1;
+ LEGAL_CONCERN = 2;
+ OFFENSIVE = 3;
+ NEGATIVE_TAG_OTHER = 4;
+ INCORRECT_TITLE = 5;
+ INCORRECT_SUMMARY = 6;
+ MISSING_CRITICAL_INFO = 7;
+ MISSING_ACTIONS = 8;
+ UNHELPFUL_ACTIONS = 9;
+ INCORRECT_RESPONSE = 10;
+ MISSING_RESPONSE_INFO = 11;
+ INCORRECT_SCREENSHOT = 12;
+ MISSING_SCREENSHOT = 13;
+ REPETIVE = 14;
+ IRRELEVANT_CONTENT_ADDED = 15;
+ MEANING_CHANGED = 16;
+ TOO_LATE = 17;
+ INCORRECT_APP = 18;
+ IRRELEVANT_NOW = 19;
+}
+
+enum KeyTypeOptionTag {
+ KEY_TYPE_OPTION_UNSPECIFIED = 0;
+ KEY_TYPE_OPTION_PROMOTION = 1;
+ KEY_TYPE_OPTION_SOCIAL_MEDIA = 2;
+ KEY_TYPE_OPTION_NEWS = 3;
+ KEY_TYPE_OPTION_CONTENT_RECOMMENDATION = 4;
+ KEY_TYPE_OPTION_OTHER = 5;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/gateway/quartz_log.proto b/src/com/google/android/as/oss/feedback/proto/gateway/quartz_log.proto
new file mode 100644
index 00000000..a3a34391
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/gateway/quartz_log.proto
@@ -0,0 +1,139 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api.gateway;
+
+import "google/protobuf/timestamp.proto";
+
+option java_multiple_files = true;
+option java_package = "com.google.android.as.oss.feedback.api.gateway";
+
+enum QuartzCUJ {
+ QUARTZ_CUJ_UNSPECIFIED = 0;
+ QUARTZ_CUJ_KEY_TYPE = 1;
+ QUARTZ_CUJ_KEY_SUMMARIZATION = 2;
+}
+
+message QuartzCommonData {
+ // Replicates the ChannelImportance enum in
+ // frameworks/base/core/java/android/app/NotificationManager.java
+ enum ChannelImportance {
+ CHANNEL_IMPORTANCE_UNSPECIFIED = 0;
+ CHANNEL_IMPORTANCE_NONE = 1;
+ CHANNEL_IMPORTANCE_MIN = 2;
+ CHANNEL_IMPORTANCE_LOW = 3;
+ CHANNEL_IMPORTANCE_DEFAULT = 4;
+ CHANNEL_IMPORTANCE_HIGH = 5;
+ CHANNEL_IMPORTANCE_MAX = 6;
+ }
+
+ // sbn_key is the Android system-generated sbnKey, derived from
+ // StatusBarNotification metadata (package name, tag, id)
+ string sbn_key = 1;
+ string uuid = 2;
+ string asi_version = 3;
+ string package_name = 4;
+ string title = 5;
+ string content = 6;
+ string notification_category = 7;
+ string notification_tag = 8;
+ bool is_conversation = 9;
+ string channel_id = 10;
+ string channel_name = 11;
+ ChannelImportance channel_importance = 12;
+ string channel_description = 13;
+ string channel_conversation_id = 14;
+ string play_store_category = 15;
+ string extra_title = 16;
+ string extra_title_big = 17;
+ string extra_text = 18;
+ string extra_text_lines = 19;
+ string extra_summary_text = 20;
+ string extra_people_list = 21;
+ string extra_messaging_person = 22;
+ string extra_messages = 23;
+ repeated string extra_historic_messages = 24;
+ string extra_conversation_title = 25;
+ string extra_big_text = 26;
+ string extra_info_text = 27;
+ string extra_sub_text = 28;
+ bool extra_is_group_conversation = 29;
+ string extra_picture_content_description = 30;
+ string extra_template = 31;
+ bool extra_show_big_picture_when_collapsed = 32;
+ bool extra_colorized = 33;
+ repeated string extra_remote_input_history = 34;
+ string locus_id = 35;
+ bool has_promotable_characteristics = 36;
+ string group_key = 37;
+}
+
+message QuartzKeyTypeData {
+ enum ClassificationMethod {
+ CLASSIFICATION_METHOD_UNKNOWN = 0;
+ CLASSIFICATION_METHOD_PREDICT_APP_PROVIDED = 1;
+ CLASSIFICATION_METHOD_PREDICT_CLOSE_CONTACT_ACTION = 2;
+ CLASSIFICATION_METHOD_PREDICT_CONTENT = 3;
+ CLASSIFICATION_METHOD_PREDICT_RULES_CORRECTION = 4;
+ CLASSIFICATION_METHOD_PREDICT_EXEMPTION_EXCLUDED_PACKAGE = 5;
+ CLASSIFICATION_METHOD_PREDICT_EXEMPTION_GROUP_SUMMARY = 6;
+ CLASSIFICATION_METHOD_PREDICT_EXEMPTION_UNSUPPORTED_NOTIFICATION_CATEGORY =
+ 7;
+ CLASSIFICATION_METHOD_PREDICT_EXEMPTION_EMPTY_OR_UNPARSEABLE_NOTIFICATION =
+ 8;
+ CLASSIFICATION_METHOD_PREDICT_EXEMPTION_SENDER_FROM_CONTACTS = 9;
+ CLASSIFICATION_METHOD_PREDICT_EXEMPTION_UNSUPPORTED_LANGUAGE = 10;
+ CLASSIFICATION_METHOD_PREDICT_DEFAULT_BUNDLE_CORRECTION = 11;
+ }
+
+ QuartzCommonData quartz_common_data = 1;
+ string notification_id = 2;
+ .google.protobuf.Timestamp post_timestamp = 3;
+ string app_category = 4;
+ repeated string model_info_list = 5;
+ ClassificationMethod classification_method = 6;
+ string classification_bert_category_result = 7;
+ float classification_bert_category_score = 8;
+ int64 classification_bert_category_executed_time_ms = 9;
+ string classification_category = 10;
+ bool is_threshold_changed_category = 11;
+ int64 classification_executed_time_ms = 12;
+ // A map containing the exemption method and execution time, will be
+ // translated into a string to use in code.
+ string exemption_executed_time_ms_string = 13;
+ string feedback_category = 14;
+ string feedback_input_category = 15;
+}
+
+message QuartzKeySummarizationData {
+ QuartzCommonData quartz_common_data = 1;
+ string feature_name = 2;
+ string model_name = 3;
+ string model_version = 4;
+ bool is_group_conversation = 5;
+ string conversation_title = 6;
+ string messages = 7;
+ int32 notification_count = 8;
+ int64 execution_time_ms = 9;
+ string summary_text = 10;
+ string feedback_type = 11;
+ string feedback_additional_detail = 12;
+}
+
+message QuartzDataDonation {
+ QuartzKeyTypeData quartz_key_type_data = 1;
+ QuartzKeySummarizationData quartz_key_summarization_data = 2;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/gateway/spoon.proto b/src/com/google/android/as/oss/feedback/proto/gateway/spoon.proto
new file mode 100644
index 00000000..0fe27d3f
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/gateway/spoon.proto
@@ -0,0 +1,55 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api.gateway;
+
+
+option java_multiple_files = true;
+option java_package = "com.google.android.as.oss.feedback.api.gateway";
+
+enum SpoonCUJ {
+ SPOON_CUJ_UNKNOWN = 0;
+ SPOON_CUJ_MR_NOTIFICATION = 1;
+ SPOON_CUJ_MR_KEYBOARD = 2;
+ SPOON_CUJ_MA_NOTIFICATION = 3;
+ SPOON_CUJ_BUGLE = 4;
+ SPOON_CUJ_SUBZERO_REMINDERS_TILE = 5;
+ SPOON_CUJ_SUNDOG_LOCATION = 6;
+ SPOON_CUJ_SUNDOG_EVENT = 7;
+ SPOON_CUJ_BEACON = 8;
+ SPOON_CUJ_SMART_CLIPBOARD = 10;
+ SPOON_CUJ_AMBIENT_CUE = 11;
+ SPOON_CUJ_OVERALL_FEEDBACK = 12;
+
+ // Retired CUJ enums.
+ SPOON_CUJ_RETIRED_T9 = 9 [deprecated = true];
+}
+
+message SpoonFeedbackDataDonation {
+ repeated string triggering_messages = 1
+ ;
+ repeated string intent_queries = 2
+ ;
+ repeated string model_outputs = 3;
+ repeated MemoryEntity memory_entities = 4;
+ string selected_entity_content = 5
+ ;
+}
+
+message MemoryEntity {
+ string entity_data = 1;
+ string model_version = 2;
+}
diff --git a/src/com/google/android/as/oss/feedback/proto/multi_feedback_dialog_data.proto b/src/com/google/android/as/oss/feedback/proto/multi_feedback_dialog_data.proto
new file mode 100644
index 00000000..093c6f5a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/proto/multi_feedback_dialog_data.proto
@@ -0,0 +1,95 @@
+// Copyright 2025 Google LLC
+//
+// 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.
+
+edition = "2023";
+
+package com.google.android.as.oss.feedback.api;
+
+import "src/com/google/android/as/oss/feedback/proto/feedback_dialog_common_data.proto";
+import "src/com/google/android/as/oss/feedback/proto/feedback_tag_data.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/quartz_log.proto";
+import "src/com/google/android/as/oss/feedback/proto/gateway/spoon.proto";
+
+option java_package = "com.google.android.as.oss.feedback.api";
+option java_multiple_files = true;
+
+// The data required to populate the feedback UI for multiple entities.
+// Next ID: 12
+message MultiFeedbackDialogData {
+ // The client session ID of the entities.
+ string client_session_id = 1;
+ // The title of the dialog.
+ string title = 2;
+
+ // The common dialog data.
+ FeedbackDialogCommonData dialog_common_data = 10;
+ // Common data shared across entities.
+ FeedbackEntityCommonData feedback_entity_common_data = 9;
+
+ // The entities in this feedback dialog.
+ repeated FeedbackEntityData feedback_entities = 3;
+
+ // Label for opt-in checkbox.
+ string opt_in_label = 4;
+ // Clickable label substring that links to the privacy policy.
+ string opt_in_label_link_privacy_policy = 5;
+ // Clickable label substring that links to the view data screen.
+ string opt_in_label_link_view_data = 11;
+ // Label for send button.
+ string button_label = 6;
+
+ // The toast to show when the feedback is submitted successfully.
+ string feedback_dialog_sent_successfully_toast = 7;
+
+ // The toast to show when the feedback submission fails.
+ string feedback_dialog_sent_failed_toast = 8;
+}
+
+message FeedbackEntityCommonData {
+ // The content description of the Thumbs Up button.
+ string thumbs_up_button_content_description = 1;
+ // The content description of the Thumbs Down button.
+ string thumbs_down_button_content_description = 2;
+}
+
+// Next ID: 8
+message FeedbackEntityData {
+ // The CUJ type of the entity.
+ oneof cuj_type {
+ gateway.SpoonCUJ cuj = 1;
+ // The CUJ type of the Quartz entity.
+ gateway.QuartzCUJ quartz_cuj = 7;
+ }
+ // The content of the entity, uniquely identifies this entity within the same
+ // session.
+ string entity_content = 2;
+ // Title shown in the entity header.
+ string title = 3;
+ // Optional label shown under the title, in the entity header.
+ string label = 4;
+
+ // Optional data to show when the user selects positive feedback.
+ FeedbackRatingData positive_rating_data = 5;
+ // Optional data to show when the user selects negative feedback.
+ FeedbackRatingData negative_rating_data = 6;
+}
+
+message FeedbackRatingData {
+ // Header for the feedback card when expanded.
+ string header = 1;
+ // Zero or more feedback tags, rendered as selectable chips.
+ repeated FeedbackTagData tags = 2;
+ // Hint for freeform text input field.
+ string free_form_hint = 3;
+}
diff --git a/src/com/google/android/as/oss/feedback/quartz/serviceclient/BUILD b/src/com/google/android/as/oss/feedback/quartz/serviceclient/BUILD
new file mode 100644
index 00000000..5c27b9b0
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/quartz/serviceclient/BUILD
@@ -0,0 +1,40 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "quartz_service_client",
+ srcs = [
+ "QuartzFeedbackDataServiceClient.kt",
+ "QuartzFeedbackDataServiceClientImpl.kt",
+ "QuartzFeedbackDataServiceClientModule.kt",
+ ],
+ deps = [
+ "//google/protobuf:timestamp_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback:view_feedback_data",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/dataservice:data_service_kt_grpc",
+ "//src/com/google/android/as/oss/feedback/proto/dataservice:data_service_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/serviceclient/serviceconnection:quartz_annotations",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClient.kt b/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClient.kt
new file mode 100644
index 00000000..efbfa420
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClient.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.quartz.serviceclient
+
+import com.google.android.`as`.oss.feedback.ViewFeedbackData
+import com.google.android.`as`.oss.feedback.api.dataservice.FeedbackUiRenderingData
+import com.google.android.`as`.oss.feedback.api.dataservice.feedbackUiRenderingData
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.protobuf.Timestamp
+
+/** Service to provide feedback donation data. */
+interface QuartzFeedbackDataServiceClient {
+ /** Gets the feedback donation data from the feedback data service. */
+ suspend fun getFeedbackDonationData(
+ clientSessionId: String,
+ uiElementType: Int,
+ uiElementIndex: Int? = null,
+ quartzCuj: QuartzCUJ? = null,
+ ): Result
+}
+
+data class QuartzKeySummarizationData(
+ val uuid: String = "",
+ val sbnKey: String = "",
+ val asiVersion: String = "",
+ val featureName: String = "",
+ val packageName: String = "",
+ val modelName: String = "",
+ val modelVersion: String = "",
+ val isGroupConversation: Boolean = false,
+ val conversationTitle: String = "",
+ val messages: String = "",
+ val notificationCount: Int = 0,
+ val executionTimeMs: Long = 0L,
+ val summaryText: String = "",
+)
+
+data class QuartzKeyTypeData(
+ // Fields from QuartzCommonData
+ val sbnKey: String = "",
+ val uuid: String = "",
+ val asiVersion: String = "",
+ val packageName: String = "",
+ val title: String = "",
+ val content: String = "",
+ val notificationCategory: String = "",
+ val notificationTag: String = "",
+ val isConversation: Boolean = false,
+ val channelId: String = "",
+ val channelName: String = "",
+ val channelImportance: String = "",
+ val channelDescription: String = "",
+ val channelConversationId: String = "",
+ val playStoreCategory: String = "",
+ val extraTitle: String = "",
+ val extraTitleBig: String = "",
+ val extraText: String = "",
+ val extraTextLines: String = "",
+ val extraSummaryText: String = "",
+ val extraPeopleList: String = "",
+ val extraMessagingPerson: String = "",
+ val extraMessages: String = "",
+ val extraHistoricMessages: List = emptyList(),
+ val extraConversationTitle: String = "",
+ val extraBigText: String = "",
+ val extraInfoText: String = "",
+ val extraSubText: String = "",
+ val extraIsGroupConversation: Boolean = false,
+ val extraPictureContentDescription: String = "",
+ val extraTemplate: String = "",
+ val extraShowBigPictureWhenCollapsed: Boolean = false,
+ val extraColorized: Boolean = false,
+ val extraRemoteInputHistory: List = emptyList(),
+ val locusId: String = "",
+ val hasPromotableCharacteristics: Boolean = false,
+ val groupKey: String = "",
+
+ // Fields specific to QuartzKeyTypeData
+ val notificationId: String = "",
+ val postTimestamp: Timestamp = Timestamp.getDefaultInstance(),
+ val appCategory: String = "",
+ val modelInfoList: List = emptyList(),
+ val classificationMethod: String = "",
+ val classificationBertCategoryResult: String = "",
+ val classificationBertCategoryScore: Float = 0.0f,
+ val classificationBertCategoryExecutedTimeMs: Long = 0L,
+ val classificationCategory: String = "",
+ val isThresholdChangedCategory: Boolean = false,
+ val classificationExecutedTimeMs: Long = 0L,
+ val exemptionExecutedTimeMsString: String = "",
+) {}
+
+data class QuartzFeedbackDonationData(
+ val appId: String = "",
+ val interactionId: String = "",
+ val quartzCuj: QuartzCUJ = QuartzCUJ.QUARTZ_CUJ_UNSPECIFIED,
+ val summarizationData: QuartzKeySummarizationData = QuartzKeySummarizationData(),
+ val typeData: QuartzKeyTypeData = QuartzKeyTypeData(),
+ val runtimeConfig: QuartzRuntimeConfig = QuartzRuntimeConfig(),
+ val feedbackUiRenderingData: FeedbackUiRenderingData = feedbackUiRenderingData {},
+) : ViewFeedbackData {
+ override val viewFeedbackHeader: String = feedbackUiRenderingData.feedbackDialogViewDataHeader
+ override val viewFeedbackBody: String = toString()
+
+ override fun toString(): String {
+ return buildString {
+ appendLine("appId: $appId")
+ appendLine("interactionId: $interactionId")
+ appendLine("quartzCuj: $quartzCuj")
+ appendLine("runtimeConfig {")
+ appendLine(" appBuildType: ${runtimeConfig.appBuildType}")
+ appendLine(" appVersion: ${runtimeConfig.appVersion}")
+ appendLine(" modelMetadata: ${runtimeConfig.modelMetadata}")
+ appendLine(" modelId: ${runtimeConfig.modelId}")
+ appendLine("}")
+ when (quartzCuj) {
+ QuartzCUJ.QUARTZ_CUJ_KEY_SUMMARIZATION -> {
+ appendLine("uuid: ${summarizationData.uuid}")
+ appendLine("sbnKey: ${summarizationData.sbnKey}")
+ appendLine("asiVersion: ${summarizationData.asiVersion}")
+ appendLine("featureName: ${summarizationData.featureName}")
+ appendLine("packageName: ${summarizationData.packageName}")
+ appendLine("modelName: ${summarizationData.modelName}")
+ appendLine("modelVersion: ${summarizationData.modelVersion}")
+ appendLine("isGroupConversation: ${summarizationData.isGroupConversation}")
+ appendLine("conversationTitle: ${summarizationData.conversationTitle}")
+ appendLine("messages: ${summarizationData.messages}")
+ appendLine("notificationCount: ${summarizationData.notificationCount}")
+ appendLine("executionTimeMs: ${summarizationData.executionTimeMs}")
+ appendLine("summaryText: ${summarizationData.summaryText}")
+ }
+ QuartzCUJ.QUARTZ_CUJ_KEY_TYPE -> {
+ appendLine("sbnKey: ${typeData.sbnKey}")
+ appendLine("uuid: ${typeData.uuid}")
+ appendLine("asiVersion: ${typeData.asiVersion}")
+ appendLine("packageName: ${typeData.packageName}")
+ appendLine("title: ${typeData.title}")
+ appendLine("content: ${typeData.content}")
+ appendLine("notificationCategory: ${typeData.notificationCategory}")
+ appendLine("notificationTag: ${typeData.notificationTag}")
+ appendLine("isConversation: ${typeData.isConversation}")
+ appendLine("channelId: ${typeData.channelId}")
+ appendLine("channelName: ${typeData.channelName}")
+ appendLine("channelImportance: ${typeData.channelImportance}")
+ appendLine("channelDescription: ${typeData.channelDescription}")
+ appendLine("channelConversationId: ${typeData.channelConversationId}")
+ appendLine("playStoreCategory: ${typeData.playStoreCategory}")
+ appendLine("extraTitle: ${typeData.extraTitle}")
+ appendLine("extraTitleBig: ${typeData.extraTitleBig}")
+ appendLine("extraText: ${typeData.extraText}")
+ appendLine("extraTextLines: ${typeData.extraTextLines}")
+ appendLine("extraSummaryText: ${typeData.extraSummaryText}")
+ appendLine("extraPeopleList: ${typeData.extraPeopleList}")
+ appendLine("extraMessagingPerson: ${typeData.extraMessagingPerson}")
+ appendLine("extraMessages: ${typeData.extraMessages}")
+ appendLine("extraHistoricMessages: ${typeData.extraHistoricMessages}")
+ appendLine("extraConversationTitle: ${typeData.extraConversationTitle}")
+ appendLine("extraBigText: ${typeData.extraBigText}")
+ appendLine("extraInfoText: ${typeData.extraInfoText}")
+ appendLine("extraSubText: ${typeData.extraSubText}")
+ appendLine("extraIsGroupConversation: ${typeData.extraIsGroupConversation}")
+ appendLine("extraPictureContentDescription: ${typeData.extraPictureContentDescription}")
+ appendLine("extraTemplate: ${typeData.extraTemplate}")
+ appendLine(
+ "extraShowBigPictureWhenCollapsed: ${typeData.extraShowBigPictureWhenCollapsed}"
+ )
+ appendLine("extraColorized: ${typeData.extraColorized}")
+ appendLine("extraRemoteInputHistory: ${typeData.extraRemoteInputHistory}")
+ appendLine("locusId: ${typeData.locusId}")
+ appendLine("hasPromotableCharacteristics: ${typeData.hasPromotableCharacteristics}")
+ appendLine("groupKey: ${typeData.groupKey}")
+ appendLine("notificationId: ${typeData.notificationId}")
+ appendLine("postTimestamp: ${typeData.postTimestamp}")
+ appendLine("appCategory: ${typeData.appCategory}")
+ appendLine("modelInfoList: ${typeData.modelInfoList}")
+ appendLine("classificationMethod: ${typeData.classificationMethod}")
+ appendLine(
+ "classificationBertCategoryResult: ${typeData.classificationBertCategoryResult}"
+ )
+ appendLine("classificationBertCategoryScore: ${typeData.classificationBertCategoryScore}")
+ appendLine(
+ "classificationBertCategoryExecutedTimeMs: ${typeData.classificationBertCategoryExecutedTimeMs}"
+ )
+ appendLine("classificationCategory: ${typeData.classificationCategory}")
+ appendLine("isThresholdChangedCategory: ${typeData.isThresholdChangedCategory}")
+ appendLine("classificationExecutedTimeMs: ${typeData.classificationExecutedTimeMs}")
+ appendLine("exemptionExecutedTimeMsString: ${typeData.exemptionExecutedTimeMsString}")
+ }
+ else -> {}
+ }
+ }
+ }
+}
+
+data class QuartzRuntimeConfig(
+ val appBuildType: String = "",
+ val appVersion: String = "",
+ val modelMetadata: String = "",
+ val modelId: String = "",
+)
diff --git a/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClientImpl.kt b/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClientImpl.kt
new file mode 100644
index 00000000..bfbff7f9
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClientImpl.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.quartz.serviceclient
+
+import com.google.android.`as`.oss.feedback.api.dataservice.FeedbackDataServiceGrpcKt
+import com.google.android.`as`.oss.feedback.api.dataservice.GetFeedbackDonationDataResponse
+import com.google.android.`as`.oss.feedback.api.dataservice.getFeedbackDonationDataRequest
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.android.`as`.oss.feedback.serviceclient.serviceconnection.QuartzAnnotations.QuartzFeedbackDataService
+import com.google.common.flogger.GoogleLogger
+import com.google.common.flogger.StackSize
+import javax.inject.Inject
+
+class QuartzFeedbackDataServiceClientImpl
+@Inject
+internal constructor(
+ @QuartzFeedbackDataService
+ private val service: FeedbackDataServiceGrpcKt.FeedbackDataServiceCoroutineStub
+) : QuartzFeedbackDataServiceClient {
+ override suspend fun getFeedbackDonationData(
+ clientSessionId: String,
+ uiElementType: Int,
+ uiElementIndex: Int?,
+ quartzCuj: QuartzCUJ?,
+ ): Result {
+ return runCatching {
+ val request = getFeedbackDonationDataRequest {
+ this.clientSessionId = clientSessionId
+ this.uiElementType = uiElementType
+ uiElementIndex?.let { this.uiElementIndex = it }
+ quartzCuj?.let { this.quartzCuj = it }
+ }
+ service.getFeedbackDonationData(request).toFeedbackDonationData(quartzCuj)
+ }
+ .onFailure { e ->
+ logger
+ .atSevere()
+ .withCause(e)
+ .withStackTrace(StackSize.SMALL)
+ .log("Error getting feedback donation data from Quartz service")
+ }
+ }
+
+ private fun GetFeedbackDonationDataResponse.toFeedbackDonationData(
+ quartzCuj: QuartzCUJ?
+ ): QuartzFeedbackDonationData {
+ return QuartzFeedbackDonationData(
+ quartzCuj = quartzCuj ?: QuartzCUJ.QUARTZ_CUJ_UNSPECIFIED,
+ appId = appId,
+ interactionId = interactionId,
+ runtimeConfig =
+ QuartzRuntimeConfig(
+ appBuildType = runtimeConfig.appBuildType,
+ appVersion = runtimeConfig.appVersion,
+ modelMetadata = runtimeConfig.modelMetadata,
+ modelId = runtimeConfig.modelId,
+ ),
+ feedbackUiRenderingData = feedbackUiRenderingData,
+ summarizationData =
+ if (quartzCuj == QuartzCUJ.QUARTZ_CUJ_KEY_SUMMARIZATION) {
+ QuartzKeySummarizationData(
+ uuid = donationData.quartzDataDonation.quartzKeySummarizationData.quartzCommonData.uuid,
+ sbnKey =
+ donationData.quartzDataDonation.quartzKeySummarizationData.quartzCommonData.sbnKey,
+ asiVersion =
+ donationData.quartzDataDonation.quartzKeySummarizationData.quartzCommonData
+ .asiVersion,
+ featureName = donationData.quartzDataDonation.quartzKeySummarizationData.featureName,
+ packageName =
+ donationData.quartzDataDonation.quartzKeySummarizationData.quartzCommonData
+ .packageName,
+ modelName = donationData.quartzDataDonation.quartzKeySummarizationData.modelName,
+ modelVersion = donationData.quartzDataDonation.quartzKeySummarizationData.modelVersion,
+ isGroupConversation =
+ donationData.quartzDataDonation.quartzKeySummarizationData.isGroupConversation,
+ conversationTitle =
+ donationData.quartzDataDonation.quartzKeySummarizationData.conversationTitle,
+ messages = donationData.quartzDataDonation.quartzKeySummarizationData.messages,
+ notificationCount =
+ donationData.quartzDataDonation.quartzKeySummarizationData.notificationCount,
+ executionTimeMs =
+ donationData.quartzDataDonation.quartzKeySummarizationData.executionTimeMs,
+ summaryText = donationData.quartzDataDonation.quartzKeySummarizationData.summaryText,
+ )
+ } else {
+ QuartzKeySummarizationData()
+ },
+ typeData =
+ if (quartzCuj == QuartzCUJ.QUARTZ_CUJ_KEY_TYPE) {
+ QuartzKeyTypeData(
+ sbnKey = donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.sbnKey,
+ uuid = donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.uuid,
+ asiVersion =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.asiVersion,
+ packageName =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.packageName,
+ title = donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.title,
+ content = donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.content,
+ notificationCategory =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .notificationCategory,
+ notificationTag =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.notificationTag,
+ isConversation =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.isConversation,
+ channelId =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.channelId,
+ channelName =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.channelName,
+ channelImportance =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.channelImportance
+ .name,
+ channelDescription =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.channelDescription,
+ channelConversationId =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .channelConversationId,
+ playStoreCategory =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.playStoreCategory,
+ extraTitle =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraTitle,
+ extraTitleBig =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraTitleBig,
+ extraText =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraText,
+ extraTextLines =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraTextLines,
+ extraSummaryText =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraSummaryText,
+ extraPeopleList =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraPeopleList,
+ extraMessagingPerson =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraMessagingPerson,
+ extraMessages =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraMessages,
+ extraHistoricMessages =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraHistoricMessagesList,
+ extraConversationTitle =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraConversationTitle,
+ extraBigText =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraBigText,
+ extraInfoText =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraInfoText,
+ extraSubText =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraSubText,
+ extraIsGroupConversation =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraIsGroupConversation,
+ extraPictureContentDescription =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraPictureContentDescription,
+ extraTemplate =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraTemplate,
+ extraShowBigPictureWhenCollapsed =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraShowBigPictureWhenCollapsed,
+ extraColorized =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.extraColorized,
+ extraRemoteInputHistory =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .extraRemoteInputHistoryList,
+ locusId = donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.locusId,
+ hasPromotableCharacteristics =
+ donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData
+ .hasPromotableCharacteristics,
+ groupKey = donationData.quartzDataDonation.quartzKeyTypeData.quartzCommonData.groupKey,
+ notificationId = donationData.quartzDataDonation.quartzKeyTypeData.notificationId,
+ postTimestamp = donationData.quartzDataDonation.quartzKeyTypeData.postTimestamp,
+ appCategory = donationData.quartzDataDonation.quartzKeyTypeData.appCategory,
+ modelInfoList = donationData.quartzDataDonation.quartzKeyTypeData.modelInfoListList,
+ classificationMethod =
+ donationData.quartzDataDonation.quartzKeyTypeData.classificationMethod.name,
+ classificationBertCategoryResult =
+ donationData.quartzDataDonation.quartzKeyTypeData.classificationBertCategoryResult,
+ classificationBertCategoryScore =
+ donationData.quartzDataDonation.quartzKeyTypeData.classificationBertCategoryScore,
+ classificationBertCategoryExecutedTimeMs =
+ donationData.quartzDataDonation.quartzKeyTypeData
+ .classificationBertCategoryExecutedTimeMs,
+ classificationCategory =
+ donationData.quartzDataDonation.quartzKeyTypeData.classificationCategory,
+ isThresholdChangedCategory =
+ donationData.quartzDataDonation.quartzKeyTypeData.isThresholdChangedCategory,
+ classificationExecutedTimeMs =
+ donationData.quartzDataDonation.quartzKeyTypeData.classificationExecutedTimeMs,
+ exemptionExecutedTimeMsString =
+ donationData.quartzDataDonation.quartzKeyTypeData.exemptionExecutedTimeMsString,
+ )
+ } else {
+ QuartzKeyTypeData()
+ },
+ )
+ }
+
+ private companion object {
+ private val logger = GoogleLogger.forEnclosingClass()
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClientModule.kt b/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClientModule.kt
new file mode 100644
index 00000000..1cc4f33d
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/quartz/serviceclient/QuartzFeedbackDataServiceClientModule.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.quartz.serviceclient
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface QuartzFeedbackDataServiceClientModule {
+ @Binds
+ fun bindQuartzFeedbackDataServiceClient(
+ impl: QuartzFeedbackDataServiceClientImpl
+ ): QuartzFeedbackDataServiceClient
+}
diff --git a/src/com/google/android/as/oss/feedback/quartz/utils/BUILD b/src/com/google/android/as/oss/feedback/quartz/utils/BUILD
new file mode 100644
index 00000000..eca75e00
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/quartz/utils/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "quartz_data_helper",
+ srcs = [
+ "QuartzDataHelper.kt",
+ ],
+ deps = [
+ "//google/protobuf:timestamp_kt_proto_lite",
+ "//java/com/google/protobuf/util/kotlin:javatime_lite-ktx",
+ "//src/com/google/android/as/oss/feedback:feedback_submission_data",
+ "//src/com/google/android/as/oss/feedback:feedback_ui_state",
+ "//src/com/google/android/as/oss/feedback/proto:feedback_tag_data_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:pixel_apex_api_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/quartz/serviceclient:quartz_service_client",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/feedback/quartz/utils/QuartzDataHelper.kt b/src/com/google/android/as/oss/feedback/quartz/utils/QuartzDataHelper.kt
new file mode 100644
index 00000000..c4f4e751
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/quartz/utils/QuartzDataHelper.kt
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.quartz.utils
+
+import com.google.android.`as`.oss.feedback.FeedbackSubmissionData
+import com.google.android.`as`.oss.feedback.FeedbackUiState
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_DOWN
+import com.google.android.`as`.oss.feedback.api.FeedbackRatingSentiment.RATING_SENTIMENT_THUMBS_UP
+import com.google.android.`as`.oss.feedback.api.gateway.FeedbackCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.KeyTypeOptionTag
+import com.google.android.`as`.oss.feedback.api.gateway.LogFeedbackV2Request
+import com.google.android.`as`.oss.feedback.api.gateway.NegativeRatingTag
+import com.google.android.`as`.oss.feedback.api.gateway.PositiveRatingTag
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCommonData
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzKeyTypeData
+import com.google.android.`as`.oss.feedback.api.gateway.Rating
+import com.google.android.`as`.oss.feedback.api.gateway.RuntimeConfig
+import com.google.android.`as`.oss.feedback.api.gateway.UserDataDonationOption
+import com.google.android.`as`.oss.feedback.api.gateway.UserDonation
+import com.google.android.`as`.oss.feedback.api.gateway.feedbackCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.logFeedbackV2Request
+import com.google.android.`as`.oss.feedback.api.gateway.quartzCommonData
+import com.google.android.`as`.oss.feedback.api.gateway.quartzDataDonation
+import com.google.android.`as`.oss.feedback.api.gateway.quartzKeySummarizationData
+import com.google.android.`as`.oss.feedback.api.gateway.quartzKeyTypeData
+import com.google.android.`as`.oss.feedback.api.gateway.runtimeConfig
+import com.google.android.`as`.oss.feedback.api.gateway.structuredUserInput
+import com.google.android.`as`.oss.feedback.api.gateway.userDonation
+import com.google.android.`as`.oss.feedback.quartz.serviceclient.QuartzFeedbackDonationData
+import com.google.common.flogger.GoogleLogger
+import com.google.protobuf.Timestamp
+import com.google.protobuf.util.kotlin.toJavaInstant
+import java.time.Instant
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Helper class for Quartz feedbackdata. */
+@Singleton
+class QuartzDataHelper @Inject constructor() {
+ fun FeedbackSubmissionData.toQuartzFeedbackUploadRequest(
+ data: QuartzFeedbackDonationData,
+ uiState: FeedbackUiState,
+ ): LogFeedbackV2Request? {
+ return logFeedbackV2Request {
+ val submissionData = this@toQuartzFeedbackUploadRequest
+ this.appId = data.appId
+ this.interactionId = data.interactionId
+ this.feedbackCuj = feedbackCUJ { this.quartzCuj = submissionData.quartzCuj ?: data.quartzCuj }
+ this.runtimeConfig = runtimeConfig {
+ appBuildType = data.runtimeConfig.appBuildType
+ appVersion = data.runtimeConfig.appVersion
+ modelMetadata = data.runtimeConfig.modelMetadata
+ modelId = data.runtimeConfig.modelId
+ }
+ this.rating =
+ when (ratingSentiment) {
+ RATING_SENTIMENT_THUMBS_UP -> Rating.THUMB_UP
+ RATING_SENTIMENT_THUMBS_DOWN -> Rating.THUMB_DOWN
+ else -> {
+ logger.atInfo().log("FeedbackViewModel#Rating sentiment not defined. Skipping.")
+ return null
+ }
+ }
+ uiState.tagsSelectionMap[selectedEntityContent]?.get(RATING_SENTIMENT_THUMBS_UP)?.let { entry
+ ->
+ this.positiveTags += entry.keys.map { PositiveRatingTag.entries[it.ratingTagOrdinal] }
+ }
+ uiState.tagsSelectionMap[selectedEntityContent]?.get(RATING_SENTIMENT_THUMBS_DOWN)?.let {
+ entry ->
+ this.negativeTags += entry.keys.map { NegativeRatingTag.entries[it.ratingTagOrdinal] }
+ }
+
+ if (feedbackCuj.quartzCuj == QuartzCUJ.QUARTZ_CUJ_KEY_TYPE) {
+ val firstNegativeRatingTagOrdinal: Int? = negativeTags.firstOrNull()?.number
+ structuredUserInput = structuredUserInput {
+ keyTypeOption =
+ if (firstNegativeRatingTagOrdinal != null) {
+ KeyTypeOptionTag.forNumber(firstNegativeRatingTagOrdinal)
+ ?: KeyTypeOptionTag.KEY_TYPE_OPTION_UNSPECIFIED
+ } else {
+ KeyTypeOptionTag.KEY_TYPE_OPTION_UNSPECIFIED
+ }
+ }
+ }
+
+ additionalComment = uiState.freeFormTextMap[selectedEntityContent] ?: ""
+ donationOption =
+ if (uiState.optInChecked) {
+ UserDataDonationOption.OPT_IN
+ } else {
+ UserDataDonationOption.OPT_OUT
+ }
+ userDonation = userDonation {
+ // Only include donation data if user has opted in the consent.
+ if (uiState.optInChecked) {
+ quartzDataDonation = quartzDataDonation {
+ when (data.quartzCuj) {
+ QuartzCUJ.QUARTZ_CUJ_KEY_TYPE -> {
+ quartzKeyTypeData = quartzKeyTypeData {
+ quartzCommonData = quartzCommonData {
+ sbnKey = data.typeData.sbnKey
+ uuid = data.typeData.uuid
+ asiVersion = data.typeData.asiVersion
+ packageName = data.typeData.packageName
+ title = data.typeData.title
+ content = data.typeData.content
+ notificationCategory = data.typeData.notificationCategory
+ notificationTag = data.typeData.notificationTag
+ isConversation = data.typeData.isConversation
+ channelId = data.typeData.channelId
+ channelName = data.typeData.channelName
+ channelImportance =
+ QuartzCommonData.ChannelImportance.valueOf(data.typeData.channelImportance)
+ channelDescription = data.typeData.channelDescription
+ channelConversationId = data.typeData.channelConversationId
+ playStoreCategory = data.typeData.playStoreCategory
+ extraTitle = data.typeData.extraTitle
+ extraTitleBig = data.typeData.extraTitleBig
+ extraText = data.typeData.extraText
+ extraTextLines = data.typeData.extraTextLines
+ extraSummaryText = data.typeData.extraSummaryText
+ extraPeopleList = data.typeData.extraPeopleList
+ extraMessagingPerson = data.typeData.extraMessagingPerson
+ extraMessages = data.typeData.extraMessages
+ extraHistoricMessages += data.typeData.extraHistoricMessages
+ extraConversationTitle = data.typeData.extraConversationTitle
+ extraBigText = data.typeData.extraBigText
+ extraInfoText = data.typeData.extraInfoText
+ extraSubText = data.typeData.extraSubText
+ extraIsGroupConversation = data.typeData.extraIsGroupConversation
+ extraPictureContentDescription = data.typeData.extraPictureContentDescription
+ extraTemplate = data.typeData.extraTemplate
+ extraShowBigPictureWhenCollapsed =
+ data.typeData.extraShowBigPictureWhenCollapsed
+ extraColorized = data.typeData.extraColorized
+ extraRemoteInputHistory += data.typeData.extraRemoteInputHistory
+ locusId = data.typeData.locusId
+ hasPromotableCharacteristics = data.typeData.hasPromotableCharacteristics
+ groupKey = data.typeData.groupKey
+ }
+ notificationId = data.typeData.notificationId
+ postTimestamp = data.typeData.postTimestamp
+ appCategory = data.typeData.appCategory
+ modelInfoList += data.typeData.modelInfoList
+ classificationMethod =
+ QuartzKeyTypeData.ClassificationMethod.valueOf(
+ data.typeData.classificationMethod
+ )
+ classificationBertCategoryResult = data.typeData.classificationBertCategoryResult
+ classificationBertCategoryScore = data.typeData.classificationBertCategoryScore
+ classificationBertCategoryExecutedTimeMs =
+ data.typeData.classificationBertCategoryExecutedTimeMs
+ classificationCategory = data.typeData.classificationCategory
+ isThresholdChangedCategory = data.typeData.isThresholdChangedCategory
+ classificationExecutedTimeMs = data.typeData.classificationExecutedTimeMs
+ exemptionExecutedTimeMsString = data.typeData.exemptionExecutedTimeMsString
+ }
+ }
+ QuartzCUJ.QUARTZ_CUJ_KEY_SUMMARIZATION -> {
+ quartzKeySummarizationData = quartzKeySummarizationData {
+ quartzCommonData = quartzCommonData {
+ sbnKey = data.summarizationData.sbnKey
+ uuid = data.summarizationData.uuid
+ asiVersion = data.summarizationData.asiVersion
+ packageName = data.summarizationData.packageName
+ }
+ featureName = data.summarizationData.featureName
+ modelName = data.summarizationData.modelName
+ modelVersion = data.summarizationData.modelVersion
+ isGroupConversation = data.summarizationData.isGroupConversation
+ conversationTitle = data.summarizationData.conversationTitle
+ messages = data.summarizationData.messages
+ notificationCount = data.summarizationData.notificationCount
+ executionTimeMs = data.summarizationData.executionTimeMs
+ summaryText = data.summarizationData.summaryText
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun LogFeedbackV2Request.convertToQuartzRequestString(): String {
+ logger
+ .atInfo()
+ .log("QuartzDataHelper#convertToQuartzRequestString interactionId: %s", interactionId)
+
+ var finalString: String =
+ "{" +
+ "${quote("appId")}: ${quote(appId)}, " +
+ "${quote("interactionId")}: ${quote(interactionId)}, " +
+ "${quote("donationOption")}: ${quote(donationOption.name)}, " +
+ "${quote("appCujType")}: ${getQuartzCujTypeString(feedbackCuj)}, " +
+ "${quote("runtimeConfig")}: ${getRuntimeConfigString(runtimeConfig)}"
+
+ if (rating == Rating.THUMB_UP) {
+ finalString = finalString.plus(", ${quote("positiveTags")}: [")
+ for (i in 0 until positiveTagsList.size) {
+ finalString = finalString.plus(quote(positiveTagsList[i].name))
+ // Add comma between tags.
+ if (i < positiveTagsList.size - 1) finalString = finalString.plus(", ")
+ }
+ finalString = finalString.plus("]")
+ }
+
+ // Add QUARTZ_CUJ_KEY_TYPE its own structured user input.
+ if (feedbackCuj.quartzCuj == QuartzCUJ.QUARTZ_CUJ_KEY_TYPE) {
+ finalString =
+ finalString.plus(
+ ", ${quote("structuredUserInput")}: {" +
+ "${quote(PREFIX + "QuartzUserInput")}: {" +
+ "${quote("key_type_option")}: ${quote(structuredUserInput.keyTypeOption.name)}" +
+ "}" +
+ "}"
+ )
+ } else {
+ if (rating == Rating.THUMB_DOWN) {
+ finalString = finalString.plus(", ${quote("negativeTags")}: [")
+ for (i in 0 until negativeTagsList.size) {
+ finalString = finalString.plus(quote(negativeTagsList[i].name))
+ // Add comma between tags.
+ if (i < negativeTagsList.size - 1) finalString = finalString.plus(", ")
+ }
+ finalString = finalString.plus("]")
+ }
+ }
+
+ // UserDonation
+ if (donationOption == UserDataDonationOption.OPT_IN) {
+ finalString = finalString.plus(getDonationDataString(userDonation, feedbackCuj))
+ }
+
+ // Feedback rating.
+ finalString =
+ finalString.plus(
+ ", ${quote("feedbackRating")}: {${quote("binaryRating")}: ${quote(rating.name)}}"
+ )
+
+ // Feedback additional comment.
+ finalString = finalString.plus(", ${quote("additionalComment")}: ${quote(additionalComment)}")
+
+ // Add the ending indicator.
+ finalString = finalString.plus("}")
+ return finalString
+ }
+
+ private fun getQuartzCujTypeString(appCujType: FeedbackCUJ): String {
+ return "{${quote(PREFIX + "QuartzCujType")}: " +
+ "{${quote(PREFIX + "QuartzCuj")}: ${quote("PIXEL_${appCujType.quartzCuj.name}")}}}"
+ }
+
+ private fun getRuntimeConfigString(config: RuntimeConfig): String {
+ return "{" +
+ "${quote("appBuildType")}: ${quote(config.appBuildType)}, " +
+ "${quote("appVersion")}: ${quote(config.appVersion)}, " +
+ "${quote("modelMetadata")}: ${quote(config.modelMetadata)}, " +
+ "${quote("modelId")}: ${quote(config.modelId)}" +
+ "}"
+ }
+
+ private fun getDonationDataString(userDonation: UserDonation, feedbackCuj: FeedbackCUJ): String {
+ val quartzDataDonation = userDonation.quartzDataDonation
+ val summarizationData = quartzDataDonation.quartzKeySummarizationData
+ val quartzKeyTypeData = quartzDataDonation.quartzKeyTypeData
+
+ when (feedbackCuj.quartzCuj) {
+ QuartzCUJ.QUARTZ_CUJ_KEY_TYPE -> {
+ var donationString = ""
+ donationString =
+ donationString.plus(
+ ", ${quote("userDonation")}: " +
+ "{${quote("structuredDataDonation")}: " +
+ "{${quote(PREFIX + "QuartzDonation")}: " +
+ "{${quote(PREFIX + "QuartzKeyTypeData")}: " +
+ "{${quote(PREFIX + "QuartzCommonData")}: " +
+ "${getQuartzCommonDataString(quartzKeyTypeData.quartzCommonData)}, " +
+ "${quote("notificationId")}: ${quote(quartzKeyTypeData.notificationId)}, " +
+ "${quote("postTimestamp")}: ${quote(timestampToJsonString(quartzKeyTypeData.postTimestamp))}, " +
+ "${quote("modelInfoList")}: ${buildRepeatedMessages(quartzKeyTypeData.modelInfoListList)}, " +
+ "${quote("appCategory")}: ${quote(quartzKeyTypeData.appCategory)}, " +
+ "${quote("classificationMethod")}: ${quote(quartzKeyTypeData.classificationMethod.name)}, " +
+ "${quote("classificationBertCategoryResult")}: ${quote(quartzKeyTypeData.classificationBertCategoryResult)}, " +
+ "${quote("classificationBertCategoryScore")}: ${quartzKeyTypeData.classificationBertCategoryScore}, " +
+ "${quote("classificationBertCategoryExecutedTimeMs")}: ${quartzKeyTypeData.classificationBertCategoryExecutedTimeMs}, " +
+ "${quote("classificationCategory")}: ${quote(quartzKeyTypeData.classificationCategory)}, " +
+ "${quote("isThresholdChangedCategory")}: ${quartzKeyTypeData.isThresholdChangedCategory}, " +
+ "${quote("classificationExecutedTimeMs")}: ${quartzKeyTypeData.classificationExecutedTimeMs}, " +
+ "${quote("exemptionExecutedTimeMsString")}: ${quote(quartzKeyTypeData.exemptionExecutedTimeMsString)}, " +
+ "${quote("feedbackCategory")}: ${quote(quartzKeyTypeData.feedbackCategory)}, " +
+ "${quote("feedbackInputCategory")}: ${quote(quartzKeyTypeData.feedbackInputCategory)}" +
+ "}}}"
+ )
+ donationString = donationString.plus("}")
+ return donationString
+ }
+
+ QuartzCUJ.QUARTZ_CUJ_KEY_SUMMARIZATION -> {
+ var donationString = ""
+ donationString =
+ donationString.plus(
+ ", ${quote("userDonation")}: " +
+ "{${quote("structuredDataDonation")}: " +
+ "{${quote(PREFIX + "QuartzDonation")}: " +
+ "{${quote(PREFIX + "QuartzKeySummarizationData")}: " +
+ "{${quote(PREFIX + "QuartzCommonData")}: " +
+ "${getQuartzCommonDataString(summarizationData.quartzCommonData)}, " +
+ "${quote("featureName")}: ${quote(summarizationData.featureName)}, " +
+ "${quote("modelName")}: ${quote(summarizationData.modelName)}, " +
+ "${quote("modelVersion")}: ${quote(summarizationData.modelVersion)}, " +
+ "${quote("isGroupConversation")}: ${summarizationData.isGroupConversation}, " +
+ "${quote("conversationTitle")}: ${quote(summarizationData.conversationTitle)}, " +
+ "${quote("messages")}: ${quote(summarizationData.messages)}, " +
+ "${quote("notificationCount")}: ${summarizationData.notificationCount}, " +
+ "${quote("executionTimeMs")}: ${summarizationData.executionTimeMs}, " +
+ "${quote("summaryText")}: ${quote(summarizationData.summaryText)}, " +
+ "${quote("feedbackType")}: ${quote(summarizationData.feedbackType)}, " +
+ "${quote("feedbackAdditionalDetail")}: ${quote(summarizationData.feedbackAdditionalDetail)}" +
+ "}}}"
+ )
+ donationString = donationString.plus("}")
+ return donationString
+ }
+ else -> {
+ return ""
+ }
+ }
+ }
+
+ private fun getQuartzCommonDataString(commonData: QuartzCommonData): String {
+ return "{" +
+ "${quote("sbnKey")}: ${quote(commonData.sbnKey)}, " +
+ "${quote("uuid")}: ${quote(commonData.uuid)}, " +
+ "${quote("asiVersion")}: ${quote(commonData.asiVersion)}, " +
+ "${quote("packageName")}: ${quote(commonData.packageName)}, " +
+ "${quote("title")}: ${quote(commonData.title)}, " +
+ "${quote("content")}: ${quote(commonData.content)}, " +
+ "${quote("notificationCategory")}: ${quote(commonData.notificationCategory)}, " +
+ "${quote("notificationTag")}: ${quote(commonData.notificationTag)}, " +
+ "${quote("isConversation")}: ${commonData.isConversation}, " +
+ "${quote("channelId")}: ${quote(commonData.channelId)}, " +
+ "${quote("channelName")}: ${quote(commonData.channelName)}, " +
+ "${quote("channelImportance")}: ${quote(commonData.channelImportance.name)}, " +
+ "${quote("channelDescription")}: ${quote(commonData.channelDescription)}, " +
+ "${quote("channelConversationId")}: ${quote(commonData.channelConversationId)}, " +
+ "${quote("playStoreCategory")}: ${quote(commonData.playStoreCategory)}, " +
+ "${quote("extraTitle")}: ${quote(commonData.extraTitle)}, " +
+ "${quote("extraTitleBig")}: ${quote(commonData.extraTitleBig)}, " +
+ "${quote("extraText")}: ${quote(commonData.extraText)}, " +
+ "${quote("extraTextLines")}: ${quote(commonData.extraTextLines)}, " +
+ "${quote("extraSummaryText")}: ${quote(commonData.extraSummaryText)}, " +
+ "${quote("extraPeopleList")}: ${quote(commonData.extraPeopleList)}, " +
+ "${quote("extraMessagingPerson")}: ${quote(commonData.extraMessagingPerson)}, " +
+ "${quote("extraMessages")}: ${quote(commonData.extraMessages)}, " +
+ "${quote("extraHistoricMessages")}: ${buildRepeatedMessages(commonData.extraHistoricMessagesList)}, " +
+ "${quote("extraConversationTitle")}: ${quote(commonData.extraConversationTitle)}, " +
+ "${quote("extraBigText")}: ${quote(commonData.extraBigText)}, " +
+ "${quote("extraInfoText")}: ${quote(commonData.extraInfoText)}, " +
+ "${quote("extraSubText")}: ${quote(commonData.extraSubText)}, " +
+ "${quote("extraIsGroupConversation")}: ${commonData.extraIsGroupConversation}, " +
+ "${quote("extraPictureContentDescription")}: ${quote(commonData.extraPictureContentDescription)}, " +
+ "${quote("extraTemplate")}: ${quote(commonData.extraTemplate)}, " +
+ "${quote("extraShowBigPictureWhenCollapsed")}: ${commonData.extraShowBigPictureWhenCollapsed}, " +
+ "${quote("extraColorized")}: ${commonData.extraColorized}, " +
+ "${quote("extraRemoteInputHistory")}: ${buildRepeatedMessages(commonData.extraRemoteInputHistoryList)}, " +
+ "${quote("locusId")}: ${quote(commonData.locusId)}, " +
+ "${quote("hasPromotableCharacteristics")}: ${commonData.hasPromotableCharacteristics}, " +
+ "${quote("groupKey")}: ${quote(commonData.groupKey)}" +
+ "}"
+ }
+
+ private fun buildRepeatedMessages(messages: List): String {
+ return "[${messages.map { quote(it) }.joinToString(", ")}]"
+ }
+
+ private fun timestampToJsonString(timestamp: Timestamp): String {
+ val instant: Instant = timestamp.toJavaInstant()
+ return DateTimeFormatter.ISO_INSTANT.format(instant.atOffset(ZoneOffset.UTC))
+ }
+
+ private fun quote(content: Any): String {
+ val escapedContent = content.toString().replace("\"", "\\\"")
+ return "\"$escapedContent\""
+ }
+
+ companion object {
+ private val logger = GoogleLogger.forEnclosingClass()
+ const val PREFIX = "pixel"
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/res/drawable/gs_thumb_down_filled_vd_theme_24.xml b/src/com/google/android/as/oss/feedback/res/drawable/gs_thumb_down_filled_vd_theme_24.xml
new file mode 100644
index 00000000..d815e2bc
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/res/drawable/gs_thumb_down_filled_vd_theme_24.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/src/com/google/android/as/oss/feedback/res/drawable/gs_thumb_up_filled_vd_theme_24.xml b/src/com/google/android/as/oss/feedback/res/drawable/gs_thumb_up_filled_vd_theme_24.xml
new file mode 100644
index 00000000..8836a93a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/res/drawable/gs_thumb_up_filled_vd_theme_24.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/BUILD b/src/com/google/android/as/oss/feedback/serviceclient/BUILD
new file mode 100644
index 00000000..78679fd7
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "service_client",
+ srcs = [
+ "FeedbackDataServiceClient.kt",
+ "FeedbackDataServiceClientImpl.kt",
+ "FeedbackDataServiceClientModule.kt",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/feedback:view_feedback_data",
+ "//src/com/google/android/as/oss/feedback/proto/dataservice:data_service_kt_grpc",
+ "//src/com/google/android/as/oss/feedback/proto/dataservice:data_service_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:quartz_log_kt_proto_lite",
+ "//src/com/google/android/as/oss/feedback/proto/gateway:spoon_kt_proto_lite",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClient.kt b/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClient.kt
new file mode 100644
index 00000000..7886a2f3
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClient.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.serviceclient
+
+import com.google.android.`as`.oss.feedback.ViewFeedbackData
+import com.google.android.`as`.oss.feedback.api.dataservice.FeedbackUiRenderingData
+import com.google.android.`as`.oss.feedback.api.dataservice.feedbackUiRenderingData
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.android.`as`.oss.feedback.api.gateway.SpoonCUJ
+
+/** Service to provide feedback donation data. */
+interface FeedbackDataServiceClient {
+ /** Gets the feedback donation data from the feedback data service. */
+ suspend fun getFeedbackDonationData(
+ clientSessionId: String,
+ uiElementType: Int,
+ uiElementIndex: Int? = null,
+ quartzCuj: QuartzCUJ? = null,
+ ): Result
+}
+
+data class FeedbackDonationData(
+ val triggeringMessages: List = emptyList(),
+ val intentQueries: List = emptyList(),
+ val modelOutputs: List = emptyList(),
+ val memoryEntities: List = emptyList(),
+ val appId: String = "",
+ val interactionId: String = "",
+ val runtimeConfig: RuntimeConfig = RuntimeConfig(),
+ val feedbackUiRenderingData: FeedbackUiRenderingData = feedbackUiRenderingData {},
+ val cuj: SpoonCUJ = SpoonCUJ.SPOON_CUJ_UNKNOWN,
+) : ViewFeedbackData {
+ override val viewFeedbackHeader: String? = feedbackUiRenderingData.feedbackDialogViewDataHeader
+ override val viewFeedbackBody: String = toString()
+
+ override fun toString(): String {
+ return buildString {
+ if (triggeringMessages.isNotEmpty()) {
+ appendLine("triggeringMessages {")
+ for (message in triggeringMessages) {
+ appendLine(" $message")
+ }
+ appendLine("}")
+ }
+
+ if (intentQueries.isNotEmpty()) {
+ appendLine("intentQueries {")
+ for (query in intentQueries) {
+ appendLine(" $query")
+ }
+ appendLine("}")
+ }
+
+ if (modelOutputs.isNotEmpty()) {
+ appendLine("modelOutputs {")
+ for (output in modelOutputs) {
+ appendLine(" $output")
+ }
+ appendLine("}")
+ }
+
+ if (memoryEntities.isNotEmpty()) {
+ appendLine("memoryEntities {")
+ for (entity in memoryEntities) {
+ appendLine(" memoryEntity {")
+ appendLine(" entityData: ${entity.entityData}")
+ appendLine(" modelVersion: ${entity.modelVersion}")
+ appendLine(" }")
+ }
+ appendLine("}")
+ }
+
+ append("appId: $appId")
+ appendLine("interactionId: $interactionId")
+ appendLine("runtimeConfig {")
+ appendLine(" appBuildType: ${runtimeConfig.appBuildType}")
+ appendLine(" appVersion: ${runtimeConfig.appVersion}")
+ appendLine(" modelMetadata: ${runtimeConfig.modelMetadata}")
+ appendLine(" modelId: ${runtimeConfig.modelId}")
+ appendLine("}")
+ if (cuj != SpoonCUJ.SPOON_CUJ_OVERALL_FEEDBACK) appendLine("cuj: $cuj")
+ }
+ }
+}
+
+data class MemoryEntity(val entityData: String, val modelVersion: String)
+
+data class RuntimeConfig(
+ val appBuildType: String = "",
+ val appVersion: String = "",
+ val modelMetadata: String = "",
+ val modelId: String = "",
+)
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClientImpl.kt b/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClientImpl.kt
new file mode 100644
index 00000000..65cd4b83
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClientImpl.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.serviceclient
+
+import com.google.android.`as`.oss.feedback.api.dataservice.FeedbackDataServiceGrpcKt
+import com.google.android.`as`.oss.feedback.api.dataservice.GetFeedbackDonationDataResponse
+import com.google.android.`as`.oss.feedback.api.dataservice.getFeedbackDonationDataRequest
+import com.google.android.`as`.oss.feedback.api.gateway.QuartzCUJ
+import com.google.common.flogger.GoogleLogger
+import com.google.common.flogger.StackSize
+import javax.inject.Inject
+
+class FeedbackDataServiceClientImpl
+@Inject
+internal constructor(
+ private val service: FeedbackDataServiceGrpcKt.FeedbackDataServiceCoroutineStub
+) : FeedbackDataServiceClient {
+ override suspend fun getFeedbackDonationData(
+ clientSessionId: String,
+ uiElementType: Int,
+ uiElementIndex: Int?,
+ quartzCuj: QuartzCUJ?,
+ ): Result {
+ return runCatching {
+ val request = getFeedbackDonationDataRequest {
+ this.clientSessionId = clientSessionId
+ this.uiElementType = uiElementType
+ uiElementIndex?.let { this.uiElementIndex = it }
+ quartzCuj?.let { this.quartzCuj = it }
+ }
+ service.getFeedbackDonationData(request).toFeedbackDonationData()
+ }
+ .onFailure { e ->
+ logger
+ .atSevere()
+ .withCause(e)
+ .withStackTrace(StackSize.SMALL)
+ .log("Error getting feedback donation data")
+ }
+ }
+
+ private fun GetFeedbackDonationDataResponse.toFeedbackDonationData(): FeedbackDonationData {
+ return FeedbackDonationData(
+ triggeringMessages = donationData.structuredDataDonation.triggeringMessagesList,
+ intentQueries = donationData.structuredDataDonation.intentQueriesList,
+ modelOutputs = donationData.structuredDataDonation.modelOutputsList,
+ runtimeConfig =
+ RuntimeConfig(
+ appBuildType = runtimeConfig.appBuildType,
+ appVersion = runtimeConfig.appVersion,
+ modelMetadata = runtimeConfig.modelMetadata,
+ modelId = runtimeConfig.modelId,
+ ),
+ appId = appId,
+ interactionId = interactionId,
+ memoryEntities =
+ donationData.structuredDataDonation.memoryEntitiesList.map {
+ MemoryEntity(it.entityData, it.modelVersion)
+ },
+ feedbackUiRenderingData = feedbackUiRenderingData,
+ cuj = cuj,
+ )
+ }
+
+ private companion object {
+ private val logger = GoogleLogger.forEnclosingClass()
+ }
+}
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClientModule.kt b/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClientModule.kt
new file mode 100644
index 00000000..9ce12b0a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/FeedbackDataServiceClientModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.serviceclient
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface FeedbackDataServiceClientModule {
+ @Binds
+ fun bindFeedbackDataServiceClient(impl: FeedbackDataServiceClientImpl): FeedbackDataServiceClient
+}
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/AndroidManifest.xml b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/AndroidManifest.xml
new file mode 100644
index 00000000..70cb1733
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/Annotations.kt b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/Annotations.kt
new file mode 100644
index 00000000..08f0054c
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/Annotations.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.serviceclient.serviceconnection
+
+import javax.inject.Qualifier
+
+/** Annotations for the data service connection. */
+object Annotations {
+ @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class FeedbackDataService
+}
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/BUILD b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/BUILD
new file mode 100644
index 00000000..547e1f9a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/BUILD
@@ -0,0 +1,49 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "annotations",
+ srcs = ["Annotations.kt"],
+ deps = ["@maven//:javax_inject_javax_inject"],
+)
+
+android_library(
+ name = "quartz_annotations",
+ srcs = ["QuartzAnnotations.kt"],
+ deps = ["@maven//:javax_inject_javax_inject"],
+)
+
+android_library(
+ name = "module",
+ srcs = ["ServiceConnectionModule.kt"],
+ exports_manifest = 1,
+ manifest = "AndroidManifest.xml",
+ deps = [
+ ":annotations",
+ ":quartz_annotations",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/common/security:security_policy_utils",
+ "//src/com/google/android/as/oss/common/security/config",
+ "//src/com/google/android/as/oss/feedback/proto/dataservice:data_service_kt_grpc",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:io_grpc_grpc_api",
+ "@maven//:io_grpc_grpc_binder",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/QuartzAnnotations.kt b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/QuartzAnnotations.kt
new file mode 100644
index 00000000..bfa6401a
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/QuartzAnnotations.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.serviceclient.serviceconnection
+
+import javax.inject.Qualifier
+
+/** Annotations for the Quartz feedback data service connection. */
+object QuartzAnnotations {
+ @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class QuartzFeedbackDataService
+}
diff --git a/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/ServiceConnectionModule.kt b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/ServiceConnectionModule.kt
new file mode 100644
index 00000000..260ceeef
--- /dev/null
+++ b/src/com/google/android/as/oss/feedback/serviceclient/serviceconnection/ServiceConnectionModule.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.feedback.serviceclient.serviceconnection
+
+import android.content.Context
+import com.google.android.`as`.oss.common.config.ConfigReader
+import com.google.android.`as`.oss.common.security.SecurityPolicyUtils
+import com.google.android.`as`.oss.common.security.config.PccSecurityConfig
+import com.google.android.`as`.oss.feedback.api.dataservice.FeedbackDataServiceGrpcKt
+import com.google.android.`as`.oss.feedback.serviceclient.serviceconnection.Annotations.FeedbackDataService
+import com.google.android.`as`.oss.feedback.serviceclient.serviceconnection.QuartzAnnotations.QuartzFeedbackDataService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import io.grpc.Channel
+import io.grpc.CompressorRegistry
+import io.grpc.DecompressorRegistry
+import io.grpc.binder.AndroidComponentAddress
+import io.grpc.binder.BinderChannelBuilder
+import io.grpc.binder.InboundParcelablePolicy
+import java.util.concurrent.TimeUnit.MINUTES
+import javax.inject.Singleton
+
+/** A module for providing the feedback data service connection. */
+@Module
+@InstallIn(SingletonComponent::class)
+internal object ServiceConnectionModule {
+
+ @Provides
+ @FeedbackDataService
+ fun feedbackDataServiceAddress(): AndroidComponentAddress =
+ AndroidComponentAddress.forRemoteComponent(
+ DATA_SERVICE_PROVIDER_PACKAGE_NAME,
+ DATA_SERVICE_CLASS_NAME,
+ )
+
+ @Provides
+ @FeedbackDataService
+ @Singleton
+ fun feedbackDataServiceChannel(
+ @ApplicationContext context: Context,
+ @FeedbackDataService address: AndroidComponentAddress,
+ pccSecurityConfigReader: ConfigReader,
+ ): Channel {
+ return BinderChannelBuilder.forAddress(address, context)
+ .securityPolicy(
+ SecurityPolicyUtils.makeSecurityPolicy(
+ pccSecurityConfigReader.config.psiPackageSecurityInfo(),
+ context,
+ /* allowTestKeys= */ !SecurityPolicyUtils.isUserBuild(),
+ )
+ )
+ .inboundParcelablePolicy(
+ InboundParcelablePolicy.newBuilder().setAcceptParcelableMetadataValues(true).build()
+ )
+ // Disable compression by default, since there's little benefit when all communication is
+ // on-device, and it means sending supported-encoding headers with every call.
+ .decompressorRegistry(DecompressorRegistry.emptyInstance())
+ .compressorRegistry(CompressorRegistry.newEmptyInstance())
+ .idleTimeout(1, MINUTES)
+ .build()
+ }
+
+ @Provides
+ fun feedbackDataServiceStub(
+ @FeedbackDataService channel: Channel
+ ): FeedbackDataServiceGrpcKt.FeedbackDataServiceCoroutineStub =
+ FeedbackDataServiceGrpcKt.FeedbackDataServiceCoroutineStub(channel)
+
+ @Provides
+ @QuartzFeedbackDataService
+ fun quartzFeedbackDataServiceAddress(): AndroidComponentAddress =
+ AndroidComponentAddress.forRemoteComponent(
+ QUARTZ_DATA_SERVICE_PROVIDER_PACKAGE_NAME,
+ QUARTZ_DATA_SERVICE_CLASS_NAME,
+ )
+
+ @Provides
+ @QuartzFeedbackDataService
+ @Singleton
+ fun quartzFeedbackDataServiceChannel(
+ @ApplicationContext context: Context,
+ @QuartzFeedbackDataService address: AndroidComponentAddress,
+ pccSecurityConfigReader: ConfigReader,
+ ): Channel {
+ return BinderChannelBuilder.forAddress(address, context)
+ .securityPolicy(
+ SecurityPolicyUtils.makeSecurityPolicy(
+ pccSecurityConfigReader.config.asiPackageSecurityInfo(),
+ context,
+ /* allowTestKeys= */ !SecurityPolicyUtils.isUserBuild(),
+ )
+ )
+ .inboundParcelablePolicy(
+ InboundParcelablePolicy.newBuilder().setAcceptParcelableMetadataValues(true).build()
+ )
+ .decompressorRegistry(DecompressorRegistry.emptyInstance())
+ .compressorRegistry(CompressorRegistry.newEmptyInstance())
+ .idleTimeout(1, MINUTES)
+ .build()
+ }
+
+ @Provides
+ @QuartzFeedbackDataService
+ fun quartzFeedbackDataServiceStub(
+ @QuartzFeedbackDataService channel: Channel
+ ): FeedbackDataServiceGrpcKt.FeedbackDataServiceCoroutineStub =
+ FeedbackDataServiceGrpcKt.FeedbackDataServiceCoroutineStub(channel)
+
+ const val DATA_SERVICE_PROVIDER_PACKAGE_NAME = "com.google.android.apps.pixel.psi"
+ const val DATA_SERVICE_CLASS_NAME =
+ "com.google.android.apps.pixel.psi.service.FeedbackDataService"
+
+ const val QUARTZ_DATA_SERVICE_PROVIDER_PACKAGE_NAME = "com.google.android.as"
+ const val QUARTZ_DATA_SERVICE_CLASS_NAME =
+ "com.google.android.apps.miphone.aiai.echo.notificationintelligence.smartnotification.feedback.service.FeedbackDataServiceEndpoint"
+}
diff --git a/src/com/google/android/as/oss/fl/Annotations.java b/src/com/google/android/as/oss/fl/Annotations.java
index 824ff385..3aa5b021 100644
--- a/src/com/google/android/as/oss/fl/Annotations.java
+++ b/src/com/google/android/as/oss/fl/Annotations.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/BUILD b/src/com/google/android/as/oss/fl/BUILD
new file mode 100644
index 00000000..b65f6e21
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/BUILD
@@ -0,0 +1,42 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "annotations",
+ srcs = ["Annotations.java"],
+ deps = ["@maven//:javax_inject_javax_inject"],
+)
+
+android_library(
+ name = "federated_module",
+ srcs = ["FederatedModule.java"],
+ deps = [
+ ":annotations",
+ "//src/com/google/android/as/oss/common/initializer",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/init",
+ "//src/com/google/android/as/oss/fl/federatedcompute/logging",
+ "//src/com/google/android/as/oss/fl/federatedcompute/training",
+ "//src/com/google/android/as/oss/fl/localcompute:file_copy_start_query",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/FederatedModule.java b/src/com/google/android/as/oss/fl/FederatedModule.java
index 9ef634ab..97334322 100644
--- a/src/com/google/android/as/oss/fl/FederatedModule.java
+++ b/src/com/google/android/as/oss/fl/FederatedModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@
@InstallIn(SingletonComponent.class)
abstract class FederatedModule {
private static final String ASI_CLIENT_NAME = "com.google.android.as";
- private static final String GPPS_CLIENT_NAME = "com.google.android.odad";
+ private static final String GPPS_CLIENT_NAME = "com.google.android.PlayProtect";
@Provides
@ExampleStoreClientsInfo
@@ -46,7 +46,7 @@ static ImmutableMap providePcsClientToExampleStoreActionMap() {
ASI_CLIENT_NAME,
"com.google.android.apps.miphone.aiai.EXAMPLE_STORE_V1",
GPPS_CLIENT_NAME,
- "com.google.android.apps.miphone.odad.EXAMPLE_STORE_V1");
+ "com.google.android.apps.miphone.PlayProtect.EXAMPLE_STORE_V1");
}
@Provides
@@ -56,7 +56,7 @@ static ImmutableMap providePcsClientToResultHandlingActionMap()
ASI_CLIENT_NAME,
"com.google.android.apps.miphone.aiai.COMPUTATION_RESULT_V1",
GPPS_CLIENT_NAME,
- "com.google.android.apps.miphone.odad.COMPUTATION_RESULT_V1");
+ "com.google.android.apps.miphone.PlayProtect.COMPUTATION_RESULT_V1");
}
@Provides
diff --git a/src/com/google/android/as/oss/fl/api/BUILD b/src/com/google/android/as/oss/fl/api/BUILD
new file mode 100644
index 00000000..8a36d09e
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/api/BUILD
@@ -0,0 +1,49 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@rules_proto_grpc//java:defs.bzl", "java_grpc_library")
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+load(
+ "//third_party/protobuf/build_defs:kt_jvm_proto_library.bzl",
+ "kt_jvm_lite_proto_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+proto_library(
+ name = "training_proto",
+ srcs = ["training.proto"],
+ has_services = True,
+)
+
+java_lite_proto_library(
+ name = "training_java_proto_lite",
+ deps = [":training_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "training_kt_proto_lite",
+ deps = [":training_proto"],
+)
+
+java_grpc_library(
+ name = "training_grpc",
+ srcs = [":training_proto"],
+ constraints = ["android"],
+ flavor = "lite",
+ deps = [
+ ":training_java_proto_lite",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/api/training.proto b/src/com/google/android/as/oss/fl/api/training.proto
index ebc40c73..32ff4d74 100644
--- a/src/com/google/android/as/oss/fl/api/training.proto
+++ b/src/com/google/android/as/oss/fl/api/training.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/client/TrainerClient.java b/src/com/google/android/as/oss/fl/client/TrainerClient.java
deleted file mode 100644
index a890f1b7..00000000
--- a/src/com/google/android/as/oss/fl/client/TrainerClient.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.client;
-
-import com.google.android.as.oss.common.ExecutorAnnotations.FlExecutorQualifier;
-import com.google.android.as.oss.fl.api.proto.TrainerOptions;
-import com.google.android.as.oss.fl.api.proto.TrainerResponse;
-import com.google.android.as.oss.fl.api.proto.TrainingServiceGrpc;
-import com.google.common.flogger.GoogleLogger;
-import io.grpc.Channel;
-import io.grpc.stub.StreamObserver;
-import java.util.concurrent.Executor;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * Client class encapsulating low-level transport logic and providing a high-level API for
- * scheduling in-app training through PCS.
- */
-@Singleton
-public class TrainerClient {
- private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
-
- private final Channel channel;
- private final Executor executor;
-
- @Inject
- TrainerClient(Channel channel, @FlExecutorQualifier Executor executor) {
- this.channel = channel;
- this.executor = executor;
- }
-
- public void scheduleTraining(TrainerOptions trainerOptions, TrainerCallback callback) {
- logger.atInfo().log(
- "Preparing to schedule training for population:%s session_name:%s",
- trainerOptions.getPopulationName(), trainerOptions.getSessionName());
-
- TrainingServiceGrpc.TrainingServiceStub stub = TrainingServiceGrpc.newStub(channel);
- stub.scheduleFederatedComputation(
- trainerOptions,
- new StreamObserver() {
- @Override
- public void onNext(TrainerResponse value) {
- executor.execute(
- () -> {
- switch (value.getResponseCode()) {
- case RESPONSE_CODE_SUCCESS:
- callback.onSuccess();
- break;
- case RESPONSE_CODE_UNSUPPORTED_JOB_TYPE:
- callback.onError(
- new IllegalArgumentException(
- "Unsupported job type" + trainerOptions.getJobType().name()));
- break;
- default:
- callback.onError(
- new IllegalArgumentException(
- "Unsupported response code" + value.getResponseCode().getNumber()));
- }
- });
- }
-
- @Override
- public void onError(Throwable t) {
- executor.execute(() -> callback.onError(t));
- }
-
- @Override
- public void onCompleted() {}
- });
- }
-}
diff --git a/src/com/google/android/as/oss/fl/brella/api/AndroidManifest.xml b/src/com/google/android/as/oss/fl/fcp/api/AndroidManifest.xml
similarity index 90%
rename from src/com/google/android/as/oss/fl/brella/api/AndroidManifest.xml
rename to src/com/google/android/as/oss/fl/fcp/api/AndroidManifest.xml
index 80d0ce2d..9d3408fe 100644
--- a/src/com/google/android/as/oss/fl/brella/api/AndroidManifest.xml
+++ b/src/com/google/android/as/oss/fl/fcp/api/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/src/com/google/android/as/oss/fl/brella/service/AstreaExampleStoreService.java b/src/com/google/android/as/oss/fl/fcp/service/AstreaExampleStoreService.java
similarity index 94%
rename from src/com/google/android/as/oss/fl/brella/service/AstreaExampleStoreService.java
rename to src/com/google/android/as/oss/fl/fcp/service/AstreaExampleStoreService.java
index 6e778d7a..dd4bf026 100644
--- a/src/com/google/android/as/oss/fl/brella/service/AstreaExampleStoreService.java
+++ b/src/com/google/android/as/oss/fl/fcp/service/AstreaExampleStoreService.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.service;
+package com.google.android.as.oss.fl.fc.service;
import static com.google.android.as.oss.networkusage.db.ConnectionDetails.ConnectionType.FC_TRAINING_START_QUERY;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -32,12 +32,12 @@
import com.google.android.as.oss.fl.Annotations.AsiPackageName;
import com.google.android.as.oss.fl.Annotations.ExampleStoreClientsInfo;
import com.google.android.as.oss.fl.Annotations.GppsPackageName;
-import com.google.android.as.oss.fl.brella.api.IExampleStore;
-import com.google.android.as.oss.fl.brella.api.StartQueryCallback;
-import com.google.android.as.oss.fl.brella.api.proto.TrainingError;
-import com.google.android.as.oss.fl.brella.service.ConnectionManager.ConnectionType;
-import com.google.android.as.oss.fl.brella.service.util.PolicyConstants;
-import com.google.android.as.oss.fl.brella.service.util.PolicyFinder;
+import com.google.android.as.oss.fl.fc.api.IExampleStore;
+import com.google.android.as.oss.fl.fc.api.StartQueryCallback;
+import com.google.android.as.oss.fl.fc.api.proto.TrainingError;
+import com.google.android.as.oss.fl.fc.service.ConnectionManager.ConnectionType;
+import com.google.android.as.oss.fl.fc.service.util.PolicyConstants;
+import com.google.android.as.oss.fl.fc.service.util.PolicyFinder;
import com.google.android.as.oss.fl.federatedcompute.config.PcsFcFlags;
import com.google.android.as.oss.fl.federatedcompute.statsd.ExampleStoreConnector;
import com.google.android.as.oss.fl.federatedcompute.statsd.config.StatsdConfig;
@@ -49,7 +49,7 @@
import com.google.android.as.oss.networkusage.db.NetworkUsageLogRepository;
import com.google.android.as.oss.networkusage.db.NetworkUsageLogUtils;
import com.google.android.as.oss.networkusage.ui.content.UnrecognizedNetworkRequestException;
-import com.google.android.as.oss.proto.PcsProtos.AstreaQuery;
+import com.google.android.as.oss.proto.PcsProtos.PcsQuery;
import com.google.fcp.client.ExampleStoreService;
import com.google.android.as.oss.policies.api.Policy;
import com.google.common.base.Optional;
@@ -77,7 +77,7 @@
* appropriate delegate in client based on the passed-in query.
*/
@AndroidEntryPoint(ExampleStoreService.class)
-public final class AstreaExampleStoreService extends Hilt_AstreaExampleStoreService {
+public final class PcsExampleStoreService extends Hilt_PcsExampleStoreService {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final String FILECOPY_COLLECTION_PREFIX = "/filecopy";
@@ -103,7 +103,7 @@ public final class AstreaExampleStoreService extends Hilt_AstreaExampleStoreServ
@Override
public void onCreate() {
super.onCreate();
- logger.atFine().log("AstreaExampleStoreService.onCreate()");
+ logger.atFine().log("PcsExampleStoreService.onCreate()");
connectionManager =
new ConnectionManager(
this,
@@ -116,7 +116,7 @@ public void onCreate() {
@Override
public void onDestroy() {
- logger.atFine().log("AstreaExampleStoreService.onDestroy()");
+ logger.atFine().log("PcsExampleStoreService.onDestroy()");
connectionManager.unbindService();
super.onDestroy();
}
@@ -128,7 +128,7 @@ public void startQuery(
@Nonnull byte[] resumptionToken,
@Nonnull QueryCallback callback,
@Nonnull SelectorContext selectorContext) {
- AstreaQuery query = parseCriteria(criteria);
+ PcsQuery query = parseCriteria(criteria);
if (query == null) {
callback.onStartQueryFailure(
TrainingError.TRAINING_ERROR_PCC_FAILED_TO_PARSE_QUERY_VALUE, "Failed to parse query.");
@@ -234,7 +234,7 @@ private boolean taskRequiresSelectorContext(SelectorContext selectorContext) {
}
private Optional extractInstalledPolicyOptional(
- @Nonnull QueryCallback callback, @Nonnull AstreaQuery query) {
+ @Nonnull QueryCallback callback, @Nonnull PcsQuery query) {
if (!query.hasPolicy()) {
logger.atWarning().log("No policy provided in the query.");
callback.onStartQueryFailure(
@@ -261,7 +261,7 @@ private void initializeConnectionAndStartQuery(
byte[] criteria,
byte[] resumptionToken,
@Nonnull QueryCallback callback,
- AstreaQuery query,
+ PcsQuery query,
SelectorContext selectorContext) {
if (!connectionManager.isClientSupported(query.getClientName())) {
callback.onStartQueryFailure(
@@ -351,7 +351,7 @@ private void logUnknownConnection(String featureName) {
}
static boolean checkFederatedConfigs(
- AstreaQuery query, SelectorContext selectorContext, Policy installedPolicy) {
+ PcsQuery query, SelectorContext selectorContext, Policy installedPolicy) {
if (!installedPolicy.getConfigs().containsKey("federatedCompute")) {
logger.atWarning().log("Policy provided doesn't have configs.");
return false;
@@ -441,10 +441,10 @@ static boolean checkFederatedConfigs(
return true;
}
- private static @Nullable AstreaQuery parseCriteria(byte[] criteria) {
+ private static @Nullable PcsQuery parseCriteria(byte[] criteria) {
try {
Any parsedCriteria = Any.parseFrom(criteria, ExtensionRegistryLite.getGeneratedRegistry());
- return AstreaQuery.parseFrom(
+ return PcsQuery.parseFrom(
parsedCriteria.getValue(), ExtensionRegistryLite.getGeneratedRegistry());
} catch (InvalidProtocolBufferException e) {
logger.atWarning().withCause(e).log("Couldn't parse criteria.");
@@ -454,7 +454,7 @@ static boolean checkFederatedConfigs(
// Note: The success/failure status and the upload size in bytes, are reported in another row
// when we receive a LogEvent of kind TRAIN_RESULT_UPLOADED or TRAIN_FAILURE_UPLOADED.
- private void insertNetworkUsageLogRowForTrainingEvent(AstreaQuery query, long runId) {
+ private void insertNetworkUsageLogRowForTrainingEvent(PcsQuery query, long runId) {
if (networkUsageLogRepository.isNetworkUsageLogEnabled()
&& networkUsageLogRepository.getDbExecutor().isPresent()) {
networkUsageLogRepository
diff --git a/src/com/google/android/as/oss/fl/brella/service/AstreaResultHandlingService.java b/src/com/google/android/as/oss/fl/fcp/service/AstreaResultHandlingService.java
similarity index 93%
rename from src/com/google/android/as/oss/fl/brella/service/AstreaResultHandlingService.java
rename to src/com/google/android/as/oss/fl/fcp/service/AstreaResultHandlingService.java
index 754c6cdf..c1bac6ac 100644
--- a/src/com/google/android/as/oss/fl/brella/service/AstreaResultHandlingService.java
+++ b/src/com/google/android/as/oss/fl/fcp/service/AstreaResultHandlingService.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.service;
+package com.google.android.as.oss.fl.fc.service;
import static com.google.android.as.oss.fl.federatedcompute.util.ClassConversionUtils.copyExampleConsumptionList;
import static com.google.android.as.oss.fl.federatedcompute.util.ClassConversionUtils.copyTrainerOptions;
@@ -26,9 +26,9 @@
import com.google.android.as.oss.fl.Annotations.AsiPackageName;
import com.google.android.as.oss.fl.Annotations.GppsPackageName;
import com.google.android.as.oss.fl.Annotations.ResultHandlingClientsInfo;
-import com.google.android.as.oss.fl.brella.api.IInAppResultHandler;
-import com.google.android.as.oss.fl.brella.api.StatusCallback;
-import com.google.android.as.oss.fl.brella.service.ConnectionManager.ConnectionType;
+import com.google.android.as.oss.fl.fc.api.IInAppResultHandler;
+import com.google.android.as.oss.fl.fc.api.StatusCallback;
+import com.google.android.as.oss.fl.fc.service.ConnectionManager.ConnectionType;
import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
import com.google.android.as.oss.fl.localcompute.LocalComputeUtils;
import com.google.android.as.oss.fl.localcompute.PathConversionUtils;
@@ -56,7 +56,7 @@
* passed-in options.
*/
@AndroidEntryPoint(ResultHandlingService.class)
-public class AstreaResultHandlingService extends Hilt_AstreaResultHandlingService {
+public class PcsResultHandlingService extends Hilt_PcsResultHandlingService {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
@Inject @ResultHandlingClientsInfo ImmutableMap packageToActionMap;
@@ -71,7 +71,7 @@ public class AstreaResultHandlingService extends Hilt_AstreaResultHandlingServic
@Override
public void onCreate() {
super.onCreate();
- logger.atFine().log("AstreaResultHandlingService.onCreate()");
+ logger.atFine().log("PcsResultHandlingService.onCreate()");
connectionManager =
new ConnectionManager(
this,
diff --git a/src/com/google/android/as/oss/fl/fcp/service/BUILD b/src/com/google/android/as/oss/fl/fcp/service/BUILD
new file mode 100644
index 00000000..9625ffc8
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/fcp/service/BUILD
@@ -0,0 +1,77 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+ARCS_POLICY = [
+ "//java/com/google/android/libraries/pcc/chronicle/api/policy",
+ "@private_compute_libraries//java/com/google/android/libraries/pcc/chronicle/api/policy/proto:policy_java_proto_lite",
+]
+
+exports_files(
+ ["AndroidManifest.xml"],
+ visibility = ["//src/com/google/android/as/oss/fl/fc/service/scheduler:__subpackages__"],
+)
+
+android_library(
+ name = "service",
+ srcs = glob(["*.java"]),
+ exports_manifest = 1,
+ manifest = "AndroidManifest.xml",
+ deps = ARCS_POLICY + [
+ "//src/com/google/android/as/oss/common",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/base",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/common/consent:opted_in",
+ "//src/com/google/android/as/oss/common/consent/config",
+ "//src/com/google/android/as/oss/common/flavor",
+ "//src/com/google/android/as/oss/fl:annotations",
+ "//src/com/google/android/as/oss/fl/fc/api",
+ "//src/com/google/android/as/oss/fl/fc/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/fc/service/util",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd/module:config_module",
+ "//src/com/google/android/as/oss/fl/federatedcompute/util",
+ "//src/com/google/android/as/oss/fl/localcompute:file_copy_start_query",
+ "//src/com/google/android/as/oss/fl/localcompute:optional_module",
+ "//src/com/google/android/as/oss/fl/localcompute:resource_manager",
+ "//src/com/google/android/as/oss/fl/localcompute:utils",
+ "//src/com/google/android/as/oss/logging:api",
+ "//src/com/google/android/as/oss/logging:atoms_java_proto_lite",
+ "//src/com/google/android/as/oss/logging:enums_java_proto_lite",
+ "//src/com/google/android/as/oss/networkusage/api:connection_key_java_proto_lite",
+ "//src/com/google/android/as/oss/networkusage/db",
+ "//src/com/google/android/as/oss/networkusage/db:repository",
+ "//src/com/google/android/as/oss/networkusage/ui/content",
+ "//src/com/google/android/as/oss/protos:pcs_query_java_proto_lite",
+ "//third_party/fcp/client:selector_context_java_proto_lite",
+ "//third_party/java/proto:any_java_proto_lite",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:androidx_core_core",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_lite",
+ "@maven//:javax_inject_javax_inject",
+ "@maven//:org_checkerframework_checker_qual",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/brella/service/ConnectionManager.java b/src/com/google/android/as/oss/fl/fcp/service/ConnectionManager.java
similarity index 97%
rename from src/com/google/android/as/oss/fl/brella/service/ConnectionManager.java
rename to src/com/google/android/as/oss/fl/fcp/service/ConnectionManager.java
index 61580ffe..b340f427 100644
--- a/src/com/google/android/as/oss/fl/brella/service/ConnectionManager.java
+++ b/src/com/google/android/as/oss/fl/fcp/service/ConnectionManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.service;
+package com.google.android.as.oss.fl.fc.service;
import android.content.ComponentName;
import android.content.Context;
@@ -25,8 +25,8 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
import com.google.android.as.oss.common.base.SafeInitializer;
-import com.google.android.as.oss.fl.brella.api.IExampleStore;
-import com.google.android.as.oss.fl.brella.api.IInAppResultHandler;
+import com.google.android.as.oss.fl.fc.api.IExampleStore;
+import com.google.android.as.oss.fl.fc.api.IInAppResultHandler;
import com.google.android.as.oss.logging.PcsAtomsProto.IntelligenceCountReported;
import com.google.android.as.oss.logging.PcsStatsEnums.CountMetricId;
import com.google.android.as.oss.logging.PcsStatsLog;
@@ -150,7 +150,7 @@ boolean isClientSupported(String clientName) {
}
// GPPS population or local computation session prefix.
- if (populationOrSessionName.startsWith("odad/")) {
+ if (populationOrSessionName.startsWith("PlayProtect/")) {
return gppsPackageName;
}
return null;
diff --git a/src/com/google/android/as/oss/fl/fcp/service/scheduler/BUILD b/src/com/google/android/as/oss/fl/fcp/service/scheduler/BUILD
new file mode 100644
index 00000000..4ecf6674
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/fcp/service/scheduler/BUILD
@@ -0,0 +1,35 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "scheduler",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/protobuf/contrib/android:proto_parsers",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/fl/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/util",
+ "//src/com/google/android/as/oss/fl/localcompute:utils",
+ "//third_party/fcp/protos/confidentialcompute:access_policy_endorsement_options_java_proto_lite",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/brella/service/scheduler/FederatedTrainingScheduler.java b/src/com/google/android/as/oss/fl/fcp/service/scheduler/FederatedTrainingScheduler.java
similarity index 93%
rename from src/com/google/android/as/oss/fl/brella/service/scheduler/FederatedTrainingScheduler.java
rename to src/com/google/android/as/oss/fl/fcp/service/scheduler/FederatedTrainingScheduler.java
index bdf77937..b126939e 100644
--- a/src/com/google/android/as/oss/fl/brella/service/scheduler/FederatedTrainingScheduler.java
+++ b/src/com/google/android/as/oss/fl/fcp/service/scheduler/FederatedTrainingScheduler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.service.scheduler;
+package com.google.android.as.oss.fl.fc.service.scheduler;
import static com.google.android.as.oss.fl.federatedcompute.util.ClassConversionUtils.schedulingModeEnumToIntDef;
@@ -33,6 +33,8 @@
import com.google.fcp.client.tasks.OnSuccessListener;
import com.google.fcp.client.tasks.Task;
import com.google.common.flogger.GoogleLogger;
+import com.google.intelligence.fcp.confidentialcompute.AccessPolicyEndorsementOptions;
+import com.google.protobuf.contrib.android.ProtoParsers;
import java.util.Optional;
import java.util.concurrent.Executor;
@@ -74,6 +76,12 @@ public void scheduleTraining(
trainerOptions.getPopulationName(), trainerOptions.getSessionName());
}
+ AccessPolicyEndorsementOptions endorsementOptions =
+ ProtoParsers.parseFromRawRes(
+ context, AccessPolicyEndorsementOptions.parser(), R.raw.pcs_endorsement_options);
+
+ inAppTrainerOptionsBuilder.setAccessPolicyEndorsementOptions(endorsementOptions);
+
Task trainerTask =
trainerSupplier.get(context, executor, inAppTrainerOptionsBuilder.build());
trainerTask
diff --git a/src/com/google/android/as/oss/fl/brella/service/scheduler/FederatedTrainingSchedulerModule.java b/src/com/google/android/as/oss/fl/fcp/service/scheduler/FederatedTrainingSchedulerModule.java
similarity index 94%
rename from src/com/google/android/as/oss/fl/brella/service/scheduler/FederatedTrainingSchedulerModule.java
rename to src/com/google/android/as/oss/fl/fcp/service/scheduler/FederatedTrainingSchedulerModule.java
index e7c5f93a..8f7b8c81 100644
--- a/src/com/google/android/as/oss/fl/brella/service/scheduler/FederatedTrainingSchedulerModule.java
+++ b/src/com/google/android/as/oss/fl/fcp/service/scheduler/FederatedTrainingSchedulerModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.service.scheduler;
+package com.google.android.as.oss.fl.fc.service.scheduler;
import android.content.Context;
import com.google.android.as.oss.common.ExecutorAnnotations.FlExecutorQualifier;
diff --git a/src/com/google/android/as/oss/fl/brella/service/scheduler/TrainingScheduler.java b/src/com/google/android/as/oss/fl/fcp/service/scheduler/TrainingScheduler.java
similarity index 94%
rename from src/com/google/android/as/oss/fl/brella/service/scheduler/TrainingScheduler.java
rename to src/com/google/android/as/oss/fl/fcp/service/scheduler/TrainingScheduler.java
index e88f60c2..0f228676 100644
--- a/src/com/google/android/as/oss/fl/brella/service/scheduler/TrainingScheduler.java
+++ b/src/com/google/android/as/oss/fl/fcp/service/scheduler/TrainingScheduler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.service.scheduler;
+package com.google.android.as.oss.fl.fc.service.scheduler;
import android.content.Context;
import com.google.android.as.oss.fl.api.proto.TrainerOptions;
diff --git a/src/com/google/android/as/oss/fl/brella/service/util/AndroidManifest.xml b/src/com/google/android/as/oss/fl/fcp/service/util/AndroidManifest.xml
similarity index 89%
rename from src/com/google/android/as/oss/fl/brella/service/util/AndroidManifest.xml
rename to src/com/google/android/as/oss/fl/fcp/service/util/AndroidManifest.xml
index 8e539965..fcbf471c 100644
--- a/src/com/google/android/as/oss/fl/brella/service/util/AndroidManifest.xml
+++ b/src/com/google/android/as/oss/fl/fcp/service/util/AndroidManifest.xml
@@ -1,6 +1,6 @@
attestationMeasurementConfigReader;
+
+ private final Random randomGenerator;
+
+ // Specifies how long the generated attestation measurement is valid. For attestation validation,
+ // if the client tries to attest a measurement older than this, then the validation result will be
+ // UNSPOOFABLE_ID_VERIFICATION_RESULT_CHALLENGE_EXPIRED.
+ private static final Duration ATTESTATION_MEASUREMENT_TTL = Duration.ofMinutes(10);
+
+ static FcAttestationClient create(
+ PccAttestationMeasurementClient attestationMeasurementClient,
+ Executor executor,
+ TimeSource timeSource,
+ ConfigReader attestationMeasurementConfigReader,
+ Random randomGenerator) {
+ return new FcAttestationClient(
+ attestationMeasurementClient,
+ executor,
+ timeSource,
+ attestationMeasurementConfigReader,
+ randomGenerator);
+ }
+
+ @Override
+ public void requestMeasurement(ResultsCallback resultsCallback) {
+ AttestationMeasurementRequest attestationMeasurementRequest =
+ AttestationMeasurementRequest.builder().setTtl(ATTESTATION_MEASUREMENT_TTL).build();
+
+ FluentFuture.from(requestMeasurementDelay())
+ .transformAsync(
+ unused ->
+ attestationMeasurementClient.requestAttestationMeasurement(
+ attestationMeasurementRequest),
+ executor)
+ .addCallback(
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(AttestationMeasurementResponse result) {
+ resultsCallback.onCompleted(encodeResponse(result));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ resultsCallback.onFailure(t);
+ }
+ },
+ executor);
+ }
+
+ // Helps to encode an AttestationMeasurementResponse, to a string, as required by
+ // federated-compute side plumbing.
+ private String encodeResponse(AttestationMeasurementResponse response) {
+ return BaseEncoding.base64().encode(response.toByteArray());
+ }
+
+ /**
+ * Add random delay before measurement request. The delay is only added if the request is on the
+ * hour, where we see the highest batch of job scheduler wake ups, for Fc.
+ *
+ * Ideally, this delay should help distribute the requests during the peak time, to avoid huge
+ * server-side spikes.
+ */
+ private ListenableFuture requestMeasurementDelay() {
+ return Futures.submit(
+ () -> {
+ {
+ if (attestationMeasurementConfigReader.getConfig().enableRandomJitter()) {
+ LocalTime localTime = timeSource.now().atZone(ZoneOffset.UTC).toLocalTime();
+ if (isCloseToTheHour(localTime)) {
+ long randomDelaySeconds =
+ (long)
+ ((double)
+ (attestationMeasurementConfigReader.getConfig().maxDelaySeconds()
+ - attestationMeasurementConfigReader
+ .getConfig()
+ .minDelaySeconds())
+ * randomGenerator.nextDouble());
+ CountDownLatch latch = new CountDownLatch(1);
+
+ try {
+ latch.await(randomDelaySeconds, SECONDS);
+ } catch (InterruptedException e) {
+ // We probably should not continue work on the thread after an interruption.
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+ },
+ executor);
+ }
+
+ /**
+ * Helper method to check if a {@link LocalTime} is close to the hour.
+ *
+ * Returns true if the time is 30 seconds before or after the hour (e.g. 1:59:30 - 2:00:29).
+ */
+ private boolean isCloseToTheHour(LocalTime localTime) {
+ // 30 seconds before the hour (m=59, s=30-59).
+ // 30 seconds after the hour (m=0, s=0-29).
+ int minBeforeTheHour = 59;
+ int minOnTheHour = 0;
+ int currentMinute = localTime.getMinute();
+
+ if (currentMinute == minBeforeTheHour) {
+ return localTime.getSecond()
+ >= 60 - attestationMeasurementConfigReader.getConfig().delaySeconds();
+ } else if (currentMinute == minOnTheHour) {
+ return localTime.getSecond() < attestationMeasurementConfigReader.getConfig().delaySeconds();
+ }
+ return false;
+ }
+
+ private FcAttestationClient(
+ PccAttestationMeasurementClient attestationMeasurementClient,
+ Executor executor,
+ TimeSource timeSource,
+ ConfigReader attestationMeasurementConfigReader,
+ Random randomGenerator) {
+ this.attestationMeasurementClient = attestationMeasurementClient;
+ this.executor = executor;
+ this.timeSource = timeSource;
+ this.attestationMeasurementConfigReader = attestationMeasurementConfigReader;
+ this.randomGenerator = randomGenerator;
+ }
+}
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/attestation/FcAttestationClientModule.java b/src/com/google/android/as/oss/fl/federatedcompute/attestation/FcAttestationClientModule.java
new file mode 100644
index 00000000..8b40d2ac
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/attestation/FcAttestationClientModule.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.as.oss.fl.federatedcompute.attestation;
+
+import com.google.android.as.oss.attestation.PccAttestationMeasurementClient;
+import com.google.android.as.oss.attestation.config.PcsAttestationMeasurementConfig;
+import com.google.android.as.oss.common.ExecutorAnnotations.AttestationExecutorQualifier;
+import com.google.android.as.oss.common.config.ConfigReader;
+import com.google.android.as.oss.common.time.TimeSource;
+import com.google.fcp.client.AttestationClient;
+import dagger.Module;
+import dagger.Provides;
+import dagger.hilt.InstallIn;
+import dagger.hilt.components.SingletonComponent;
+import java.util.Random;
+import java.util.concurrent.Executor;
+import javax.annotation.Nullable;
+import javax.inject.Singleton;
+
+/** Convenience module to provide {@link FcAttestationClient}. */
+@Module
+@InstallIn(SingletonComponent.class)
+interface FcAttestationClientModule {
+ @Provides
+ @Singleton
+ @Nullable
+ static AttestationClient provideFcAttestationClient(
+ ConfigReader attestationMeasurementConfigReader,
+ PccAttestationMeasurementClient attestationMeasurementClient,
+ @AttestationExecutorQualifier Executor executor,
+ TimeSource timeSource) {
+ return attestationMeasurementConfigReader.getConfig().enableAttestationMeasurement()
+ ? FcAttestationClient.create(
+ attestationMeasurementClient,
+ executor,
+ timeSource,
+ attestationMeasurementConfigReader,
+ new Random())
+ : null;
+ }
+}
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/config/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/config/BUILD
new file mode 100644
index 00000000..785c06fb
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/config/BUILD
@@ -0,0 +1,27 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "config",
+ srcs = [
+ "PcsFcFlags.java",
+ ],
+ deps = [
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/config/PcsFcFlags.java b/src/com/google/android/as/oss/fl/federatedcompute/config/PcsFcFlags.java
index 61306dc9..c5d9adba 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/config/PcsFcFlags.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/config/PcsFcFlags.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/config/impl/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/config/impl/BUILD
new file mode 100644
index 00000000..b1f04a0b
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/config/impl/BUILD
@@ -0,0 +1,43 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "config_impl",
+ srcs = ["PcsFcDeviceConfigFlags.java"],
+ deps = [
+ "//experiments/proto:typed_features_java_proto_lite",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "@maven//:androidx_core_core",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_lite",
+ ],
+)
+
+android_library(
+ name = "config_module",
+ srcs = ["ConfigModule.java"],
+ deps = [
+ ":config_impl",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/config/impl/ConfigModule.java b/src/com/google/android/as/oss/fl/federatedcompute/config/impl/ConfigModule.java
index b6e0d0f2..f2e57187 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/config/impl/ConfigModule.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/config/impl/ConfigModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/config/impl/PcsFcDeviceConfigFlags.java b/src/com/google/android/as/oss/fl/federatedcompute/config/impl/PcsFcDeviceConfigFlags.java
index 22182d53..8fb3bc8a 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/config/impl/PcsFcDeviceConfigFlags.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/config/impl/PcsFcDeviceConfigFlags.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,7 +31,7 @@
/** Implementation of {@link PcsFcFlags} which reads flag values from DeviceConfig. */
public final class PcsFcDeviceConfigFlags extends PcsFcFlags {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
- private static final String FLAG_PREFIX = "Brella__";
+ private static final String FLAG_PREFIX = "Fc__";
private static final String FLAG_NAMESPACE =
FlagNamespace.DEVICE_PERSONALIZATION_SERVICES.toString();
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/init/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/init/BUILD
new file mode 100644
index 00000000..abbcd2f7
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/init/BUILD
@@ -0,0 +1,29 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "init",
+ srcs = ["PcsFcInit.java"],
+ deps = [
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/logging",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:org_checkerframework_checker_qual",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/init/PcsFcInit.java b/src/com/google/android/as/oss/fl/federatedcompute/init/PcsFcInit.java
index 8d62fff1..0d4c64ba 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/init/PcsFcInit.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/init/PcsFcInit.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,7 +33,7 @@
public final class PcsFcInit {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
- private static final String CLIENT_NAME = "astrea";
+ private static final String CLIENT_NAME = "pcs";
// Name of the custom TensorFlow native lib that contains a selection of regular TensorFlow and
// custom ops necessary for local computation tasks.
private static final String TENSORFLOW_NATIVE_LIB = "pcs_tensorflow_jni";
diff --git a/third_party/java/proto/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/logging/BUILD
similarity index 73%
rename from third_party/java/proto/BUILD
rename to src/com/google/android/as/oss/fl/federatedcompute/logging/BUILD
index e4bfb7d0..dabb5959 100644
--- a/third_party/java/proto/BUILD
+++ b/src/com/google/android/as/oss/fl/federatedcompute/logging/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2024 Google LLC
+# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,11 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
package(default_visibility = ["//visibility:public"])
-java_lite_proto_library(
- name = "any_java_proto_lite",
+android_library(
+ name = "logging",
+ srcs = ["FcLogManager.java"],
deps = [
- "@com_google_protobuf//:any_proto",
+ "@federated_compute//fcp/client:fl_runner",
],
)
diff --git a/src/com/google/android/as/oss/fl/brella/api/ExampleConsumption.aidl b/src/com/google/android/as/oss/fl/federatedcompute/logging/FcLogManager.java
similarity index 67%
rename from src/com/google/android/as/oss/fl/brella/api/ExampleConsumption.aidl
rename to src/com/google/android/as/oss/fl/federatedcompute/logging/FcLogManager.java
index 4a3021cd..546e9eec 100644
--- a/src/com/google/android/as/oss/fl/brella/api/ExampleConsumption.aidl
+++ b/src/com/google/android/as/oss/fl/federatedcompute/logging/FcLogManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,6 +14,9 @@
* limitations under the License.
*/
-package com.google.android.as.oss.fl.brella.api;
+package com.google.android.as.oss.fl.federatedcompute.logging;
-parcelable ExampleConsumption;
+import com.google.fcp.client.LogManager;
+
+/** PCS extension of {@link LogManager} to limit direct dependency on FCP code. */
+public interface FcLogManager extends LogManager {}
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/BUILD
new file mode 100644
index 00000000..3d799e2c
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/BUILD
@@ -0,0 +1,57 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "impl",
+ srcs = [
+ "FcClientStatsdLogManager.java",
+ "TfErrorParser.java",
+ ],
+ deps = [
+ "//logs/proto/wireless/android/play/playlog/fc:fc_log_proto_lite",
+ "//logs/proto/wireless/android/play/playlog/fc:secagg_log_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/logging",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd",
+ "//src/com/google/android/as/oss/logging:api",
+ "//src/com/google/android/as/oss/logging:atoms_java_proto_lite",
+ "//src/com/google/android/as/oss/logging:enums_java_proto_lite",
+ "//src/com/google/android/as/oss/logging/converter",
+ "//src/com/google/android/as/oss/networkusage/db",
+ "//src/com/google/android/as/oss/networkusage/db:repository",
+ "//third_party/fcp/client:histogram_counters_java_proto_lite",
+ "//third_party/java/androidx/legacy/support_core_utils",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_lite",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "module",
+ srcs = ["FcpStatsdLoggingModule.java"],
+ deps = [
+ ":impl",
+ "//src/com/google/android/as/oss/fl/federatedcompute/logging",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/FcClientStatsdLogManager.java b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/FcClientStatsdLogManager.java
new file mode 100644
index 00000000..bcd89081
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/FcClientStatsdLogManager.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.as.oss.fl.federatedcompute.logging.statsd;
+
+import static com.google.android.as.oss.logging.converter.PcsLogMessageConverter.HISTOGRAM_COUNTER_CONVERTER;
+import static com.google.android.as.oss.logging.converter.PcsLogMessageConverter.LONG_HISTOGRAM_COUNTER_CONVERTER;
+import static com.google.android.as.oss.logging.converter.PcsLogMessageConverter.TIMER_HISTOGRAM_COUNTER_CONVERTER;
+import static com.google.android.as.oss.networkusage.db.NetworkUsageLogUtils.createFcTrainingResultNetworkUsageEntity;
+import static com.google.common.logging.fc.logs.FcLog.TrainerLogEvent.TrainingEventKind.SECAGG_CLIENT_LOG_EVENT;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import android.os.SystemClock;
+import androidx.annotation.Nullable;
+import com.google.android.as.oss.asi.common.logging.DurationBucketLogic;
+import com.google.android.as.oss.asi.logging.Logcat;
+import com.google.android.as.oss.fl.federatedcompute.config.PcsFcFlags;
+import com.google.android.as.oss.fl.federatedcompute.logging.FcLogManager;
+import com.google.android.as.oss.fl.federatedcompute.statsd.StatsdExampleStoreConnector;
+import com.google.android.as.oss.logging.PcsAtomsProto.IntelligenceFederatedLearningDiagnosisLogReported;
+import com.google.android.as.oss.logging.PcsAtomsProto.IntelligenceFederatedLearningSecAggClientLogReported;
+import com.google.android.as.oss.logging.PcsAtomsProto.IntelligenceFederatedLearningTrainingLogReported;
+import com.google.android.as.oss.logging.PcsStatsEnums;
+import com.google.android.as.oss.logging.PcsStatsEnums.CollectionName;
+import com.google.android.as.oss.logging.PcsStatsEnums.SecAggClientCryptoOperationType;
+import com.google.android.as.oss.logging.PcsStatsEnums.SecAggClientErrorCode;
+import com.google.android.as.oss.logging.PcsStatsEnums.SecAggClientEventKind;
+import com.google.android.as.oss.logging.PcsStatsEnums.SecAggClientRound;
+import com.google.android.as.oss.logging.PcsStatsEnums.TrainingDataSourceType;
+import com.google.android.as.oss.logging.PcsStatsEnums.TrainingErrorCode;
+import com.google.android.as.oss.logging.PcsStatsEnums.TrainingEventKind;
+import com.google.android.as.oss.logging.PcsStatsLog;
+import com.google.android.as.oss.networkusage.db.NetworkUsageEntity;
+import com.google.android.as.oss.networkusage.db.NetworkUsageLogRepository;
+import com.google.fcp.client.LogManager.LoggingTimer;
+import com.google.fcp.client.LogManager.LongHistogramCounter;
+import com.google.fcp.client.LogManager.TimerHistogramCounter;
+import com.google.fcp.client.internal.ClientConstants;
+import com.google.android.libraries.learning.proto.DebugDiagCode;
+import com.google.android.libraries.learning.proto.ProdDiagCode;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.GoogleLogger;
+import com.google.common.flogger.android.AndroidFluentLogger;
+import com.google.common.logging.fc.logs.FcLog.FcLogEvent;
+import com.google.common.logging.fc.logs.FcLog.CountersDimensions;
+import com.google.common.logging.fc.logs.FcLog.DiagnosisLogEvent;
+import com.google.common.logging.fc.logs.FcLog.TrainerLogEvent;
+import com.google.common.logging.fc.logs.FcLog.TrainerLogEvent.DataTransferInfo;
+import com.google.common.logging.fc.logs.FcLog.TrainerLogEvent.MemoryUsageInfo;
+import com.google.common.logging.fc.logs.SecAggLog.SecAggClientLogEvent;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.intelligence.fcp.client.HistogramCounters;
+import com.google.protobuf.MessageLite;
+import java.util.Random;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Statsd implementation of {@link FcLogManager}. PCS owns the logs generated by FC from inside PCS
+ * and we log them via Statsd.
+ */
+@Singleton
+public class FcClientStatsdLogManager implements FcLogManager {
+ private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+ private static final AndroidFluentLogger nonRedactingLogger = Logcat.fc();
+ private static final String PCS_PACKAGE_NAME = "com.google.android.as.oss";
+ private static final int INFO_LOG_MSG_PERIOD_MINUTES = 60;
+
+ private static final int FEDERATED_COMPUTE_VERSION = ClientConstants.MODULE_VERSION;
+
+ private static final ImmutableSet TENSOR_FLOW_ERROR_EVENTS =
+ ImmutableSet.of(
+ TrainerLogEvent.TrainingEventKind.TRAIN_COMPUTATION_ERROR_TENSORFLOW,
+ TrainerLogEvent.TrainingEventKind.TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ERROR_TENSORFLOW);
+
+ private final PcsFcFlags flags;
+ private final PcsStatsLog statsdLogger;
+ private final Random random;
+ private final DurationBucketLogic durationBucketLogic;
+
+ private final NetworkUsageLogRepository networkUsageLogRepository;
+
+ @Inject
+ FcClientStatsdLogManager(
+ PcsFcFlags flags,
+ PcsStatsLog statsdLogger,
+ DurationBucketLogic durationBucketLogic,
+ NetworkUsageLogRepository networkUsageLogRepository) {
+ this.flags = flags;
+ this.statsdLogger = statsdLogger;
+ this.random = new Random();
+ this.durationBucketLogic = durationBucketLogic;
+ this.networkUsageLogRepository = networkUsageLogRepository;
+ }
+
+ @Override
+ public void logEvent(@Nullable FcLogEvent event) {
+ if (event == null || !isLoggingEnabled()) {
+ return;
+ }
+
+ if (networkUsageLogRepository.getDbExecutor().isPresent() && hasNetworkDataTransfer(event)) {
+ networkUsageLogRepository
+ .getDbExecutor()
+ .get()
+ .execute(() -> insertNetworkUsageLogRow(event));
+ }
+
+ nonRedactingLogger.atDebug().log(event);
+ if (event.hasDiagnosisLog()) {
+ logDiag(event.getDiagnosisLog());
+ return;
+ }
+
+ if (event.hasTrainerLog()) {
+ logTrainerEvent(event.getTrainerLog());
+ return;
+ }
+
+ // Only diag codes and trainer logs are supported right now.
+ logger.atFine().log("Unsupported type of log from FL.");
+ }
+
+ private boolean hasNetworkDataTransfer(FcLogEvent event) {
+ DataTransferInfo dataTransferInfo = event.getTrainerLog().getDataTransfer();
+ return dataTransferInfo.getChunkingLayerBytesDownloaded() != 0
+ || dataTransferInfo.getChunkingLayerBytesUploaded() != 0;
+ }
+
+ @Override
+ public void logDiag(DebugDiagCode code) {
+ nonRedactingLogger.atDebug().log("DebugDiagCode: %s", code);
+ logDiag(code, null);
+ }
+
+ @Override
+ public void logDiag(DebugDiagCode code, @Nullable String clientPackageName) {
+ if (!flags.isProductionDiagnosisEnabled() || !flags.isDebugDiagnosisEnabled()) {
+ return;
+ }
+ logDiag(code.getNumber(), 0);
+ }
+
+ @Override
+ public void logDiag(ProdDiagCode code) {
+ logDiag(code, null);
+ }
+
+ @Override
+ public void logDiag(ProdDiagCode code, @Nullable String clientPackageName) {
+ if (!flags.isProductionDiagnosisEnabled()) {
+ return;
+ }
+ logDiag(code.getNumber(), 0);
+ }
+
+ private void logDiag(DiagnosisLogEvent diagnosisLog) {
+ if (diagnosisLog.hasRunId()) {
+ logDiag(diagnosisLog.getDiagCode(), diagnosisLog.getRunId());
+ } else {
+ logDiag(diagnosisLog.getDiagCode(), 0);
+ }
+ }
+
+ private void logDiag(long diagCode, long runId) {
+ if (!isLoggingEnabled()) {
+ return;
+ }
+ IntelligenceFederatedLearningDiagnosisLogReported.Builder diagBuilder =
+ IntelligenceFederatedLearningDiagnosisLogReported.newBuilder()
+ .setFederatedComputeVersion(FEDERATED_COMPUTE_VERSION)
+ .setDiagCode(diagCode);
+ if (runId != 0) {
+ diagBuilder.setRunId(runId);
+ }
+ logger.atInfo().atMostEvery(INFO_LOG_MSG_PERIOD_MINUTES, MINUTES).log(
+ "Sending FL diagnosis log.");
+ statsdLogger.logIntelligenceFlDiagLogReported(diagBuilder.build());
+ }
+
+ @CanIgnoreReturnValue
+ @Override
+ public FcLogEvent mergeWithDefaultDimensions(FcLogEvent additionalDimensions) {
+ return additionalDimensions;
+ }
+
+ @Override
+ public LoggingTimer measureTimeFromNow(
+ TimerHistogramCounter counter, @Nullable FcLogEvent dimensions) {
+ return measureTimeFromNow(TIMER_HISTOGRAM_COUNTER_CONVERTER.apply(counter), dimensions);
+ }
+
+ @Override
+ public LoggingTimer measureTimeFromNow(
+ HistogramCounters counter, @Nullable FcLogEvent dimensions) {
+ return measureTimeFromNow(HISTOGRAM_COUNTER_CONVERTER.apply(counter), dimensions);
+ }
+
+ private LoggingTimer measureTimeFromNow(
+ PcsStatsEnums.HistogramCounters counter, @Nullable FcLogEvent dimensions) {
+ return measureTimeFromReference(counter, dimensions, SystemClock.elapsedRealtime());
+ }
+
+ @Override
+ public LoggingTimer measureTimeFromReference(
+ TimerHistogramCounter counter, @Nullable FcLogEvent dimensions, long referenceTimeMs) {
+ return measureTimeFromReference(
+ TIMER_HISTOGRAM_COUNTER_CONVERTER.apply(counter), dimensions, referenceTimeMs);
+ }
+
+ @Override
+ public LoggingTimer measureTimeFromReference(
+ HistogramCounters counter, @Nullable FcLogEvent dimensions, long referenceTimeMs) {
+ return measureTimeFromReference(
+ HISTOGRAM_COUNTER_CONVERTER.apply(counter), dimensions, referenceTimeMs);
+ }
+
+ private LoggingTimer measureTimeFromReference(
+ PcsStatsEnums.HistogramCounters counter,
+ @Nullable FcLogEvent dimensions,
+ long referenceTimeMs) {
+ return () ->
+ incrementCounter(
+ counter,
+ dimensions,
+ snapDurationToBucket(SystemClock.elapsedRealtime() - referenceTimeMs));
+ }
+
+ @Override
+ public void incrementCounter(
+ LongHistogramCounter counter, @Nullable FcLogEvent dimensions, long key) {
+ incrementCounter(LONG_HISTOGRAM_COUNTER_CONVERTER.apply(counter), dimensions, key);
+ }
+
+ @Override
+ public void incrementCounter(
+ HistogramCounters counter, @Nullable FcLogEvent dimensions, long key) {
+ incrementCounter(HISTOGRAM_COUNTER_CONVERTER.apply(counter), dimensions, key);
+ }
+
+ private void incrementCounter(
+ PcsStatsEnums.HistogramCounters counter, @Nullable FcLogEvent dimensions, long key) {
+ if (!isTrainingCounterLoggingEnabled()) {
+ return;
+ }
+
+ IntelligenceFederatedLearningTrainingLogReported.Builder atomBuilder =
+ IntelligenceFederatedLearningTrainingLogReported.newBuilder()
+ .setFederatedComputeVersion(FEDERATED_COMPUTE_VERSION);
+ if (dimensions != null) {
+ if (dimensions.hasTrainerLog()) {
+ TrainerLogEvent trainerLogEvent = dimensions.getTrainerLog();
+ if (trainerLogEvent.hasConfigName()) {
+ atomBuilder.setConfigName(trainerLogEvent.getConfigName());
+ }
+ if (trainerLogEvent.hasRunId()) {
+ atomBuilder.setRunId(trainerLogEvent.getRunId());
+ }
+ if (trainerLogEvent.hasModelIdentifier()) {
+ atomBuilder.setModelIdentifier(trainerLogEvent.getModelIdentifier());
+ }
+ }
+
+ if (dimensions.hasAdditionalCountersDimensions()) {
+ CountersDimensions additionalDimensions = dimensions.getAdditionalCountersDimensions();
+ if (additionalDimensions.hasExampleStoreDimensions()) {
+ atomBuilder.setCollectionName(
+ getCollectionNameEnum(
+ additionalDimensions.getExampleStoreDimensions().getCollection()));
+ }
+ }
+ }
+
+ atomBuilder.setHistogramCounter(counter);
+ atomBuilder.setCounterValue(key);
+
+ IntelligenceFederatedLearningTrainingLogReported atom = atomBuilder.build();
+ if (validateSerializedAtomSize(atom)) {
+ logger.atInfo().atMostEvery(INFO_LOG_MSG_PERIOD_MINUTES, MINUTES).log(
+ "Sending Training Counter log.");
+ statsdLogger.logIntelligenceFlTrainingLogReported(atom);
+ }
+ }
+
+ @Override
+ public void flushLogs() {}
+
+ private CollectionName getCollectionNameEnum(String collection) {
+ return switch (collection) {
+ case "/simple_storage_collection" -> CollectionName.COLLECTION_NAME_SIMPLESTORAGE;
+ case StatsdExampleStoreConnector.STATSD_COLLECTION_NAME ->
+ CollectionName.COLLECTION_NAME_STATSD;
+ default -> CollectionName.COLLECTION_NAME_UNDEFINED;
+ };
+ }
+
+ private void logTrainerEvent(TrainerLogEvent event) {
+ if (event.hasSecaggClientLogEvent()) {
+ logSecAggEvent(event);
+ return;
+ }
+
+ IntelligenceFederatedLearningTrainingLogReported.Builder atomBuilder =
+ IntelligenceFederatedLearningTrainingLogReported.newBuilder()
+ .setFederatedComputeVersion(FEDERATED_COMPUTE_VERSION);
+
+ if (event.hasKind()) {
+ if (event.getKind().equals(SECAGG_CLIENT_LOG_EVENT)) {
+ logger.atWarning().log(
+ "Received secagg client log event without the message set. Dropping log.");
+ return;
+ }
+ atomBuilder.setKind(translateTrainingEventKind(event.getKind()));
+ }
+
+ if (event.hasConfigName()) {
+ atomBuilder.setConfigName(event.getConfigName());
+ }
+
+ if (event.hasDurationMs()) {
+ atomBuilder.setDurationMillis(snapDurationToBucket(event.getDurationMs()));
+ }
+
+ if (event.hasExampleSize()) {
+ atomBuilder.setExampleSize(event.getExampleSize());
+ }
+
+ if (event.hasRunId()) {
+ atomBuilder.setRunId(event.getRunId());
+ }
+
+ if (event.hasErrorCode()) {
+ atomBuilder.setErrorCode(TrainingErrorCode.forNumber(event.getErrorCode().getNumber()));
+ }
+
+ if (event.hasMemoryUsage()) {
+ MemoryUsageInfo memInfo = event.getMemoryUsage();
+
+ if (memInfo.hasNativeHeapBytesAllocated()) {
+ atomBuilder.setNativeHeapBytesAllocated(
+ snapBytesToKb(memInfo.getNativeHeapBytesAllocated()));
+ }
+
+ if (memInfo.hasJavaHeapTotalMemory()) {
+ atomBuilder.setJavaHeapTotalMemory(snapBytesToKb(memInfo.getJavaHeapTotalMemory()));
+ }
+
+ if (memInfo.hasJavaHeapFreeMemory()) {
+ atomBuilder.setJavaHeapFreeMemory(snapBytesToKb(memInfo.getJavaHeapFreeMemory()));
+ }
+
+ if (memInfo.hasHighWaterMarkMemoryBytes()) {
+ atomBuilder.setHighWaterMarkMemoryBytes(
+ snapBytesToKb(memInfo.getHighWaterMarkMemoryBytes()));
+ }
+ }
+
+ if (event.hasModelIdentifier()) {
+ atomBuilder.setModelIdentifier(event.getModelIdentifier());
+ }
+
+ if (event.hasDataTransfer()) {
+ DataTransferInfo transferInfo = event.getDataTransfer();
+
+ if (transferInfo.hasDurationMs()) {
+ atomBuilder.setDataTransferDurationMillis(
+ snapDurationToBucket(transferInfo.getDurationMs()));
+ }
+
+ if (transferInfo.hasChunkingLayerBytesUploaded()) {
+ atomBuilder.setBytesUploaded(snapBytesToKb(transferInfo.getChunkingLayerBytesUploaded()));
+ }
+
+ if (transferInfo.hasChunkingLayerBytesDownloaded()) {
+ atomBuilder.setBytesDownloaded(
+ snapBytesToKb(transferInfo.getChunkingLayerBytesDownloaded()));
+ }
+ }
+
+ if (event.hasErrorMessage() && flags.allowLoggingErrorMessage()) {
+ atomBuilder.setErrorMessage(
+ extractSafeErrorString(event.getErrorCode(), event.getKind(), event.getErrorMessage()));
+ }
+
+ if (event.hasDataSource()) {
+ atomBuilder.setDataSource(
+ TrainingDataSourceType.forNumber(event.getDataSource().getNumber()));
+ }
+
+ IntelligenceFederatedLearningTrainingLogReported atom = atomBuilder.build();
+ if (validateSerializedAtomSize(atom)) {
+ logger.atInfo().atMostEvery(INFO_LOG_MSG_PERIOD_MINUTES, MINUTES).log(
+ "Sending FL Training log.");
+ statsdLogger.logIntelligenceFlTrainingLogReported(atom);
+ }
+ }
+
+ private void logSecAggEvent(TrainerLogEvent trainingEvent) {
+ if (!flags.allowLoggingSecAggClientEvent() || !trainingEvent.hasSecaggClientLogEvent()) {
+ return;
+ }
+
+ SecAggClientLogEvent secAggEvent = trainingEvent.getSecaggClientLogEvent();
+ IntelligenceFederatedLearningSecAggClientLogReported.Builder atomBuilder =
+ IntelligenceFederatedLearningSecAggClientLogReported.newBuilder()
+ .setFederatedComputeVersion(FEDERATED_COMPUTE_VERSION);
+
+ if (trainingEvent.hasRunId()) {
+ atomBuilder.setRunId(trainingEvent.getRunId());
+ }
+
+ if (trainingEvent.hasConfigName()) {
+ atomBuilder.setConfigName(trainingEvent.getConfigName());
+ }
+
+ if (trainingEvent.hasModelIdentifier()) {
+ atomBuilder.setModelIdentifier(trainingEvent.getModelIdentifier());
+ }
+
+ if (secAggEvent.hasClientSessionId()) {
+ atomBuilder.setClientSessionId(secAggEvent.getClientSessionId());
+ }
+
+ if (secAggEvent.hasExecutionSessionId()) {
+ atomBuilder.setExecutionSessionId(secAggEvent.getExecutionSessionId());
+ }
+
+ if (secAggEvent.hasKind()) {
+ atomBuilder.setKind(SecAggClientEventKind.forNumber(secAggEvent.getKind().getNumber()));
+ }
+
+ if (secAggEvent.hasDurationMs()) {
+ atomBuilder.setDurationMillis(snapDurationToBucket(secAggEvent.getDurationMs()));
+ }
+
+ if (secAggEvent.hasRound()) {
+ atomBuilder.setRound(SecAggClientRound.forNumber(secAggEvent.getRound().getNumber()));
+ }
+
+ if (secAggEvent.hasCryptoType()) {
+ atomBuilder.setCryptoType(
+ SecAggClientCryptoOperationType.forNumber(secAggEvent.getCryptoType().getNumber()));
+ }
+
+ if (secAggEvent.hasNumDroppedClients()) {
+ atomBuilder.setNumDroppedClients(secAggEvent.getNumDroppedClients());
+ }
+
+ if (secAggEvent.hasReceivedMessageSize()) {
+ atomBuilder.setReceivedMessageSize(snapBytesToKb(secAggEvent.getReceivedMessageSize()));
+ }
+
+ if (secAggEvent.hasSentMessageSize()) {
+ atomBuilder.setSentMessageSize(snapBytesToKb(secAggEvent.getSentMessageSize()));
+ }
+
+ if (secAggEvent.hasErrorCode()) {
+ atomBuilder.setErrorCode(
+ SecAggClientErrorCode.forNumber(secAggEvent.getErrorCode().getNumber()));
+ }
+
+ IntelligenceFederatedLearningSecAggClientLogReported atom = atomBuilder.build();
+ if (validateSerializedAtomSize(atom)) {
+ logger.atInfo().atMostEvery(INFO_LOG_MSG_PERIOD_MINUTES, MINUTES).log(
+ "Sending FL SecAgg log.");
+ statsdLogger.logIntelligenceFlSecaggClientLogReported(atom);
+ }
+ }
+
+ private boolean isLoggingEnabled() {
+ return random.nextInt(100) < flags.logSamplingPercentage();
+ }
+
+ private boolean isTrainingCounterLoggingEnabled() {
+ return random.nextInt(100) < flags.logCounterSamplingPercentage();
+ }
+
+ private long snapDurationToBucket(long durationMillis) {
+ return durationBucketLogic.approximateDurationMs(
+ durationBucketLogic.encodeDurationToBucket(durationMillis));
+ }
+
+ private boolean validateSerializedAtomSize(MessageLite atom) {
+ int size = atom.getSerializedSize();
+ if (size > flags.maxSerializedAtomSize()) {
+ logger.atWarning().log("FL log event is larger [%d bytes] than size limit.", size);
+ // TODO: We should log how often this happens.
+ return false;
+ }
+
+ logger.atFine().log("FL log event is of size [%d bytes].", size);
+ return true;
+ }
+
+ // Inserts all FcLogEvent containing network data transfer (upload/download).
+ private void insertNetworkUsageLogRow(FcLogEvent event) {
+ String packageName = event.getClientKey().getPackageName();
+
+ if (!networkUsageLogRepository.isNetworkUsageLogEnabled()
+ || !PCS_PACKAGE_NAME.equals(packageName)) {
+ return;
+ }
+
+ TrainerLogEvent trainerLogEvent = event.getTrainerLog();
+ long downloadSize =
+ trainerLogEvent.getDataTransfer().hasChunkingLayerBytesDownloaded()
+ ? trainerLogEvent.getDataTransfer().getChunkingLayerBytesDownloaded()
+ : 0;
+ long uploadSize =
+ trainerLogEvent.getDataTransfer().hasChunkingLayerBytesUploaded()
+ ? trainerLogEvent.getDataTransfer().getChunkingLayerBytesUploaded()
+ : 0;
+ NetworkUsageEntity entity =
+ createFcTrainingResultNetworkUsageEntity(
+ trainerLogEvent.getRunId(), downloadSize, uploadSize);
+
+ networkUsageLogRepository.insertNetworkUsageEntity(entity);
+ }
+
+ private static String extractSafeErrorString(
+ TrainerLogEvent.TrainingErrorCode errorCode,
+ TrainerLogEvent.TrainingEventKind eventKind,
+ String errorMsg) {
+ if (isTensorFlowError(errorCode, eventKind)) {
+ errorMsg = TfErrorParser.parse(errorMsg);
+ }
+
+ if (errorMsg.length() > 160) {
+ errorMsg = errorMsg.substring(0, 160);
+ }
+
+ return errorMsg;
+ }
+
+ private static boolean isTensorFlowError(
+ TrainerLogEvent.TrainingErrorCode errorCode, TrainerLogEvent.TrainingEventKind eventKind) {
+ return errorCode.equals(TrainerLogEvent.TrainingErrorCode.TRAIN_ERROR_TENSORFLOW)
+ || TENSOR_FLOW_ERROR_EVENTS.contains(eventKind);
+ }
+
+ private static TrainingEventKind translateTrainingEventKind(
+ TrainerLogEvent.TrainingEventKind kind) {
+ TrainingEventKind translated = TrainingEventKind.forNumber(kind.getNumber());
+ if (translated == null) {
+ logger.atWarning().log("Encountered unknown kind of TrainingEvent: [%d]", kind.getNumber());
+ translated = TrainingEventKind.TRAIN_UNDEFINED;
+ }
+
+ return translated;
+ }
+
+ private static long snapBytesToKb(long bytes) {
+ return (bytes >>> 10) << 10;
+ }
+}
diff --git a/src/com/google/android/as/oss/networkusage/db/noop/NetworkUsageLogNoOpModule.java b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/FcpStatsdLoggingModule.java
similarity index 64%
rename from src/com/google/android/as/oss/networkusage/db/noop/NetworkUsageLogNoOpModule.java
rename to src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/FcpStatsdLoggingModule.java
index f38df016..e8d16a40 100644
--- a/src/com/google/android/as/oss/networkusage/db/noop/NetworkUsageLogNoOpModule.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/FcpStatsdLoggingModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,20 +14,21 @@
* limitations under the License.
*/
-package com.google.android.as.oss.networkusage.db.noop;
+package com.google.android.as.oss.fl.federatedcompute.logging.statsd;
-import com.google.android.as.oss.networkusage.db.NetworkUsageLogRepository;
+import com.google.android.as.oss.fl.federatedcompute.logging.FcLogManager;
import dagger.Binds;
import dagger.Module;
import dagger.hilt.InstallIn;
import dagger.hilt.components.SingletonComponent;
-/** The module that provides no-op implementation of {@link NetworkUsageLog}. */
+/** Provides configuration resources for fc. */
@Module
@InstallIn(SingletonComponent.class)
-abstract class NetworkUsageLogNoOpModule {
-
+public abstract class FcpStatsdLoggingModule {
@Binds
- abstract NetworkUsageLogRepository bindNetworkUsageLogRepository(
- NetworkUsageLogRepositoryNoOpImpl impl);
+ abstract FcLogManager providesFcClientLogManager(
+ FcClientStatsdLogManager fcClientStatsdLogManager);
+
+ private FcpStatsdLoggingModule() {}
}
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/TfErrorParser.java b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/TfErrorParser.java
new file mode 100644
index 00000000..5b9f2216
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/logging/statsd/TfErrorParser.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.as.oss.fl.federatedcompute.logging.statsd;
+
+import static com.google.common.base.Strings.nullToEmpty;
+
+import androidx.annotation.NonNull;
+import com.google.common.collect.ImmutableList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Utility class for TF errors to extract safe error messages by using matchers. */
+public class TfErrorParser {
+ private static final Pattern TF_CC_PATTERN =
+ Pattern.compile(
+ "^(\\(at tf_wrapper.cc:\\d+\\) Error in \\w+::\\w*\\(\\): [^:]*): (.*)$", Pattern.DOTALL);
+ private static final Pattern TF_JAVA_PATTERN =
+ Pattern.compile("^(TensorflowException \\(code=\\d+\\)): (.*)$", Pattern.DOTALL);
+ private static final Pattern MISSING_OP_PATTERN =
+ Pattern.compile("(Op type not registered '[^']*')");
+ private static final Pattern TF_EE_ERROR =
+ Pattern.compile(
+ "^(Error during eligibility eval computation: code: \\d+, error: Error in"
+ + " \\w+::\\w*\\(\\): [^:]*): (.*)$",
+ Pattern.DOTALL);
+ private static final Pattern TF_COMP_ERROR =
+ Pattern.compile(
+ "^(Error during computation: code: \\d+, error: Error in \\w+::\\w*\\(\\): [^:]*): (.*)$",
+ Pattern.DOTALL);
+
+ private static final String REDACTED_STRING = "";
+ private static final ImmutableList TF_ERROR_PATTERNS;
+
+ static {
+ TF_ERROR_PATTERNS =
+ ImmutableList.of(TF_CC_PATTERN, TF_COMP_ERROR, TF_JAVA_PATTERN, TF_EE_ERROR);
+ }
+
+ private TfErrorParser() {}
+
+ public static String parse(String errorMsg) {
+ for (Pattern pattern : TF_ERROR_PATTERNS) {
+ Matcher matcher = pattern.matcher(errorMsg);
+ if (matcher.find()) {
+ return parseMessageFromMatcher(matcher);
+ }
+ }
+
+ return REDACTED_STRING;
+ }
+
+ private static String extractSafeDetailsFromTFSuffix(String tfErrorMessageSuffix) {
+ Matcher missingOpMatcher = MISSING_OP_PATTERN.matcher(tfErrorMessageSuffix);
+ if (missingOpMatcher.find()) {
+ return getNonNullGroup(missingOpMatcher, 1);
+ }
+ // TODO: Extend this to more buckets of error messages. See
+ // http://screen/qVUGbFE3Z7h.
+
+ return REDACTED_STRING;
+ }
+
+ @NonNull
+ private static String parseMessageFromMatcher(Matcher tfErrorMatcher) {
+ String prefix = getNonNullGroup(tfErrorMatcher, 1);
+ String detail = getNonNullGroup(tfErrorMatcher, 2);
+ return String.format("%s: %s", prefix, extractSafeDetailsFromTFSuffix(detail));
+ }
+
+ private static String getNonNullGroup(Matcher m, int groupIdx) {
+ return nullToEmpty(m.group(groupIdx));
+ }
+}
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/statsd/BUILD
new file mode 100644
index 00000000..966d3083
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/BUILD
@@ -0,0 +1,44 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "statsd",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/common/android/base:ticker",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/fl/fc/api:empty_example_iterator",
+ "//src/com/google/android/as/oss/fl/fc/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator:example_generator",
+ "//src/com/google/android/as/oss/logging:api",
+ "//src/com/google/android/as/oss/logging:atoms_java_proto_lite",
+ "//src/com/google/android/as/oss/logging:enums_java_proto_lite",
+ "//src/com/google/android/as/oss/protos:pcs_query_java_proto_lite",
+ "//src/com/google/android/as/oss/protos:pcs_statsquery_java_proto_lite",
+ "//third_party/fcp/client:selector_context_java_proto_lite",
+ "//third_party/java/android_libs/guava_jdk5:primitives",
+ "//third_party/java/proto:any_java_proto_lite",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_javalite",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/CursorExampleIterator.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/CursorExampleIterator.java
index b6190d5a..1ad5cc23 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/CursorExampleIterator.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/CursorExampleIterator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/ExampleStoreConnector.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/ExampleStoreConnector.java
index a2f9c915..84e52ebc 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/ExampleStoreConnector.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/ExampleStoreConnector.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/StatsdExampleStoreConnector.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/StatsdExampleStoreConnector.java
index 48ad1b5e..0f627517 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/StatsdExampleStoreConnector.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/StatsdExampleStoreConnector.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,10 +31,10 @@
import androidx.annotation.RequiresApi;
import com.android.internal.os.StatsPolicyConfigProto.StatsPolicyConfig;
import com.google.android.as.oss.common.ExecutorAnnotations.FlExecutorQualifier;
-import com.google.android.as.oss.fl.brella.api.EmptyExampleStoreIterator;
-import com.google.android.as.oss.fl.brella.api.proto.TrainingError;
-import com.google.android.as.oss.proto.PcsProtos.AstreaQuery;
-import com.google.android.as.oss.proto.PcsStatsquery.AstreaStatsQuery;
+import com.google.android.as.oss.fl.fc.api.EmptyExampleStoreIterator;
+import com.google.android.as.oss.fl.fc.api.proto.TrainingError;
+import com.google.android.as.oss.proto.PcsProtos.PcsQuery;
+import com.google.android.as.oss.proto.PcsStatsquery.PcsStatsQuery;
import com.google.fcp.client.ExampleStoreIterator;
import com.google.fcp.client.ExampleStoreService.QueryCallback;
import com.google.common.base.Preconditions;
@@ -61,9 +61,9 @@ public class StatsdExampleStoreConnector implements ExampleStoreConnector {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
public static final String STATSD_COLLECTION_NAME = "/statsd";
private static final String PCS_CRITERIA_TYPE_URL =
- "type.googleapis.com/com.google.android.as.oss.proto.AstreaQuery";
+ "type.googleapis.com/com.google.android.as.oss.proto.PcsQuery";
public static final String PCS_STATSQUERY_CRITERIA_TYPE_URL =
- "type.googleapis.com/com.google.android.as.oss.proto.AstreaStatsQuery";
+ "type.googleapis.com/com.google.android.as.oss.proto.PcsStatsQuery";
private static long configKey = 175747355; // [redacted]
private static String configPackage = "com.google.fcp.client";
@@ -169,16 +169,16 @@ private static String parseSqlQuery(byte[] criteria) {
if (!parsedCriteria.getTypeUrl().equals(PCS_CRITERIA_TYPE_URL)) {
return null;
}
- AstreaQuery query =
- AstreaQuery.parseFrom(
+ PcsQuery query =
+ PcsQuery.parseFrom(
parsedCriteria.getValue(), ExtensionRegistryLite.getGeneratedRegistry());
if (query.hasDataSelectionCriteria()
&& query
.getDataSelectionCriteria()
.getTypeUrl()
.equals(PCS_STATSQUERY_CRITERIA_TYPE_URL)) {
- AstreaStatsQuery statsQuery =
- AstreaStatsQuery.parseFrom(
+ PcsStatsQuery statsQuery =
+ PcsStatsQuery.parseFrom(
query.getDataSelectionCriteria().getValue(),
ExtensionRegistryLite.getGeneratedRegistry());
return statsQuery.getSqlQuery();
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/BUILD
new file mode 100644
index 00000000..e6ed1fed
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/BUILD
@@ -0,0 +1,26 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "config",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/common/config",
+ "//third_party/java/auto:auto_value",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfig.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfig.java
index 845eb3b2..ca1b0c38 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfig.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfigReader.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfigReader.java
index 9c44f2f3..f03104b3 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfigReader.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/config/StatsdConfigReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/BUILD
new file mode 100644
index 00000000..4433270a
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/BUILD
@@ -0,0 +1,36 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "example_generator",
+ srcs = glob(["*.kt"]),
+ deps = [
+ ":tff_feature_creator",
+ "//third_party/tensorflow/core/example:example_protos_java_proto_lite",
+ ],
+)
+
+android_library(
+ name = "tff_feature_creator",
+ srcs = ["TFFeatureCreator.java"],
+ deps = [
+ "//third_party/tensorflow/core/example:example_protos_java_proto_lite",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_javalite",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/DefaultExamplesGenerator.kt b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/DefaultExamplesGenerator.kt
index f5a8c687..cde50458 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/DefaultExamplesGenerator.kt
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/DefaultExamplesGenerator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/ExamplesGenerator.kt b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/ExamplesGenerator.kt
index b27dfb30..f343d880 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/ExamplesGenerator.kt
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/ExamplesGenerator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/TFFeatureCreator.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/TFFeatureCreator.java
index 8a6b4b3b..70c215df 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/TFFeatureCreator.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/examplegenerator/TFFeatureCreator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/BUILD
new file mode 100644
index 00000000..33d344c2
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/BUILD
@@ -0,0 +1,52 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "module",
+ exports = [
+ ":config_module",
+ ":connector_module",
+ ],
+)
+
+android_library(
+ name = "connector_module",
+ srcs = ["StatsdConnectorModule.java"],
+ deps = [
+ "//java/com/google/apps/tiktok/inject:qualifiers",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd",
+ "//src/com/google/android/as/oss/logging:api",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "config_module",
+ srcs = ["StatsdConfigModule.java"],
+ deps = [
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd/config",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConfigModule.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConfigModule.java
index 2b8cd42c..8312ab94 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConfigModule.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConfigModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConnectorModule.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConnectorModule.java
index 7be4ea7f..29ce3a79 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConnectorModule.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/module/StatsdConnectorModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/BUILD
new file mode 100644
index 00000000..8062755d
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "scheduler_module",
+ srcs = [
+ "StatsdTrainingSchedulerModule.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/common/flavor",
+ "//src/com/google/android/as/oss/fl/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/fc/service/scheduler",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd/config",
+ "//src/com/google/android/as/oss/fl/federatedcompute/training",
+ "//src/com/google/android/as/oss/fl/populations",
+ "@maven//:androidx_core_core",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/StatsdTrainingSchedulerModule.java b/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/StatsdTrainingSchedulerModule.java
index a2b554e4..bdfb6b0d 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/StatsdTrainingSchedulerModule.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/statsd/scheduler/StatsdTrainingSchedulerModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/training/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/training/BUILD
new file mode 100644
index 00000000..8719c2a7
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/training/BUILD
@@ -0,0 +1,59 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "training",
+ srcs = [
+ "PopulationTrainingScheduler.java",
+ "TrainingCriteria.java",
+ "TrainingSchedulerCallback.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/fl/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/fc/service/scheduler",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd",
+ "//src/com/google/android/as/oss/fl/populations",
+ "//src/com/google/android/as/oss/fl/populations:jobid_population_mapper",
+ "//src/com/google/android/as/oss/logging:api",
+ "//src/com/google/android/as/oss/logging:atoms_java_proto_lite",
+ "//src/com/google/android/as/oss/logging:enums_java_proto_lite",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:androidx_concurrent_futures",
+ "@maven//:androidx_core_core",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+android_library(
+ name = "training_module",
+ srcs = [
+ "PopulationTrainingSchedulerModule.java",
+ ],
+ deps = [
+ ":training",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/initializer",
+ "//src/com/google/android/as/oss/fl/fc/service/scheduler",
+ "//src/com/google/android/as/oss/fl/federatedcompute/statsd",
+ "//src/com/google/android/as/oss/logging:api",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingScheduler.java b/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingScheduler.java
index 9cc00b0d..a911081b 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingScheduler.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingScheduler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@
import com.google.android.as.oss.fl.api.proto.TrainerOptions;
import com.google.android.as.oss.fl.api.proto.TrainerOptions.JobType;
import com.google.android.as.oss.fl.api.proto.TrainerOptions.TrainingMode;
-import com.google.android.as.oss.fl.brella.service.scheduler.TrainingScheduler;
+import com.google.android.as.oss.fl.fc.service.scheduler.TrainingScheduler;
import com.google.android.as.oss.fl.populations.Population;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingSchedulerModule.java b/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingSchedulerModule.java
index f86878f9..60ecc96d 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingSchedulerModule.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/training/PopulationTrainingSchedulerModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@
import com.google.android.as.oss.common.ExecutorAnnotations.FlExecutorQualifier;
import com.google.android.as.oss.common.initializer.PcsInitializer;
-import com.google.android.as.oss.fl.brella.service.scheduler.TrainingScheduler;
+import com.google.android.as.oss.fl.fc.service.scheduler.TrainingScheduler;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingCriteria.java b/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingCriteria.java
index ea158d1f..b6281c6a 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingCriteria.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingCriteria.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingSchedulerCallback.java b/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingSchedulerCallback.java
index 79343c75..acf623b9 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingSchedulerCallback.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/training/TrainingSchedulerCallback.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/training/noop/NoOpTrainingCriteriaModule.java b/src/com/google/android/as/oss/fl/federatedcompute/training/noop/NoOpTrainingCriteriaModule.java
deleted file mode 100644
index f3373733..00000000
--- a/src/com/google/android/as/oss/fl/federatedcompute/training/noop/NoOpTrainingCriteriaModule.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.federatedcompute.training.noop;
-
-import androidx.core.os.BuildCompat;
-import com.google.android.as.oss.fl.api.proto.TrainerOptions;
-import com.google.android.as.oss.fl.federatedcompute.training.PopulationTrainingScheduler;
-import com.google.android.as.oss.fl.federatedcompute.training.TrainingCriteria;
-import com.google.android.as.oss.fl.populations.Population;
-import dagger.Module;
-import dagger.Provides;
-import dagger.hilt.InstallIn;
-import dagger.hilt.components.SingletonComponent;
-import dagger.multibindings.IntoSet;
-import java.util.Optional;
-
-@Module
-@InstallIn(SingletonComponent.class)
-abstract class NoOpTrainingCriteriaModule {
- @Provides
- @IntoSet
- static Optional provideNoOpTrainingCriterionCanSchedule() {
- return BuildCompat.isAtLeastU()
- ? Optional.of(
- new TrainingCriteria() {
- @Override
- public TrainerOptions getTrainerOptions() {
- return PopulationTrainingScheduler.buildTrainerOpts(
- Population.PLATFORM_LOGGING_DEV.populationName());
- }
-
- @Override
- public boolean canScheduleTraining() {
- return true;
- }
- })
- : Optional.empty();
- }
-
- @Provides
- @IntoSet
- static Optional provideNoOpTrainingCriterionCannotSchedule() {
- return BuildCompat.isAtLeastU()
- ? Optional.of(
- new TrainingCriteria() {
- @Override
- public TrainerOptions getTrainerOptions() {
- return PopulationTrainingScheduler.buildTrainerOpts(
- Population.PLATFORM_LOGGING.populationName());
- }
-
- @Override
- public boolean canScheduleTraining() {
- return false;
- }
- })
- : Optional.empty();
- }
-}
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/util/BUILD b/src/com/google/android/as/oss/fl/federatedcompute/util/BUILD
new file mode 100644
index 00000000..4ab894eb
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/federatedcompute/util/BUILD
@@ -0,0 +1,27 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "util",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/fl/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/fc/api",
+ "@federated_compute//fcp/client:fl_runner",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/federatedcompute/util/ClassConversionUtils.java b/src/com/google/android/as/oss/fl/federatedcompute/util/ClassConversionUtils.java
index aa8192c9..efebc933 100644
--- a/src/com/google/android/as/oss/fl/federatedcompute/util/ClassConversionUtils.java
+++ b/src/com/google/android/as/oss/fl/federatedcompute/util/ClassConversionUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,11 +21,11 @@
import android.net.Uri;
import com.google.android.as.oss.fl.api.proto.TrainerOptions;
-import com.google.android.as.oss.fl.brella.api.ExampleConsumption;
-import com.google.android.as.oss.fl.brella.api.InAppTrainerOptions;
-import com.google.android.as.oss.fl.brella.api.InAppTrainingConstraints;
-import com.google.android.as.oss.fl.brella.api.TrainingInterval;
-import com.google.android.as.oss.fl.brella.api.TrainingInterval.SchedulingMode;
+import com.google.android.as.oss.fl.fc.api.ExampleConsumption;
+import com.google.android.as.oss.fl.fc.api.InAppTrainerOptions;
+import com.google.android.as.oss.fl.fc.api.InAppTrainingConstraints;
+import com.google.android.as.oss.fl.fc.api.TrainingInterval;
+import com.google.android.as.oss.fl.fc.api.TrainingInterval.SchedulingMode;
import java.util.ArrayList;
import java.util.List;
@@ -97,27 +97,23 @@ public static com.google.fcp.client.ExampleConsumption pcsToFcExampleConsumption
}
public static TrainerOptions.SchedulingMode schedulingModeIntDefToEnum(int schedulingMode) {
- switch (schedulingMode) {
- case SchedulingMode.RECURRENT:
- return SCHEDULING_MODE_RECURRENT;
- case SchedulingMode.ONE_TIME:
- return SCHEDULING_MODE_ONE_TIME;
- default:
- throw new IllegalArgumentException(
- String.format("Unknown Scheduling Mode: %d", schedulingMode));
- }
+ return switch (schedulingMode) {
+ case SchedulingMode.RECURRENT -> SCHEDULING_MODE_RECURRENT;
+ case SchedulingMode.ONE_TIME -> SCHEDULING_MODE_ONE_TIME;
+ default ->
+ throw new IllegalArgumentException(
+ String.format("Unknown Scheduling Mode: %d", schedulingMode));
+ };
}
public static int schedulingModeEnumToIntDef(TrainerOptions.SchedulingMode schedulingMode) {
- switch (schedulingMode) {
- case SCHEDULING_MODE_RECURRENT:
- return SchedulingMode.RECURRENT;
- case SCHEDULING_MODE_ONE_TIME:
- return SchedulingMode.ONE_TIME;
- default:
- throw new IllegalArgumentException(
- String.format("Unknown Scheduling Mode: %d", schedulingMode.getNumber()));
- }
+ return switch (schedulingMode) {
+ case SCHEDULING_MODE_RECURRENT -> SchedulingMode.RECURRENT;
+ case SCHEDULING_MODE_ONE_TIME -> SchedulingMode.ONE_TIME;
+ default ->
+ throw new IllegalArgumentException(
+ String.format("Unknown Scheduling Mode: %d", schedulingMode.getNumber()));
+ };
}
/**
diff --git a/src/com/google/android/as/oss/fl/localcompute/BUILD b/src/com/google/android/as/oss/fl/localcompute/BUILD
new file mode 100644
index 00000000..cc18c403
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/localcompute/BUILD
@@ -0,0 +1,60 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+android_library(
+ name = "resource_manager",
+ srcs = ["LocalComputeResourceManager.java"],
+ deps = ["@maven//:com_google_guava_guava"],
+)
+
+android_library(
+ name = "file_copy_start_query",
+ srcs = ["FileCopyStartQuery.java"],
+ deps = [
+ "//third_party/fcp/client:selector_context_java_proto_lite",
+ "@federated_compute//fcp/client:fl_runner",
+ ],
+)
+
+android_library(
+ name = "utils",
+ srcs = [
+ "LocalComputeUtils.java",
+ "PathConversionUtils.java",
+ ],
+ deps = [
+ ":resource_manager",
+ "//src/com/google/android/as/oss/fl/api:training_java_proto_lite",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+android_library(
+ name = "optional_module",
+ srcs = ["OptionalBindsModule.java"],
+ deps = [
+ ":file_copy_start_query",
+ ":resource_manager",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/localcompute/FileCopyStartQuery.java b/src/com/google/android/as/oss/fl/localcompute/FileCopyStartQuery.java
index 045e9f4c..2bb1173d 100644
--- a/src/com/google/android/as/oss/fl/localcompute/FileCopyStartQuery.java
+++ b/src/com/google/android/as/oss/fl/localcompute/FileCopyStartQuery.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/localcompute/LocalComputeResourceManager.java b/src/com/google/android/as/oss/fl/localcompute/LocalComputeResourceManager.java
index fa367d29..3c15896a 100644
--- a/src/com/google/android/as/oss/fl/localcompute/LocalComputeResourceManager.java
+++ b/src/com/google/android/as/oss/fl/localcompute/LocalComputeResourceManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/localcompute/LocalComputeUtils.java b/src/com/google/android/as/oss/fl/localcompute/LocalComputeUtils.java
index 3c06c0df..acf58da6 100644
--- a/src/com/google/android/as/oss/fl/localcompute/LocalComputeUtils.java
+++ b/src/com/google/android/as/oss/fl/localcompute/LocalComputeUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/localcompute/OptionalBindsModule.java b/src/com/google/android/as/oss/fl/localcompute/OptionalBindsModule.java
index db097c67..f4e7e41c 100644
--- a/src/com/google/android/as/oss/fl/localcompute/OptionalBindsModule.java
+++ b/src/com/google/android/as/oss/fl/localcompute/OptionalBindsModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/localcompute/PathConversionUtils.java b/src/com/google/android/as/oss/fl/localcompute/PathConversionUtils.java
index 3a362590..26454c09 100644
--- a/src/com/google/android/as/oss/fl/localcompute/PathConversionUtils.java
+++ b/src/com/google/android/as/oss/fl/localcompute/PathConversionUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/localcompute/api/MetadataContextKeys.java b/src/com/google/android/as/oss/fl/localcompute/api/MetadataContextKeys.java
deleted file mode 100644
index 20a7a405..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/api/MetadataContextKeys.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.api;
-
-import android.os.ParcelFileDescriptor;
-import io.grpc.Context;
-import io.grpc.Metadata;
-import io.grpc.binder.ParcelableUtils;
-
-/** The Context/Metadata keys for wrapping Parcelables in gRPC service/client. */
-public final class MetadataContextKeys {
-
- public static final Metadata.Key FILE_DESCRIPTOR_METADATA_KEY =
- ParcelableUtils.metadataKey("FileDescriptor-bin", ParcelFileDescriptor.CREATOR);
-
- public static final Context.Key FILE_DESCRIPTOR_CONTEXT_KEY =
- Context.key("FileDescriptor-bin");
-
- private MetadataContextKeys() {}
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/api/filecopy.proto b/src/com/google/android/as/oss/fl/localcompute/api/filecopy.proto
deleted file mode 100644
index d54607e1..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/api/filecopy.proto
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2024 Google LLC
-//
-// 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.
-
-edition = "2023";
-
-package java.com.google.android.as.oss.fl.localcompute.api;
-
-option features.utf8_validation = NONE;
-option java_multiple_files = true;
-option java_package = "com.google.android.as.oss.fl.localcompute.api.proto";
-
-message UriQueryRequest {
- // The session name of the Local Compute task.
- string session_name = 1;
-
- // A string key mapping to a file's {@link android.net.Uri} in the server app.
- string file_key = 2;
-}
-
-message UriQueryResponse {
- // A {@link android.net.Uri} of the file queried.
- string result_file_uri = 1;
-
- // If the queried file is a directory or not.
- bool is_directory = 2;
-}
-
-message TraverseDirRequest {
- // A {@link android.net.Uri} of the directory to traverse in server app.
- string dir_uri = 1;
-}
-
-message TraverseDirResponse {
- // List of {@link android.net.Uri} of the child files traversed.
- repeated string child_file_uri = 1;
-}
-
-message FileCopyRequest {
- // The destination or source file Uri depending on if it's a copy-to or
- // copy-from request.
- string file_uri = 2;
-}
-
-message FileCopyResponse {
- bool success = 1;
-}
-
-service FileCopyService {
- // Query the Uri of a certain file by a string key in the server app.
- rpc QueryFileUri(UriQueryRequest) returns (UriQueryResponse);
-
- // Traverse the directory in the server app and return the list of Uris of
- // child files.
- rpc TraverseDir(TraverseDirRequest) returns (TraverseDirResponse);
-
- // Copy the content of the readable {@link ParcelFileDescriptor} that is
- // wrapped in the request's metadata header, to the destination file provided
- // in {@link FileCopyRequest}.
- rpc CopyFromFileDescriptor(FileCopyRequest) returns (FileCopyResponse);
-
- // Copy the content of the source file provided in the {@link FileCopyRequest}
- // to the writable {@link ParcelFileDescriptor} that is wrapped in the
- // request's metadata header.
- rpc CopyToFileDescriptor(FileCopyRequest) returns (FileCopyResponse);
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/client/ChannelModule.java b/src/com/google/android/as/oss/fl/localcompute/client/ChannelModule.java
deleted file mode 100644
index f50d1371..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/client/ChannelModule.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.client;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-
-import android.content.Context;
-import dagger.Module;
-import dagger.Provides;
-import dagger.hilt.InstallIn;
-import dagger.hilt.android.qualifiers.ApplicationContext;
-import dagger.hilt.components.SingletonComponent;
-import io.grpc.Channel;
-import io.grpc.CompressorRegistry;
-import io.grpc.DecompressorRegistry;
-import io.grpc.binder.AndroidComponentAddress;
-import io.grpc.binder.BinderChannelBuilder;
-import io.grpc.binder.InboundParcelablePolicy;
-import io.grpc.binder.UntrustedSecurityPolicies;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import javax.inject.Qualifier;
-import javax.inject.Singleton;
-
-/** Provides OnDeviceChannel that communicates with gRPC server in ASI. */
-@Module
-@InstallIn(SingletonComponent.class)
-abstract class ChannelModule {
-
- private static final String PACKAGE_NAME = "com.google.android.as";
-
- private static final String CLASS_NAME =
- "com.google.android.apps.miphone.aiai.common.brella.filecopy.service.FileCopyEndpointService";
-
- @Provides
- @AsiGrpcChannel
- @Singleton
- static Channel providesChannel(@ApplicationContext Context context) {
- return BinderChannelBuilder.forAddress(
- AndroidComponentAddress.forRemoteComponent(PACKAGE_NAME, CLASS_NAME), context)
- .securityPolicy(UntrustedSecurityPolicies.untrustedPublic())
- .inboundParcelablePolicy(
- InboundParcelablePolicy.newBuilder().setAcceptParcelableMetadataValues(true).build())
- .decompressorRegistry(DecompressorRegistry.emptyInstance())
- .compressorRegistry(CompressorRegistry.newEmptyInstance())
- .idleTimeout(1, MINUTES)
- .build();
- }
-
- @Qualifier
- @Retention(RetentionPolicy.RUNTIME)
- public @interface AsiGrpcChannel {}
-
- private ChannelModule() {}
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/client/FileCopyGrpcClient.java b/src/com/google/android/as/oss/fl/localcompute/client/FileCopyGrpcClient.java
deleted file mode 100644
index 41577623..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/client/FileCopyGrpcClient.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.client;
-
-import static com.google.android.as.oss.fl.localcompute.api.MetadataContextKeys.FILE_DESCRIPTOR_METADATA_KEY;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.ParcelFileDescriptor;
-import com.google.android.as.oss.common.ExecutorAnnotations.IoExecutorQualifier;
-import com.google.android.as.oss.fl.localcompute.PathConversionUtils;
-import com.google.android.as.oss.fl.localcompute.api.proto.FileCopyRequest;
-import com.google.android.as.oss.fl.localcompute.api.proto.FileCopyResponse;
-import com.google.android.as.oss.fl.localcompute.api.proto.FileCopyServiceGrpc;
-import com.google.android.as.oss.fl.localcompute.api.proto.TraverseDirRequest;
-import com.google.android.as.oss.fl.localcompute.api.proto.TraverseDirResponse;
-import com.google.android.as.oss.fl.localcompute.api.proto.UriQueryRequest;
-import com.google.android.as.oss.fl.localcompute.api.proto.UriQueryResponse;
-import com.google.android.as.oss.fl.localcompute.client.ChannelModule.AsiGrpcChannel;
-import com.google.common.flogger.GoogleLogger;
-import com.google.common.io.Files;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import dagger.hilt.android.qualifiers.ApplicationContext;
-import io.grpc.Channel;
-import io.grpc.Metadata;
-import io.grpc.stub.MetadataUtils;
-import java.io.File;
-import java.io.IOException;
-import java.util.concurrent.Executor;
-import javax.inject.Inject;
-
-/** A Grpc client that handles file copy between ASI and PCS needed for local compute tasks. */
-public class FileCopyGrpcClient {
- private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
-
- private final Context context;
- private final Channel onDeviceChannel;
- private final Executor executor;
-
- @Inject
- FileCopyGrpcClient(
- @ApplicationContext Context context,
- @AsiGrpcChannel Channel onDeviceChannel,
- @IoExecutorQualifier Executor executor) {
- this.context = context;
- this.onDeviceChannel = onDeviceChannel;
- this.executor = executor;
- }
-
- public ListenableFuture queryFileUriByStringKey(
- String sessionName, String fileStringKey) {
- logger.atFine().log(
- "Start query file Uri by string key %s for session %s", fileStringKey, sessionName);
-
- return Futures.submit(
- () -> {
- FileCopyServiceGrpc.FileCopyServiceBlockingStub stub =
- FileCopyServiceGrpc.newBlockingStub(onDeviceChannel);
- UriQueryRequest request =
- UriQueryRequest.newBuilder()
- .setSessionName(sessionName)
- .setFileKey(fileStringKey)
- .build();
- return stub.queryFileUri(request);
- },
- executor);
- }
-
- public ListenableFuture copyFileFromServer(Uri srcFileUri, Uri destFileUri) {
- return Futures.submit(() -> copyFileFromServerBlocking(srcFileUri, destFileUri), executor);
- }
-
- public ListenableFuture copyFileToServer(Uri srcFileUri, Uri destFileUri) {
- return Futures.submit(() -> copyFileToServerBlocking(srcFileUri, destFileUri), executor);
- }
-
- public ListenableFuture copyDirFromServer(Uri srcDirUri, Uri destDirUri) {
- return Futures.submit(
- () -> {
- FileCopyServiceGrpc.FileCopyServiceBlockingStub stub =
- FileCopyServiceGrpc.newBlockingStub(onDeviceChannel);
- TraverseDirRequest request =
- TraverseDirRequest.newBuilder().setDirUri(srcDirUri.toString()).build();
- TraverseDirResponse response = stub.traverseDir(request);
-
- File srcDir = PathConversionUtils.convertUriToFile(context, srcDirUri);
- for (String childFileUriStr : response.getChildFileUriList()) {
- Uri srcFileUri = Uri.parse(childFileUriStr);
- File srcFile = PathConversionUtils.convertUriToFile(context, srcFileUri);
- String srcFileRelativePath = srcDir.toPath().relativize(srcFile.toPath()).toString();
- File destFile =
- new File(
- PathConversionUtils.convertUriToFile(context, destDirUri), srcFileRelativePath);
- Uri destFileUri = PathConversionUtils.convertFileToUri(context, destFile);
- if (!copyFileFromServerBlocking(srcFileUri, destFileUri)) {
- return false;
- }
- }
- return true;
- },
- executor);
- }
-
- public ListenableFuture copyDirToServer(Uri srcDirUri, Uri destDirUri) {
- return Futures.submit(
- () -> {
- File srcDir = PathConversionUtils.convertUriToFile(context, srcDirUri);
- if (!srcDir.isDirectory()) {
- logger.atSevere().log(
- "The given source directory %s is not a directory or does not exist", srcDir);
- return false;
- }
- for (File srcFile : Files.fileTraverser().depthFirstPreOrder(srcDir)) {
- if (!srcFile.isDirectory()) {
- String srcfileRelativePath = srcDir.toPath().relativize(srcFile.toPath()).toString();
- File destFile =
- new File(
- PathConversionUtils.convertUriToFile(context, destDirUri),
- srcfileRelativePath);
- Uri destFileUri = PathConversionUtils.convertFileToUri(context, destFile);
- Uri srcFileUri = PathConversionUtils.convertFileToUri(context, srcFile);
- if (!copyFileToServerBlocking(srcFileUri, destFileUri)) {
- return false;
- }
- }
- }
- return true;
- },
- executor);
- }
-
- private Boolean copyFileFromServerBlocking(Uri srcFileUri, Uri destFileUri) {
- File destFile = PathConversionUtils.convertUriToFile(context, destFileUri);
- try {
- if (destFile.getParentFile() != null) {
- destFile.getParentFile().mkdirs();
- }
- if (!destFile.createNewFile()) {
- logger.atWarning().log("File %s already exists, overwriting it.", destFileUri);
- }
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to create the file %s", destFileUri);
- destFile.delete();
- return false;
- }
-
- boolean copySuccess = false;
- try (ParcelFileDescriptor destFileDescriptor =
- ParcelFileDescriptor.open(destFile, ParcelFileDescriptor.MODE_WRITE_ONLY)) {
- Metadata headers = new Metadata();
- headers.put(FILE_DESCRIPTOR_METADATA_KEY, destFileDescriptor);
- FileCopyServiceGrpc.FileCopyServiceBlockingStub stub =
- FileCopyServiceGrpc.newBlockingStub(onDeviceChannel)
- .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers));
- FileCopyRequest request =
- FileCopyRequest.newBuilder().setFileUri(srcFileUri.toString()).build();
- FileCopyResponse response = stub.copyToFileDescriptor(request);
- copySuccess = response.getSuccess();
- } catch (IOException e) {
- logger.atWarning().withCause(e).log("Failed to close the file descriptor.");
- } finally {
- if (!copySuccess) {
- destFile.delete();
- }
- }
- return copySuccess;
- }
-
- private Boolean copyFileToServerBlocking(Uri srcFileUri, Uri destFileUri) {
- File srcFile = PathConversionUtils.convertUriToFile(context, srcFileUri);
- if (!srcFile.isFile()) {
- logger.atSevere().log("This file %s is not a normal file or does not exist", srcFile);
- return false;
- }
-
- try (ParcelFileDescriptor srcFileDescriptor =
- ParcelFileDescriptor.open(srcFile, ParcelFileDescriptor.MODE_READ_ONLY)) {
- Metadata headers = new Metadata();
- headers.put(FILE_DESCRIPTOR_METADATA_KEY, srcFileDescriptor);
- FileCopyServiceGrpc.FileCopyServiceBlockingStub stub =
- FileCopyServiceGrpc.newBlockingStub(onDeviceChannel)
- .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers));
- FileCopyRequest request =
- FileCopyRequest.newBuilder().setFileUri(destFileUri.toString()).build();
- FileCopyResponse response = stub.copyFromFileDescriptor(request);
- return response.getSuccess();
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to close the file descriptor.");
- return false;
- }
- }
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/impl/AndroidManifest_EdgeTpu.xml b/src/com/google/android/as/oss/fl/localcompute/impl/AndroidManifest_EdgeTpu.xml
deleted file mode 100644
index d6000b0e..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/impl/AndroidManifest_EdgeTpu.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/com/google/android/as/oss/fl/localcompute/impl/FileCopyStartQueryImpl.java b/src/com/google/android/as/oss/fl/localcompute/impl/FileCopyStartQueryImpl.java
deleted file mode 100644
index 5275d002..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/impl/FileCopyStartQueryImpl.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.impl;
-
-import androidx.annotation.VisibleForTesting;
-import com.google.android.as.oss.common.ExecutorAnnotations.IoExecutorQualifier;
-import com.google.android.as.oss.fl.brella.api.proto.TrainingError;
-import com.google.android.as.oss.fl.localcompute.FileCopyStartQuery;
-import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
-import com.google.fcp.client.ExampleStoreIterator;
-import com.google.fcp.client.ExampleStoreService.QueryCallback;
-import com.google.common.flogger.GoogleLogger;
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.intelligence.fcp.client.SelectorContext;
-import com.google.protobuf.ByteString;
-import java.util.concurrent.Executor;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import org.tensorflow.example.BytesList;
-import org.tensorflow.example.Example;
-import org.tensorflow.example.Feature;
-import org.tensorflow.example.Features;
-
-/** A Singleton implementation of handling the filecopy ExampleStore startQuery call */
-@Singleton
-class FileCopyStartQueryImpl implements FileCopyStartQuery {
- private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
-
- @VisibleForTesting
- static final String ABSOLUTE_PATH_TF_EXAMPLE_KEY = "copied_input_resource_path";
-
- private final Executor executor;
- private final LocalComputeResourceManager resourceManager;
-
- @Inject
- FileCopyStartQueryImpl(
- @IoExecutorQualifier Executor executor, LocalComputeResourceManager resourceManager) {
- this.executor = executor;
- this.resourceManager = resourceManager;
- }
-
- /**
- * A dummy startQuery that actually copies the input resource from ASI by providing collection as
- * a string key.
- */
- @Override
- public void startQuery(
- String collection,
- byte[] criteria,
- byte[] resumptionToken,
- QueryCallback callback,
- SelectorContext selectorContext) {
- String sessionName = selectorContext.getComputationProperties().getSessionName();
-
- Futures.addCallback(
- resourceManager.copyResourceAtTraining(sessionName, collection),
- new FutureCallback() {
- @Override
- public void onSuccess(String absolutePath) {
- if (!absolutePath.isEmpty()) {
- ExampleStoreIterator iterator = createAbsolutePathExampleStoreIterator(absolutePath);
- callback.onStartQuerySuccess(iterator);
- } else {
- logger.atWarning().log("Failed to copy the input resource.");
- callback.onStartQueryFailure(
- TrainingError.TRAINING_ERROR_PCC_COPY_LOCAL_COMPUTE_RESOURCE_FAILED_VALUE,
- "Failed to copy the input resource.");
- }
- }
-
- @Override
- public void onFailure(Throwable t) {
- logger.atWarning().log("Failed to copy the input resource.");
- callback.onStartQueryFailure(
- TrainingError.TRAINING_ERROR_PCC_COPY_LOCAL_COMPUTE_RESOURCE_FAILED_VALUE,
- t.getMessage());
- }
- },
- executor);
- }
-
- private ExampleStoreIterator createAbsolutePathExampleStoreIterator(String absolutePath) {
- return new ExampleStoreIterator() {
- private boolean hasNext = true;
-
- @Override
- public void next(Callback callback) {
- if (hasNext) {
- callback.onIteratorNextSuccess(
- absolutePathToExample(absolutePath).toByteArray(), true, null);
- hasNext = false;
- } else {
- callback.onIteratorNextSuccess(null, true, null);
- }
- }
-
- @Override
- public void request(int numExamples) {}
-
- @Override
- public void close() {}
- };
- }
-
- @VisibleForTesting
- Example absolutePathToExample(String absolutePath) {
- Feature.Builder pathFeature = Feature.newBuilder();
- pathFeature.setBytesList(
- BytesList.newBuilder().addValue(ByteString.copyFromUtf8(absolutePath)).build());
- Features.Builder features = Features.newBuilder();
- features.putFeature(ABSOLUTE_PATH_TF_EXAMPLE_KEY, pathFeature.build());
- return Example.newBuilder().setFeatures(features).build();
- }
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeModule.java b/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeModule.java
deleted file mode 100644
index 6564c2c1..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeModule.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.impl;
-
-import android.content.Context;
-import com.google.android.as.oss.common.initializer.PcsInitializer;
-import com.google.android.as.oss.fl.localcompute.FileCopyStartQuery;
-import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
-import dagger.Binds;
-import dagger.Module;
-import dagger.Provides;
-import dagger.hilt.InstallIn;
-import dagger.hilt.android.qualifiers.ApplicationContext;
-import dagger.hilt.components.SingletonComponent;
-import dagger.multibindings.IntoSet;
-
-/** Provides a resource manager and filecopy startquery for local compute tasks. */
-@Module
-@InstallIn(SingletonComponent.class)
-abstract class LocalComputeModule {
-
- @Binds
- abstract LocalComputeResourceManager bindLocalComputeResourceManager(
- LocalComputeResourceManagerImpl impl);
-
- @Binds
- abstract FileCopyStartQuery bindFileCopyStartQuery(FileCopyStartQueryImpl impl);
-
- @Provides
- @IntoSet
- static PcsInitializer providePcsInitializer(@ApplicationContext Context context) {
- return () -> LocalComputeResourceTtlService.scheduleCleanUpRoutineJob(context);
- }
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeResourceManagerImpl.java b/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeResourceManagerImpl.java
deleted file mode 100644
index 1c837dd0..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeResourceManagerImpl.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.impl;
-
-import android.content.Context;
-import android.net.Uri;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.as.oss.common.ExecutorAnnotations.IoExecutorQualifier;
-import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
-import com.google.android.as.oss.fl.localcompute.PathConversionUtils;
-import com.google.android.as.oss.fl.localcompute.client.FileCopyGrpcClient;
-import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.GoogleLogger;
-import com.google.common.time.TimeSource;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import dagger.hilt.android.qualifiers.ApplicationContext;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.concurrent.Executor;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import org.apache.commons.io.FileUtils;
-
-/**
- * A local compute task resource manager that provides APIs for handling the plan|input|output files
- * at different time points.
- */
-@Singleton
-class LocalComputeResourceManagerImpl implements LocalComputeResourceManager {
- private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
-
- /**
- * The marker file is located in each session's resource root folder, the same layer as
- * inputs|plans|outputs. The marker file will track the start time of the most recent training and
- * relative path of input directory.
- */
- private static final String MARKER_FILENAME = "__local_compute_marker__";
-
- /**
- * The freeze window used to avoid accidental resource clean-up while the particular local compute
- * task is in training.
- */
- private static final long FREEZE_WINDOW_MILLIS = Duration.ofMinutes(15).toMillis();
-
- /** This TTL is for the entire resource folder for a particular session. */
- private static final long RESOURCE_TTL_MILLIS = Duration.ofDays(7).toMillis();
-
- private final Context context;
- private final Executor executor;
- private final TimeSource timeSource;
- private final FileCopyGrpcClient fileCopyGrpcClient;
-
- @Inject
- LocalComputeResourceManagerImpl(
- @ApplicationContext Context context,
- @IoExecutorQualifier Executor executor,
- FileCopyGrpcClient fileCopyGrpcClient) {
- this.context = context;
- this.executor = executor;
- this.timeSource = TimeSource.system();
- this.fileCopyGrpcClient = fileCopyGrpcClient;
- }
-
- @VisibleForTesting
- LocalComputeResourceManagerImpl(
- Context context,
- Executor executor,
- TimeSource timeSource,
- FileCopyGrpcClient fileCopyGrpcClient) {
- this.context = context;
- this.executor = executor;
- this.timeSource = timeSource;
- this.fileCopyGrpcClient = fileCopyGrpcClient;
- }
-
- /**
- * At training job's scheduling, we firstly check the marker file. If it's within the freeze
- * window, we do nothing. If not, we delete the entire resource folder for this particular
- * session. Then we copy the plan file from PCC and create an empty input directory. A marker file
- * will be created and the input directory relative path will be tracked in marker file.
- *
- * @return The future is true if computation plan is copied, a marker file is created and an empty
- * input directory is created successfully. It is false if the associated session is in
- * training or any file operation fails.
- */
- @Override
- public ListenableFuture prepareResourceAtScheduling(
- String sessionName, Uri originalPlanUri, Uri originalInputDirUri) {
- String resourceRootRelativePath =
- PathConversionUtils.getResourceRootDirRelativePathForSession(sessionName);
- File resourceRoot = new File(context.getFilesDir(), resourceRootRelativePath);
- File marker = new File(resourceRoot, MARKER_FILENAME);
- return Futures.submitAsync(
- () -> {
- if (marker.exists() && isPossiblyInTraining(marker)) {
- logger.atWarning().log("A training session is possibly in progress!");
- return Futures.immediateFuture(false);
- }
-
- if (resourceRoot.exists()) {
- try {
- FileUtils.forceDelete(resourceRoot);
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to delete the leftover resource files.");
- return Futures.immediateFuture(false);
- }
- }
-
- resourceRoot.mkdirs();
- Uri convertedPlanUri =
- PathConversionUtils.addPlanPathPrefix(originalPlanUri, sessionName);
- Uri convertedInputDirUri =
- PathConversionUtils.addInputPathPrefix(originalInputDirUri, sessionName);
- try {
- marker.createNewFile();
- FileUtils.writeLines(marker, ImmutableList.of(convertedInputDirUri.toString()));
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to create the marker file.");
- return Futures.immediateFuture(false);
- }
- File inputDir = PathConversionUtils.convertUriToFile(context, convertedInputDirUri);
- if (!inputDir.mkdirs()) {
- logger.atSevere().log("Failed to create the input directory.");
- return Futures.immediateFuture(false);
- }
- return fileCopyGrpcClient.copyFileFromServer(originalPlanUri, convertedPlanUri);
- },
- executor);
- }
-
- /**
- * At training job's cancellation, we forcefully delete the entire resource folder for this
- * particular session.
- *
- * @return The future is true if the resource is cleaned successfully. It is false if any file
- * operation fails.
- */
- @Override
- public ListenableFuture cleanResourceAtCancellation(String sessionName) {
- String resourceRootRelativePath =
- PathConversionUtils.getResourceRootDirRelativePathForSession(sessionName);
- return Futures.submit(
- () -> {
- try {
- FileUtils.forceDelete(new File(context.getFilesDir(), resourceRootRelativePath));
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(
- "Failed to delete the resource files at cancellation.");
- return false;
- }
- return true;
- },
- executor);
- }
-
- /**
- * At the beginning of training, we copy the necessary input resources from ASI based on the given
- * file string key. We also refresh the start time of most recent training in marker file.
- *
- * @return The future of the absolute path of the copied file/directory if the input resource
- * contents are copied from remote app and marker file is refreshed successfully. Otherwise,
- * an {@link ImmediateFailedFuture} will be returned.
- */
- @Override
- public ListenableFuture copyResourceAtTraining(String sessionName, String fileStringKey) {
- String resourceRootRelativePath =
- PathConversionUtils.getResourceRootDirRelativePathForSession(sessionName);
- File resourceRoot = new File(context.getFilesDir(), resourceRootRelativePath);
- File marker = new File(resourceRoot, MARKER_FILENAME);
- return Futures.submitAsync(
- () -> {
- try {
- String inputDirUriStr = FileUtils.readLines(marker, Charset.defaultCharset()).get(0);
- String currentTimeMillis = String.valueOf(timeSource.instant().toEpochMilli());
- FileUtils.writeLines(marker, ImmutableList.of(inputDirUriStr, currentTimeMillis));
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Failed to create the marker file.");
- return Futures.immediateFailedFuture(e);
- }
-
- return Futures.transformAsync(
- fileCopyGrpcClient.queryFileUriByStringKey(sessionName, fileStringKey),
- response -> {
- Uri srcUri = Uri.parse(response.getResultFileUri());
- Uri destUri = PathConversionUtils.addInputPathPrefix(srcUri, sessionName);
- final String destAbsolutePath =
- PathConversionUtils.convertUriToFile(context, destUri).getAbsolutePath();
- ListenableFuture copyFuture;
- if (response.getIsDirectory()) {
- copyFuture = fileCopyGrpcClient.copyDirFromServer(srcUri, destUri);
- } else {
- copyFuture = fileCopyGrpcClient.copyFileFromServer(srcUri, destUri);
- }
- return Futures.transformAsync(
- copyFuture,
- result -> {
- if (result) {
- return Futures.immediateFuture(destAbsolutePath);
- } else {
- return Futures.immediateFailedFuture(
- new RuntimeException(
- String.format(
- "Failed to copy from server by string key %s", fileStringKey)));
- }
- },
- executor);
- },
- executor);
- },
- executor);
- }
-
- /**
- * At the result handling, we copy the output directory from PCS back to PCC and then clean the
- * input directory.
- *
- * @return The future is true if the output directory is copied to remote app and input directory
- * is cleaned successfully. It is false if any file operation fails.
- */
- @Override
- public ListenableFuture cleanResourceAtResultHandling(
- String sessionName, Uri convertedInputDirUri, Uri convertedOutputDirUri) {
- File convertedInputDir = PathConversionUtils.convertUriToFile(context, convertedInputDirUri);
- Uri originalOutputDirUri =
- PathConversionUtils.trimOutputPathPrefix(convertedOutputDirUri, sessionName);
- ListenableFuture copyFuture;
- copyFuture = fileCopyGrpcClient.copyDirToServer(convertedOutputDirUri, originalOutputDirUri);
- return Futures.transform(
- copyFuture,
- result -> {
- try {
- FileUtils.cleanDirectory(convertedInputDir);
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(
- "Failed to clean the input directory at Result Handling.");
- return false;
- }
- return result;
- },
- executor);
- }
-
- /**
- * At clean-up routine job, we firstly check the marker file. If it's within the freeze window, we
- * do nothing. Else if it's within TTL limit, we delete everything in the /inputs root and
- * re-create the input directory using the recorded information in marker file. If it exceeds the
- * TTL limit, we forcefully delete entire the session's resource root.
- */
- @Override
- public ListenableFuture cleanResourceAtRoutineJob() {
- File localComputeRootDir =
- new File(context.getFilesDir(), PathConversionUtils.LOCAL_COMPUTE_ROOT);
- return Futures.submit(
- () -> {
- File[] allSessionDirs = localComputeRootDir.listFiles();
- if (allSessionDirs != null) {
- for (File sessionDir : allSessionDirs) {
- File sessionMarker = new File(sessionDir, MARKER_FILENAME);
- if (!sessionMarker.exists() || isExpired(sessionMarker)) {
- try {
- FileUtils.forceDelete(sessionDir);
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(
- "Failed to delete session resource dir: %s", sessionDir);
- }
- continue;
- }
-
- if (isPossiblyInTraining(sessionMarker)) {
- logger.atWarning().log(
- "A training session is possibly in progress for session root: %s!", sessionDir);
- continue;
- }
-
- File inputRootDir = new File(sessionDir, PathConversionUtils.INPUT_DIRECTORY_PREFIX);
- File outputRootDir =
- new File(sessionDir, PathConversionUtils.OUTPUT_DIRECTORY_PREFIX);
- try {
- if (inputRootDir.exists()) {
- FileUtils.forceDelete(inputRootDir);
- }
- if (outputRootDir.exists()) {
- FileUtils.forceDelete(outputRootDir);
- }
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(
- "Failed to clean input or output root dir at Routine clean-up job.");
- }
-
- try {
- List allLines =
- Files.readAllLines(Path.of(sessionMarker.getAbsolutePath()));
- if (!allLines.isEmpty()) {
- Uri convertedInputDirUri = Uri.parse(allLines.get(0));
- File convertedInputDir =
- PathConversionUtils.convertUriToFile(context, convertedInputDirUri);
- convertedInputDir.mkdirs();
- }
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(
- "Failed to create a new empty input dir based on the marker file.");
- }
- }
- }
- },
- executor);
- }
-
- private boolean isPossiblyInTraining(File marker) {
- try {
- List allLines = Files.readAllLines(Path.of(marker.getAbsolutePath()));
- if (allLines.size() > 1) {
- long lastTrainingStartTime = Long.parseLong(allLines.get(1));
- if (timeSource
- .instant()
- .isBefore(
- Instant.ofEpochMilli(lastTrainingStartTime).plusMillis(FREEZE_WINDOW_MILLIS))) {
- return true;
- }
- }
- } catch (IOException e) {
- logger.atWarning().withCause(e).log("Failed to read the marker file.");
- }
-
- return false;
- }
-
- private boolean isExpired(File marker) {
- return timeSource
- .instant()
- .isAfter(Instant.ofEpochMilli(marker.lastModified()).plusMillis(RESOURCE_TTL_MILLIS));
- }
-}
diff --git a/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeResourceTtlService.java b/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeResourceTtlService.java
deleted file mode 100644
index ab5bdeaa..00000000
--- a/src/com/google/android/as/oss/fl/localcompute/impl/LocalComputeResourceTtlService.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2024 Google LLC
- *
- * 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 com.google.android.as.oss.fl.localcompute.impl;
-
-import android.app.job.JobInfo;
-import android.app.job.JobParameters;
-import android.app.job.JobScheduler;
-import android.app.job.JobService;
-import android.content.ComponentName;
-import android.content.Context;
-import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
-import com.google.common.flogger.GoogleLogger;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-import dagger.hilt.android.AndroidEntryPoint;
-import java.time.Duration;
-import java.util.Optional;
-import javax.inject.Inject;
-
-/** Invokes the clean-up job for leftover local compute resources periodlically */
-@AndroidEntryPoint(JobService.class)
-public class LocalComputeResourceTtlService extends Hilt_LocalComputeResourceTtlService {
- private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
-
- private static final long JOB_INTERVAL_MILLIS = Duration.ofHours(12).toMillis();
- @VisibleForTesting static final int JOB_ID = 452613538; // first committed CL number
-
- @Inject Optional resourceManager;
-
- @Override
- public boolean onStartJob(JobParameters jobParameters) {
- logger.atInfo().log("Starting local compute resource clean-up routine job.");
-
- if (resourceManager.isPresent()) {
- ListenableFuture future = resourceManager.get().cleanResourceAtRoutineJob();
- future.addListener(
- () -> {
- logger.atInfo().log("Finished local compute resource clean-up routine job.");
- jobFinished(jobParameters, /* wantsReschedule= */ false);
- },
- MoreExecutors.directExecutor());
- }
-
- return true;
- }
-
- @Override
- public boolean onStopJob(JobParameters jobParameters) {
- return false; // Do not retry
- }
-
- static void scheduleCleanUpRoutineJob(Context context) {
- JobScheduler jobScheduler =
- (JobScheduler)
- Preconditions.checkNotNull(context.getSystemService(Context.JOB_SCHEDULER_SERVICE));
-
- if (jobScheduler.getPendingJob(JOB_ID) != null) {
- logger.atInfo().log("LocalComputeResourceTtlService is already scheduled.");
- return;
- }
-
- int result =
- jobScheduler.schedule(
- new JobInfo.Builder(
- JOB_ID, new ComponentName(context, LocalComputeResourceTtlService.class))
- .setPeriodic(JOB_INTERVAL_MILLIS)
- .setRequiresCharging(false)
- .setRequiresDeviceIdle(true)
- .build());
-
- if (result == JobScheduler.RESULT_SUCCESS) {
- logger.atInfo().log("Successfully scheduled LocalComputeResourceTtlService.");
- } else {
- logger.atWarning().log("Failed to schedule LocalComputeResourceTtlService.");
- }
- }
-}
diff --git a/src/com/google/android/as/oss/fl/populations/BUILD b/src/com/google/android/as/oss/fl/populations/BUILD
new file mode 100644
index 00000000..3833803c
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/populations/BUILD
@@ -0,0 +1,27 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "populations",
+ srcs = ["Population.java"],
+ manifest = "//src/com/google/android/as/oss/common:AndroidManifest.xml",
+ deps = [
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/populations/JobIdPopulationCounterMapper.java b/src/com/google/android/as/oss/fl/populations/JobIdPopulationCounterMapper.java
new file mode 100644
index 00000000..92048f43
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/populations/JobIdPopulationCounterMapper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.as.oss.fl.populations;
+
+import androidx.annotation.Nullable;
+import com.google.android.as.oss.logging.PcsStatsEnums.ValueMetricId;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * JobId to Population feature mapper. Every job scheduled in pcs either has a persistent hash as
+ * generated from farmHashFingerprint64() of the population name or supplied explicitly using {@link
+ * FederationConfig#trainerJobId()}
+ */
+public final class JobIdPopulationCounterMapper {
+
+ /**
+ * Static map contains mapping for PlayProtect related jobs as they are not present in the common
+ * {@link com.google.android.as.oss.fl.populations.Population} enum.
+ */
+ private static final ImmutableMap JOB_ID_METRIC_MAP =
+ ImmutableMap.of(
+ 369941447, ValueMetricId.PLAY_PROTECTION_POPULATION_SCHEDULED_COUNT); // - clean up after
+
+ // moving the
+
+ // PlayProtect population to common Population enum file
+
+ /**
+ * Returns the {@link ValueMetricId} corresponding to the given {@code jobId}.
+ *
+ * If the {@code jobId} is not found in the static {@link #JOB_ID_METRIC_MAP} then it will try
+ * to find the population name from the hash fingerprint.
+ */
+ @Nullable
+ public static ValueMetricId getValueMetricId(int jobId) {
+ // JobId is converted to positive to allow comparison with the hash fingerprint which is also
+ // stored in the in-memory map as always positive.
+ jobId = Math.abs(jobId);
+
+ if (JOB_ID_METRIC_MAP.containsKey(jobId)) {
+ return JOB_ID_METRIC_MAP.get(jobId);
+ }
+
+ Population population = Population.getPopulationByHashFingerprint(jobId);
+ if (population != null) {
+ return getFeatureValueMetricIdByPopulationName(population.populationName());
+ }
+
+ return null;
+ }
+
+ private static ValueMetricId getFeatureValueMetricIdByPopulationName(String populationName) {
+ if (populationName.startsWith("nowplaying")) {
+ return ValueMetricId.NOW_PLAYING_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("smartselect")) {
+ return ValueMetricId.SMARTSELECT_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("safecomms")) {
+ return ValueMetricId.SAFE_COMMS_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("nextconversation")) {
+ return ValueMetricId.NEXT_CONVERSATION_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("autofill")) {
+ return ValueMetricId.AUTOFILL_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("contentcapture")) {
+ return ValueMetricId.CONTENT_CAPTURE_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("echo")) {
+ return ValueMetricId.ECHO_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("overview")) {
+ return ValueMetricId.OVERVIEW_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("pecan")) {
+ return ValueMetricId.PECAN_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("livetranslate")) {
+ return ValueMetricId.LIVE_TRANSLATE_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("search")) {
+ return ValueMetricId.TOAST_SEARCH_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("toastquery")) {
+ return ValueMetricId.TOAST_QUERY_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("ambientcontext")) {
+ return ValueMetricId.AMBIENT_CONTEXT_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("PlayProtect")) {
+ return ValueMetricId.PLAY_PROTECTION_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.contains("platform_logging")) {
+ return ValueMetricId.PLATFORM_LOGGING_POPULATION_SCHEDULED_COUNT;
+ } else if (populationName.startsWith("smartnotification")) {
+ return ValueMetricId.SMART_NOTIFICATION_POPULATION_SCHEDULED_COUNT;
+ }
+
+ return ValueMetricId.UNKNOWN_POPULATION_SCHEDULED_COUNT;
+ }
+
+ private JobIdPopulationCounterMapper() {}
+}
diff --git a/src/com/google/android/as/oss/fl/populations/Population.java b/src/com/google/android/as/oss/fl/populations/Population.java
index c46b9af7..fc91b422 100644
--- a/src/com/google/android/as/oss/fl/populations/Population.java
+++ b/src/com/google/android/as/oss/fl/populations/Population.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/fl/server/BUILD b/src/com/google/android/as/oss/fl/server/BUILD
new file mode 100644
index 00000000..d4f0dbf1
--- /dev/null
+++ b/src/com/google/android/as/oss/fl/server/BUILD
@@ -0,0 +1,52 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "service",
+ srcs = [
+ "TrainerGrpcBindableService.java",
+ "TrainerGrpcModule.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/fl/api:training_grpc",
+ "//src/com/google/android/as/oss/fl/api:training_java_proto_lite",
+ "//src/com/google/android/as/oss/fl/fc/service/scheduler",
+ "//src/com/google/android/as/oss/fl/federatedcompute/config",
+ "//src/com/google/android/as/oss/fl/localcompute:file_copy_start_query",
+ "//src/com/google/android/as/oss/fl/localcompute:optional_module",
+ "//src/com/google/android/as/oss/fl/localcompute:resource_manager",
+ "//src/com/google/android/as/oss/fl/localcompute:utils",
+ "//src/com/google/android/as/oss/grpc:annotations",
+ "@federated_compute//fcp/client:fl_runner",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:io_grpc_grpc_api",
+ "@maven//:io_grpc_grpc_stub",
+ ],
+)
+
+android_library(
+ name = "service_for_tests",
+ testonly = True,
+ visibility = ["//visibility:public"],
+ exports = [
+ ":service",
+ ],
+)
diff --git a/src/com/google/android/as/oss/fl/server/TrainerGrpcBindableService.java b/src/com/google/android/as/oss/fl/server/TrainerGrpcBindableService.java
index 3038d28b..7a240548 100644
--- a/src/com/google/android/as/oss/fl/server/TrainerGrpcBindableService.java
+++ b/src/com/google/android/as/oss/fl/server/TrainerGrpcBindableService.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@
import com.google.android.as.oss.fl.api.proto.TrainerResponse;
import com.google.android.as.oss.fl.api.proto.TrainerResponse.ResponseCode;
import com.google.android.as.oss.fl.api.proto.TrainingServiceGrpc;
-import com.google.android.as.oss.fl.brella.service.scheduler.TrainingScheduler;
+import com.google.android.as.oss.fl.fc.service.scheduler.TrainingScheduler;
import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
import com.google.android.as.oss.fl.localcompute.LocalComputeUtils;
import com.google.fcp.client.tasks.OnFailureListener;
diff --git a/src/com/google/android/as/oss/fl/server/TrainerGrpcModule.java b/src/com/google/android/as/oss/fl/server/TrainerGrpcModule.java
index 87d611e6..690582df 100644
--- a/src/com/google/android/as/oss/fl/server/TrainerGrpcModule.java
+++ b/src/com/google/android/as/oss/fl/server/TrainerGrpcModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,10 @@
import com.google.android.as.oss.common.ExecutorAnnotations.FlExecutorQualifier;
import com.google.android.as.oss.fl.api.proto.TrainingServiceGrpc;
-import com.google.android.as.oss.fl.brella.service.scheduler.TrainingScheduler;
+import com.google.android.as.oss.fl.fc.service.scheduler.TrainingScheduler;
import com.google.android.as.oss.fl.localcompute.LocalComputeResourceManager;
-import com.google.android.apps.miphone.astrea.grpc.Annotations.GrpcService;
-import com.google.android.apps.miphone.astrea.grpc.Annotations.GrpcServiceName;
+import com.google.android.apps.miphone.pcs.grpc.Annotations.GrpcService;
+import com.google.android.apps.miphone.pcs.grpc.Annotations.GrpcServiceName;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
diff --git a/src/com/google/android/as/oss/grpc/AndroidManifest.xml b/src/com/google/android/as/oss/grpc/AndroidManifest.xml
index ccd619cf..38a418de 100644
--- a/src/com/google/android/as/oss/grpc/AndroidManifest.xml
+++ b/src/com/google/android/as/oss/grpc/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_file_upload_24.xml b/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_file_upload_24.xml
index a815a033..f6cb14c6 100644
--- a/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_file_upload_24.xml
+++ b/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_file_upload_24.xml
@@ -1,6 +1,5 @@
-
-
+
diff --git a/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_system_security_update_good_24.xml b/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_system_security_update_good_24.xml
index cbf61338..b7887ade 100644
--- a/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_system_security_update_good_24.xml
+++ b/src/com/google/android/as/oss/networkusage/ui/user/res/drawable/outline_system_security_update_good_24.xml
@@ -1,6 +1,5 @@
-
-
+
diff --git a/src/com/google/android/as/oss/networkusage/ui/user/res/layout/date_divider.xml b/src/com/google/android/as/oss/networkusage/ui/user/res/layout/date_divider.xml
index d32bf7aa..5f09c200 100644
--- a/src/com/google/android/as/oss/networkusage/ui/user/res/layout/date_divider.xml
+++ b/src/com/google/android/as/oss/networkusage/ui/user/res/layout/date_divider.xml
@@ -1,6 +1,6 @@
Updated the on-device protections of Google Play Protect Service
- com.google.android.odad(:.*)?
+ com.google.android.PlayProtect(:.*)?
https://www.gstatic.com/on-device-safety/bt_log_signature_key.txt\\?c=tnAbOw
@@ -329,4 +329,15 @@
https://scone-pa.googleapis.com/v1/survey/.*
+
+
+ Device Intelligence
+
+ Updated the on-device model for Device Intelligence
+ https://edgedl.me.gvt1.com/edgedl/mdi-serving/psi-models/.*
+ com.google.android.apps.pixel.psi(:.*)?
+
+ Updated the on-device protections for Device Intelligence
+
+
diff --git a/src/com/google/android/as/oss/networkusage/ui/user/res/values/styles.xml b/src/com/google/android/as/oss/networkusage/ui/user/res/values/styles.xml
index 3e0f7811..d064c6ca 100644
--- a/src/com/google/android/as/oss/networkusage/ui/user/res/values/styles.xml
+++ b/src/com/google/android/as/oss/networkusage/ui/user/res/values/styles.xml
@@ -1,6 +1,6 @@
-
-# Protected Download Protocol / API
-
-Protected Download enables downloading of resources to the device with support
-for a binary transparency log based verification, ensuring these are the
-official resources provided by Google.
-
-This API is used to deliver sensitive models/heuristics to Private Compute Core
-apps. The mechanism of download is open-sourced to show that through the
-connection to the server personal user data is not sent to Google, but rather
-receiving the model or heuristics in an encrypted and verified manner.
-
-As a first use case, this API is used by Google Play Protect Service. As Google
-Play Protect Service keeps users safe from malware, the models and heuristics
-themselves need to be protected from malware authors.
-
-An extra layer of security that Protected Download provides is the ability to
-instantiate a Virtual Machine and use its public key for downloads. The virtual
-machine is then transferred to the client application. By having the VM
-instantiated within the Protected Download service, it demonstrates that the
-public key does not contain any sensitive data.
diff --git a/src/com/google/android/as/oss/pd/api/BUILD b/src/com/google/android/as/oss/pd/api/BUILD
new file mode 100644
index 00000000..f96b65dc
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/api/BUILD
@@ -0,0 +1,76 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@rules_proto_grpc//java:defs.bzl", "java_grpc_library")
+load("//third_party/bazel_rules/rules_python/python:proto.bzl", "py_proto_library")
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+load(
+ "//third_party/protobuf/build_defs:kt_jvm_proto_library.bzl",
+ "kt_jvm_lite_proto_library",
+ "kt_jvm_proto_library",
+)
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+proto_library(
+ name = "protected_download_proto",
+ srcs = [
+ "protected_download.proto",
+ ],
+ has_services = 1,
+ visibility = [
+ "//visibility:public",
+ ],
+ deps = [
+ "",
+ "//third_party/protobuf:cpp_features_proto",
+ ],
+)
+
+java_lite_proto_library(
+ name = "protected_download_java_proto_lite",
+ deps = [
+ ":protected_download_proto",
+ ],
+)
+
+kt_jvm_lite_proto_library(
+ name = "protected_download_kt_proto_lite",
+ deps = [
+ ":protected_download_proto",
+ ],
+)
+
+kt_jvm_proto_library(
+ name = "protected_download_kt_proto",
+ deps = [":protected_download_proto"],
+)
+
+py_proto_library(
+ name = "protected_download_py_pb2",
+ deps = [":protected_download_proto"],
+)
+
+java_grpc_library(
+ name = "protected_download_grpc",
+ srcs = [":protected_download_proto"],
+ constraints = ["android"],
+ flavor = "lite",
+ deps = [
+ ":protected_download_java_proto_lite",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/api/protected_download.proto b/src/com/google/android/as/oss/pd/api/protected_download.proto
index 4eabbf1f..79917e99 100644
--- a/src/com/google/android/as/oss/pd/api/protected_download.proto
+++ b/src/com/google/android/as/oss/pd/api/protected_download.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,10 +16,12 @@ edition = "2023";
package com.google.android.as.oss.pd.api;
+import "third_party/protobuf/cpp_features.proto";
+
+option features.(pb.cpp).string_type = VIEW;
option features.field_presence = IMPLICIT;
option java_multiple_files = true;
option java_package = "com.google.android.as.oss.pd.api.proto";
-option use_java_stubby_library = true;
// Request to download a blob.
message DownloadBlobRequest {
@@ -127,6 +129,13 @@ message BlobConstraints {
AI_CORE_CLIENT_49 = 49;
AI_CORE_CLIENT_50 = 50;
+ // PSI clients reserved range from 101 to 200
+ PSI_MDD_MODELS_CLIENT = 101;
+ PSI_LLM_OUTPUT_CLASSIFIER_CLIENT = 102;
+ PSI_NON_LLM_OUTPUT_CLASSIFIER_CLIENT = 103;
+ PSI_TEXT_INPUT_REGEXT_ONLY_CLASSIFIER_CLIENT = 104;
+ PSI_TEXT_OUTPUT_REGEX_ONLY_CLASSIFIER_CLIENT = 105;
+
reserved 10000 to 10004, 99997 to 99999;
}
@@ -139,6 +148,7 @@ message BlobConstraints {
THIRD_PARTY_EAP = 3;
THIRD_PARTY_EXPERIMENTAL = 4;
}
+
ClientGroup client_group = 1003;
// Client SDK Version to help server estimate SDK capabilities.
@@ -211,6 +221,9 @@ message BlobConstraints {
// Variant of the Android device requests a new download.
Variant variant = 1005;
+ // An optional build id to override the value returned by the DeviceConfig.
+ int64 build_id = 1006;
+
reserved 1, 2, 3, 4, 5;
}
@@ -226,8 +239,7 @@ enum ProtectionType {
TYPE_RULE = 1;
TYPE_MODEL = 2;
TYPE_BLM_BLOCK_LIST = 4;
-
- reserved 3;
+ reserved 3, 5, 6;
}
message ProtectionComponent {
diff --git a/src/com/google/android/as/oss/pd/attestation/AttestationClient.java b/src/com/google/android/as/oss/pd/attestation/AttestationClient.java
index dbcc22ac..ea8f6e87 100644
--- a/src/com/google/android/as/oss/pd/attestation/AttestationClient.java
+++ b/src/com/google/android/as/oss/pd/attestation/AttestationClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/attestation/AttestationResponse.java b/src/com/google/android/as/oss/pd/attestation/AttestationResponse.java
index 50953ee2..f19b3937 100644
--- a/src/com/google/android/as/oss/pd/attestation/AttestationResponse.java
+++ b/src/com/google/android/as/oss/pd/attestation/AttestationResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/attestation/BUILD b/src/com/google/android/as/oss/pd/attestation/BUILD
new file mode 100644
index 00000000..d0405eed
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/attestation/BUILD
@@ -0,0 +1,32 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "attestation",
+ srcs = [
+ "AttestationClient.java",
+ "AttestationResponse.java",
+ ],
+ deps = [
+ "//third_party/java/auto:auto_value",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_javalite",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientImpl.java b/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientImpl.java
index 66e04598..4749a028 100644
--- a/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientImpl.java
+++ b/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientModule.java b/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientModule.java
index f9636275..f329faaa 100644
--- a/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientModule.java
+++ b/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientNoopImpl.java b/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientNoopImpl.java
index a2d6fe96..b69ad80e 100644
--- a/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientNoopImpl.java
+++ b/src/com/google/android/as/oss/pd/attestation/impl/AttestationClientNoopImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/attestation/impl/BUILD b/src/com/google/android/as/oss/pd/attestation/impl/BUILD
new file mode 100644
index 00000000..9cdcba5d
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/attestation/impl/BUILD
@@ -0,0 +1,38 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "impl",
+ srcs = [
+ "AttestationClientImpl.java",
+ "AttestationClientModule.java",
+ "AttestationClientNoopImpl.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/attestation",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/common/flavor",
+ "//src/com/google/android/as/oss/pd/attestation",
+ "//src/com/google/android/as/oss/pd/config",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/channel/BUILD b/src/com/google/android/as/oss/pd/channel/BUILD
new file mode 100644
index 00000000..30b09833
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/channel/BUILD
@@ -0,0 +1,28 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "channel",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "@maven//:io_grpc_grpc_api",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/channel/ChannelProvider.java b/src/com/google/android/as/oss/pd/channel/ChannelProvider.java
index 07273a22..82ffde36 100644
--- a/src/com/google/android/as/oss/pd/channel/ChannelProvider.java
+++ b/src/com/google/android/as/oss/pd/channel/ChannelProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/channel/impl/BUILD b/src/com/google/android/as/oss/pd/channel/impl/BUILD
new file mode 100644
index 00000000..038eafe4
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/channel/impl/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "impl",
+ srcs = [
+ "ChannelProviderImpl.java",
+ "ChannelProviderModule.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/channel",
+ "//third_party/java/android_libs/guava_jdk5:cache",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@maven//:io_grpc_grpc_api",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderImpl.java b/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderImpl.java
index d421ec7c..608f47df 100644
--- a/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderImpl.java
+++ b/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderModule.java b/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderModule.java
index 42f131cc..bb6cc356 100644
--- a/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderModule.java
+++ b/src/com/google/android/as/oss/pd/channel/impl/ChannelProviderModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/common/BUILD b/src/com/google/android/as/oss/pd/common/BUILD
new file mode 100644
index 00000000..3aabb70d
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/common/BUILD
@@ -0,0 +1,50 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "common",
+ srcs = [
+ "ClientConfig.java",
+ "ProtoConversions.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//third_party/java/auto:auto_value",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+android_library(
+ name = "common_module",
+ srcs = [
+ "ProtoConversionsModule.java",
+ ],
+ deps = [
+ ":common",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/common/ClientConfig.java b/src/com/google/android/as/oss/pd/common/ClientConfig.java
index 51f995d9..1591b7cb 100644
--- a/src/com/google/android/as/oss/pd/common/ClientConfig.java
+++ b/src/com/google/android/as/oss/pd/common/ClientConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/common/ProtoConversions.java b/src/com/google/android/as/oss/pd/common/ProtoConversions.java
index e4d6f759..2e98422a 100644
--- a/src/com/google/android/as/oss/pd/common/ProtoConversions.java
+++ b/src/com/google/android/as/oss/pd/common/ProtoConversions.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/common/ProtoConversionsModule.java b/src/com/google/android/as/oss/pd/common/ProtoConversionsModule.java
index 627ae45c..5d404171 100644
--- a/src/com/google/android/as/oss/pd/common/ProtoConversionsModule.java
+++ b/src/com/google/android/as/oss/pd/common/ProtoConversionsModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,21 +42,21 @@ final class ProtoConversionsModule {
@IntoMap
@ClientMapKey(Client.PLAY_PROTECT_SERVICE)
static ClientConfig providePlayProtectClientConfig() {
- return ClientConfig.create("com.google.android.odad");
+ return ClientConfig.create("com.google.android.PlayProtect");
}
@Provides
@IntoMap
@ClientMapKey(Client.PLAY_PROTECT_SERVICE_CORE_DEFAULT)
static ClientConfig providePlayProtectCoreDefaultClientConfig() {
- return ClientConfig.create("com.google.android.odad:2793571637033546290");
+ return ClientConfig.create("com.google.android.PlayProtect:2793571637033546290");
}
@Provides
@IntoMap
@ClientMapKey(Client.PLAY_PROTECT_SERVICE_PVM_DEFAULT)
static ClientConfig providePlayProtectPvmDefaultClientConfig() {
- return ClientConfig.create("com.google.android.odad:2525461103339185322");
+ return ClientConfig.create("com.google.android.PlayProtect:2525461103339185322");
}
@Provides
@@ -561,6 +561,71 @@ static ClientConfig provideAiCoreClient50ClientConfig() {
.build();
}
+ @Provides
+ @IntoMap
+ @ClientMapKey(Client.PSI_MDD_MODELS_CLIENT)
+ static ClientConfig providePsiMddModelsClientConfig() {
+ return ClientConfig.builder()
+ .setClientId("com.google.android.apps.pixel.psi:11791126134479005147")
+ .setBuildIdFlag(
+ ClientConfig.BuildIdFlag.create(
+ FlagNamespace.DEVICE_PERSONALIZATION_SERVICES,
+ "PsiModelDownload__psi_build_id_11791126134479005147"))
+ .build();
+ }
+
+ @Provides
+ @IntoMap
+ @ClientMapKey(Client.PSI_LLM_OUTPUT_CLASSIFIER_CLIENT)
+ static ClientConfig providePsiLlmOutputClassifierClientConfig() {
+ return ClientConfig.builder()
+ .setClientId("com.google.android.apps.pixel.psi:3177959871173576590")
+ .setBuildIdFlag(
+ ClientConfig.BuildIdFlag.create(
+ FlagNamespace.DEVICE_PERSONALIZATION_SERVICES,
+ "PsiModelDownload__psi_build_id_3177959871173576590"))
+ .build();
+ }
+
+ @Provides
+ @IntoMap
+ @ClientMapKey(Client.PSI_NON_LLM_OUTPUT_CLASSIFIER_CLIENT)
+ static ClientConfig providePsiNonLlmOutputClassifierClientConfig() {
+ return ClientConfig.builder()
+ .setClientId("com.google.android.apps.pixel.psi:12033173399242171289")
+ .setBuildIdFlag(
+ ClientConfig.BuildIdFlag.create(
+ FlagNamespace.DEVICE_PERSONALIZATION_SERVICES,
+ "PsiModelDownload__psi_build_id_12033173399242171289"))
+ .build();
+ }
+
+ @Provides
+ @IntoMap
+ @ClientMapKey(Client.PSI_TEXT_INPUT_REGEXT_ONLY_CLASSIFIER_CLIENT)
+ static ClientConfig providePsiTextIutputClassifierClientConfig() {
+ return ClientConfig.builder()
+ .setClientId("com.google.android.apps.pixel.psi:17453388543208459382")
+ .setBuildIdFlag(
+ ClientConfig.BuildIdFlag.create(
+ FlagNamespace.DEVICE_PERSONALIZATION_SERVICES,
+ "PsiModelDownload__psi_build_id_17453388543208459382"))
+ .build();
+ }
+
+ @Provides
+ @IntoMap
+ @ClientMapKey(Client.PSI_TEXT_OUTPUT_REGEX_ONLY_CLASSIFIER_CLIENT)
+ static ClientConfig providePsiTextOutputClassifierClientConfig() {
+ return ClientConfig.builder()
+ .setClientId("com.google.android.apps.pixel.psi:11987824919611589942")
+ .setBuildIdFlag(
+ ClientConfig.BuildIdFlag.create(
+ FlagNamespace.DEVICE_PERSONALIZATION_SERVICES,
+ "PsiModelDownload__psi_build_id_11987824919611589942"))
+ .build();
+ }
+
@Provides
@Singleton
static ProtoConversions provideProtoConversions(Map clientToClientId) {
diff --git a/src/com/google/android/as/oss/pd/config/BUILD b/src/com/google/android/as/oss/pd/config/BUILD
new file mode 100644
index 00000000..2aa0b8b8
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/config/BUILD
@@ -0,0 +1,28 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "config",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//third_party/java/auto:auto_value",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/config/ClientBuildVersionReader.java b/src/com/google/android/as/oss/pd/config/ClientBuildVersionReader.java
index 989080b5..da960524 100644
--- a/src/com/google/android/as/oss/pd/config/ClientBuildVersionReader.java
+++ b/src/com/google/android/as/oss/pd/config/ClientBuildVersionReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/config/ProtectedDownloadConfig.java b/src/com/google/android/as/oss/pd/config/ProtectedDownloadConfig.java
index 46577643..f118be33 100644
--- a/src/com/google/android/as/oss/pd/config/ProtectedDownloadConfig.java
+++ b/src/com/google/android/as/oss/pd/config/ProtectedDownloadConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/common/config/noop/BUILD b/src/com/google/android/as/oss/pd/config/impl/BUILD
similarity index 62%
rename from src/com/google/android/as/oss/common/config/noop/BUILD
rename to src/com/google/android/as/oss/pd/config/impl/BUILD
index c0fc2ca6..dbaf8af9 100644
--- a/src/com/google/android/as/oss/common/config/noop/BUILD
+++ b/src/com/google/android/as/oss/pd/config/impl/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2024 Google LLC
+# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,26 +14,22 @@
load("@bazel_rules_android//android:rules.bzl", "android_library")
+package(default_visibility = [
+ "//visibility:public",
+])
+
android_library(
- name = "flags_impl_noop",
- srcs = [
- "DeviceFlagManagerFactoryNoOp.java",
- "FlagManagerModuleNoOp.java",
- ],
+ name = "impl",
+ srcs = glob(["*.java"]),
deps = [
- ":flag_manager_impl_noop",
+ "//src/com/google/android/as/oss/common:annotation",
"//src/com/google/android/as/oss/common/config",
- "//third_party/java/hilt:hilt-android",
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/common",
+ "//src/com/google/android/as/oss/pd/config",
"@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
"@maven//:javax_inject_javax_inject",
],
)
-
-android_library(
- name = "flag_manager_impl_noop",
- srcs = ["DeviceFlagManagerNoOp.java"],
- deps = [
- "//src/com/google/android/as/oss/common/config",
- "@maven//:org_checkerframework_checker_qual",
- ],
-)
diff --git a/src/com/google/android/as/oss/pd/config/impl/ClientBuildVersionReaderImpl.java b/src/com/google/android/as/oss/pd/config/impl/ClientBuildVersionReaderImpl.java
index 9d8e0c78..2f743c88 100644
--- a/src/com/google/android/as/oss/pd/config/impl/ClientBuildVersionReaderImpl.java
+++ b/src/com/google/android/as/oss/pd/config/impl/ClientBuildVersionReaderImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigModule.java b/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigModule.java
index 38921464..c886f7dc 100644
--- a/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigModule.java
+++ b/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigReader.java b/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigReader.java
index 73b56405..b620b242 100644
--- a/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigReader.java
+++ b/src/com/google/android/as/oss/pd/config/impl/ProtectedDownloadConfigReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/third_party/java/hilt/BUILD b/src/com/google/android/as/oss/pd/keys/BUILD
similarity index 71%
rename from third_party/java/hilt/BUILD
rename to src/com/google/android/as/oss/pd/keys/BUILD
index 3c6775a0..974c41c5 100644
--- a/third_party/java/hilt/BUILD
+++ b/src/com/google/android/as/oss/pd/keys/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2024 Google LLC
+# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,8 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-load("@dagger//:workspace_defs.bzl", "hilt_android_rules")
+load("@bazel_rules_android//android:rules.bzl", "android_library")
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = [
+ "//visibility:public",
+])
-hilt_android_rules()
+android_library(
+ name = "keys",
+ srcs = glob(["*.java"]),
+)
diff --git a/src/com/google/android/as/oss/pd/keys/EncryptionHelper.java b/src/com/google/android/as/oss/pd/keys/EncryptionHelper.java
index 55c4607f..c98c2a4e 100644
--- a/src/com/google/android/as/oss/pd/keys/EncryptionHelper.java
+++ b/src/com/google/android/as/oss/pd/keys/EncryptionHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/EncryptionHelperFactory.java b/src/com/google/android/as/oss/pd/keys/EncryptionHelperFactory.java
index 918721c2..431f2cac 100644
--- a/src/com/google/android/as/oss/pd/keys/EncryptionHelperFactory.java
+++ b/src/com/google/android/as/oss/pd/keys/EncryptionHelperFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/impl/AndroidKeyStoreMasterKeyProviderModule.java b/src/com/google/android/as/oss/pd/keys/impl/AndroidKeyStoreMasterKeyProviderModule.java
index 4a99e21a..67c5e8a6 100644
--- a/src/com/google/android/as/oss/pd/keys/impl/AndroidKeyStoreMasterKeyProviderModule.java
+++ b/src/com/google/android/as/oss/pd/keys/impl/AndroidKeyStoreMasterKeyProviderModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/impl/BUILD b/src/com/google/android/as/oss/pd/keys/impl/BUILD
new file mode 100644
index 00000000..5aaaeb26
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/keys/impl/BUILD
@@ -0,0 +1,87 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library", "java_library_with_nullness_check")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "impl",
+ srcs = [
+ "MasterKeyProvider.java",
+ "TinkEncryptionHelper.java",
+ "TinkEncryptionHelperFactory.java",
+ ],
+ deps = [
+ ":stable_key_hash",
+ "//src/com/google/android/as/oss/pd/keys",
+ "@maven//:com_google_crypto_tink_tink_android",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "encryption_module",
+ srcs = [
+ "TinkEncryptionHelperModule.java",
+ ],
+ deps = [
+ ":impl",
+ "//src/com/google/android/as/oss/pd/keys",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ ],
+)
+
+android_library(
+ name = "android_keystore_module",
+ srcs = [
+ "AndroidKeyStoreMasterKeyProviderModule.java",
+ ],
+ deps = [
+ ":impl",
+ "@maven//:com_google_crypto_tink_tink_android",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "stable_key_hash",
+ srcs = ["StableKeyHash.java"],
+ visibility = [
+ "//visibility:public",
+ ],
+ deps = [
+ "@maven//:com_google_crypto_tink_tink_android",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+java_library_with_nullness_check(
+ name = "stable_key_hash_server",
+ srcs = ["StableKeyHash.java"],
+ visibility = [
+ "//visibility:public",
+ ],
+ deps = [
+ "//third_party/java/tink:accesses_partial_key",
+ "//third_party/java/tink:hybrid",
+ "//third_party/java/tink:tink_core",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/keys/impl/MasterKeyProvider.java b/src/com/google/android/as/oss/pd/keys/impl/MasterKeyProvider.java
index 88d54479..7fc8bb05 100644
--- a/src/com/google/android/as/oss/pd/keys/impl/MasterKeyProvider.java
+++ b/src/com/google/android/as/oss/pd/keys/impl/MasterKeyProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/impl/StableKeyHash.java b/src/com/google/android/as/oss/pd/keys/impl/StableKeyHash.java
index 22c1869e..6e155dd1 100644
--- a/src/com/google/android/as/oss/pd/keys/impl/StableKeyHash.java
+++ b/src/com/google/android/as/oss/pd/keys/impl/StableKeyHash.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelper.java b/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelper.java
index cdd9e144..eff2fc9f 100644
--- a/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelper.java
+++ b/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperFactory.java b/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperFactory.java
index 7392ea33..087aa8f8 100644
--- a/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperFactory.java
+++ b/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperModule.java b/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperModule.java
index 6f8436c2..221641d6 100644
--- a/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperModule.java
+++ b/src/com/google/android/as/oss/pd/keys/impl/TinkEncryptionHelperModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/networkusage/ui/content/noop/BUILD b/src/com/google/android/as/oss/pd/networkusage/BUILD
similarity index 77%
rename from src/com/google/android/as/oss/networkusage/ui/content/noop/BUILD
rename to src/com/google/android/as/oss/pd/networkusage/BUILD
index 4e624877..7a28d7c9 100644
--- a/src/com/google/android/as/oss/networkusage/ui/content/noop/BUILD
+++ b/src/com/google/android/as/oss/pd/networkusage/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2024 Google LLC
+# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,16 +14,15 @@
load("@bazel_rules_android//android:rules.bzl", "android_library")
-package(default_visibility = ["//visibility:public"])
+package(default_visibility = [
+ "//visibility:public",
+])
android_library(
- name = "noop",
+ name = "networkusage",
srcs = glob(["*.java"]),
deps = [
"//src/com/google/android/as/oss/networkusage/db",
"//src/com/google/android/as/oss/networkusage/ui/content",
- "//third_party/java/hilt:hilt-android",
- "@maven//:com_google_dagger_dagger",
- "@maven//:javax_inject_javax_inject",
],
)
diff --git a/src/com/google/android/as/oss/pd/networkusage/PDNetworkUsageLogHelper.java b/src/com/google/android/as/oss/pd/networkusage/PDNetworkUsageLogHelper.java
index d59ccb57..f888e9fd 100644
--- a/src/com/google/android/as/oss/pd/networkusage/PDNetworkUsageLogHelper.java
+++ b/src/com/google/android/as/oss/pd/networkusage/PDNetworkUsageLogHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/networkusage/db/noop/BUILD b/src/com/google/android/as/oss/pd/networkusage/impl/BUILD
similarity index 77%
rename from src/com/google/android/as/oss/networkusage/db/noop/BUILD
rename to src/com/google/android/as/oss/pd/networkusage/impl/BUILD
index 3bf438db..2f632041 100644
--- a/src/com/google/android/as/oss/networkusage/db/noop/BUILD
+++ b/src/com/google/android/as/oss/pd/networkusage/impl/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2024 Google LLC
+# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,18 +14,21 @@
load("@bazel_rules_android//android:rules.bzl", "android_library")
+package(default_visibility = [
+ "//visibility:public",
+])
+
android_library(
- name = "noop",
+ name = "impl",
srcs = glob(["*.java"]),
- visibility = ["//visibility:public"],
deps = [
"//src/com/google/android/as/oss/networkusage/db",
"//src/com/google/android/as/oss/networkusage/db:repository",
"//src/com/google/android/as/oss/networkusage/ui/content",
- "//third_party/java/hilt:hilt-android",
- "@maven//:androidx_lifecycle_lifecycle_livedata_core",
+ "//src/com/google/android/as/oss/pd/networkusage",
"@maven//:com_google_dagger_dagger",
- "@maven//:com_google_guava_guava",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
"@maven//:javax_inject_javax_inject",
],
)
diff --git a/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperImpl.java b/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperImpl.java
index 3c5c8cbd..f2ed95b6 100644
--- a/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperImpl.java
+++ b/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperModule.java b/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperModule.java
index 3b89ae93..011c73bd 100644
--- a/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperModule.java
+++ b/src/com/google/android/as/oss/pd/networkusage/impl/PDNetworkUsageLogHelperModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/persistence/BUILD b/src/com/google/android/as/oss/pd/persistence/BUILD
new file mode 100644
index 00000000..b499fdfe
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/persistence/BUILD
@@ -0,0 +1,40 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+proto_library(
+ name = "client_persistent_state_proto",
+ srcs = ["client_persistent_state.proto"],
+)
+
+java_lite_proto_library(
+ name = "client_persistent_state_java_proto_lite",
+ deps = [":client_persistent_state_proto"],
+)
+
+android_library(
+ name = "persistence",
+ srcs = glob(["*.java"]),
+ deps = [
+ ":client_persistent_state_java_proto_lite",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/persistence/PersistentStateManager.java b/src/com/google/android/as/oss/pd/persistence/PersistentStateManager.java
index dec1d292..b31603cb 100644
--- a/src/com/google/android/as/oss/pd/persistence/PersistentStateManager.java
+++ b/src/com/google/android/as/oss/pd/persistence/PersistentStateManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/persistence/client_persistent_state.proto b/src/com/google/android/as/oss/pd/persistence/client_persistent_state.proto
index c4d40a59..16041cf3 100644
--- a/src/com/google/android/as/oss/pd/persistence/client_persistent_state.proto
+++ b/src/com/google/android/as/oss/pd/persistence/client_persistent_state.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/persistence/preferencesimpl/BUILD b/src/com/google/android/as/oss/pd/persistence/preferencesimpl/BUILD
new file mode 100644
index 00000000..50ef13d6
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/persistence/preferencesimpl/BUILD
@@ -0,0 +1,35 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "preferencesimpl",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/pd/persistence",
+ "//src/com/google/android/as/oss/pd/persistence:client_persistent_state_java_proto_lite",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_javalite",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesImpl.java b/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesImpl.java
index ea3f35d1..1a6bd1e8 100644
--- a/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesImpl.java
+++ b/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesModule.java b/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesModule.java
index 4733bc51..53a0bef1 100644
--- a/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesModule.java
+++ b/src/com/google/android/as/oss/pd/persistence/preferencesimpl/PersistentStateManagerSharedPreferencesModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/processor/BUILD b/src/com/google/android/as/oss/pd/processor/BUILD
new file mode 100644
index 00000000..d8ffe735
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/processor/BUILD
@@ -0,0 +1,29 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "processor",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "@maven//:com_google_guava_guava",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/processor/ProtectedDownloadProcessor.java b/src/com/google/android/as/oss/pd/processor/ProtectedDownloadProcessor.java
index b07741af..54ffe9ed 100644
--- a/src/com/google/android/as/oss/pd/processor/ProtectedDownloadProcessor.java
+++ b/src/com/google/android/as/oss/pd/processor/ProtectedDownloadProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/processor/impl/BUILD b/src/com/google/android/as/oss/pd/processor/impl/BUILD
new file mode 100644
index 00000000..6c2fac97
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/processor/impl/BUILD
@@ -0,0 +1,74 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "impl",
+ srcs = [
+ "BlobProtoUtils.java",
+ "ProtectedDownloadProcessorImpl.java",
+ "SanityChecks.java",
+ ],
+ manifest = "//src/com/google/android/as/oss/common:AndroidManifest.xml",
+ deps = [
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/time",
+ "//src/com/google/android/as/oss/networkusage/db",
+ "//src/com/google/android/as/oss/networkusage/ui/content",
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/attestation",
+ "//src/com/google/android/as/oss/pd/channel",
+ "//src/com/google/android/as/oss/pd/common",
+ "//src/com/google/android/as/oss/pd/config",
+ "//src/com/google/android/as/oss/pd/keys",
+ "//src/com/google/android/as/oss/pd/networkusage",
+ "//src/com/google/android/as/oss/pd/persistence",
+ "//src/com/google/android/as/oss/pd/persistence:client_persistent_state_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/processor",
+ "//src/com/google/android/as/oss/pd/service/api:download_service_java_grpc_lite",
+ "//src/com/google/android/as/oss/pd/service/api:download_service_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/service/api:programblobs_java_grpc_lite",
+ "//src/com/google/android/as/oss/pd/service/api:programblobs_java_proto_lite",
+ "//third_party/java/auto:auto_value",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_javalite",
+ "@maven//:io_grpc_grpc_api",
+ "@maven//:io_grpc_grpc_stub",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "module",
+ srcs = [
+ "BlobProtoUtilsModule.java",
+ "ProtectedDownloadProcessorModule.java",
+ ],
+ deps = [
+ ":impl",
+ "//src/com/google/android/as/oss/pd/common",
+ "//src/com/google/android/as/oss/pd/config",
+ "//src/com/google/android/as/oss/pd/processor",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtils.java b/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtils.java
index a5f32672..0726be8e 100644
--- a/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtils.java
+++ b/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -429,10 +429,17 @@ ManifestConfigConstraints buildConstraints(
.addLabel(toLabelM(DEVICE_TIER_LABEL_KEY, getDeviceTier(constraints)));
getVariant(constraints)
.ifPresent(value -> constraintsBuilder.addLabel(toLabelM(VARIANT_LABEL_KEY, value)));
- clientBuildVersionReader
- .getBuildId(constraints.getClient())
- .ifPresent(
- value -> constraintsBuilder.addLabel(toLabelM(BUILD_ID_LABEL_KEY, value.toString())));
+
+ // If the build ID is not set in the constraints, we will try to get it from the client.
+ Optional buildId;
+ if (constraints.getBuildId() != 0) {
+ buildId = Optional.of(constraints.getBuildId());
+ } else {
+ buildId = clientBuildVersionReader.getBuildId(constraints.getClient());
+ }
+ buildId.ifPresent(
+ value -> constraintsBuilder.addLabel(toLabelM(BUILD_ID_LABEL_KEY, value.toString())));
+
return constraintsBuilder.build();
}
diff --git a/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtilsModule.java b/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtilsModule.java
index f562f71d..e3477ef0 100644
--- a/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtilsModule.java
+++ b/src/com/google/android/as/oss/pd/processor/impl/BlobProtoUtilsModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorImpl.java b/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorImpl.java
index 6ff17f64..1390885c 100644
--- a/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorImpl.java
+++ b/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorModule.java b/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorModule.java
index 14468c85..0bc1e627 100644
--- a/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorModule.java
+++ b/src/com/google/android/as/oss/pd/processor/impl/ProtectedDownloadProcessorModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/processor/impl/SanityChecks.java b/src/com/google/android/as/oss/pd/processor/impl/SanityChecks.java
index c61b1129..e3806bb5 100644
--- a/src/com/google/android/as/oss/pd/processor/impl/SanityChecks.java
+++ b/src/com/google/android/as/oss/pd/processor/impl/SanityChecks.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/service/BUILD b/src/com/google/android/as/oss/pd/service/BUILD
new file mode 100644
index 00000000..8d3cd18e
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/service/BUILD
@@ -0,0 +1,42 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "service",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/common/flavor",
+ "//src/com/google/android/as/oss/grpc:annotations",
+ "//src/com/google/android/as/oss/pd/api:protected_download_grpc",
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/config",
+ "//src/com/google/android/as/oss/pd/processor",
+ "//src/com/google/android/as/oss/pd/virtualmachine",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:io_grpc_grpc_api",
+ "@maven//:io_grpc_grpc_stub",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcBindableService.java b/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcBindableService.java
index 428fe4b1..4ec660d4 100644
--- a/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcBindableService.java
+++ b/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcBindableService.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcModule.java b/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcModule.java
index 14936210..1dcb75b5 100644
--- a/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcModule.java
+++ b/src/com/google/android/as/oss/pd/service/ProtectedDownloadGrpcModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
package com.google.android.as.oss.pd.service;
-import com.google.android.apps.miphone.astrea.grpc.Annotations.GrpcService;
-import com.google.android.apps.miphone.astrea.grpc.Annotations.GrpcServiceName;
+import com.google.android.apps.miphone.pcs.grpc.Annotations.GrpcService;
+import com.google.android.apps.miphone.pcs.grpc.Annotations.GrpcServiceName;
import com.google.android.as.oss.pd.api.proto.ProtectedDownloadServiceGrpc;
import com.google.android.as.oss.pd.virtualmachine.VirtualMachineRunner;
import dagger.Binds;
diff --git a/src/com/google/android/as/oss/pd/service/api/BUILD b/src/com/google/android/as/oss/pd/service/api/BUILD
new file mode 100644
index 00000000..4a705c21
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/service/api/BUILD
@@ -0,0 +1,134 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+# BUILD file for the Program Blobs Protos.
+load("@rules_proto_grpc//java:defs.bzl", "java_grpc_library")
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:java_proto_library.bzl", "java_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+load(
+ "//third_party/protobuf/build_defs:kt_jvm_proto_library.bzl",
+ "kt_jvm_lite_proto_library",
+ "kt_jvm_proto_library",
+)
+load("//tools/build_defs/kotlin:rules.bzl", "kt_jvm_grpc_library")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+proto_library(
+ name = "programblobs_proto",
+ srcs = ["programblobs.proto"],
+ has_services = 1,
+ deps = [
+ "//google/api:annotations",
+ ],
+)
+
+java_proto_library(
+ name = "programblobs_java_proto",
+ deps = [":programblobs_proto"],
+)
+
+java_lite_proto_library(
+ name = "programblobs_java_proto_lite",
+ deps = [":programblobs_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "programblobs_kt_proto_lite",
+ deps = [":programblobs_proto"],
+)
+
+java_grpc_library(
+ name = "programblobs_java_grpc_lite",
+ srcs = [":programblobs_proto"],
+ constraints = ["android"],
+ flavor = "lite",
+ deps = [
+ ":programblobs_java_proto_lite",
+ ],
+)
+
+kt_jvm_grpc_library(
+ name = "programblobs_kt_grpc_lite",
+ srcs = [":programblobs_proto"],
+ flavor = "lite",
+ deps = [":programblobs_kt_proto_lite"],
+)
+
+kt_jvm_proto_library(
+ name = "programblobs_kt_proto",
+ deps = [":programblobs_proto"],
+)
+
+kt_jvm_grpc_library(
+ name = "programblobs_kt_grpc",
+ srcs = [":programblobs_proto"],
+ deps = [":programblobs_kt_proto"],
+)
+
+proto_library(
+ name = "download_service_proto",
+ srcs = ["download_service.proto"],
+ has_services = 1,
+ deps = [
+ "",
+ "//google/api:annotations",
+ ],
+)
+
+java_proto_library(
+ name = "download_service_java_proto",
+ deps = [":download_service_proto"],
+)
+
+java_lite_proto_library(
+ name = "download_service_java_proto_lite",
+ deps = [":download_service_proto"],
+)
+
+kt_jvm_lite_proto_library(
+ name = "download_service_kt_proto_lite",
+ deps = [":download_service_proto"],
+)
+
+java_grpc_library(
+ name = "download_service_java_grpc_lite",
+ srcs = [":download_service_proto"],
+ constraints = ["android"],
+ flavor = "lite",
+ deps = [
+ ":download_service_java_proto_lite",
+ ],
+)
+
+kt_jvm_grpc_library(
+ name = "download_service_kt_grpc_lite",
+ srcs = [":download_service_proto"],
+ flavor = "lite",
+ deps = [":download_service_kt_proto_lite"],
+)
+
+kt_jvm_proto_library(
+ name = "download_service_kt_proto",
+ deps = [":download_service_proto"],
+)
+
+kt_jvm_grpc_library(
+ name = "download_service_kt_grpc",
+ srcs = [":download_service_proto"],
+ deps = [":download_service_kt_proto"],
+)
diff --git a/src/com/google/android/as/oss/pd/service/api/download_service.proto b/src/com/google/android/as/oss/pd/service/api/download_service.proto
index 242e2e0f..a8a8d7a1 100644
--- a/src/com/google/android/as/oss/pd/service/api/download_service.proto
+++ b/src/com/google/android/as/oss/pd/service/api/download_service.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -17,13 +17,13 @@ edition = "2023";
package google.internal.abuse.ondevicesafety.v2;
import "google/api/annotations.proto";
-import "storage/datapol/annotations/proto/semantic_annotations.proto";
+
option features.field_presence = IMPLICIT;
option java_multiple_files = true;
option java_outer_classname = "ProtectedDownloadServiceProto";
option java_package = "com.google.android.as.oss.pd.manifest.api.proto";
-option (datapol.file_vetting_status) = "latest";
+
// Service API for providing download manifests.
service ProtectedDownloadService {
@@ -39,21 +39,21 @@ service ProtectedDownloadService {
// Label expresses a generic constraint in config selection.
message Label {
- string attribute = 1 [(datapol.semantic_type) = ST_KEY];
+ string attribute = 1;
- string value = 2 [(datapol.semantic_type) = ST_VALUE];
+ string value = 2;
}
// Client SDK Version to help server estimate client capabilities.
message ClientVersion {
// Build CL for the SDK.
- int64 version = 1 [(datapol.semantic_type) = ST_SOFTWARE_ID];
+ int64 version = 1;
}
// Constraints used to select the resources that will be downloaded.
message ManifestConfigConstraints {
// Application identifier.
- string client_id = 1 [(datapol.semantic_type) = ST_IDENTIFYING_ID];
+ string client_id = 1;
// Additional labels that the requested resources should carry.
repeated Label label = 2;
@@ -66,13 +66,13 @@ message ManifestConfigConstraints {
// The cryptographic keys to use for encrypting the config.
message CryptoKeys {
// The device generated public key to encrypt the manifest config.
- bytes public_key = 1 [(datapol.semantic_type) = ST_SECURITY_KEY];
+ bytes public_key = 1;
}
message IntegrityResponse {
// Response from content binding with Key Attestation.
bytes key_attestation_token = 2
- [(datapol.semantic_type) = ST_SECURITY_MATERIAL];
+ ;
// Status of the integrity check from the client side.
enum ClientStatus {
@@ -113,7 +113,7 @@ message GetManifestConfigRequest {
message GetManifestConfigResponse {
// Contains the requested manifest.
bytes encrypted_manifest_config = 1
- [(datapol.semantic_type) = ST_NOT_REQUIRED];
+ ;
// Transformations applied to the manifest config.
ManifestTransformResult manifest_transform_result = 2;
diff --git a/src/com/google/android/as/oss/pd/service/api/programblobs.proto b/src/com/google/android/as/oss/pd/service/api/programblobs.proto
index aba50a93..4f11ece8 100644
--- a/src/com/google/android/as/oss/pd/service/api/programblobs.proto
+++ b/src/com/google/android/as/oss/pd/service/api/programblobs.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -202,8 +202,7 @@ enum ProtectionType {
TYPE_RULE = 1;
TYPE_MODEL = 2;
TYPE_BLM_BLOCK_LIST = 4;
-
- reserved 3;
+ reserved 3, 5, 6;
}
message ProtectionComponent {
diff --git a/src/com/google/android/as/oss/pd/virtualmachine/BUILD b/src/com/google/android/as/oss/pd/virtualmachine/BUILD
new file mode 100644
index 00000000..bb9b81ce
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/virtualmachine/BUILD
@@ -0,0 +1,50 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "virtualmachine",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+filegroup(
+ name = "aidl_files",
+ srcs = glob(["aidl/**/*.aidl"]),
+)
+
+android_library(
+ name = "aidl_java",
+ idl_import_root = "aidl",
+ idl_srcs = [":aidl_files"],
+ # Communicating with the VM uses Binder RPC (over vsock), which needs the
+ # --rpc option, which is only supported by the AOSP IDL compiler, and only
+ # from SDK 33 (T).
+ idl_uses_aosp_compiler = True,
+ idlopts = [
+ "--rpc",
+ "--min_sdk_version=33",
+ ],
+ visibility = [
+ "//src/com/google/android/as/oss/pd/virtualmachine/impl:__pkg__",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/virtualmachine/VirtualMachineRunner.java b/src/com/google/android/as/oss/pd/virtualmachine/VirtualMachineRunner.java
index f65b5c09..1875c3ca 100644
--- a/src/com/google/android/as/oss/pd/virtualmachine/VirtualMachineRunner.java
+++ b/src/com/google/android/as/oss/pd/virtualmachine/VirtualMachineRunner.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/virtualmachine/aidl/com/google/android/pd/ISecureService.aidl b/src/com/google/android/as/oss/pd/virtualmachine/aidl/com/google/android/pd/ISecureService.aidl
index bfe37162..af0b1103 100644
--- a/src/com/google/android/as/oss/pd/virtualmachine/aidl/com/google/android/pd/ISecureService.aidl
+++ b/src/com/google/android/as/oss/pd/virtualmachine/aidl/com/google/android/pd/ISecureService.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pd/virtualmachine/impl/BUILD b/src/com/google/android/as/oss/pd/virtualmachine/impl/BUILD
new file mode 100644
index 00000000..9af8406c
--- /dev/null
+++ b/src/com/google/android/as/oss/pd/virtualmachine/impl/BUILD
@@ -0,0 +1,61 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = [
+ "//visibility:public",
+])
+
+android_library(
+ name = "impl",
+ srcs = [
+ "VirtualMachineRunnerImpl.java",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/grpc:api",
+ "//src/com/google/android/as/oss/pd/api:protected_download_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/persistence",
+ "//src/com/google/android/as/oss/pd/persistence:client_persistent_state_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/service/api:programblobs_java_grpc_lite",
+ "//src/com/google/android/as/oss/pd/service/api:programblobs_java_proto_lite",
+ "//src/com/google/android/as/oss/pd/virtualmachine",
+ "//src/com/google/android/as/oss/pd/virtualmachine:aidl_java",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:androidx_concurrent_futures",
+ "@maven//:androidx_lifecycle_lifecycle_livedata_core",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_protobuf_protobuf_javalite",
+ ],
+)
+
+android_library(
+ name = "module",
+ srcs = [
+ "VirtualMachineRunnerModule.java",
+ ],
+ deps = [
+ ":impl",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/config",
+ "//src/com/google/android/as/oss/pd/config",
+ "//src/com/google/android/as/oss/pd/persistence",
+ "//src/com/google/android/as/oss/pd/virtualmachine",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerImpl.java b/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerImpl.java
index 4f604c24..c3534d11 100644
--- a/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerImpl.java
+++ b/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
package com.google.android.as.oss.pd.virtualmachine.impl;
-import static com.google.android.apps.miphone.astrea.grpc.VirtualMachineContextKeys.VM_DESCRIPTOR_CONTEXT_KEY;
+import static com.google.android.apps.miphone.pcs.grpc.VirtualMachineContextKeys.VM_DESCRIPTOR_CONTEXT_KEY;
import android.annotation.TargetApi;
import android.content.Context;
diff --git a/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerModule.java b/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerModule.java
index cabb31e5..1ace5a7c 100644
--- a/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerModule.java
+++ b/src/com/google/android/as/oss/pd/virtualmachine/impl/VirtualMachineRunnerModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pir/api/BUILD b/src/com/google/android/as/oss/pir/api/BUILD
new file mode 100644
index 00000000..804de82d
--- /dev/null
+++ b/src/com/google/android/as/oss/pir/api/BUILD
@@ -0,0 +1,43 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@rules_proto_grpc//java:defs.bzl", "java_grpc_library")
+load("//third_party/protobuf/bazel:java_lite_proto_library.bzl", "java_lite_proto_library")
+load("//third_party/protobuf/bazel:proto_library.bzl", "proto_library")
+
+package(default_visibility = ["//visibility:public"])
+
+proto_library(
+ name = "pir_proto",
+ srcs = ["pir.proto"],
+ has_services = True,
+ deps = [
+ "//logs/proto/wireless/android/play/playlog/privateretrieval:private_retrieval_log_proto",
+ ],
+)
+
+java_lite_proto_library(
+ name = "pir_java_proto_lite",
+ deps = [":pir_proto"],
+)
+
+java_grpc_library(
+ name = "pir_grpc",
+ srcs = [":pir_proto"],
+ constraints = ["android"],
+ flavor = "lite",
+ deps = [
+ ":pir_java_proto_lite",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pir/api/pir.proto b/src/com/google/android/as/oss/pir/api/pir.proto
index 36a25d2c..bc1102c2 100644
--- a/src/com/google/android/as/oss/pir/api/pir.proto
+++ b/src/com/google/android/as/oss/pir/api/pir.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pir/service/BUILD b/src/com/google/android/as/oss/pir/service/BUILD
new file mode 100644
index 00000000..f7b2ac8c
--- /dev/null
+++ b/src/com/google/android/as/oss/pir/service/BUILD
@@ -0,0 +1,52 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "service",
+ srcs = glob(
+ ["*.java"],
+ ),
+ deps = [
+ "//java/com/google/common/android/base:ticker",
+ "//logs/proto/wireless/android/play/playlog/privateretrieval:private_retrieval_log_java_proto_lite",
+ "//src/com/google/android/as/oss/common:annotation",
+ "//src/com/google/android/as/oss/common/flavor",
+ "//src/com/google/android/as/oss/grpc:annotations",
+ "//src/com/google/android/as/oss/logging:api",
+ "//src/com/google/android/as/oss/logging:atoms_java_proto_lite",
+ "//src/com/google/android/as/oss/logging:enums_java_proto_lite",
+ "//src/com/google/android/as/oss/networkusage/db",
+ "//src/com/google/android/as/oss/networkusage/db:repository",
+ "//src/com/google/android/as/oss/networkusage/ui/content",
+ "//src/com/google/android/as/oss/pir/api:pir_grpc",
+ "//src/com/google/android/as/oss/pir/api:pir_java_proto_lite",
+ "@maven//:androidx_annotation_annotation",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_flogger_google_extensions",
+ "@maven//:com_google_protobuf_protobuf_lite",
+ "@maven//:io_grpc_grpc_api",
+ "@maven//:io_grpc_grpc_context",
+ "@maven//:io_grpc_grpc_stub",
+ "@maven//:javax_inject_javax_inject",
+ "@maven//:org_checkerframework_checker_qual",
+ "@private_retrieval//private_retrieval/java:pir",
+ "@private_retrieval//private_retrieval/java/core",
+ ],
+)
diff --git a/src/com/google/android/as/oss/pir/service/DelegatingDownloadListener.java b/src/com/google/android/as/oss/pir/service/DelegatingDownloadListener.java
index 2940890e..73978af0 100644
--- a/src/com/google/android/as/oss/pir/service/DelegatingDownloadListener.java
+++ b/src/com/google/android/as/oss/pir/service/DelegatingDownloadListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pir/service/PirGrpcBindableService.java b/src/com/google/android/as/oss/pir/service/PirGrpcBindableService.java
index 49817e14..39f981ed 100644
--- a/src/com/google/android/as/oss/pir/service/PirGrpcBindableService.java
+++ b/src/com/google/android/as/oss/pir/service/PirGrpcBindableService.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/pir/service/PirGrpcModule.java b/src/com/google/android/as/oss/pir/service/PirGrpcModule.java
index 56f31ecc..9ef4a256 100644
--- a/src/com/google/android/as/oss/pir/service/PirGrpcModule.java
+++ b/src/com/google/android/as/oss/pir/service/PirGrpcModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
package com.google.android.as.oss.pir.service;
-import com.google.android.apps.miphone.astrea.grpc.Annotations.GrpcService;
-import com.google.android.apps.miphone.astrea.grpc.Annotations.GrpcServiceName;
+import com.google.android.apps.miphone.pcs.grpc.Annotations.GrpcService;
+import com.google.android.apps.miphone.pcs.grpc.Annotations.GrpcServiceName;
import com.google.android.as.oss.pir.api.pir.proto.PirServiceGrpc;
import com.google.private_retrieval.pir.AndroidLocalPirDownloadTaskBuilderFactory;
import com.google.private_retrieval.pir.PirDownloadTask.Builder.PirDownloadTaskBuilderFactory;
diff --git a/src/com/google/android/as/oss/pir/service/StreamingResponseWriter.java b/src/com/google/android/as/oss/pir/service/StreamingResponseWriter.java
index 78b3c74b..980f2e75 100644
--- a/src/com/google/android/as/oss/pir/service/StreamingResponseWriter.java
+++ b/src/com/google/android/as/oss/pir/service/StreamingResponseWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/BUILD b/src/com/google/android/as/oss/policies/api/BUILD
new file mode 100644
index 00000000..4fd55399
--- /dev/null
+++ b/src/com/google/android/as/oss/policies/api/BUILD
@@ -0,0 +1,33 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "policy_map",
+ srcs = ["PolicyMap.kt"],
+ deps = [
+ "@maven//:com_google_guava_guava",
+ "@private_compute_libraries//java/com/google/android/libraries/pcc/chronicle/api/policy/proto:policy_java_proto_lite",
+ ],
+)
+
+android_library(
+ name = "policy_map_java",
+ exports = [
+ ":policy_map",
+ ],
+)
diff --git a/src/com/google/android/as/oss/policies/api/Policy.kt b/src/com/google/android/as/oss/policies/api/Policy.kt
index 4fea3ea3..6adc95e1 100644
--- a/src/com/google/android/as/oss/policies/api/Policy.kt
+++ b/src/com/google/android/as/oss/policies/api/Policy.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/PolicyConformanceCheck.kt b/src/com/google/android/as/oss/policies/api/PolicyConformanceCheck.kt
new file mode 100644
index 00000000..be52ffdf
--- /dev/null
+++ b/src/com/google/android/as/oss/policies/api/PolicyConformanceCheck.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.policies.api
+
+/**
+ * Verifies that a set of [Policies][Policy] all conform to requirements of [Chronicle] which may be
+ * more restrictive than what is imposed directly by Arcs [Policy].
+ */
+interface PolicyConformanceCheck {
+ /**
+ * Applies conformance rules to the set of [policies] and throws a [MalformedPolicy]
+ * [com.google.android.libraries.pcc.chronicle.api.error.MalformedPolicySet] error if any do not
+ * follow the rules.
+ */
+ fun checkPoliciesConform(policies: Set)
+}
diff --git a/src/com/google/android/as/oss/policies/api/PolicyMap.kt b/src/com/google/android/as/oss/policies/api/PolicyMap.kt
index a067f4b5..b218dc6a 100644
--- a/src/com/google/android/as/oss/policies/api/PolicyMap.kt
+++ b/src/com/google/android/as/oss/policies/api/PolicyMap.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/annotation/Annotation.kt b/src/com/google/android/as/oss/policies/api/annotation/Annotation.kt
index 66d12d31..0b045056 100644
--- a/src/com/google/android/as/oss/policies/api/annotation/Annotation.kt
+++ b/src/com/google/android/as/oss/policies/api/annotation/Annotation.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/annotation/AnnotationBuilder.kt b/src/com/google/android/as/oss/policies/api/annotation/AnnotationBuilder.kt
index b94ebfe6..5eef86dc 100644
--- a/src/com/google/android/as/oss/policies/api/annotation/AnnotationBuilder.kt
+++ b/src/com/google/android/as/oss/policies/api/annotation/AnnotationBuilder.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/annotation/AnnotationParam.kt b/src/com/google/android/as/oss/policies/api/annotation/AnnotationParam.kt
index 0d14f8dd..3c677f8e 100644
--- a/src/com/google/android/as/oss/policies/api/annotation/AnnotationParam.kt
+++ b/src/com/google/android/as/oss/policies/api/annotation/AnnotationParam.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/capabilities/Capabilities.kt b/src/com/google/android/as/oss/policies/api/capabilities/Capabilities.kt
index e38c605f..d389651d 100644
--- a/src/com/google/android/as/oss/policies/api/capabilities/Capabilities.kt
+++ b/src/com/google/android/as/oss/policies/api/capabilities/Capabilities.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/capabilities/Capability.kt b/src/com/google/android/as/oss/policies/api/capabilities/Capability.kt
index dbd3b3a7..dbd7af5e 100644
--- a/src/com/google/android/as/oss/policies/api/capabilities/Capability.kt
+++ b/src/com/google/android/as/oss/policies/api/capabilities/Capability.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/contextrules/PolicyContextRule.kt b/src/com/google/android/as/oss/policies/api/contextrules/PolicyContextRule.kt
new file mode 100644
index 00000000..49b6e134
--- /dev/null
+++ b/src/com/google/android/as/oss/policies/api/contextrules/PolicyContextRule.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * 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 com.google.android.`as`.oss.policies.api.contextrules
+
+import com.google.android.libraries.pcc.chronicle.util.TypedMap
+
+/**
+ * This defines the basic structure of a context rule.
+ *
+ * `name` and `operands` are used internally for the ledger. `operands` refers to any rules that are
+ * used within the current rule.
+ */
+interface PolicyContextRule {
+ val name: String
+ val operands: List
+
+ /** Returns whether a rule is true/false, for the given context */
+ operator fun invoke(context: TypedMap): Boolean
+}
+
+/**
+ * This context rule always returns true, regardless of the context. It can be used as a default
+ * ContextRule if no rules need to be applied.
+ */
+object All : PolicyContextRule {
+ override val name = "All"
+ override val operands: List = emptyList()
+
+ override fun invoke(context: TypedMap): Boolean = true
+}
+
+/** Allows policy rule expressions such as `Rule1 and Rule2` */
+infix fun PolicyContextRule.and(other: PolicyContextRule): PolicyContextRule = And(this, other)
+
+/** Used to perform an `&&` operation on the boolean evaluations of two PolicyContextRules */
+class And(private val lhs: PolicyContextRule, private val rhs: PolicyContextRule) :
+ PolicyContextRule {
+ override val name = "And"
+ override val operands: List = listOf(lhs, rhs)
+
+ override fun invoke(context: TypedMap): Boolean = lhs(context) && rhs(context)
+}
+
+/** Allows policy rule expressions such as `Rule1 or Rule2` */
+infix fun PolicyContextRule.or(other: PolicyContextRule): PolicyContextRule = Or(this, other)
+
+/** Used to perform an `||` operation on the boolean evaluations of two PolicyContextRules */
+class Or(private val lhs: PolicyContextRule, private val rhs: PolicyContextRule) :
+ PolicyContextRule {
+ override val name = "Or"
+ override val operands: List = listOf(lhs, rhs)
+
+ override fun invoke(context: TypedMap): Boolean = lhs(context) || rhs(context)
+}
+
+/** Allows policy rule expressions such as `not(Rule1)` */
+fun not(rule: PolicyContextRule): PolicyContextRule = Not(rule)
+
+/** Used to perform an `!` operation on the boolean evaluation of a PolicyContextRule */
+class Not(private val inner: PolicyContextRule) : PolicyContextRule {
+ override val name = "Not"
+ override val operands: List = listOf(inner)
+
+ override fun invoke(context: TypedMap): Boolean = !inner(context)
+}
diff --git a/src/com/google/android/as/oss/policies/api/proto/AnnotationProto.kt b/src/com/google/android/as/oss/policies/api/proto/AnnotationProto.kt
index a416722e..121c3d92 100644
--- a/src/com/google/android/as/oss/policies/api/proto/AnnotationProto.kt
+++ b/src/com/google/android/as/oss/policies/api/proto/AnnotationProto.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/api/proto/PolicyProto.kt b/src/com/google/android/as/oss/policies/api/proto/PolicyProto.kt
index 5536564f..d3a1bd9b 100644
--- a/src/com/google/android/as/oss/policies/api/proto/PolicyProto.kt
+++ b/src/com/google/android/as/oss/policies/api/proto/PolicyProto.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/impl/AssetLoader.kt b/src/com/google/android/as/oss/policies/impl/AssetLoader.kt
index e799bb93..734e4f0d 100644
--- a/src/com/google/android/as/oss/policies/impl/AssetLoader.kt
+++ b/src/com/google/android/as/oss/policies/impl/AssetLoader.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/policies/impl/BUILD b/src/com/google/android/as/oss/policies/impl/BUILD
new file mode 100644
index 00000000..41c72945
--- /dev/null
+++ b/src/com/google/android/as/oss/policies/impl/BUILD
@@ -0,0 +1,84 @@
+# Copyright 2025 Google LLC
+#
+# 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.
+
+load("@bazel_rules_android//android:rules.bzl", "android_library")
+
+package(default_visibility = ["//visibility:public"])
+
+android_library(
+ name = "asset_loader",
+ srcs = [
+ "AssetLoader.kt",
+ ],
+ deps = [
+ "//java/com/google/android/libraries/pcc/chronicle/api/policy/proto:manifest_java_proto_lite",
+ "//src/com/google/android/as/oss/policies/api:policy_map",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@private_compute_libraries//java/com/google/android/libraries/pcc/chronicle/api/policy/proto:policy_java_proto_lite",
+ ],
+)
+
+android_library(
+ name = "no_policies",
+ srcs = [
+ "NoPoliciesModule.kt",
+ ],
+ deps = [
+ "//src/com/google/android/as/oss/policies/api:policy_map",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:com_google_guava_guava",
+ "@maven//:javax_inject_javax_inject",
+ ],
+)
+
+android_library(
+ name = "prod_policies",
+ srcs = [
+ "ProdPoliciesModule.kt",
+ ],
+ assets = [
+ "//src/com/google/android/as/oss/assets/federatedcompute:AmbientContextPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:AppLaunchPredictionMetricsPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:AutofillPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:ContentCapturePerformanceDataPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:GPPServicePolicy_FederatedCompute_GPPS_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:GPPServicePolicyV2_FederatedCompute_GPPS_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:LiveTranslatePolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:NowPlayingUsagePolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PecanContextPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PecanConversationFragmentEventPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PecanConversationThreadEventPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PecanLatencyAnalyticsEventPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PecanMessageEventPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PecanUsageEventPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:SafecommsPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:SearchPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:SmartSelectAnalyticsPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:SmartSelectLearningPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:ToastQueryPolicy_FederatedCompute_ASI_PROD",
+ "//src/com/google/android/as/oss/assets/federatedcompute:PlatformLoggingPolicy_FederatedCompute_PCS_RELEASE",
+ ],
+ assets_dir = "",
+ manifest = "//src/com/google/android/as/oss/common:AndroidManifest.xml",
+ deps = [
+ ":asset_loader",
+ "//src/com/google/android/as/oss/policies/api:policy_map",
+ "@maven//:com_google_dagger_dagger",
+ "@maven//:com_google_dagger_hilt-android",
+ "@maven//:javax_inject_javax_inject",
+ "@private_compute_libraries//java/com/google/android/libraries/pcc/chronicle/api/policy/proto:policy_java_proto_lite",
+ ],
+)
diff --git a/src/com/google/android/as/oss/policies/impl/ProdPoliciesModule.kt b/src/com/google/android/as/oss/policies/impl/ProdPoliciesModule.kt
index cf2556c1..2422879c 100644
--- a/src/com/google/android/as/oss/policies/impl/ProdPoliciesModule.kt
+++ b/src/com/google/android/as/oss/policies/impl/ProdPoliciesModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Google LLC
+ * Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/protos/BUILD b/src/com/google/android/as/oss/protos/BUILD
index 208ad28a..c227b905 100644
--- a/src/com/google/android/as/oss/protos/BUILD
+++ b/src/com/google/android/as/oss/protos/BUILD
@@ -1,4 +1,4 @@
-# Copyright 2024 Google LLC
+# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/protos/pcs_feature-enum.proto b/src/com/google/android/as/oss/protos/pcs_feature-enum.proto
index ef909402..ea87ca01 100644
--- a/src/com/google/android/as/oss/protos/pcs_feature-enum.proto
+++ b/src/com/google/android/as/oss/protos/pcs_feature-enum.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
diff --git a/src/com/google/android/as/oss/protos/pcs_query.proto b/src/com/google/android/as/oss/protos/pcs_query.proto
index 2ff05231..5df633db 100644
--- a/src/com/google/android/as/oss/protos/pcs_query.proto
+++ b/src/com/google/android/as/oss/protos/pcs_query.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ option java_outer_classname = "PcsProtos";
// Federated compute plans (task groups) must use this protocol buffer as
// selection criteria.
-message AstreaQuery {
+message PcsQuery {
// Client to which the query needs to be routed (Google Play Protect
// Service/ASI/StatsD).
string client_name = 1;
diff --git a/src/com/google/android/as/oss/protos/pcs_statsquery.proto b/src/com/google/android/as/oss/protos/pcs_statsquery.proto
index 246cc51a..1153e13f 100644
--- a/src/com/google/android/as/oss/protos/pcs_statsquery.proto
+++ b/src/com/google/android/as/oss/protos/pcs_statsquery.proto
@@ -1,4 +1,4 @@
-// Copyright 2024 Google LLC
+// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@ package com.google.android.as.oss.proto;
option features.field_presence = IMPLICIT;
option java_package = "com.google.android.as.oss.proto";
-message AstreaStatsQuery {
+message PcsStatsQuery {
// SQL query for querying statsd platform logs.
string sql_query = 1;
}
diff --git a/src/com/google/android/as/oss/res/values/strings.xml b/src/com/google/android/as/oss/res/values/strings.xml
index 2a7b4eb9..0502aa2e 100644
--- a/src/com/google/android/as/oss/res/values/strings.xml
+++ b/src/com/google/android/as/oss/res/values/strings.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_prompt_selector.xml b/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_prompt_selector.xml
index d2fe336c..d74fed24 100644
--- a/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_prompt_selector.xml
+++ b/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_prompt_selector.xml
@@ -1,6 +1,5 @@
-
-
+
diff --git a/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_rounded_button_background.xml b/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_rounded_button_background.xml
index 16e877fc..96f5aa9f 100644
--- a/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_rounded_button_background.xml
+++ b/src/com/google/android/as/oss/survey/service/ui/res/drawable/survey_rounded_button_background.xml
@@ -1,6 +1,5 @@
-
-
+
diff --git a/src/com/google/android/as/oss/survey/service/ui/res/layout/survey_close_button.xml b/src/com/google/android/as/oss/survey/service/ui/res/layout/survey_close_button.xml
index 6c8d9525..4ebee2f8 100644
--- a/src/com/google/android/as/oss/survey/service/ui/res/layout/survey_close_button.xml
+++ b/src/com/google/android/as/oss/survey/service/ui/res/layout/survey_close_button.xml
@@ -1,6 +1,6 @@
+ @color/google_grey200
#2D2E30
- #8AB4F8
- #E8EAED
+ @color/google_blue300
+ @color/google_grey200
@color/google_marterial_color_primary_variant_dark
@@ -32,6 +32,6 @@
@color/ripple_material_dark
@color/google_marterial_button_text_color
@color/google_marterial_color_primary_text_dark
- #1E1F20
+ @color/gm3_dark_default_color_surface_container
\ No newline at end of file
diff --git a/src/com/google/android/as/oss/survey/service/ui/res/values-night/drawables.xml b/src/com/google/android/as/oss/survey/service/ui/res/values-night/drawables.xml
index 5f509309..8c035ed3 100644
--- a/src/com/google/android/as/oss/survey/service/ui/res/values-night/drawables.xml
+++ b/src/com/google/android/as/oss/survey/service/ui/res/values-night/drawables.xml
@@ -1,6 +1,6 @@
-
-
-
-
-
- @android:color/system_accent1_100
- @android:color/system_accent1_600
- @android:color/system_accent1_300
- @android:color/system_neutral1_50
- @android:color/system_neutral1_900
-
diff --git a/src/com/google/android/as/oss/survey/service/ui/res/values/colors.xml b/src/com/google/android/as/oss/survey/service/ui/res/values/colors.xml
index eb5c003e..226f7801 100644
--- a/src/com/google/android/as/oss/survey/service/ui/res/values/colors.xml
+++ b/src/com/google/android/as/oss/survey/service/ui/res/values/colors.xml
@@ -1,6 +1,6 @@
- #FFFFFF
- #1A73E8
- #3C4043
+ @color/google_grey800
+ @color/google_white
+ @color/google_blue600
+ @color/google_grey800
- #D9E2FF
- #475D92
- #94AAE4
- #F1F0F7
- #1A1B20
+ @android:color/system_accent1_100
+ @android:color/system_accent1_600
+ @android:color/system_accent1_300
+ @android:color/system_neutral1_50
+ @android:color/system_neutral1_900
@color/google_marterial_color_button_background_light
@color/google_marterial_color_button_text_light
@@ -40,6 +40,6 @@
@color/google_marterial_button_text_color
@color/google_marterial_button_text_color
@color/google_marterial_button_stroke_color
- #F0F4F9
+ @color/gm3_default_color_surface_container
diff --git a/src/com/google/android/as/oss/survey/service/ui/res/values/dimens.xml b/src/com/google/android/as/oss/survey/service/ui/res/values/dimens.xml
index 167edd04..2f7a8094 100644
--- a/src/com/google/android/as/oss/survey/service/ui/res/values/dimens.xml
+++ b/src/com/google/android/as/oss/survey/service/ui/res/values/dimens.xml
@@ -1,6 +1,6 @@