package info.nightscout.androidaps import android.content.Context import android.content.Intent import android.graphics.Rect import android.os.Bundle import android.os.PersistableBundle import android.text.SpannableString import android.text.method.LinkMovementMethod import android.text.style.ForegroundColorSpan import android.text.util.Linkify import android.util.TypedValue import android.view.Menu import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.widget.Toolbar import androidx.core.view.GravityCompat import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator import com.google.firebase.crashlytics.FirebaseCrashlytics import com.joanzapata.iconify.Iconify import com.joanzapata.iconify.fonts.FontAwesomeModule import dev.doubledot.doki.ui.DokiActivity import info.nightscout.androidaps.activities.* import info.nightscout.androidaps.database.entities.UserEntry.Action import info.nightscout.androidaps.database.entities.UserEntry.Sources import info.nightscout.androidaps.databinding.ActivityMainBinding import info.nightscout.androidaps.events.EventAppExit import info.nightscout.androidaps.events.EventInitializationChanged import info.nightscout.androidaps.events.EventPreferenceChange import info.nightscout.androidaps.events.EventRebuildTabs import info.nightscout.androidaps.interfaces.* import info.nightscout.androidaps.logging.UserEntryLogger import info.nightscout.androidaps.plugins.bus.RxBus import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker import info.nightscout.androidaps.plugins.constraints.signatureVerifier.SignatureVerifierPlugin import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerUtils import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin import info.nightscout.androidaps.setupwizard.SetupWizardActivity import info.nightscout.androidaps.utils.AndroidPermission import info.nightscout.androidaps.utils.FabricPrivacy import info.nightscout.androidaps.utils.alertDialogs.OKDialog import info.nightscout.androidaps.utils.buildHelper.BuildHelper import info.nightscout.androidaps.utils.extensions.isRunningRealPumpTest import info.nightscout.androidaps.utils.locale.LocaleHelper import info.nightscout.androidaps.utils.protection.PasswordCheck import info.nightscout.androidaps.utils.protection.ProtectionCheck import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.androidaps.utils.tabs.TabPageAdapter import info.nightscout.androidaps.utils.ui.UIRunnable import info.nightscout.shared.logging.LTag import info.nightscout.shared.sharedPreferences.SP import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import java.util.* import javax.inject.Inject import kotlin.system.exitProcess class MainActivity : NoSplashAppCompatActivity() { private val disposable = CompositeDisposable() @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var androidPermission: AndroidPermission @Inject lateinit var sp: SP @Inject lateinit var versionCheckerUtils: VersionCheckerUtils @Inject lateinit var smsCommunicatorPlugin: SmsCommunicatorPlugin @Inject lateinit var loop: Loop @Inject lateinit var nsSettingsStatus: NSSettingsStatus @Inject lateinit var buildHelper: BuildHelper @Inject lateinit var activePlugin: ActivePlugin @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var protectionCheck: ProtectionCheck @Inject lateinit var iconsProvider: IconsProvider @Inject lateinit var constraintChecker: ConstraintChecker @Inject lateinit var signatureVerifierPlugin: SignatureVerifierPlugin @Inject lateinit var config: Config @Inject lateinit var uel: UserEntryLogger @Inject lateinit var profileFunction: ProfileFunction @Inject lateinit var passwordCheck: PasswordCheck private lateinit var actionBarDrawerToggle: ActionBarDrawerToggle private var pluginPreferencesMenuItem: MenuItem? = null private var menu: Menu? = null private var menuOpen = false private var isProtectionCheckActive = false private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Iconify.with(FontAwesomeModule()) LocaleHelper.update(applicationContext) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setHomeButtonEnabled(true) actionBarDrawerToggle = ActionBarDrawerToggle(this, binding.mainDrawerLayout, R.string.open_navigation, R.string.close_navigation).also { binding.mainDrawerLayout.addDrawerListener(it) it.syncState() } // initialize screen wake lock processPreferenceChange(EventPreferenceChange(rh.gs(R.string.key_keep_screen_on))) binding.mainPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageScrollStateChanged(state: Int) {} override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} override fun onPageSelected(position: Int) { setPluginPreferenceMenuName() checkPluginPreferences(binding.mainPager) setDisabledMenuItemColorPluginPreferences() } }) //Check here if loop plugin is disabled. Else check via constraints if (!(loop as PluginBase).isEnabled()) versionCheckerUtils.triggerCheckVersion() setUserStats() setupViews() disposable += rxBus .toObservable(EventRebuildTabs::class.java) .observeOn(aapsSchedulers.main) .subscribe({ if (it.recreate) recreate() else setupViews() setWakeLock() }, fabricPrivacy::logException) disposable += rxBus .toObservable(EventPreferenceChange::class.java) .observeOn(aapsSchedulers.main) .subscribe({ processPreferenceChange(it) }, fabricPrivacy::logException) disposable += rxBus .toObservable(EventInitializationChanged::class.java) .observeOn(aapsSchedulers.main) .subscribe({ passwordCheck.passwordResetCheck(this) }, fabricPrivacy::logException) if (startWizard() && !isRunningRealPumpTest()) { protectionCheck.queryProtection(this, ProtectionCheck.Protection.PREFERENCES, { startActivity(Intent(this, SetupWizardActivity::class.java)) }) } androidPermission.notifyForStoragePermission(this) androidPermission.notifyForBatteryOptimizationPermission(this) if (!config.NSCLIENT) androidPermission.notifyForLocationPermissions(this) if (config.PUMPDRIVERS) { androidPermission.notifyForSMSPermissions(this, smsCommunicatorPlugin) androidPermission.notifyForSystemWindowPermissions(this) androidPermission.notifyForBtConnectPermission(this) } } private fun checkPluginPreferences(viewPager: ViewPager2) { if (viewPager.currentItem >= 0) pluginPreferencesMenuItem?.isEnabled = (viewPager.adapter as TabPageAdapter).getPluginAt(viewPager.currentItem).preferencesId != -1 } private fun startWizard(): Boolean = !sp.getBoolean(R.string.key_setupwizard_processed, false) override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { super.onPostCreate(savedInstanceState, persistentState) actionBarDrawerToggle.syncState() } override fun onDestroy() { super.onDestroy() disposable.clear() } override fun onResume() { super.onResume() if (!isProtectionCheckActive) { isProtectionCheckActive = true protectionCheck.queryProtection(this, ProtectionCheck.Protection.APPLICATION, UIRunnable { isProtectionCheckActive = false }, UIRunnable { OKDialog.show(this, "", rh.gs(R.string.authorizationfailed)) { isProtectionCheckActive = false; finish() } }, UIRunnable { OKDialog.show(this, "", rh.gs(R.string.authorizationfailed)) { isProtectionCheckActive = false; finish() } } ) } } private fun setWakeLock() { val keepScreenOn = sp.getBoolean(R.string.key_keep_screen_on, false) if (keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } private fun processPreferenceChange(ev: EventPreferenceChange) { if (ev.isChanged(rh, R.string.key_keep_screen_on)) setWakeLock() if (ev.isChanged(rh, R.string.key_skin)) recreate() } private fun setupViews() { // Menu val pageAdapter = TabPageAdapter(this) binding.mainNavigationView.setNavigationItemSelectedListener { true } val menu = binding.mainNavigationView.menu.also { it.clear() } for (p in activePlugin.getPluginsList()) { pageAdapter.registerNewFragment(p) if (p.isEnabled() && p.hasFragment() && !p.isFragmentVisible() && !p.pluginDescription.neverVisible) { val menuItem = menu.add(p.name) menuItem.isCheckable = true if (p.menuIcon != -1) { menuItem.setIcon(p.menuIcon) } else { menuItem.setIcon(R.drawable.ic_settings) } menuItem.setOnMenuItemClickListener { val intent = Intent(this, SingleFragmentActivity::class.java) intent.putExtra("plugin", activePlugin.getPluginsList().indexOf(p)) startActivity(intent) binding.mainDrawerLayout.closeDrawers() true } } } binding.mainPager.adapter = pageAdapter binding.mainPager.offscreenPageLimit = 8 // This may cause more memory consumption checkPluginPreferences(binding.mainPager) // Tabs if (sp.getBoolean(R.string.key_short_tabtitles, false)) { binding.tabsNormal.visibility = View.GONE binding.tabsCompact.visibility = View.VISIBLE binding.toolbar.layoutParams = LinearLayout.LayoutParams(Toolbar.LayoutParams.MATCH_PARENT, resources.getDimension(R.dimen.compact_height).toInt()) TabLayoutMediator(binding.tabsCompact, binding.mainPager) { tab, position -> tab.text = (binding.mainPager.adapter as TabPageAdapter).getPluginAt(position).nameShort }.attach() } else { binding.tabsNormal.visibility = View.VISIBLE binding.tabsCompact.visibility = View.GONE val typedValue = TypedValue() if (theme.resolveAttribute(R.attr.actionBarSize, typedValue, true)) { binding.toolbar.layoutParams = LinearLayout.LayoutParams( Toolbar.LayoutParams.MATCH_PARENT, TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics) ) } TabLayoutMediator(binding.tabsNormal, binding.mainPager) { tab, position -> tab.text = (binding.mainPager.adapter as TabPageAdapter).getPluginAt(position).name }.attach() } } override fun dispatchTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_DOWN) { val v = currentFocus if (v is EditText) { val outRect = Rect() v.getGlobalVisibleRect(outRect) if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { v.clearFocus() val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(v.getWindowToken(), 0) } } } return super.dispatchTouchEvent(event) } private fun setDisabledMenuItemColorPluginPreferences() { if (pluginPreferencesMenuItem?.isEnabled == false) { val spanString = SpannableString(this.menu?.findItem(R.id.nav_plugin_preferences)?.title.toString()) spanString.setSpan(ForegroundColorSpan(rh.gac(R.attr.disabledTextColor)), 0, spanString.length, 0) this.menu?.findItem(R.id.nav_plugin_preferences)?.title = spanString } } private fun setPluginPreferenceMenuName() { if (binding.mainPager.currentItem >= 0) { val plugin = (binding.mainPager.adapter as TabPageAdapter).getPluginAt(binding.mainPager.currentItem) this.menu?.findItem(R.id.nav_plugin_preferences)?.title = rh.gs(R.string.nav_preferences_plugin, plugin.name) } } override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { menuOpen = true if (binding.mainDrawerLayout.isDrawerOpen(GravityCompat.START)) { binding.mainDrawerLayout.closeDrawers() } val result = super.onMenuOpened(featureId, menu) menu.findItem(R.id.nav_treatments)?.isEnabled = profileFunction.getProfile() != null return result } override fun onPanelClosed(featureId: Int, menu: Menu) { menuOpen = false super.onPanelClosed(featureId, menu) } override fun onCreateOptionsMenu(menu: Menu): Boolean { this.menu = menu menuInflater.inflate(R.menu.menu_main, menu) pluginPreferencesMenuItem = menu.findItem(R.id.nav_plugin_preferences) setPluginPreferenceMenuName() checkPluginPreferences(binding.mainPager) setDisabledMenuItemColorPluginPreferences() return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.nav_preferences -> { protectionCheck.queryProtection(this, ProtectionCheck.Protection.PREFERENCES, { val i = Intent(this, PreferencesActivity::class.java) i.putExtra("id", -1) startActivity(i) }) return true } R.id.nav_historybrowser -> { startActivity(Intent(this, HistoryBrowseActivity::class.java)) return true } R.id.nav_treatments -> { startActivity(Intent(this, TreatmentsActivity::class.java)) return true } R.id.nav_setupwizard -> { protectionCheck.queryProtection(this, ProtectionCheck.Protection.PREFERENCES, { startActivity(Intent(this, SetupWizardActivity::class.java)) }) return true } R.id.nav_about -> { var message = "Build: ${BuildConfig.BUILDVERSION}\n" message += "Flavor: ${BuildConfig.FLAVOR}${BuildConfig.BUILD_TYPE}\n" message += "${rh.gs(R.string.configbuilder_nightscoutversion_label)} ${nsSettingsStatus.getVersion()}" if (buildHelper.isEngineeringMode()) message += "\n${rh.gs(R.string.engineering_mode_enabled)}" if (!fabricPrivacy.fabricEnabled()) message += "\n${rh.gs(R.string.fabric_upload_disabled)}" message += rh.gs(R.string.about_link_urls) val messageSpanned = SpannableString(message) Linkify.addLinks(messageSpanned, Linkify.WEB_URLS) MaterialAlertDialogBuilder(this, R.style.DialogTheme) .setTitle(rh.gs(R.string.app_name) + " " + BuildConfig.VERSION) .setIcon(iconsProvider.getIcon()) .setMessage(messageSpanned) .setPositiveButton(rh.gs(R.string.ok), null) .setNeutralButton(rh.gs(R.string.cta_dont_kill_my_app_info)) { _, _ -> DokiActivity.start(context = this@MainActivity) } .create().apply { show() findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() } return true } R.id.nav_exit -> { aapsLogger.debug(LTag.CORE, "Exiting") uel.log(Action.EXIT_AAPS, Sources.Aaps) rxBus.send(EventAppExit()) finish() System.runFinalization() exitProcess(0) } R.id.nav_plugin_preferences -> { val plugin = (binding.mainPager.adapter as TabPageAdapter).getPluginAt(binding.mainPager.currentItem) protectionCheck.queryProtection(this, ProtectionCheck.Protection.PREFERENCES, { val i = Intent(this, PreferencesActivity::class.java) i.putExtra("id", plugin.preferencesId) startActivity(i) }) return true } /* R.id.nav_survey -> { startActivity(Intent(this, SurveyActivity::class.java)) return true } */ R.id.nav_defaultprofile -> { startActivity(Intent(this, ProfileHelperActivity::class.java)) return true } R.id.nav_stats -> { startActivity(Intent(this, StatsActivity::class.java)) return true } } return actionBarDrawerToggle.onOptionsItemSelected(item) } override fun onBackPressed() { if (binding.mainDrawerLayout.isDrawerOpen(GravityCompat.START)) { binding.mainDrawerLayout.closeDrawers() return } if (menuOpen) { this.menu?.close() return } if (binding.mainPager.currentItem != 0) { binding.mainPager.currentItem = 0 return } super.onBackPressed() } // Correct place for calling setUserStats() would be probably MainApp // but we need to have it called at least once a day. Thus this location private fun setUserStats() { if (!fabricPrivacy.fabricEnabled()) return val closedLoopEnabled = if (constraintChecker.isClosedLoopAllowed().value()) "CLOSED_LOOP_ENABLED" else "CLOSED_LOOP_DISABLED" // Size is limited to 36 chars val remote = BuildConfig.REMOTE.lowercase(Locale.getDefault()) .replace("https://", "") .replace("http://", "") .replace(".git", "") .replace(".com/", ":") .replace(".org/", ":") .replace(".net/", ":") fabricPrivacy.firebaseAnalytics.setUserProperty("Mode", BuildConfig.APPLICATION_ID + "-" + closedLoopEnabled) fabricPrivacy.firebaseAnalytics.setUserProperty("Language", sp.getString(R.string.key_language, Locale.getDefault().language)) fabricPrivacy.firebaseAnalytics.setUserProperty("Version", BuildConfig.VERSION) fabricPrivacy.firebaseAnalytics.setUserProperty("HEAD", BuildConfig.HEAD) fabricPrivacy.firebaseAnalytics.setUserProperty("Remote", remote) val hashes: List = signatureVerifierPlugin.shortHashes() if (hashes.isNotEmpty()) fabricPrivacy.firebaseAnalytics.setUserProperty("Hash", hashes[0]) activePlugin.activePump.let { fabricPrivacy.firebaseAnalytics.setUserProperty("Pump", it::class.java.simpleName) } if (!config.NSCLIENT && !config.PUMPCONTROL) activePlugin.activeAPS.let { fabricPrivacy.firebaseAnalytics.setUserProperty("Aps", it::class.java.simpleName) } activePlugin.activeBgSource.let { fabricPrivacy.firebaseAnalytics.setUserProperty("BgSource", it::class.java.simpleName) } fabricPrivacy.firebaseAnalytics.setUserProperty("Profile", activePlugin.activeProfileSource.javaClass.simpleName) activePlugin.activeSensitivity.let { fabricPrivacy.firebaseAnalytics.setUserProperty("Sensitivity", it::class.java.simpleName) } activePlugin.activeInsulin.let { fabricPrivacy.firebaseAnalytics.setUserProperty("Insulin", it::class.java.simpleName) } // Add to crash log too FirebaseCrashlytics.getInstance().setCustomKey("HEAD", BuildConfig.HEAD) FirebaseCrashlytics.getInstance().setCustomKey("Version", BuildConfig.VERSION) FirebaseCrashlytics.getInstance().setCustomKey("BuildType", BuildConfig.BUILD_TYPE) FirebaseCrashlytics.getInstance().setCustomKey("BuildFlavor", BuildConfig.FLAVOR) FirebaseCrashlytics.getInstance().setCustomKey("Remote", remote) FirebaseCrashlytics.getInstance().setCustomKey("Committed", BuildConfig.COMMITTED) FirebaseCrashlytics.getInstance().setCustomKey("Hash", hashes[0]) FirebaseCrashlytics.getInstance().setCustomKey("Email", sp.getString(R.string.key_email_for_crash_report, "")) } }