2024-04-22 14:06:37 +00:00
|
|
|
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<BaseActivity>? = 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|