Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 9 additions & 26 deletions app/src/main/java/org/hogwarts/android/hogwarts-application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import android.os.StrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
Expand All @@ -17,7 +16,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.hogwarts.android.core.data.preferences.AppPreferences
import timber.log.Timber
import org.hogwarts.android.startup.AppStartupInitializer
import javax.inject.Inject

/**
Expand All @@ -34,6 +33,9 @@ class HogwartsApplication : Application(), Configuration.Provider, ImageLoaderFa
@Inject
lateinit var appPreferences: AppPreferences

@Inject
lateinit var appStartupInitializer: AppStartupInitializer

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
Expand All @@ -49,6 +51,11 @@ class HogwartsApplication : Application(), Configuration.Provider, ImageLoaderFa
override fun onCreate() {
super.onCreate()

// All startup hooks (Timber, StrictMode, WorkManager + Firebase
// init confirmation) funnel through AppStartupInitializer so the
// boot sequence stays in one place.
appStartupInitializer.initialize()

// AppLocalesMetadataHolderService (manifest, autoStoreLocales=true) handles
// locale persistence on Android <13; the platform LocaleManager handles 13+.
// We just warm the DataStore cache off-main so settings reads stay snappy.
Expand All @@ -57,33 +64,9 @@ class HogwartsApplication : Application(), Configuration.Provider, ImageLoaderFa
appPreferences.themeMode.first()
}

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
enableStrictMode()
}

createNotificationChannels()
}

private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectActivityLeaks()
.penaltyLog()
.build()
)
}

private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(NotificationManager::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,112 @@
package org.hogwarts.android.startup

import android.content.Context
import android.os.StrictMode
import android.util.Log
import androidx.work.WorkManager
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import dagger.hilt.android.qualifiers.ApplicationContext
import org.hogwarts.android.BuildConfig
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

/**
* Single entry point for all startup hooks invoked from
* `HogwartsApplication.onCreate()`. Adding a new hook here keeps `onCreate`
* legible and makes the boot sequence testable.
*/
@Singleton
class AppStartupInitializer @Inject constructor(
private val context: Context
@ApplicationContext private val context: Context
) {

fun initialize() {
Timber.d("AppStartupInitializer: Beginning app initialization")
initializeTimber()
initializeCoil()
Timber.d("AppStartupInitializer: App initialization complete")
// Why: Timber must plant before any other startup log statements so
// both debug console output and release Crashlytics breadcrumbs catch
// the boot sequence.
plantTimber()

if (BuildConfig.DEBUG) {
// Why: StrictMode surfaces accidental main-thread disk/network IO
// and leaked Closeables during development. Release payloads never
// include the policy so users don't pay the perf hit.
enableStrictMode()
}

// Why: WorkManager is created lazily on the first getInstance() call.
// Touching it at boot confirms HiltWorkerFactory wiring and surfaces
// any misconfiguration as a startup crash instead of a silent runtime
// failure when the first periodic sync would have fired.
confirmWorkManager()

// Why: Firebase auto-inits via FirebaseInitProvider in the manifest.
// Verifying the instance exists at boot means a missing
// google-services.json (E01.S01) shows up as one loud log line on
// every launch instead of as a confusing failure deep inside the FCM
// token upload path.
confirmFirebase()

Timber.i("AppStartupInitializer complete")
}

private fun plantTimber() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
} else {
Timber.plant(CrashlyticsTree())
}
}

private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectActivityLeaks()
.penaltyLog()
.build()
)
}

private fun initializeTimber() {
// Timber is already initialized in HogwartsApp, this is for future enhancements
Timber.d("Timber initialized")
private fun confirmWorkManager() {
val wm = WorkManager.getInstance(context)
Timber.i("WorkManager initialized: ${wm.javaClass.simpleName}")
}

private fun initializeCoil() {
// Coil uses default ImageLoader, customize for disk cache size if needed
Timber.d("Coil image loader ready")
private fun confirmFirebase() {
try {
val app = FirebaseApp.getInstance()
Timber.i("Firebase initialized: project=${app.options.projectId}")
} catch (e: IllegalStateException) {
// E01.S01 (commit google-services.json) hasn't landed yet — log
// loud so the gap is obvious, but don't crash the app.
Timber.w("Firebase not initialized: ${e.message}")
}
}
}

/**
* Release-build Timber tree that funnels warnings + errors to Crashlytics.
* Below WARN is dropped to keep the breadcrumb retention focused on signals
* a release user would actually want to debug.
*/
private class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority < Log.WARN) return
val crashlytics = FirebaseCrashlytics.getInstance()
crashlytics.log(if (tag != null) "$tag: $message" else message)
if (t != null && priority >= Log.ERROR) {
crashlytics.recordException(t)
}
}
}