package com.habitrpg.android.habitica import android.app.Activity import android.app.Application import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.Resources import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.edit import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.preference.PreferenceManager import com.google.android.gms.wearable.Wearable import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings import com.gu.toolargetool.TooLargeTool import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.extensions.DateUtils import com.habitrpg.android.habitica.helpers.AdHandler import com.habitrpg.android.habitica.helpers.Analytics import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.ui.activities.BaseActivity import com.habitrpg.android.habitica.ui.activities.LoginActivity import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper import com.habitrpg.common.habitica.extensions.setupCoil import com.habitrpg.common.habitica.helpers.ExceptionHandler import com.habitrpg.common.habitica.helpers.LanguageHelper import com.habitrpg.common.habitica.helpers.MarkdownParser import com.habitrpg.common.habitica.helpers.launchCatching import dagger.hilt.android.HiltAndroidApp import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.MainScope import java.lang.ref.WeakReference import java.util.Date import javax.inject.Inject class ApplicationLifecycleTracker(private val sharedPreferences: SharedPreferences) : DefaultLifecycleObserver { private var lastResumeTime = 0L override fun onResume(owner: LifecycleOwner) { super.onResume(owner) lastResumeTime = Date().time } override fun onPause(owner: LifecycleOwner) { super.onPause(owner) val duration = Date().time - lastResumeTime addDurationToDay(duration / 1000) } private fun addDurationToDay(duration: Long) { var currentTotal = sharedPreferences.getLong("usage_time_total", 0L) currentTotal += duration var currentDay = Date() if (sharedPreferences.contains("usage_time_day")) { currentDay = Date(sharedPreferences.getLong("usage_time_day", 0L)) } var current = sharedPreferences.getLong("usage_time_current", 0L) if (!DateUtils.isSameDay(currentDay, Date())) { var average = sharedPreferences.getLong("usage_time_daily_average", 0L) var observedDays = sharedPreferences.getInt("usage_time_day_count", 0) average = ((average * observedDays) + current) / (observedDays + 1) sharedPreferences.edit { putInt("usage_time_day_count", ++observedDays) putLong("usage_time_daily_average", average) } Analytics.setUserProperty("usage_time_daily_average", average) Analytics.setUserProperty("usage_time_total", currentTotal) current = 0 currentDay = Date() } current += duration sharedPreferences.edit { putLong("usage_time_current", current) putLong("usage_time_total", currentTotal) putLong("usage_time_day", currentDay.time) } } } @HiltAndroidApp abstract class HabiticaBaseApplication : Application(), Application.ActivityLifecycleCallbacks { @Inject internal lateinit var lazyApiHelper: ApiClient @Inject internal lateinit var sharedPrefs: SharedPreferences @Inject internal lateinit var pushNotificationManager: PushNotificationManager @Inject internal lateinit var authenticationHandler: AuthenticationHandler private lateinit var lifecycleTracker: ApplicationLifecycleTracker // endregion override fun onCreate() { super.onCreate() lifecycleTracker = ApplicationLifecycleTracker(sharedPrefs) ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleTracker) if (!BuildConfig.DEBUG) { TooLargeTool.startLogging(this) try { Analytics.initialize(this) } catch (ignored: Resources.NotFoundException) { } Analytics.identify(sharedPrefs) Analytics.setUserID(lazyApiHelper.hostConfig.userID) } registerActivityLifecycleCallbacks(this) setupRealm() setLocale() setupRemoteConfig() setupNotifications() setupAdHandler() HabiticaIconsHelper.init(this) MarkdownParser.setup(this) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) setupCoil() ExceptionHandler.init { Analytics.logException(it) } Analytics.setUserProperty("app_testing_level", BuildConfig.TESTING_LEVEL) checkIfNewVersion() } private fun setupAdHandler() { AdHandler.setup(sharedPrefs) } private fun setLocale() { val resources = resources val configuration: Configuration = resources.configuration val languageHelper = LanguageHelper(sharedPrefs.getString("language", "en")) if (if (SDK_INT >= Build.VERSION_CODES.N) { configuration.locales.isEmpty || configuration.locales[0] != languageHelper.locale } else { @Suppress("DEPRECATION") configuration.locale != languageHelper.locale } ) { configuration.setLocale(languageHelper.locale) resources.updateConfiguration(configuration, null) } } protected open fun setupRealm() { Realm.init(this) val builder = RealmConfiguration.Builder() .schemaVersion(1) .deleteRealmIfMigrationNeeded() .allowWritesOnUiThread(true) .compactOnLaunch { totalBytes, usedBytes -> // Compact if the file is over 100MB in size and less than 50% 'used' val oneHundredMB = 50 * 1024 * 1024 (totalBytes > oneHundredMB) && (usedBytes / totalBytes) < 0.5 } try { Realm.setDefaultConfiguration(builder.build()) } catch (ignored: UnsatisfiedLinkError) { // Catch crash in tests } } private fun checkIfNewVersion() { var info: PackageInfo? = null try { info = packageManager.getPackageInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { Log.e("MyApplication", "couldn't get package info!") } if (info == null) { return } val lastInstalledVersion = sharedPrefs.getInt("last_installed_version", 0) @Suppress("DEPRECATION") if (lastInstalledVersion < info.versionCode) { @Suppress("DEPRECATION") sharedPrefs.edit { putInt("last_installed_version", info.versionCode) } } } override fun openOrCreateDatabase( name: String, mode: Int, factory: SQLiteDatabase.CursorFactory?, ): SQLiteDatabase { return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory) } override fun openOrCreateDatabase( name: String, mode: Int, factory: SQLiteDatabase.CursorFactory?, errorHandler: DatabaseErrorHandler?, ): SQLiteDatabase { return super.openOrCreateDatabase( getDatabasePath(name).absolutePath, mode, factory, errorHandler, ) } // endregion // region IAP - Specific override fun deleteDatabase(name: String): Boolean { val realm = Realm.getDefaultInstance() realm.executeTransaction { realm1 -> realm1.deleteAll() realm1.close() } return true } private fun setupRemoteConfig() { val remoteConfig = FirebaseRemoteConfig.getInstance() val configSettings = FirebaseRemoteConfigSettings.Builder() .setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600) .build() remoteConfig.setConfigSettingsAsync(configSettings) remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) remoteConfig.fetchAndActivate() } private fun setupNotifications() { FirebaseInstallations.getInstance().id.addOnCompleteListener { task -> if (!task.isSuccessful) { Log.w("Token", "getInstanceId failed", task.exception) return@addOnCompleteListener } val token = task.result if (BuildConfig.DEBUG) { Log.d("Token", "Firebase Notification Token: $token") } } } var currentActivity: WeakReference? = null override fun onActivityResumed(activity: Activity) { currentActivity = WeakReference(activity as? BaseActivity) } override fun onActivityStarted(activity: Activity) { currentActivity = WeakReference(activity as? BaseActivity) } override fun onActivityPaused(activity: Activity) { if (currentActivity?.get() == activity) { currentActivity = null } } override fun onActivityCreated( p0: Activity, p1: Bundle?, ) { } override fun onActivityDestroyed(p0: Activity) { } override fun onActivitySaveInstanceState( p0: Activity, p1: Bundle, ) { } override fun onActivityStopped(p0: Activity) { } companion object { fun getInstance(context: Context): HabiticaBaseApplication? { return context.applicationContext as? HabiticaBaseApplication } fun logout(context: Context) { MainScope().launchCatching { getInstance(context)?.pushNotificationManager?.removePushDeviceUsingStoredToken() val realm = Realm.getDefaultInstance() getInstance(context)?.deleteDatabase(realm.path) realm.close() val preferences = PreferenceManager.getDefaultSharedPreferences(context) val useReminder = preferences.getBoolean("use_reminder", false) val reminderTime = preferences.getString("reminder_time", "19:00") val lightMode = preferences.getString("theme_mode", "system") val launchScreen = preferences.getString("launch_screen", "") preferences.edit { clear() putBoolean("use_reminder", useReminder) putString("reminder_time", reminderTime) putString("theme_mode", lightMode) putString("launch_screen", launchScreen) } getInstance(context)?.lazyApiHelper?.updateAuthenticationCredentials(null, null) Wearable.getCapabilityClient(context).removeLocalCapability("provide_auth") startActivity(LoginActivity::class.java, context) } } private fun startActivity( activityClass: Class<*>, context: Context, ) { val intent = Intent(context, activityClass) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } } }