diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt index c7c45d7babc6cc52af544b1390ee3c23df91ad01..6f8e10d248095717d40e0de6284c41a731b541bc 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt @@ -33,7 +33,6 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentResult import com.google.zxing.qrcode.QRCodeWriter -import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R import de.rki.coronawarnapp.RiskLevelAndKeyRetrievalBenchmark import de.rki.coronawarnapp.databinding.FragmentTestForAPIBinding @@ -55,6 +54,7 @@ import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository import de.rki.coronawarnapp.transaction.RiskLevelTransaction import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.KeyFileHelper import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.AutoInject @@ -298,9 +298,9 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), false } - binding.testLogfileToggle.isChecked = CoronaWarnApplication.fileLogger?.isLogging ?: false + binding.testLogfileToggle.isChecked = CWADebug.fileLogger?.isLogging ?: false binding.testLogfileToggle.setOnClickListener { buttonView -> - CoronaWarnApplication.fileLogger?.let { + CWADebug.fileLogger?.let { if (binding.testLogfileToggle.isChecked) { it.start() } else { @@ -310,7 +310,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), } binding.testLogfileShare.setOnClickListener { - CoronaWarnApplication.fileLogger?.let { + CWADebug.fileLogger?.let { lifecycleScope.launch { val targetPath = withContext(Dispatchers.IO) { async { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index 184ec476975eeee93a524b35091a2208054fc6ad..abeedeea482c5d135171cb36b9cd80306fd62db4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -6,14 +6,11 @@ import android.app.Application import android.content.Context import android.content.IntentFilter import android.content.pm.ActivityInfo -import android.net.wifi.WifiManager import android.os.Bundle -import android.os.PowerManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner -import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.Configuration import androidx.work.WorkManager @@ -23,187 +20,115 @@ import dagger.android.HasAndroidInjector import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL import de.rki.coronawarnapp.notification.NotificationHelper -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction import de.rki.coronawarnapp.util.CWADebug -import de.rki.coronawarnapp.util.ConnectivityHelper -import de.rki.coronawarnapp.util.debug.FileLogger +import de.rki.coronawarnapp.util.WatchdogService import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.worker.BackgroundWorkHelper -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler -import kotlinx.coroutines.launch import org.conscrypt.Conscrypt import timber.log.Timber import java.security.Security -import java.util.UUID import javax.inject.Inject -class CoronaWarnApplication : Application(), LifecycleObserver, - Application.ActivityLifecycleCallbacks, HasAndroidInjector { +class CoronaWarnApplication : Application(), HasAndroidInjector { - companion object { - val TAG: String? = CoronaWarnApplication::class.simpleName - private lateinit var instance: CoronaWarnApplication - - /* describes if the app is in foreground - * Initialized to false, because app could also be started by a background job. - * For the cases where the app is started via the launcher icon, the onAppForegrounded - * event will be called, setting it to true - */ - var isAppInForeground = false - - fun getAppContext(): Context = - instance.applicationContext - - const val TEN_MINUTE_TIMEOUT_IN_MS = 10 * 60 * 1000L - var fileLogger: FileLogger? = null - } + @Inject lateinit var component: ApplicationComponent - private lateinit var errorReceiver: ErrorReportReceiver - - @Inject - lateinit var component: ApplicationComponent - - @Inject - lateinit var androidInjector: DispatchingAndroidInjector<Any> + @Inject lateinit var androidInjector: DispatchingAndroidInjector<Any> override fun androidInjector(): AndroidInjector<Any> = androidInjector + @Inject lateinit var watchdogService: WatchdogService + override fun onCreate() { instance = this super.onCreate() - AppInjector.init(this) + CWADebug.init(this) - if (CWADebug.isDebugBuildOrMode) System.setProperty("kotlinx.coroutines.debug", "on") + Timber.v("onCreate(): Initializing Dagger") + AppInjector.init(this) - val configuration = Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.DEBUG) - .build() - WorkManager.initialize(this, configuration) + Timber.v("onCreate(): Initializing WorkManager") + Configuration.Builder() + .apply { setMinimumLoggingLevel(android.util.Log.DEBUG) }.build() + .let { WorkManager.initialize(this, it) } NotificationHelper.createNotificationChannel() + // Enable Conscrypt for TLS1.3 Support below API Level 29 Security.insertProviderAt(Conscrypt.newProvider(), 1) - ProcessLifecycleOwner.get().lifecycle.addObserver(this) - registerActivityLifecycleCallbacks(this) - if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) - } - if ((BuildConfig.FLAVOR == "deviceForTesters" || BuildConfig.DEBUG)) { - fileLogger = FileLogger(this) - } + ProcessLifecycleOwner.get().lifecycle.addObserver(foregroundStateUpdater) + registerActivityLifecycleCallbacks(activityLifecycleCallback) - // notification to test the WakeUpService from Google when the app - // was force stopped + // notification to test the WakeUpService from Google when the app was force stopped BackgroundWorkHelper.sendDebugNotification( "Application onCreate", "App was woken up" ) - // Only do this if the background jobs are enabled - if (ConnectivityHelper.autoModeEnabled(applicationContext)) { - ProcessLifecycleOwner.get().lifecycleScope.launch { - // we want a wakelock as the OS does not handle this for us like in the background - // job execution - val wakeLock = createWakeLock() - // we keep a wifi lock to wake up the wifi connection in case the device is dozing - val wifiLock = createWifiLock() - try { - BackgroundWorkHelper.sendDebugNotification( - "Automatic mode is on", "Check if we have downloaded keys already today" - ) - RetrieveDiagnosisKeysTransaction.startWithConstraints() - } catch (e: Exception) { - BackgroundWorkHelper.sendDebugNotification( - "RetrieveDiagnosisKeysTransaction failed", - (e.localizedMessage - ?: "Unknown exception occurred in onCreate") + "\n\n" + (e.cause - ?: "Cause is unknown").toString() - ) - // retry the key retrieval in case of an error with a scheduled work - BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - } - - if (wifiLock.isHeld) wifiLock.release() - if (wakeLock.isHeld) wakeLock.release() - } + watchdogService.launch() + } + + private val foregroundStateUpdater = object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun onAppForegrounded() { + isAppInForeground = true + Timber.v("App is in the foreground") + } - // if the user is onboarded we will schedule period background jobs - // in case the app was force stopped and woken up again by the Google WakeUpService - if (LocalData.onboardingCompletedTimestamp() != null) BackgroundWorkScheduler.startWorkScheduler() + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun onAppBackgrounded() { + isAppInForeground = false + Timber.v("App is in the background") } } - private fun createWakeLock(): PowerManager.WakeLock = - (getSystemService(Context.POWER_SERVICE) as PowerManager) - .run { - newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, - TAG + "-WAKE-" + UUID.randomUUID().toString() - ).apply { - acquire(TEN_MINUTE_TIMEOUT_IN_MS) - } - } + private val activityLifecycleCallback = object : ActivityLifecycleCallbacks { + private val localBM by lazy { + LocalBroadcastManager.getInstance(this@CoronaWarnApplication) + } + private var errorReceiver: ErrorReportReceiver? = null - private fun createWifiLock(): WifiManager.WifiLock = - (getSystemService(Context.WIFI_SERVICE) as WifiManager) - .run { - createWifiLock( - WifiManager.WIFI_MODE_FULL_HIGH_PERF, - TAG + "-WIFI-" + UUID.randomUUID().toString() - ).apply { - acquire() - } + override fun onActivityPaused(activity: Activity) { + errorReceiver?.let { + localBM.unregisterReceiver(it) + errorReceiver = null } + } - /** - * Callback when the app is open but backgrounded - */ - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - fun onAppBackgrounded() { - isAppInForeground = false - Timber.v("App backgrounded") - } + override fun onActivityStarted(activity: Activity) {} - /** - * Callback when the app is foregrounded - */ - @OnLifecycleEvent(Lifecycle.Event.ON_START) - fun onAppForegrounded() { - isAppInForeground = true - Timber.v("App foregrounded") - } + override fun onActivityDestroyed(activity: Activity) {} - override fun onActivityPaused(activity: Activity) { - // unregisters error receiver - LocalBroadcastManager.getInstance(this).unregisterReceiver(errorReceiver) - } + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - override fun onActivityStarted(activity: Activity) { - // does not override function. Empty on intention - } + override fun onActivityStopped(activity: Activity) {} - override fun onActivityDestroyed(activity: Activity) { - // does not override function. Empty on intention - } + @SuppressLint("SourceLockedOrientationActivity") + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT + } - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { - // does not override function. Empty on intention - } + override fun onActivityResumed(activity: Activity) { + errorReceiver?.let { + localBM.unregisterReceiver(it) + errorReceiver = null + } - override fun onActivityStopped(activity: Activity) { - // does not override function. Empty on intention + errorReceiver = ErrorReportReceiver(activity).also { + localBM.registerReceiver(it, IntentFilter(ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)) + } + } } - @SuppressLint("SourceLockedOrientationActivity") - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - // set screen orientation to portrait - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT - } + companion object { + private lateinit var instance: CoronaWarnApplication + + /* describes if the app is in foreground + * Initialized to false, because app could also be started by a background job. + * For the cases where the app is started via the launcher icon, the onAppForegrounded + * event will be called, setting it to true + */ + var isAppInForeground = false - override fun onActivityResumed(activity: Activity) { - errorReceiver = - ErrorReportReceiver(activity) - LocalBroadcastManager.getInstance(this) - .registerReceiver(errorReceiver, IntentFilter(ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)) + fun getAppContext(): Context = instance.applicationContext } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt index cc614f2797b1f7b66ff248131d4cbc5860716793..4c0373fa49911ef8e3eaac545eeccafa7d61cace 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt @@ -1,8 +1,24 @@ package de.rki.coronawarnapp.util +import android.app.Application import de.rki.coronawarnapp.BuildConfig +import de.rki.coronawarnapp.util.debug.FileLogger +import timber.log.Timber object CWADebug { + var fileLogger: FileLogger? = null + + fun init(application: Application) { + if (isDebugBuildOrMode) System.setProperty("kotlinx.coroutines.debug", "on") + + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + if ((BuildConfig.FLAVOR == "deviceForTesters" || BuildConfig.DEBUG)) { + fileLogger = FileLogger(application) + } + } + val isDebugBuildOrMode: Boolean get() = BuildConfig.DEBUG || BuildConfig.BUILD_VARIANT == "deviceForTesters" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt new file mode 100644 index 0000000000000000000000000000000000000000..d2e5aa97c9fc787f2874d65145e9c203a8457d9b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt @@ -0,0 +1,77 @@ +package de.rki.coronawarnapp.util + +import android.content.Context +import android.net.wifi.WifiManager +import android.os.PowerManager +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.lifecycleScope +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.worker.BackgroundWorkHelper +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class WatchdogService @Inject constructor(private val context: Context) { + + private val powerManager by lazy { + context.getSystemService(Context.POWER_SERVICE) as PowerManager + } + private val wifiManager by lazy { + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + } + + fun launch() { + // Only do this if the background jobs are enabled + if (!ConnectivityHelper.autoModeEnabled(context)) { + Timber.d("Background jobs are not enabled, aborting.") + return + } + + Timber.v("Acquiring wakelocks for watchdog routine.") + ProcessLifecycleOwner.get().lifecycleScope.launch { + // A wakelock as the OS does not handle this for us like in the background job execution + val wakeLock = createWakeLock() + // A wifi lock to wake up the wifi connection in case the device is dozing + val wifiLock = createWifiLock() + try { + BackgroundWorkHelper.sendDebugNotification( + "Automatic mode is on", "Check if we have downloaded keys already today" + ) + RetrieveDiagnosisKeysTransaction.startWithConstraints() + } catch (e: Exception) { + BackgroundWorkHelper.sendDebugNotification( + "RetrieveDiagnosisKeysTransaction failed", + (e.localizedMessage + ?: "Unknown exception occurred in onCreate") + "\n\n" + (e.cause + ?: "Cause is unknown").toString() + ) + // retry the key retrieval in case of an error with a scheduled work + BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() + } + + if (wifiLock.isHeld) wifiLock.release() + if (wakeLock.isHeld) wakeLock.release() + } + + // if the user is onboarded we will schedule period background jobs + // in case the app was force stopped and woken up again by the Google WakeUpService + if (LocalData.onboardingCompletedTimestamp() != null) BackgroundWorkScheduler.startWorkScheduler() + } + + private fun createWakeLock(): PowerManager.WakeLock = powerManager + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CWA-WAKE-${UUID.randomUUID()}") + .apply { acquire(TEN_MINUTE_TIMEOUT_IN_MS) } + + private fun createWifiLock(): WifiManager.WifiLock = wifiManager + .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "CWA-WIFI-${UUID.randomUUID()}") + .apply { acquire() } + + companion object { + private const val TEN_MINUTE_TIMEOUT_IN_MS = 10 * 60 * 1000L + } +}