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/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.** 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..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 @@ -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,33 @@ 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) + } + + 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 } 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/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 @@ - - 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/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt index 574fda1e..f8a4d478 100644 --- a/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt +++ b/feature/creation/src/test/kotlin/com/android/developers/androidify/creation/CreationViewModelTest.kt @@ -52,6 +52,7 @@ class CreationViewModelTest { private val imageGenerationRepository = FakeImageGenerationRepository() private val fakeUri = Uri.parse("test.jpeg") + @Before fun setup() { viewModel = CreationViewModel( @@ -62,7 +63,6 @@ class CreationViewModelTest { FakeDropImageFactory(), context = RuntimeEnvironment.getApplication(), ) - } @Test diff --git a/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt b/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt index 49439c55..f9984e22 100644 --- a/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt +++ b/feature/home/src/screenshotTest/java/com/android/developers/androidify/home/HomeScreenScreenshotTest.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.Preview import com.android.developers.androidify.theme.AndroidifyTheme import com.android.developers.androidify.theme.SharedElementContextPreview import com.android.developers.androidify.util.AdaptivePreview -import com.android.developers.androidify.util.isAtLeastMedium class HomeScreenScreenshotTest { 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" } diff --git a/wear/src/main/java/com/android/developers/androidify/service/AndroidifyDataListenerService.kt b/wear/src/main/java/com/android/developers/androidify/service/AndroidifyDataListenerService.kt index 46730be9..f5c650aa 100644 --- a/wear/src/main/java/com/android/developers/androidify/service/AndroidifyDataListenerService.kt +++ b/wear/src/main/java/com/android/developers/androidify/service/AndroidifyDataListenerService.kt @@ -226,7 +226,8 @@ class AndroidifyDataListenerService : WearableListenerService() { delay(TRANSFER_TIMEOUT_MS) val transferState = watchFacePushStateManager.watchFaceInstallationStatus.first() if (transferState is WatchFaceInstallationStatus.Receiving && - transferState.transferId == initialRequest.transferId) { + transferState.transferId == initialRequest.transferId + ) { watchFacePushStateManager.setWatchFaceInstallationStatus( WatchFaceInstallationStatus.Complete( success = false, @@ -285,8 +286,8 @@ class AndroidifyDataListenerService : WearableListenerService() { @Suppress("DEPRECATION") val wakeLock = powerManager.newWakeLock( PowerManager.FULL_WAKE_LOCK - or PowerManager.ACQUIRE_CAUSES_WAKEUP - or PowerManager.ON_AFTER_RELEASE, + or PowerManager.ACQUIRE_CAUSES_WAKEUP + or PowerManager.ON_AFTER_RELEASE, WAKELOCK_TAG, ) diff --git a/wear/src/main/java/com/android/developers/androidify/ui/TransmissionScreen.kt b/wear/src/main/java/com/android/developers/androidify/ui/TransmissionScreen.kt index 00bd7f19..d35b50a3 100644 --- a/wear/src/main/java/com/android/developers/androidify/ui/TransmissionScreen.kt +++ b/wear/src/main/java/com/android/developers/androidify/ui/TransmissionScreen.kt @@ -100,12 +100,14 @@ fun TransmissionScreen(modifier: Modifier = Modifier) { @Composable fun FourColorProgressIndicator() { - val colors = remember { listOf( - LimeGreen, - Primary80, - Primary90, - Blue, - ) } + val colors = remember { + listOf( + LimeGreen, + Primary80, + Primary90, + Blue, + ) + } val infiniteTransition = rememberInfiniteTransition(label = "transition") val progress by infiniteTransition.animateFloat( diff --git a/wear/src/main/java/com/android/developers/androidify/ui/WatchFaceOnboardingScreen.kt b/wear/src/main/java/com/android/developers/androidify/ui/WatchFaceOnboardingScreen.kt index 7b37c321..8f3140fc 100644 --- a/wear/src/main/java/com/android/developers/androidify/ui/WatchFaceOnboardingScreen.kt +++ b/wear/src/main/java/com/android/developers/androidify/ui/WatchFaceOnboardingScreen.kt @@ -50,13 +50,13 @@ fun WatchFaceOnboardingScreen( when (state) { is WatchFaceInstallationStatus.Receiving, is WatchFaceInstallationStatus.Sending, - -> { + -> { TransmissionScreen() } is WatchFaceInstallationStatus.Unknown, WatchFaceInstallationStatus.NotStarted, - -> { + -> { if (launchedFromWatchFaceTransfer) { TransmissionScreen() } else {