From 9e26d5aed278830f441a0f9c2b6afc8bae4c18a2 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Wed, 10 Sep 2025 20:48:24 +0100 Subject: [PATCH 1/4] Replace Log.d etc with Timber. Add additional logs for AppCheck --- app/build.gradle.kts | 1 + .../androidify/AndroidifyApplication.kt | 7 +++ .../developers/androidify/CrashlyticsTree.kt | 43 +++++++++++++++++++ core/network/build.gradle.kts | 1 + .../ondevice/LocalSegmentationDataSource.kt | 16 +++---- .../startup/FirebaseAppCheckInitializer.kt | 24 +++++++++-- .../FirebaseRemoteConfigInitializer.kt | 6 +-- data/build.gradle.kts | 1 + .../androidify/data/GeminiNanoDownloader.kt | 14 +++--- .../data/GeminiNanoGenerationDataSource.kt | 4 +- .../data/ImageGenerationRepository.kt | 4 +- feature/camera/build.gradle.kts | 1 + .../androidify/camera/CameraViewModel.kt | 6 +-- feature/creation/build.gradle.kts | 1 + .../androidify/creation/CreationViewModel.kt | 12 +++--- feature/results/build.gradle.kts | 1 + .../customize/CustomizeExportViewModel.kt | 13 +++--- gradle/libs.versions.toml | 6 ++- 18 files changed, 116 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/android/developers/androidify/CrashlyticsTree.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 100540af..d8b7f703 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,6 +136,7 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.crashlytics) + implementation(libs.timber) implementation(libs.firebase.ai) implementation(libs.firebase.app.check) implementation(libs.firebase.config) diff --git a/app/src/main/java/com/android/developers/androidify/AndroidifyApplication.kt b/app/src/main/java/com/android/developers/androidify/AndroidifyApplication.kt index ff717ebc..fd59a6a9 100644 --- a/app/src/main/java/com/android/developers/androidify/AndroidifyApplication.kt +++ b/app/src/main/java/com/android/developers/androidify/AndroidifyApplication.kt @@ -23,7 +23,9 @@ import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber import javax.inject.Inject + @HiltAndroidApp class AndroidifyApplication : Application(), SingletonImageLoader.Factory { @@ -32,6 +34,11 @@ class AndroidifyApplication : Application(), SingletonImageLoader.Factory { override fun onCreate() { super.onCreate() + if (isDebuggable()) { + Timber.plant(Timber.DebugTree()) + } else { + Timber.plant(CrashlyticsTree()) + } setStrictModePolicy() } diff --git a/app/src/main/java/com/android/developers/androidify/CrashlyticsTree.kt b/app/src/main/java/com/android/developers/androidify/CrashlyticsTree.kt new file mode 100644 index 00000000..bab79937 --- /dev/null +++ b/app/src/main/java/com/android/developers/androidify/CrashlyticsTree.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.android.developers.androidify + +import android.util.Log +import com.google.firebase.crashlytics.FirebaseCrashlytics +import timber.log.Timber + +/** + * A Timber tree that logs to Crashlytics. + * + * In debug builds, this tree does nothing. In release builds, it logs non-fatal exceptions + * to Crashlytics. + */ +class CrashlyticsTree : Timber.Tree() { + + private val crashlytics by lazy { FirebaseCrashlytics.getInstance() } + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (priority == Log.VERBOSE || priority == Log.DEBUG) { + return + } + + crashlytics.log(message) + + if (t != null) { + crashlytics.recordException(t) + } + } +} diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 0738a116..2cfc168e 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(libs.coil.compose.http) implementation(libs.coil.gif) implementation(platform(libs.firebase.bom)) + implementation(libs.timber) implementation(libs.firebase.ai) implementation(libs.firebase.analytics) { exclude(group = "com.google.guava") diff --git a/core/network/src/main/java/com/android/developers/androidify/ondevice/LocalSegmentationDataSource.kt b/core/network/src/main/java/com/android/developers/androidify/ondevice/LocalSegmentationDataSource.kt index e9118cf4..58122708 100644 --- a/core/network/src/main/java/com/android/developers/androidify/ondevice/LocalSegmentationDataSource.kt +++ b/core/network/src/main/java/com/android/developers/androidify/ondevice/LocalSegmentationDataSource.kt @@ -16,7 +16,6 @@ package com.android.developers.androidify.ondevice import android.graphics.Bitmap -import android.util.Log import com.google.android.gms.common.moduleinstall.InstallStatusListener import com.google.android.gms.common.moduleinstall.ModuleInstallClient import com.google.android.gms.common.moduleinstall.ModuleInstallRequest @@ -28,6 +27,7 @@ import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -64,7 +64,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor( ) : InstallStatusListener { override fun onInstallStatusUpdated(update: ModuleInstallStatusUpdate) { - Log.d("LocalSegmentationDataSource", "Download progress: ${update.installState}.. ${continuation.hashCode()} ${continuation.isActive}") + Timber.d("Download progress: ${update.installState}.. ${continuation.hashCode()} ${continuation.isActive}") if (!continuation.isActive) return if (update.installState == ModuleInstallStatusUpdate.InstallState.STATE_COMPLETED) { continuation.resume(true) @@ -73,7 +73,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor( ImageSegmentationException("Module download failed or was canceled. State: ${update.installState}"), ) } else { - Log.d("LocalSegmentationDataSource", "State update: ${update.installState}") + Timber.d("State update: ${update.installState}") } } } @@ -88,11 +88,11 @@ class LocalSegmentationDataSourceImpl @Inject constructor( moduleInstallClient .installModules(moduleInstallRequest) .addOnFailureListener { - Log.e("LocalSegmentationDataSource", "Failed to download module", it) + Timber.e(it, "Failed to download module") continuation.resumeWithException(it) } .addOnCompleteListener { - Log.d("LocalSegmentationDataSource", "Successfully triggered download - await download progress updates") + Timber.d("Successfully triggered download - await download progress updates") } } return result @@ -102,13 +102,13 @@ class LocalSegmentationDataSourceImpl @Inject constructor( val areModulesAvailable = isSubjectSegmentationModuleInstalled() if (!areModulesAvailable) { - Log.d("LocalSegmentationDataSource", "Modules not available - downloading") + Timber.d("Modules not available - downloading") val result = installSubjectSegmentationModule() if (!result) { throw Exception("Failed to download module") } } else { - Log.d("LocalSegmentationDataSource", "Modules available") + Timber.d("Modules available") } val image = InputImage.fromBitmap(bitmap, 0) return suspendCancellableCoroutine { continuation -> @@ -121,7 +121,7 @@ class LocalSegmentationDataSourceImpl @Inject constructor( } } .addOnFailureListener { e -> - Log.e("LocalSegmentationDataSource", "Exception while executing background removal", e) + Timber.e(e, "Exception while executing background removal") continuation.resumeWithException(e) } } diff --git a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt index ffa5d0c0..a166ed30 100644 --- a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt +++ b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt @@ -17,7 +17,6 @@ package com.android.developers.androidify.startup import android.annotation.SuppressLint import android.content.Context -import android.util.Log import androidx.startup.Initializer import com.android.developers.androidify.network.BuildConfig import com.google.firebase.Firebase @@ -25,6 +24,7 @@ import com.google.firebase.appcheck.FirebaseAppCheck import com.google.firebase.appcheck.appCheck import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory +import timber.log.Timber /** * Initialize [FirebaseAppCheck] using the App Startup Library. @@ -32,19 +32,35 @@ import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderF @SuppressLint("EnsureInitializerMetadata") // Registered in :app module class FirebaseAppCheckInitializer : Initializer { override fun create(context: Context): FirebaseAppCheck { - return Firebase.appCheck.apply { + val appCheck = Firebase.appCheck.apply { if (BuildConfig.DEBUG) { - Log.i("AndroidifyAppCheck", "Firebase debug") + Timber.i( + "Installing Firebase debug, ensure your " + + "debug token is registered on Firebase Console", + ) installAppCheckProviderFactory( DebugAppCheckProviderFactory.getInstance(), ) } else { - Log.i("AndroidifyAppCheck", "Play integrity") + Timber.i("Play integrity installing...") installAppCheckProviderFactory( PlayIntegrityAppCheckProviderFactory.getInstance(), ) } + setTokenAutoRefreshEnabled(true) + } + if (!BuildConfig.DEBUG) { + val token = appCheck.getAppCheckToken(false) + token.addOnCompleteListener { + if (token.isSuccessful) { + Timber.i("Firebase app check token success: ${token.result.token}") + Timber.i("Firebase app check token success: ${token.result.expireTimeMillis}") + } else { + Timber.e(token.exception, "Firebase app check token failure") + } + } } + return appCheck } override fun dependencies(): List?>?> { diff --git a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseRemoteConfigInitializer.kt b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseRemoteConfigInitializer.kt index 479f83e8..4d4821f5 100644 --- a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseRemoteConfigInitializer.kt +++ b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseRemoteConfigInitializer.kt @@ -17,13 +17,13 @@ package com.android.developers.androidify.startup import android.annotation.SuppressLint import android.content.Context -import android.util.Log import androidx.startup.Initializer import com.android.developers.androidify.network.R import com.google.firebase.Firebase import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.remoteConfig import com.google.firebase.remoteconfig.remoteConfigSettings +import timber.log.Timber /** * Initialize [FirebaseRemoteConfig] using the App Startup Library. @@ -39,10 +39,10 @@ class FirebaseRemoteConfigInitializer : Initializer { setDefaultsAsync(R.xml.remote_config_defaults) fetchAndActivate() .addOnSuccessListener { - Log.d("FirebaseRemoteConfig", "Config params updated: $it") + Timber.d("Config params updated: $it") } .addOnFailureListener { - Log.d("FirebaseRemoteConfig", "Config params failed: $it") + Timber.d("Config params failed: $it") } } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index a37ecd93..1677d959 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.retrofit) + implementation(libs.timber) implementation(libs.converter.gson) implementation(libs.logging.interceptor) implementation(libs.hilt.android) diff --git a/data/src/main/java/com/android/developers/androidify/data/GeminiNanoDownloader.kt b/data/src/main/java/com/android/developers/androidify/data/GeminiNanoDownloader.kt index 404a576e..809a3653 100644 --- a/data/src/main/java/com/android/developers/androidify/data/GeminiNanoDownloader.kt +++ b/data/src/main/java/com/android/developers/androidify/data/GeminiNanoDownloader.kt @@ -16,12 +16,12 @@ package com.android.developers.androidify.data import android.app.Application -import android.util.Log import com.google.ai.edge.aicore.DownloadCallback import com.google.ai.edge.aicore.DownloadConfig import com.google.ai.edge.aicore.GenerativeAIException import com.google.ai.edge.aicore.GenerativeModel import com.google.ai.edge.aicore.generationConfig +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -35,34 +35,34 @@ class GeminiNanoDownloader @Inject constructor(private val application: Applicat fun isModelDownloaded() = modelDownloaded suspend fun downloadModel() { - Log.d("GeminiNanoDownloader", "downloadModel") + Timber.d("downloadModel") try { setup() generativeModel?.prepareInferenceEngine() } catch (e: Exception) { - Log.e("GeminiNanoDownloader", "Error preparing inference engine", e) + Timber.e(e, "Error preparing inference engine") } - Log.d("GeminiNanoDownloader", "prepare inference engine") + Timber.d("prepare inference engine") } private fun setup() { val downloadCallback = object : DownloadCallback { override fun onDownloadStarted(bytesToDownload: Long) { super.onDownloadStarted(bytesToDownload) - Log.i("GeminiNanoDownloader", "onDownloadStarted for Gemini Nano $bytesToDownload") + Timber.i("onDownloadStarted for Gemini Nano $bytesToDownload") } override fun onDownloadCompleted() { super.onDownloadCompleted() modelDownloaded = true - Log.i("GeminiNanoDownloader", "onDownloadCompleted for Gemini Nano") + Timber.i("onDownloadCompleted for Gemini Nano") } override fun onDownloadFailed(failureStatus: String, e: GenerativeAIException) { super.onDownloadFailed(failureStatus, e) // downloading the model has failed so make the model null as we can't use it generativeModel = null - Log.i("GeminiNanoDownloader", "onDownloadFailed for Gemini Nano") + Timber.i("onDownloadFailed for Gemini Nano") } } diff --git a/data/src/main/java/com/android/developers/androidify/data/GeminiNanoGenerationDataSource.kt b/data/src/main/java/com/android/developers/androidify/data/GeminiNanoGenerationDataSource.kt index 32196c2a..ad05d44e 100644 --- a/data/src/main/java/com/android/developers/androidify/data/GeminiNanoGenerationDataSource.kt +++ b/data/src/main/java/com/android/developers/androidify/data/GeminiNanoGenerationDataSource.kt @@ -15,7 +15,7 @@ */ package com.android.developers.androidify.data -import android.util.Log +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -39,7 +39,7 @@ internal class GeminiNanoGenerationDataSourceImpl @Inject constructor(private va override suspend fun generatePrompt(prompt: String): String? { if (!downloader.isModelDownloaded()) return null val response = downloader.generativeModel?.generateContent(prompt) - Log.d("GeminiNanoGenerationDataSource", "generatePrompt: ${response?.text}") + Timber.d("generatePrompt: ${response?.text}") return response?.text } } diff --git a/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt b/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt index 9e335077..e215b218 100644 --- a/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt +++ b/data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt @@ -18,13 +18,13 @@ package com.android.developers.androidify.data import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import android.util.Log import com.android.developers.androidify.RemoteConfigDataSource import com.android.developers.androidify.model.ValidatedDescription import com.android.developers.androidify.model.ValidatedImage import com.android.developers.androidify.ondevice.LocalSegmentationDataSource import com.android.developers.androidify.util.LocalFileProvider import com.android.developers.androidify.vertexai.FirebaseAiDataSource +import timber.log.Timber import java.io.File import java.util.UUID import javax.inject.Inject @@ -53,7 +53,7 @@ internal class ImageGenerationRepositoryImpl @Inject constructor( ) : ImageGenerationRepository { override suspend fun initialize() { - Log.d("ImageGenerationRepository", "Initializing") + Timber.d("Initializing") geminiNanoDataSource.initialize() } diff --git a/feature/camera/build.gradle.kts b/feature/camera/build.gradle.kts index 0bb44abb..b1f17cc5 100644 --- a/feature/camera/build.gradle.kts +++ b/feature/camera/build.gradle.kts @@ -73,6 +73,7 @@ dependencies { implementation(libs.kotlinx.coroutines.play.services) implementation(libs.mlkit.pose.detection) implementation(libs.guava) + implementation(libs.timber) ksp(libs.hilt.compiler) implementation(libs.androidx.ui.tooling) diff --git a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraViewModel.kt b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraViewModel.kt index 9a6ea969..5f672505 100644 --- a/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraViewModel.kt +++ b/feature/camera/src/main/java/com/android/developers/androidify/camera/CameraViewModel.kt @@ -19,7 +19,6 @@ import android.app.Application import android.media.Image import android.media.MediaActionSound import android.net.Uri -import android.util.Log import androidx.activity.ComponentActivity import androidx.annotation.OptIn import androidx.camera.core.CameraControl @@ -66,6 +65,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.tasks.await +import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -192,10 +192,10 @@ class CameraViewModel try { // Use the suspend version of takePicture() to get the result val outputFileResults = cameraCaptureUseCase.takePicture(outputFileOptions) - Log.d("CameraViewModel", "Image captured: ${outputFileResults.savedUri}") + Timber.d("Image captured: ${outputFileResults.savedUri}") _uiState.update { it.copy(imageUri = outputFileResults.savedUri) } } catch (exception: ImageCaptureException) { - Log.e("CameraViewModel", "Error capturing image $exception") + Timber.e(exception, "Error capturing image") // TODO handle error on screen } } diff --git a/feature/creation/build.gradle.kts b/feature/creation/build.gradle.kts index bfcbc42c..86e8ea7b 100644 --- a/feature/creation/build.gradle.kts +++ b/feature/creation/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(libs.coil.gif) implementation(libs.coil.compose.http) implementation(libs.hilt.android) + implementation(libs.timber) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.adaptive) implementation(libs.androidx.adaptive.layout) diff --git a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt index b9e34558..04c0d90e 100644 --- a/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt +++ b/feature/creation/src/main/java/com/android/developers/androidify/creation/CreationViewModel.kt @@ -18,7 +18,6 @@ package com.android.developers.androidify.creation import android.content.Context import android.graphics.Bitmap import android.net.Uri -import android.util.Log import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.graphics.Color @@ -41,6 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -98,13 +98,13 @@ class CreationViewModel @Inject constructor( fun onPromptGenerationClicked() { promptGenerationJob?.cancel() promptGenerationJob = viewModelScope.launch { - Log.d("CreationViewModel", "Generating prompt...") + Timber.d("Generating prompt...") _uiState.update { it.copy(promptGenerationInProgress = true) } try { val prompt = textGenerationRepository.getNextGeneratedBotPrompt() - Log.d("CreationViewModel", "Prompt: $prompt") + Timber.d("Prompt: $prompt") if (prompt != null) { _uiState.update { it.copy( @@ -114,7 +114,7 @@ class CreationViewModel @Inject constructor( } } } catch (exception: Exception) { - Log.e("CreationViewModel", "Error generating prompt", exception) + Timber.e(exception, "Error generating prompt") _uiState.update { it.copy(promptGenerationInProgress = false) } @@ -165,7 +165,7 @@ class CreationViewModel @Inject constructor( } private suspend fun handleImageGenerationError(exception: Exception) { - Log.d("CreationViewModel", "Exception in generating image", exception) + Timber.d(exception, "Exception in generating image") _uiState.update { it.copy(screenState = ScreenState.EDIT) } @@ -184,7 +184,7 @@ class CreationViewModel @Inject constructor( is NoInternetException -> context.getString(R.string.error_connectivity) is ImageDescriptionFailedGenerationException -> context.getString(R.string.error_image_validation) else -> { - Log.e("CreationViewModel", "Unknown error:", exception) + Timber.e(exception, "Unknown error:") context.getString(R.string.error_upload_generic) } } diff --git a/feature/results/build.gradle.kts b/feature/results/build.gradle.kts index 0a02b2a5..49b14939 100644 --- a/feature/results/build.gradle.kts +++ b/feature/results/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { implementation(libs.accompanist.permissions) implementation(libs.androidx.lifecycle.process) implementation(libs.mlkit.segmentation) + implementation(libs.timber) ksp(libs.hilt.compiler) implementation(libs.androidx.ui.tooling) diff --git a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt index 2cb605b9..9fe834f2 100644 --- a/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt +++ b/feature/results/src/main/java/com/android/developers/androidify/customize/CustomizeExportViewModel.kt @@ -18,7 +18,6 @@ package com.android.developers.androidify.customize import android.app.Application import android.graphics.Bitmap import android.net.Uri -import android.util.Log import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.Modifier @@ -31,9 +30,6 @@ import com.android.developers.androidify.watchface.WatchFaceAsset import com.android.developers.androidify.watchface.transfer.WatchFaceInstallationRepository import com.android.developers.androidify.wear.common.WatchFaceInstallError import com.android.developers.androidify.wear.common.WatchFaceInstallationStatus -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -43,6 +39,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.collections.isNotEmpty @@ -175,7 +172,7 @@ class CustomizeExportViewModel @Inject constructor( ) } } catch (exception: Exception) { - Log.e("CustomizeExportViewModel", "Background removal failed", exception) + Timber.e(exception, "Background removal failed") snackbarHostState.value.showSnackbar("Background removal failed") _state.update { val aspectRatioToolState = (it.toolState[CustomizeTool.Size] as AspectRatioToolState) @@ -282,7 +279,7 @@ class CustomizeExportViewModel @Inject constructor( ) } } catch (e: Exception) { - Log.e("CustomizeExportViewModel", "Image generation failed", e) + Timber.e(e, "Image generation failed") snackbarHostState.value.showSnackbar("Background vibe generation failed") } finally { _state.update { it.copy(showImageEditProgress = false) } @@ -307,8 +304,8 @@ class CustomizeExportViewModel @Inject constructor( _state.update { it.copy(externalOriginalSavedUri = savedOriginalUri) } - } catch (exception : Exception) { - Log.d("CustomizeExportViewModel", "Original image save failed: ", exception) + } catch (exception: Exception) { + Timber.d(exception, "Original image save failed: ") } } if (resultBitmap != null) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a6d434c..c523f4d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ coreSplashscreen = "1.0.1" crashlytics = "3.0.4" datastore = "1.1.7" espressoCore = "3.6.1" -firebaseBom = "33.16.0" +firebaseBom = "34.2.0" googleServices = "4.4.3" googleOss = "17.2.0" googleOssPlugin = "0.10.6" @@ -81,7 +81,8 @@ aiEdge = "0.0.1-exp02" lifecycleProcess = "2.9.1" mlkitCommon = "18.11.0" mlkitSegmentation = "16.0.0-beta1" -playServicesBase = "18.4.0" +playServicesBase = "18.7.2" +timber = "5.0.1" xr-compose = "1.0.0-alpha06" [libraries] @@ -177,6 +178,7 @@ validator-push-android = { module = "com.google.android.wearable.watchface.valid validator-push-cli = { module = "com.google.android.wearable.watchface.validator:validator-push-cli", version.ref = "validatorPush" } watchface-push = { group = "androidx.wear.watchfacepush", name="watchfacepush", version.ref = "watchFacePush" } play-services-base = { group = "com.google.android.gms", name = "play-services-base", version.ref = "playServicesBase" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } google-firebase-appcheck-debug = { group = "com.google.firebase", name = "firebase-appcheck-debug"} androidx-xr-compose = { group = "androidx.xr.compose", name="compose", version.ref = "xr-compose"} androidx-xr-extensions = { module = "com.android.extensions.xr:extensions-xr", version.ref = "extensionsXr" } From 818a19b47ae67ae3dbabb13a90410c4f01f6c75d Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Wed, 10 Sep 2025 20:48:39 +0100 Subject: [PATCH 2/4] Spotless --- .../FakeWatchFaceInstallationRepository.kt | 17 ++++++++++++++++- .../src/main/res/drawable/squiggle_full.xml | 4 ++-- .../androidify/xr/MainPanelWorkaround.kt | 2 +- .../creation/CreationViewModelTest.kt | 2 +- .../androidify/home/HomeScreenScreenshotTest.kt | 1 - .../service/AndroidifyDataListenerService.kt | 7 ++++--- .../androidify/ui/TransmissionScreen.kt | 14 ++++++++------ .../androidify/ui/WatchFaceOnboardingScreen.kt | 4 ++-- 8 files changed, 34 insertions(+), 17 deletions(-) diff --git a/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt b/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt index 923c643c..ae9c81aa 100644 --- a/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt +++ b/core/testing/src/main/java/com/android/developers/testing/repository/FakeWatchFaceInstallationRepository.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.android.developers.testing.repository import android.graphics.Bitmap @@ -65,4 +80,4 @@ class FakeWatchFaceInstallationRepository : WatchFaceInstallationRepository { public fun setWatchAsConnected() { _connectedWatch.value = watch } -} \ No newline at end of file +} diff --git a/core/theme/src/main/res/drawable/squiggle_full.xml b/core/theme/src/main/res/drawable/squiggle_full.xml index 4fc308ca..3d6d0b21 100644 --- a/core/theme/src/main/res/drawable/squiggle_full.xml +++ b/core/theme/src/main/res/drawable/squiggle_full.xml @@ -1,4 +1,5 @@ - - { + -> { TransmissionScreen() } is WatchFaceInstallationStatus.Unknown, WatchFaceInstallationStatus.NotStarted, - -> { + -> { if (launchedFromWatchFaceTransfer) { TransmissionScreen() } else { From 1822c4e277994f233ed61e5eecb7fc70d6ed8bc9 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Thu, 11 Sep 2025 10:46:11 +0100 Subject: [PATCH 3/4] Add proguard for OkHttp rules --- app/proguard-rules.pro | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a80b8bb0..3ad8efb8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -43,3 +43,11 @@ # Ignore missing Java SE annotation processing classes, often from libraries like AutoValue -dontwarn javax.lang.model.** + +# OkHttp +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** + +# Ignore SAX parser warning +-dontwarn org.xml.sax.** From c03fb7b9ab1f69c3bf1819ec45d23b63cf9c23f6 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Thu, 11 Sep 2025 11:18:32 +0100 Subject: [PATCH 4/4] Only log expiry time and failure --- .../startup/FirebaseAppCheckInitializer.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt index a166ed30..f5c90a23 100644 --- a/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt +++ b/core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt @@ -49,15 +49,13 @@ class FirebaseAppCheckInitializer : Initializer { } setTokenAutoRefreshEnabled(true) } - if (!BuildConfig.DEBUG) { - val token = appCheck.getAppCheckToken(false) - token.addOnCompleteListener { - if (token.isSuccessful) { - Timber.i("Firebase app check token success: ${token.result.token}") - Timber.i("Firebase app check token success: ${token.result.expireTimeMillis}") - } else { - Timber.e(token.exception, "Firebase app check token failure") - } + + val token = appCheck.getAppCheckToken(false) + token.addOnCompleteListener { + if (token.isSuccessful) { + Timber.i("Firebase app check token success: ${token.result.expireTimeMillis}") + } else { + Timber.e(token.exception, "Firebase app check token failure") } } return appCheck