[gallery] add Analytics events: model_download

PiperOrigin-RevId: 778630845
This commit is contained in:
Wai Hon Law 2025-07-02 13:52:19 -07:00 committed by Copybara-Service
parent d97e115993
commit 315820b146
8 changed files with 146 additions and 16 deletions

View file

@ -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.
*/
package com.google.ai.edge.gallery
import android.util.Log
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
private var hasLoggedAnalyticsWarning = false
val firebaseAnalytics: FirebaseAnalytics?
get() =
runCatching { Firebase.analytics }
.onFailure { exception ->
// Firebase.analytics can throw an exception if goolgle-services is not set up, e.g.,
// missing google-services.json.
if (!hasLoggedAnalyticsWarning) {
Log.w("AGAnalyticsFirebase", "Firebase Analytics is not available", exception)
}
}
.getOrNull()

View file

@ -18,7 +18,6 @@ package com.google.ai.edge.gallery
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -26,31 +25,20 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.google.ai.edge.gallery.ui.theme.GalleryTheme import com.google.ai.edge.gallery.ui.theme.GalleryTheme
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private var firebaseAnalytics: FirebaseAnalytics? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
firebaseAnalytics = super.onCreate(savedInstanceState)
runCatching { Firebase.analytics }
.onFailure { exception ->
// Firebase.analytics can throw an exception if goolgle-services is not set up, e.g.,
// missing google-services.json.
Log.w(TAG, "Firebase Analytics is not available", exception)
}
.getOrNull()
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Fix for three-button nav not properly going edge-to-edge. // Fix for three-button nav not properly going edge-to-edge.
@ -62,6 +50,19 @@ class MainActivity : ComponentActivity() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
override fun onResume() {
super.onResume()
firebaseAnalytics?.logEvent(
FirebaseAnalytics.Event.APP_OPEN,
bundleOf(
"app_version" to BuildConfig.VERSION_NAME,
"os_version" to Build.VERSION.SDK_INT.toString(),
"device_model" to Build.MODEL,
),
)
}
companion object { companion object {
private const val TAG = "AGMainActivity" private const val TAG = "AGMainActivity"
} }

View file

@ -28,6 +28,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
@ -39,6 +40,7 @@ import androidx.work.WorkQuery
import com.google.ai.edge.gallery.AppLifecycleProvider import com.google.ai.edge.gallery.AppLifecycleProvider
import com.google.ai.edge.gallery.R import com.google.ai.edge.gallery.R
import com.google.ai.edge.gallery.common.readLaunchInfo import com.google.ai.edge.gallery.common.readLaunchInfo
import com.google.ai.edge.gallery.firebaseAnalytics
import com.google.ai.edge.gallery.worker.DownloadWorker import com.google.ai.edge.gallery.worker.DownloadWorker
import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
@ -82,6 +84,15 @@ class DefaultDownloadRepository(
private val lifecycleProvider: AppLifecycleProvider, private val lifecycleProvider: AppLifecycleProvider,
) : DownloadRepository { ) : DownloadRepository {
private val workManager = WorkManager.getInstance(context) private val workManager = WorkManager.getInstance(context)
/**
* Stores the start time of a model download.
*
* We use SharedPreferences to persist the download start times. This ensures that the data is
* still available after the app restarts. The key is the model name and the value is the download
* start time in milliseconds.
*/
private val downloadStartTimeSharedPreferences =
context.getSharedPreferences("download_start_time_ms", Context.MODE_PRIVATE)
override fun downloadModel( override fun downloadModel(
model: Model, model: Model,
@ -175,6 +186,17 @@ class DefaultDownloadRepository(
workManager.getWorkInfoByIdLiveData(workerId).observeForever { workInfo -> workManager.getWorkInfoByIdLiveData(workerId).observeForever { workInfo ->
if (workInfo != null) { if (workInfo != null) {
when (workInfo.state) { when (workInfo.state) {
WorkInfo.State.ENQUEUED -> {
with(downloadStartTimeSharedPreferences.edit()) {
putLong(model.name, System.currentTimeMillis())
apply()
}
firebaseAnalytics?.logEvent(
"model_download",
bundleOf("event_type" to "start", "model_id" to model.name),
)
}
WorkInfo.State.RUNNING -> { WorkInfo.State.RUNNING -> {
val receivedBytes = workInfo.progress.getLong(KEY_MODEL_DOWNLOAD_RECEIVED_BYTES, 0L) val receivedBytes = workInfo.progress.getLong(KEY_MODEL_DOWNLOAD_RECEIVED_BYTES, 0L)
val downloadRate = workInfo.progress.getLong(KEY_MODEL_DOWNLOAD_RATE, 0L) val downloadRate = workInfo.progress.getLong(KEY_MODEL_DOWNLOAD_RATE, 0L)
@ -210,6 +232,21 @@ class DefaultDownloadRepository(
text = context.getString(R.string.notification_content_success).format(model.name), text = context.getString(R.string.notification_content_success).format(model.name),
modelName = model.name, modelName = model.name,
) )
val startTime = downloadStartTimeSharedPreferences.getLong(model.name, 0L)
val duration = System.currentTimeMillis() - startTime
firebaseAnalytics?.logEvent(
"model_download",
bundleOf(
"event_type" to "success",
"model_id" to model.name,
"duration_ms" to duration,
),
)
with(downloadStartTimeSharedPreferences.edit()) {
remove(model.name)
apply()
}
} }
WorkInfo.State.FAILED, WorkInfo.State.FAILED,
@ -233,6 +270,22 @@ class DefaultDownloadRepository(
model, model,
ModelDownloadStatus(status = status, errorMessage = errorMessage), ModelDownloadStatus(status = status, errorMessage = errorMessage),
) )
val startTime = downloadStartTimeSharedPreferences.getLong(model.name, 0L)
val duration = System.currentTimeMillis() - startTime
// TODO: Add failure reasons
firebaseAnalytics?.logEvent(
"model_download",
bundleOf(
"event_type" to "failure",
"model_id" to model.name,
"duration_ms" to duration,
),
)
with(downloadStartTimeSharedPreferences.edit()) {
remove(model.name)
apply()
}
} }
else -> {} else -> {}

View file

@ -34,6 +34,8 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import com.google.ai.edge.gallery.firebaseAnalytics
import com.google.ai.edge.gallery.ui.theme.customColors import com.google.ai.edge.gallery.ui.theme.customColors
@Composable @Composable
@ -62,7 +64,11 @@ fun ClickableLink(url: String, linkText: String, icon: ImageVector) {
text = annotatedText, text = annotatedText,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 6.dp).clickable { uriHandler.openUri(url) }, modifier =
Modifier.padding(start = 6.dp).clickable {
uriHandler.openUri(url)
firebaseAnalytics?.logEvent("resource_link_click", bundleOf("link_destination" to url))
},
) )
} }
} }

