[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.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -26,31 +25,20 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.google.ai.edge.gallery.ui.theme.GalleryTheme
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private var firebaseAnalytics: FirebaseAnalytics? = null
override fun onCreate(savedInstanceState: Bundle?) {
firebaseAnalytics =
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()
super.onCreate(savedInstanceState)
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 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)
}
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 {
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.NotificationManagerCompat
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
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.R
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.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
@ -82,6 +84,15 @@ class DefaultDownloadRepository(
private val lifecycleProvider: AppLifecycleProvider,
) : DownloadRepository {
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(
model: Model,
@ -175,6 +186,17 @@ class DefaultDownloadRepository(
workManager.getWorkInfoByIdLiveData(workerId).observeForever { workInfo ->
if (workInfo != null) {
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 -> {
val receivedBytes = workInfo.progress.getLong(KEY_MODEL_DOWNLOAD_RECEIVED_BYTES, 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),
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,
@ -233,6 +270,22 @@ class DefaultDownloadRepository(
model,
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 -> {}

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.TextDecoration
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
@Composable
@ -62,7 +64,11 @@ fun ClickableLink(url: String, linkText: String, icon: ImageVector) {
text = annotatedText,
textAlign = TextAlign.Center,
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.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.os.bundleOf
import com.google.ai.edge.gallery.GalleryTopAppBar
import com.google.ai.edge.gallery.R
import com.google.ai.edge.gallery.data.AppBarAction
import com.google.ai.edge.gallery.data.AppBarActionType
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.ui.common.TaskIcon
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 sizeFraction = remember { ((screenWidthDp - 360.dp) / (410.dp - 360.dp)).coerceIn(0f, 1f) }
val linkColor = MaterialTheme.customColors.linkColor
val url = "https://huggingface.co/litert-community"
val uriHandler = LocalUriHandler.current
val introText = buildAnnotatedString {
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(
link =
LinkAnnotation.Url(
url = "https://huggingface.co/litert-community", // Replace with the actual URL
url = url,
styles =
TextLinkStyles(
style = SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
),
linkInteractionListener = { _ ->
firebaseAnalytics?.logEvent("resource_link_click", bundleOf("link_destination" to url))
uriHandler.openUri(url)
},
)
) {
append("LiteRT community")

View file

@ -20,6 +20,8 @@ import android.graphics.Bitmap
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.ChatMessageImage
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 ->

View file

@ -43,8 +43,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
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.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.ModelPageAppBar
import com.google.ai.edge.gallery.ui.common.chat.ModelDownloadStatusInfoPanel
@ -167,6 +169,14 @@ fun LlmSingleTurnScreen(
modelManagerViewModel = modelManagerViewModel,
onSend = { 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) },
modifier = Modifier.fillMaxSize(),

View file

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