View file

@ -88,6 +88,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.LinkAnnotation
@ -100,11 +101,13 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.os.bundleOf
import com.google.ai.edge.gallery.GalleryTopAppBar import com.google.ai.edge.gallery.GalleryTopAppBar
import com.google.ai.edge.gallery.R import com.google.ai.edge.gallery.R
import com.google.ai.edge.gallery.data.AppBarAction import com.google.ai.edge.gallery.data.AppBarAction
import com.google.ai.edge.gallery.data.AppBarActionType import com.google.ai.edge.gallery.data.AppBarActionType
import com.google.ai.edge.gallery.data.Task import com.google.ai.edge.gallery.data.Task
import com.google.ai.edge.gallery.firebaseAnalytics
import com.google.ai.edge.gallery.proto.ImportedModel import com.google.ai.edge.gallery.proto.ImportedModel
import com.google.ai.edge.gallery.ui.common.TaskIcon import com.google.ai.edge.gallery.ui.common.TaskIcon
import com.google.ai.edge.gallery.ui.common.getTaskBgColor import com.google.ai.edge.gallery.ui.common.getTaskBgColor
@ -334,17 +337,24 @@ private fun TaskList(
val screenHeightDp = remember { with(density) { windowInfo.containerSize.height.toDp() } } val screenHeightDp = remember { with(density) { windowInfo.containerSize.height.toDp() } }
val sizeFraction = remember { ((screenWidthDp - 360.dp) / (410.dp - 360.dp)).coerceIn(0f, 1f) } val sizeFraction = remember { ((screenWidthDp - 360.dp) / (410.dp - 360.dp)).coerceIn(0f, 1f) }
val linkColor = MaterialTheme.customColors.linkColor val linkColor = MaterialTheme.customColors.linkColor
val url = "https://huggingface.co/litert-community"
val uriHandler = LocalUriHandler.current
val introText = buildAnnotatedString { val introText = buildAnnotatedString {
append("Welcome to Google AI Edge Gallery! Explore a world of amazing on-device models from ") append("Welcome to Google AI Edge Gallery! Explore a world of amazing on-device models from ")
// TODO: Consolidate the link clicking logic into ui/common/ClickableLink.kt.
withLink( withLink(
link = link =
LinkAnnotation.Url( LinkAnnotation.Url(
url = "https://huggingface.co/litert-community", // Replace with the actual URL url = url,
styles = styles =
TextLinkStyles( TextLinkStyles(
style = SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline) style = SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
), ),
linkInteractionListener = { _ ->
firebaseAnalytics?.logEvent("resource_link_click", bundleOf("link_destination" to url))
uriHandler.openUri(url)
},
) )
) { ) {
append("LiteRT community") append("LiteRT community")

View file

@ -20,6 +20,8 @@ import android.graphics.Bitmap
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.os.bundleOf
import com.google.ai.edge.gallery.firebaseAnalytics
import com.google.ai.edge.gallery.ui.common.chat.ChatMessageAudioClip import com.google.ai.edge.gallery.ui.common.chat.ChatMessageAudioClip
import com.google.ai.edge.gallery.ui.common.chat.ChatMessageImage import com.google.ai.edge.gallery.ui.common.chat.ChatMessageImage
import com.google.ai.edge.gallery.ui.common.chat.ChatMessageText import com.google.ai.edge.gallery.ui.common.chat.ChatMessageText
@ -132,6 +134,11 @@ fun ChatViewWrapper(
) )
}, },
) )
firebaseAnalytics?.logEvent(
"generate_action",
bundleOf("capability_name" to viewModel.task.type.toString(), "model_id" to model.name),
)
} }
}, },
onRunAgainClicked = { model, message -> onRunAgainClicked = { model, message ->

View file

@ -43,8 +43,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.core.os.bundleOf
import com.google.ai.edge.gallery.data.ModelDownloadStatusType import com.google.ai.edge.gallery.data.ModelDownloadStatusType
import com.google.ai.edge.gallery.data.TASK_LLM_PROMPT_LAB import com.google.ai.edge.gallery.data.TASK_LLM_PROMPT_LAB
import com.google.ai.edge.gallery.firebaseAnalytics
import com.google.ai.edge.gallery.ui.common.ErrorDialog import com.google.ai.edge.gallery.ui.common.ErrorDialog
import com.google.ai.edge.gallery.ui.common.ModelPageAppBar import com.google.ai.edge.gallery.ui.common.ModelPageAppBar
import com.google.ai.edge.gallery.ui.common.chat.ModelDownloadStatusInfoPanel import com.google.ai.edge.gallery.ui.common.chat.ModelDownloadStatusInfoPanel
@ -167,6 +169,14 @@ fun LlmSingleTurnScreen(
modelManagerViewModel = modelManagerViewModel, modelManagerViewModel = modelManagerViewModel,
onSend = { fullPrompt -> onSend = { fullPrompt ->
viewModel.generateResponse(model = selectedModel, input = fullPrompt) viewModel.generateResponse(model = selectedModel, input = fullPrompt)
firebaseAnalytics?.logEvent(
"generate_action",
bundleOf(
"capability_name" to task.type.toString(),
"model_id" to selectedModel.name,
),
)
}, },
onStopButtonClicked = { model -> viewModel.stopResponse(model = model) }, onStopButtonClicked = { model -> viewModel.stopResponse(model = model) },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),

View file

@ -36,6 +36,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.os.bundleOf
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@ -54,6 +55,7 @@ import com.google.ai.edge.gallery.data.TASK_LLM_PROMPT_LAB
import com.google.ai.edge.gallery.data.Task import com.google.ai.edge.gallery.data.Task
import com.google.ai.edge.gallery.data.TaskType import com.google.ai.edge.gallery.data.TaskType
import com.google.ai.edge.gallery.data.getModelByName import com.google.ai.edge.gallery.data.getModelByName
import com.google.ai.edge.gallery.firebaseAnalytics
import com.google.ai.edge.gallery.ui.home.HomeScreen import com.google.ai.edge.gallery.ui.home.HomeScreen
import com.google.ai.edge.gallery.ui.llmchat.LlmAskAudioDestination import com.google.ai.edge.gallery.ui.llmchat.LlmAskAudioDestination
import com.google.ai.edge.gallery.ui.llmchat.LlmAskAudioScreen import com.google.ai.edge.gallery.ui.llmchat.LlmAskAudioScreen
@ -144,6 +146,11 @@ fun GalleryNavHost(
navigateToTaskScreen = { task -> navigateToTaskScreen = { task ->
pickedTask = task pickedTask = task
showModelManager = true showModelManager = true
firebaseAnalytics?.logEvent(
"capability_select",
bundleOf("capability_name" to task.type.toString()),
)
}, },
) )