diff --git a/app/BUILD.bazel b/app/BUILD.bazel index ed984f51f98..c35351582f8 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -558,6 +558,7 @@ kt_android_library( "//third_party:androidx_databinding_databinding-common", "//third_party:androidx_databinding_databinding-runtime", "//utility", + "//utility/src/main/java/org/oppia/android/util/enumfilter:enum_filter_util", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", @@ -677,6 +678,7 @@ kt_android_library( "//third_party:org_jetbrains_kotlin_kotlin-stdlib-jdk8_jar", "//utility", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", + "//utility/src/main/java/org/oppia/android/util/logging:current_app_screen_name_intent_decorator", "//utility/src/main/java/org/oppia/android/util/parser/image:image_loader", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_annonations", "//utility/src/main/java/org/oppia/android/util/profile:current_user_profile_id_intent_decorator", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33f54765791..a0448e4dc6f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/OppiaTheme"> - + + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + tools:node="merge"> + + diff --git a/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt index 2502340e04c..c66744a8780 100644 --- a/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt +++ b/app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt @@ -5,23 +5,29 @@ import android.app.Application import androidx.appcompat.app.AppCompatActivity import androidx.multidex.MultiDexApplication import androidx.work.Configuration -import androidx.work.WorkManager import com.google.firebase.FirebaseApp +import com.google.firebase.appcheck.AppCheckProviderFactory import com.google.firebase.appcheck.FirebaseAppCheck -import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import org.oppia.android.app.activity.ActivityComponent import org.oppia.android.app.activity.ActivityComponentFactory -import org.oppia.android.app.model.BuildFlavor import org.oppia.android.domain.oppialogger.ApplicationStartupListener -import org.oppia.android.util.extensions.safeForEach -/** The root base [Application] of the Oppia app. */ +/** + * The root base [Application] of the Oppia app. + * + * @param createComponentBuilder the [ApplicationComponent.Builder] used to construct the root + * Dagger class for the implementation's flavor of the app + * @property firebaseAppCheckProviderFactory the [AppCheckProviderFactory] used when initializing + * FirebaseAppCheck. This is defaulted for production use cases, but implementations may choose + * to provide a different one for improved debugging support. + */ abstract class AbstractOppiaApplication( - createComponentBuilder: () -> ApplicationComponent.Builder + createComponentBuilder: () -> ApplicationComponent.Builder, + private val firebaseAppCheckProviderFactory: AppCheckProviderFactory = + PlayIntegrityAppCheckProviderFactory.getInstance() ) : MultiDexApplication(), ActivityComponentFactory, ApplicationInjectorProvider, @@ -42,48 +48,40 @@ abstract class AbstractOppiaApplication( override fun onCreate() { super.onCreate() - // Platform parameter initialization must happen very early since even workers can use both - // feature flags and platform parameters. - component.getPlatformParameterController().loadParametersAsync().invokeOnCompletion { e -> - if (e != null) { - throw Exception("Failed to initialize platform parameters.", e) - } + // Allow startup listeners to early initialize. + val startupListeners = component.getApplicationStartupListeners() + startupListeners.forEach(ApplicationStartupListener::onCreateStarted) + + // Initialize high-level third-party systems. Note that WorkManager doesn't need to be + // initialized here because it will automatically initialize itself due to the application being + // a Configuration provider. + FirebaseApp.initializeApp(applicationContext) + // FirebaseAppCheck protects our API resources from abuse. It works with Firebase + // services, Google Cloud services, and can also be implemented for our own APIs. See + // https://firebase.google.com/docs/app-check for currently supported Firebase products. + // Note that as of this code being checked in, only the app's Firestore usage is affected + // by App Check (Analytics is NOT affected). + FirebaseAppCheck.getInstance().installAppCheckProviderFactory(firebaseAppCheckProviderFactory) - // Continue initializing the startup state. Due to the asynchronous nature of platform - // parameter initialization, this happens after onCreate() finishes at the application level. - // This can introduce some inconsistencies in SplashActivity, though by the time - // SplashActivity completes the following should be fully initialized. - CoroutineScope(Dispatchers.Main).async { - FirebaseApp.initializeApp(applicationContext) - // FirebaseAppCheck protects our API resources from abuse. It works with Firebase - // services, Google Cloud services, and can also be implemented for our own APIs. See - // https://firebase.google.com/docs/app-check for currently supported Firebase products. - // Note that as of this code being checked in, only the app's Firestore usage is affected - // by App Check (Analytics is NOT affected). - if (component.getCurrentBuildFlavor() == BuildFlavor.DEVELOPER) { - FirebaseAppCheck.getInstance().installAppCheckProviderFactory( - DebugAppCheckProviderFactory.getInstance(), - ) - } else { - FirebaseAppCheck.getInstance().installAppCheckProviderFactory( - PlayIntegrityAppCheckProviderFactory.getInstance(), - ) - } - WorkManager.initialize(applicationContext, workManagerConfiguration) - val workManager = WorkManager.getInstance(applicationContext) - component.getAnalyticsStartupListenerStartupListeners().safeForEach { - it.onCreate(workManager) - } - component.getApplicationStartupListeners().forEach(ApplicationStartupListener::onCreate) - }.invokeOnCompletion { - if (it != null) { - throw Exception("Failed to continue application initialization.", it) - } + // Kick off a background task to finish startup initialization. Nothing at this stage should be + // startup-state sensitive. It's also fine for parameters to not be fully initialized at this + // point because each app entry point already accounts for potentially uninitialized parameters: + // splash, direct activity recreation, and waking up the app to kick off a worker. + CoroutineScope(component.getBackgroundDispatcher()).async { + // Wait for parameters to load before running any startup routines that may depend on them. + component.getPlatformParameterController().loadParametersAsync().await() + startupListeners.forEach(ApplicationStartupListener::onCompletedInitialization) + }.invokeOnCompletion { e -> + if (e != null) { + // NOTE TO DEVELOPERS: It's normally highly discouraged to throw in invokeOnCompletion + // because it muddies the error propagation state. However this is a case where the app + // cannot safely recover and it should absolutely hard fail because the alternative is a + // potentially inconsistent state. + throw Exception("Failed to finish app initialization.", e) } } } - override fun getWorkManagerConfiguration(): Configuration { - return component.getWorkManagerConfiguration() - } + override fun getWorkManagerConfiguration(): Configuration = + component.getWorkManagerConfiguration() } diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt index 4fc1daefb7d..1c92ad5c6c6 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt @@ -4,9 +4,7 @@ import android.app.Application import androidx.work.Configuration import dagger.BindsInstance import org.oppia.android.app.activity.ActivityComponentImpl -import org.oppia.android.app.model.BuildFlavor import org.oppia.android.domain.oppialogger.ApplicationStartupListener -import org.oppia.android.domain.oppialogger.analytics.AnalyticsStartupListener import javax.inject.Provider /** @@ -28,9 +26,5 @@ interface ApplicationComponent : ApplicationInjector { fun getApplicationStartupListeners(): Set - fun getAnalyticsStartupListenerStartupListeners(): Set - fun getWorkManagerConfiguration(): Configuration - - fun getCurrentBuildFlavor(): BuildFlavor } diff --git a/app/src/main/java/org/oppia/android/app/application/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/BUILD.bazel index 19ef82d522b..5efcf78e8e7 100644 --- a/app/src/main/java/org/oppia/android/app/application/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/application/BUILD.bazel @@ -122,6 +122,7 @@ android_library( "//domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler:metric_log_scheduler_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", "//domain/src/main/java/org/oppia/android/domain/platformparameter:prod_module", + "//domain/src/main/java/org/oppia/android/domain/workmanager:work_manager_configuration_module", "//utility/src/main/java/org/oppia/android/util/accessibility:prod_module", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/caching:caching_prod_module", diff --git a/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt index 9bf42f35695..0bbdfa77b03 100644 --- a/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/alpha/AlphaApplicationComponent.kt @@ -52,7 +52,6 @@ import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.oppia.android.util.logging.SyncStatusModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.logging.firebase.LogReportingModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule @@ -85,7 +84,7 @@ import javax.inject.Singleton RatioInputModule::class, UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, LogReportWorkerModule::class, WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, - FirebaseLogUploaderModule::class, RetrofitModule::class, RetrofitServiceModule::class, + RetrofitModule::class, RetrofitServiceModule::class, PlatformParameterModule::class, PlatformParameterSingletonModule::class, ExplorationStorageModule::class, DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, diff --git a/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt index 669ec60b44e..985cb3dcc58 100644 --- a/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/beta/BetaApplicationComponent.kt @@ -52,7 +52,6 @@ import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.oppia.android.util.logging.SyncStatusModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.logging.firebase.LogReportingModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule @@ -85,7 +84,7 @@ import javax.inject.Singleton RatioInputModule::class, UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, LogReportWorkerModule::class, WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, - FirebaseLogUploaderModule::class, RetrofitModule::class, RetrofitServiceModule::class, + RetrofitModule::class, RetrofitServiceModule::class, PlatformParameterModule::class, PlatformParameterSingletonModule::class, ExplorationStorageModule::class, DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, diff --git a/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel b/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel index 12f288041cd..b6458c216e7 100644 --- a/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/application/dev/BUILD.bazel @@ -23,6 +23,8 @@ kt_android_library( "//app/src/main/java/org/oppia/android/app/application:common_application_modules", "//domain/src/main/java/org/oppia/android/domain/auth:auth_module", "//domain/src/main/java/org/oppia/android/domain/platformparameter:debug_module", + "//domain/src/main/java/org/oppia/android/domain/workmanager/debug:debug_module", + "//third_party:com_google_firebase_firebase-common", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", "//utility/src/main/java/org/oppia/android/util/networking:debug_module", ], diff --git a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt index e7b150bf9df..3cd29477abe 100644 --- a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperApplicationComponent.kt @@ -46,6 +46,7 @@ import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModu import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerModule import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.domain.workmanager.debug.DebugWorkerDebugModule import org.oppia.android.util.accessibility.AccessibilityProdModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CachingModule @@ -54,7 +55,6 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.oppia.android.util.logging.SyncStatusModule import org.oppia.android.util.logging.firebase.DebugLogReportingModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule @@ -87,7 +87,7 @@ import javax.inject.Singleton UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, LogReportWorkerModule::class, WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionDebugModule::class, - FirebaseLogUploaderModule::class, RetrofitModule::class, RetrofitServiceModule::class, + RetrofitModule::class, RetrofitServiceModule::class, PlatformParameterSingletonModule::class, PlatformParameterDebugModule::class, ExplorationStorageModule::class, DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, @@ -101,7 +101,7 @@ import javax.inject.Singleton PerformanceMetricsAssessorModule::class, PerformanceMetricsConfigurationsModule::class, DeveloperBuildFlavorModule::class, CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class, - AuthenticationModule::class + AuthenticationModule::class, DebugWorkerDebugModule::class ] ) interface DeveloperApplicationComponent : ApplicationComponent { diff --git a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt index 495a21d7a3e..66cf4a6b62e 100644 --- a/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt +++ b/app/src/main/java/org/oppia/android/app/application/dev/DeveloperOppiaApplication.kt @@ -1,8 +1,10 @@ package org.oppia.android.app.application.dev +import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory import org.oppia.android.app.application.AbstractOppiaApplication /** The root [AbstractOppiaApplication] for developer builds of the Oppia app. */ class DeveloperOppiaApplication : AbstractOppiaApplication( - DaggerDeveloperApplicationComponent::builder + DaggerDeveloperApplicationComponent::builder, + firebaseAppCheckProviderFactory = DebugAppCheckProviderFactory.getInstance() ) diff --git a/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt index a8306471f90..84aed99b6ab 100644 --- a/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt @@ -52,7 +52,6 @@ import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.oppia.android.util.logging.SyncStatusModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.logging.firebase.LogReportingModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule @@ -85,7 +84,7 @@ import javax.inject.Singleton RatioInputModule::class, UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, LogReportWorkerModule::class, WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, - FirebaseLogUploaderModule::class, RetrofitModule::class, RetrofitServiceModule::class, + RetrofitModule::class, RetrofitServiceModule::class, PlatformParameterModule::class, PlatformParameterSingletonModule::class, ExplorationStorageModule::class, DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConfigProdModule::class, AssetModule::class, diff --git a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt index 3722431d16d..2fed989e2e7 100644 --- a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt @@ -54,11 +54,11 @@ import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.domain.oppialogger.LoggingIdentifierModule -import org.oppia.android.domain.oppialogger.analytics.AnalyticsStartupListener import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.firebase.TestAuthenticationModule @@ -206,9 +206,9 @@ class DateTimeUtilTest { } @Module - interface AnalyticsStartupListenerTestModule { + interface StartupWorkerScheduleReadinessListenerTestModule { @Multibinds - fun provideAnalyticsListenerSet(): Set + fun provideAnalyticsListenerSet(): Set } // TODO(#89): Move this to a common test application component. @@ -219,7 +219,7 @@ class DateTimeUtilTest { ActivityRecreatorTestModule::class, ActivityRouterModule::class, AlgebraicExpressionInputModule::class, - AnalyticsStartupListenerTestModule::class, + StartupWorkerScheduleReadinessListenerTestModule::class, ApplicationLifecycleModule::class, ApplicationModule::class, ApplicationStartupListenerModule::class, diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 307082d92ee..6f7aba82f48 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -127,10 +127,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify:classification_result", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:analytics_startup_listener", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:application_lifecycle_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler:metric_log_scheduling_worker_factory", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_factory", "//domain/src/main/java/org/oppia/android/domain/platformparameter:debug_impl", "//domain/src/main/java/org/oppia/android/domain/platformparameter/syncup", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", @@ -141,6 +139,7 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/util:asset", "//domain/src/main/java/org/oppia/android/domain/util:extensions", "//domain/src/main/java/org/oppia/android/domain/util:retriever", + "//domain/src/main/java/org/oppia/android/domain/workmanager:startup_worker_schedule_readiness_listener", "//model/src/main/proto:exploration_checkpoint_java_proto_lite", "//model/src/main/proto:onboarding_java_proto_lite", "//model/src/main/proto:platform_parameter_java_proto_lite", @@ -153,7 +152,6 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", - "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", @@ -203,15 +201,12 @@ TEST_DEPS = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger:prod_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:cpu_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing:fake_log_scheduler", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler:metric_log_scheduler_module", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_factory", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", "//domain/src/main/java/org/oppia/android/domain/platformparameter:debug_module", "//domain/src/main/java/org/oppia/android/domain/spotlight:spotlight_state_controller", - "//domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader:fake_log_uploader", "//model/src/main/proto:spotlight_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/ApplicationStartupListener.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/ApplicationStartupListener.kt index 1a0b2c91411..02202d262e2 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/ApplicationStartupListener.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/ApplicationStartupListener.kt @@ -1,8 +1,53 @@ package org.oppia.android.domain.oppialogger -/** Listener that gets created at application startup. */ +/** + * Monitors the creation lifecycle of the app. + * + * NOTE TO DEVELOPERS: Implementations of this listener run *very* early in the app lifecycle, even + * before platform parameters have been fully initialized. Extreme care must be taken to ensure that + * all injections are either injected as `Provider`s or do not transitively depend on platform + * parameters. Further restrictions about these types of dependencies are covered in the method + * documentation below. + * + * Also, note that the two lifecycle callback methods below are deliberately called on distinct + * thread types (main/background) so care must be taken in implementations to account for this. The + * two methods will never race each other, and are both guaranteed to be called in order and exactly + * once for the lifetime of the application. + */ interface ApplicationStartupListener { - /** Gets called at application creation. */ - fun onCreate() + /** + * Called immediately upon application start. + * + * This function must not interact with any other components that may require state initialization + * in the app, including analytics, `WorkManager`, and especially platform parameters. + * Implementations can use this to perform critical startup self state initialization. If any + * persistence or broad system interaction is needed during startup then that should happen in + * [onCompletedInitialization]. + * + * Note: This is guaranteed to always be called before any other entrypoint logic executes + * including `SplashActivity`, direct activity recreation, and waking up the app to start a + * background worker. + * + * Important: This will be called on the main thread and should never block. + */ + fun onCreateStarted() + + /** + * Called when the application has been initialized sufficiently well to now be safe for broad + * system interaction. + * + * Most critically this will be called after platform parameters have fully initialized making + * broad app system interaction safe. + * + * Note: This may get called after other entry points are called (such as `SplashActivity`, direct + * activity creation, and background worker interaction), so care needs to be taken if this + * listener influences entry point state (it's highly recommended to avoid that if possible). + * + * Most startup initialization logic is expected to go in the implementation of this function to + * help ensure general startup safety. + * + * Important: This will be called on a background thread, NOT the main thread. + */ + fun onCompletedInitialization() } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index ea208554ded..2e7c76e8d98 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -29,9 +29,7 @@ kt_android_library( kt_android_library( name = "startup_listener", - srcs = [ - "ApplicationStartupListener.kt", - ], + srcs = ["ApplicationStartupListener.kt"], visibility = ["//:oppia_api_visibility"], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsStartupListener.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsStartupListener.kt deleted file mode 100644 index b80868a13d7..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsStartupListener.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.oppia.android.domain.oppialogger.analytics - -import androidx.work.WorkManager - -/** - * Analytics-specific application startup listener that receives an instance of [WorkManager] to - * perform analytics-specific initialization. - */ -interface AnalyticsStartupListener { - /** - * Called on application creation with the singleton, application-wide [WorkManager] that should - * be used for scheduling background analytics tasks. - */ - fun onCreate(workManager: WorkManager) -} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleListener.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleListener.kt index 5cc55000885..e5484dd12e9 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleListener.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleListener.kt @@ -1,6 +1,16 @@ package org.oppia.android.domain.oppialogger.analytics -/** Listener for when the app goes to the foreground or the background. */ +/** + * Listener for when the app goes to the foreground or the background. + * + * Note that implementations may not receive initial signals of app switches due to there being a + * slight delay in app startup between monitoring for foreground/background switches and actually + * reporting them. No switches will be missed but they may not arrive until other entry points in + * the app have already made execution progress (such as the base activity classes). + * + * Care should be taken when using implementations of this listener to synchronize initialization + * state. + */ interface ApplicationLifecycleListener { /** Fired when the app comes to the foreground. */ fun onAppInForeground() diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleLogger.kt new file mode 100644 index 00000000000..7f8cee99220 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleLogger.kt @@ -0,0 +1,217 @@ +package org.oppia.android.domain.oppialogger.analytics + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.ScreenName +import org.oppia.android.app.model.ScreenName.BACKGROUND_SCREEN +import org.oppia.android.app.model.ScreenName.FOREGROUND_SCREEN +import org.oppia.android.domain.oppialogger.LoggingIdentifierController +import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.AppIconification.APP_IN_BACKGROUND +import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.AppIconification.APP_IN_FOREGROUND +import org.oppia.android.util.platformparameter.EnablePerformanceMetricsCollection +import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.threading.BackgroundDispatcher +import javax.inject.Inject +import javax.inject.Singleton + +/** Observer that observes application and activity lifecycle. */ +@Singleton +class ApplicationLifecycleLogger @Inject constructor( + private val loggingIdentifierController: LoggingIdentifierController, + private val learnerAnalyticsLogger: LearnerAnalyticsLogger, + private val profileManagementController: ProfileManagementController, + private val oppiaLogger: OppiaLogger, + private val performanceMetricsLogger: PerformanceMetricsLogger, + private val featureFlagsLogger: FeatureFlagsLogger, + private val performanceMetricsController: PerformanceMetricsController, + private val cpuPerformanceSnapshotter: CpuPerformanceSnapshotter, + @LearnerAnalyticsInactivityLimitMillis private val inactivityLimitMillis: Long, + @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher, + @EnablePerformanceMetricsCollection + private val enablePerformanceMetricsCollection: PlatformParameterValue, + private val analyticsController: AnalyticsController +) { + private var isStartupLatencyLogged: Boolean = false + private var currentScreen: ScreenName = ScreenName.SCREEN_NAME_UNSPECIFIED + + /** + * Returns the current active UI screen that's visible to the user. + * + * A few exceptions: + * [BACKGROUND_SCREEN] is returned when the UI is inactive or when the app is backgrounded. + * [FOREGROUND_SCREEN] is never returned. + * [SCREEN_NAME_UNSPECIFIED] is the default value for [currentScreen] and is returned until a + * currentScreen value has been set by the launcher activity's onResume method. + */ + fun getCurrentScreen(): ScreenName = currentScreen + + fun recordAppOpened(appStartTimeMillis: Long) { + logApplicationStartupMetrics(appStartTimeMillis) + logAllFeatureFlags(appStartTimeMillis) + cpuPerformanceSnapshotter.initialiseSnapshotter() + + analyticsController.listenForConsoleErrorLogs() + analyticsController.listenForNetworkCallLogs() + analyticsController.listenForFailedNetworkCallLogs() + } + + fun recordAppInForeground(timestamp: Long) { + val timeSpentInBackgroundMs = ForegroundBackgroundRecordKeeper.recordAppForegrounded(timestamp) + if (timeSpentInBackgroundMs > inactivityLimitMillis) { + loggingIdentifierController.updateSessionId() + } + if (enablePerformanceMetricsCollection.value) { + cpuPerformanceSnapshotter.updateAppIconification(APP_IN_FOREGROUND) + } + performanceMetricsController.setAppInForeground() + logAppLifecycleEventInBackground(learnerAnalyticsLogger::logAppInForeground, timestamp) + } + + fun recordAppInBackground(timestamp: Long) { + val timeSpentInForegroundMs = ForegroundBackgroundRecordKeeper.recordAppBackgrounded(timestamp) + if (enablePerformanceMetricsCollection.value) { + cpuPerformanceSnapshotter.updateAppIconification(APP_IN_BACKGROUND) + } + performanceMetricsController.setAppInBackground() + logAppLifecycleEventInBackground(learnerAnalyticsLogger::logAppInBackground, timestamp) + logAppInForegroundTime(timeSpentInForegroundMs, timestamp) + } + + fun recordActivityResumed( + currentActivityScreen: ScreenName, + appStartTimeMillis: Long, + timestamp: Long + ) { + currentScreen = currentActivityScreen + if (!isStartupLatencyLogged) { + performanceMetricsLogger.logStartupLatency( + timestamp - appStartTimeMillis, + currentScreen, + timestamp + ) + isStartupLatencyLogged = true + } + performanceMetricsLogger.logMemoryUsage(currentScreen, timestamp) + } + + fun recordActivityPaused() { + currentScreen = BACKGROUND_SCREEN + } + + private fun logAppLifecycleEventInBackground( + logMethod: (String?, ProfileId?, String?, Long) -> Unit, + timestamp: Long + ) { + CoroutineScope(backgroundDispatcher).launch { + val installationId = loggingIdentifierController.fetchInstallationId() + val profileId = profileManagementController.getCurrentProfileId() + val learnerId = profileManagementController.fetchCurrentLearnerId() + logMethod(installationId, profileId, learnerId, timestamp) + }.invokeOnCompletion { failure -> + if (failure != null) { + oppiaLogger.e( + "ApplicationLifecycleLogger", + "Encountered error while trying to log app lifecycle event.", + failure + ) + } + } + } + + private fun logApplicationStartupMetrics(timestamp: Long) { + CoroutineScope(backgroundDispatcher).launch { + performanceMetricsLogger.logApkSize(currentScreen, timestamp) + performanceMetricsLogger.logStorageUsage(currentScreen, timestamp) + }.invokeOnCompletion { failure -> + if (failure != null) { + oppiaLogger.e( + "ActivityLifecycleObserver", + "Encountered error while trying to log app's performance metrics.", + failure + ) + } + } + } + + private fun logAllFeatureFlags(timestamp: Long) { + CoroutineScope(backgroundDispatcher).launch { + // TODO(#5341): Replace appSessionId generation to the modified Twitter snowflake algorithm. + val appSessionId = loggingIdentifierController.getAppSessionIdFlow().value + featureFlagsLogger.logAllFeatureFlags(appSessionId, timestamp) + }.invokeOnCompletion { failure -> + if (failure != null) { + oppiaLogger.e( + "ActivityLifecycleObserver", + "Encountered error while logging feature flags.", + failure + ) + } + } + } + + private fun logAppInForegroundTime(timeSpentInForegroundMs: Long, timestamp: Long) { + CoroutineScope(backgroundDispatcher).launch { + val sessionId = loggingIdentifierController.getSessionIdFlow().value + val installationId = loggingIdentifierController.fetchInstallationId() + analyticsController.logLowPriorityEvent( + oppiaLogger.createAppInForegroundTimeContext( + installationId = installationId, + appSessionId = sessionId, + foregroundTime = timeSpentInForegroundMs + ), + profileId = null, + timestamp + ) + }.invokeOnCompletion { failure -> + if (failure != null) { + oppiaLogger.e( + "ApplicationLifecycleLogger", + "Encountered error while trying to log app's time in the foreground.", + failure + ) + } + } + } + + private object ForegroundBackgroundRecordKeeper { + private var timestampSinceLastChange: Long? = null + private var currentState: State = State.BACKGROUND // Apps always begin in the background. + + fun recordAppForegrounded(timestamp: Long): Long { + val lastTimestamp = timestampSinceLastChange + return when (currentState) { + State.FOREGROUND -> error("App is already thought to be in the foreground.") + // A null timestamp means the app just opened so don't return any duration since it's not + // an explicit delay of the user backgrounding the app. + State.BACKGROUND -> if (lastTimestamp != null) timestamp - lastTimestamp else 0L + }.also { + timestampSinceLastChange = timestamp + currentState = State.FOREGROUND + } + } + + fun recordAppBackgrounded(timestamp: Long): Long { + val lastTimestamp = timestampSinceLastChange + return when (currentState) { + State.FOREGROUND -> { + if (lastTimestamp != null) { + timestamp - lastTimestamp + } else error("App can't be foregrounded without a time record.") + } + State.BACKGROUND -> error("App is already thought to be in the background.") + }.also { + timestampSinceLastChange = timestamp + currentState = State.BACKGROUND + } + } + + private enum class State { + FOREGROUND, + BACKGROUND + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt index 24eaf81f248..5fc59d8948a 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/ApplicationLifecycleObserver.kt @@ -9,221 +9,163 @@ import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.oppia.android.app.model.ProfileId +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.actor import org.oppia.android.app.model.ScreenName -import org.oppia.android.app.model.ScreenName.BACKGROUND_SCREEN -import org.oppia.android.app.model.ScreenName.FOREGROUND_SCREEN import org.oppia.android.domain.oppialogger.ApplicationStartupListener -import org.oppia.android.domain.oppialogger.LoggingIdentifierController -import org.oppia.android.domain.oppialogger.OppiaLogger -import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName -import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.AppIconification.APP_IN_BACKGROUND -import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.AppIconification.APP_IN_FOREGROUND -import org.oppia.android.util.platformparameter.EnablePerformanceMetricsCollection -import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.system.OppiaClock import org.oppia.android.util.threading.BackgroundDispatcher import javax.inject.Inject +import javax.inject.Provider import javax.inject.Singleton /** Observer that observes application and activity lifecycle. */ @Singleton +@OptIn(ObsoleteCoroutinesApi::class) class ApplicationLifecycleObserver @Inject constructor( private val application: Application, private val oppiaClock: OppiaClock, - private val loggingIdentifierController: LoggingIdentifierController, - private val learnerAnalyticsLogger: LearnerAnalyticsLogger, - private val profileManagementController: ProfileManagementController, - private val oppiaLogger: OppiaLogger, - private val performanceMetricsLogger: PerformanceMetricsLogger, - private val featureFlagsLogger: FeatureFlagsLogger, - private val performanceMetricsController: PerformanceMetricsController, - private val cpuPerformanceSnapshotter: CpuPerformanceSnapshotter, - @LearnerAnalyticsInactivityLimitMillis private val inactivityLimitMillis: Long, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher, - @EnablePerformanceMetricsCollection - private val enablePerformanceMetricsCollection: PlatformParameterValue, - private val analyticsController: AnalyticsController, - private val applicationLifecycleListeners: Set<@JvmSuppressWildcards ApplicationLifecycleListener> + @BackgroundDispatcher private val backgroundCoroutineDispatcher: CoroutineDispatcher, + private val applicationLifecycleLoggerProvider: Provider, + private val listenersProvider: Provider> ) : ApplicationStartupListener, LifecycleObserver, Application.ActivityLifecycleCallbacks { - - /** - * Timestamp indicating the time of application start-up. It will be used to calculate the - * cold-startup latency of the application. - * - * We're using a large Long value such that the time difference based on any timestamp will be - * negative and thus ignored until the app records initial time during [onCreate]. - */ - private var appStartTimeMillis: Long = Long.MAX_VALUE - - /** - * Returns a boolean flag that makes sure that startup latency is logged only once in the entire - * application lifecycle. - */ - private var isStartupLatencyLogged: Boolean = false - - private var currentScreen: ScreenName = ScreenName.SCREEN_NAME_UNSPECIFIED - - /** - * Returns the current active UI screen that's visible to the user. - * - * A few exceptions: - * [BACKGROUND_SCREEN] is returned when the UI is inactive or when the app is backgrounded. - * [FOREGROUND_SCREEN] is never returned. - * [SCREEN_NAME_UNSPECIFIED] is the default value for [currentScreen] and is returned until a - * currentScreen value has been set by the launcher activity's onResume method. - */ - fun getCurrentScreen(): ScreenName = currentScreen - - /** Returns the time in millis at which the application started. */ - fun getAppStartupTimeMs(): Long = appStartTimeMillis - - override fun onCreate() { - appStartTimeMillis = oppiaClock.getCurrentTimeMs() + private lateinit var commandQueue: SendChannel + + override fun onCreateStarted() { + // Create the command queue so that lifecycle messages can be recorded without loss, then start + // listening for them. Messages cannot be processed immediately since logging requirements and + // other listener callbacks may require dependencies not yet available this early in the app + // lifecycle. + commandQueue = createObserverCommandActor(appStartupTimeMillis = oppiaClock.getCurrentTimeMs()) ProcessLifecycleOwner.get().lifecycle.addObserver(this) application.registerActivityLifecycleCallbacks(this) - logApplicationStartupMetrics() - logAllFeatureFlags() - cpuPerformanceSnapshotter.initialiseSnapshotter() } - // Use a large Long value such that the time difference based on any timestamp will be negative - // and thus ignored until the app goes into the background at least once. - private var firstTimestamp: Long = Long.MAX_VALUE + override fun onCompletedInitialization() { + // The app is fully ready to go so start enable full logging and process any previously missed + // commands. + enqueueMessage(LifecycleChangeMessage::Initialize) + } - /** Occurs when application comes to foreground. */ @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onAppInForeground() { - applicationLifecycleListeners.forEach(ApplicationLifecycleListener::onAppInForeground) - val timeDifferenceMs = oppiaClock.getCurrentTimeMs() - firstTimestamp - if (timeDifferenceMs > inactivityLimitMillis) { - loggingIdentifierController.updateSessionId() - } - if (enablePerformanceMetricsCollection.value) { - cpuPerformanceSnapshotter.updateAppIconification(APP_IN_FOREGROUND) - } - performanceMetricsController.setAppInForeground() - logAppLifecycleEventInBackground(learnerAnalyticsLogger::logAppInForeground) - - analyticsController.listenForConsoleErrorLogs() - analyticsController.listenForNetworkCallLogs() - analyticsController.listenForFailedNetworkCallLogs() + enqueueMessage(LifecycleChangeMessage::AppInForeground) } - /** Occurs when application goes to background. */ @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun onAppInBackground() { - applicationLifecycleListeners.forEach(ApplicationLifecycleListener::onAppInBackground) - firstTimestamp = oppiaClock.getCurrentTimeMs() - if (enablePerformanceMetricsCollection.value) { - cpuPerformanceSnapshotter.updateAppIconification(APP_IN_BACKGROUND) - } - performanceMetricsController.setAppInBackground() - logAppLifecycleEventInBackground(learnerAnalyticsLogger::logAppInBackground) - - logAppInForegroundTime() + enqueueMessage(LifecycleChangeMessage::AppInBackground) } override fun onActivityResumed(activity: Activity) { - currentScreen = activity.intent.extractCurrentAppScreenName() - if (!isStartupLatencyLogged) { - performanceMetricsLogger.logStartupLatency( - getStartupLatencyMillis(appStartTimeMillis), - currentScreen - ) - isStartupLatencyLogged = true - } - performanceMetricsLogger.logMemoryUsage(currentScreen) + val currentScreen = activity.intent.extractCurrentAppScreenName() + enqueueMessage { time -> LifecycleChangeMessage.ActivityResumed(time, currentScreen) } } override fun onActivityPaused(activity: Activity) { - currentScreen = BACKGROUND_SCREEN + enqueueMessage(LifecycleChangeMessage::ActivityPaused) } - private fun logAppLifecycleEventInBackground(logMethod: (String?, ProfileId?, String?) -> Unit) { - CoroutineScope(backgroundDispatcher).launch { - val installationId = loggingIdentifierController.fetchInstallationId() - val profileId = profileManagementController.getCurrentProfileId() - val learnerId = profileManagementController.fetchCurrentLearnerId() - logMethod(installationId, profileId, learnerId) - }.invokeOnCompletion { failure -> - if (failure != null) { - oppiaLogger.e( - "ApplicationLifecycleObserver", - "Encountered error while trying to log app lifecycle event.", - failure - ) + override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} + + override fun onActivityDestroyed(activity: Activity) {} + + private fun createObserverCommandActor( + appStartupTimeMillis: Long + ): SendChannel { + lateinit var state: ObserverState + var replayBuffer: MutableSet? = mutableSetOf() + return CoroutineScope(backgroundCoroutineDispatcher).actor(capacity = Channel.UNLIMITED) { + for (message in channel) { + // First check this is a case of a post-initialization message coming in before + // initialization. That's supported by this command queue since it will store them in a + // replay buffer. + val buffer = replayBuffer + when { + message is LifecycleChangeMessage.Initialize -> { + checkNotNull(buffer) { "Attempting to re-initialize command queue." } + + // It's now safe to fetch all dependencies for processing. + state = ObserverState( + appStartupTimeMillis, + applicationLifecycleLoggerProvider.get(), + listenersProvider.get() + ) + + // Process any other messages that came in. + state.processMessage(message) + for (it in buffer) { + check(it !is LifecycleChangeMessage.Initialize) { + "Attempting to re-initialize command queue." + } + state.processMessage(it) + } + buffer.clear() + replayBuffer = null + } + buffer != null -> buffer += message // Queue the message for later. + else -> state.processMessage(message) // Otherwise it can be processed immediately. + } } } } - private fun logApplicationStartupMetrics() { - CoroutineScope(backgroundDispatcher).launch { - performanceMetricsLogger.logApkSize(currentScreen) - performanceMetricsLogger.logStorageUsage(currentScreen) - }.invokeOnCompletion { failure -> - if (failure != null) { - oppiaLogger.e( - "ActivityLifecycleObserver", - "Encountered error while trying to log app's performance metrics.", - failure - ) - } + private fun enqueueMessage(factory: (Long) -> LifecycleChangeMessage) { + // Failures to enqueue lifecycle changes could be catastrophic to internal app state so it's + // almost certainly better to crash than try to recover. It's also expected that such a failure + // should be impossible since the queue is configured to be unlimited. + check(commandQueue.trySend(factory(oppiaClock.getCurrentTimeMs())).isSuccess) { + "Failed to enqueue command to capture lifecycle change." } } - private fun logAllFeatureFlags() { - CoroutineScope(backgroundDispatcher).launch { - // TODO(#5341): Replace appSessionId generation to the modified Twitter snowflake algorithm. - val appSessionId = loggingIdentifierController.getAppSessionIdFlow().value - featureFlagsLogger.logAllFeatureFlags(appSessionId) - }.invokeOnCompletion { failure -> - if (failure != null) { - oppiaLogger.e( - "ActivityLifecycleObserver", - "Encountered error while logging feature flags.", - failure - ) - } - } + private sealed class LifecycleChangeMessage { + abstract val timestamp: Long + + data class Initialize(override val timestamp: Long) : LifecycleChangeMessage() + data class AppInForeground(override val timestamp: Long) : LifecycleChangeMessage() + data class AppInBackground(override val timestamp: Long) : LifecycleChangeMessage() + data class ActivityResumed( + override val timestamp: Long, + val activityScreen: ScreenName + ) : LifecycleChangeMessage() + data class ActivityPaused(override val timestamp: Long) : LifecycleChangeMessage() } - private fun logAppInForegroundTime() { - CoroutineScope(backgroundDispatcher).launch { - val sessionId = loggingIdentifierController.getSessionIdFlow().value - val installationId = loggingIdentifierController.fetchInstallationId() - val timeInForeground = oppiaClock.getCurrentTimeMs() - appStartTimeMillis - analyticsController.logLowPriorityEvent( - oppiaLogger.createAppInForegroundTimeContext( - installationId = installationId, - appSessionId = sessionId, - foregroundTime = timeInForeground - ), - profileId = null - ) - }.invokeOnCompletion { failure -> - if (failure != null) { - oppiaLogger.e( - "ApplicationLifecycleObserver", - "Encountered error while trying to log app's time in the foreground.", - failure - ) + private class ObserverState( + val appStartupTimeMillis: Long, + val applicationLifecycleLogger: ApplicationLifecycleLogger, + val applicationLifecycleListeners: Set + ) { + fun processMessage(message: LifecycleChangeMessage) { + when (message) { + is LifecycleChangeMessage.Initialize -> + applicationLifecycleLogger.recordAppOpened(appStartupTimeMillis) + is LifecycleChangeMessage.AppInForeground -> { + applicationLifecycleListeners.forEach(ApplicationLifecycleListener::onAppInForeground) + applicationLifecycleLogger.recordAppInForeground(message.timestamp) + } + is LifecycleChangeMessage.AppInBackground -> { + applicationLifecycleListeners.forEach(ApplicationLifecycleListener::onAppInBackground) + applicationLifecycleLogger.recordAppInBackground(message.timestamp) + } + is LifecycleChangeMessage.ActivityResumed -> { + applicationLifecycleLogger.recordActivityResumed( + message.activityScreen, appStartupTimeMillis, message.timestamp + ) + } + is LifecycleChangeMessage.ActivityPaused -> + applicationLifecycleLogger.recordActivityPaused() } } } - - private fun getStartupLatencyMillis(initialTimestampMillis: Long): Long = - oppiaClock.getCurrentTimeMs() - initialTimestampMillis - - override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} - - override fun onActivityStarted(activity: Activity) {} - - override fun onActivityStopped(activity: Activity) {} - - override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} - - override fun onActivityDestroyed(activity: Activity) {} } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel index 733d6558259..4c0d677c9e6 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel @@ -4,17 +4,6 @@ Library for providing logging analytics to the Oppia android app. load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") -kt_android_library( - name = "analytics_startup_listener", - srcs = [ - "AnalyticsStartupListener.kt", - ], - visibility = ["//:oppia_api_visibility"], - deps = [ - "//third_party:androidx_work_work-runtime-ktx", - ], -) - kt_android_library( name = "controller", srcs = [ @@ -114,22 +103,29 @@ kt_android_library( ) kt_android_library( - name = "application_lifecycle_observer", - srcs = [ - "ApplicationLifecycleObserver.kt", - ], + name = "application_lifecycle_logger", + srcs = ["ApplicationLifecycleLogger.kt"], visibility = ["//:oppia_api_visibility"], deps = [ - ":application_lifecycle_listener", ":cpu_performance_snapshotter", ":feature_flags_logger", ":learner_analytics_inactivity_limit_millis", ":performance_metrics_controller", "//domain/src/main/java/org/oppia/android/domain/oppialogger:logging_identifier_controller", - "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:performance_metrics_logger", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", "//model/src/main/proto:screens_java_proto_lite", + ], +) + +kt_android_library( + name = "application_lifecycle_observer", + srcs = ["ApplicationLifecycleObserver.kt"], + deps = [ + ":application_lifecycle_listener", + ":application_lifecycle_logger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + "//model/src/main/proto:screens_java_proto_lite", "//third_party:androidx_lifecycle_lifecycle-extensions", "//utility/src/main/java/org/oppia/android/util/logging:current_app_screen_name_intent_decorator", "//utility/src/main/java/org/oppia/android/util/system:oppia_clock", @@ -187,9 +183,7 @@ kt_android_library( kt_android_library( name = "application_lifecycle_listener", - srcs = [ - "ApplicationLifecycleListener.kt", - ], + srcs = ["ApplicationLifecycleListener.kt"], visibility = ["//:oppia_api_visibility"], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotter.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotter.kt index 7bf3d657058..97297704ee2 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotter.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotter.kt @@ -20,20 +20,26 @@ import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAsses import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.AppIconification.APP_IN_FOREGROUND import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.AppIconification.UNINITIALIZED import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor.CpuSnapshot +import org.oppia.android.util.system.OppiaClock +import org.oppia.android.util.threading.BackgroundDispatcher +import javax.inject.Inject +import javax.inject.Singleton /** * Snapshotter that gracefully and sequentially logs CPU usage across foreground and background * moves of the application. */ -class CpuPerformanceSnapshotter( - private val backgroundCoroutineDispatcher: CoroutineDispatcher, +@Singleton +class CpuPerformanceSnapshotter @Inject constructor( + @BackgroundDispatcher private val backgroundCoroutineDispatcher: CoroutineDispatcher, private val performanceMetricsLogger: PerformanceMetricsLogger, private val consoleLogger: ConsoleLogger, private val exceptionLogger: ExceptionLogger, + private val oppiaClock: OppiaClock, private val performanceMetricsAssessor: PerformanceMetricsAssessor, - private val foregroundCpuLoggingTimePeriodMillis: Long, - private val backgroundCpuLoggingTimePeriodMillis: Long, - private val initialIconificationCutOffTimePeriodMillis: Long + @ForegroundCpuLoggingTimePeriodMillis private val foregroundCpuLoggingTimePeriodMillis: Long, + @BackgroundCpuLoggingTimePeriodMillis private val backgroundCpuLoggingTimePeriodMillis: Long, + @InitialIconificationCutOffTimePeriodMillis private val initialIconCutOffTimePeriodMillis: Long ) { private var currentIconification = UNINITIALIZED @@ -52,7 +58,7 @@ class CpuPerformanceSnapshotter( } isSnapshotterInitialized = true CoroutineScope(backgroundCoroutineDispatcher).launch { - delay(initialIconificationCutOffTimePeriodMillis) + delay(initialIconCutOffTimePeriodMillis) if (currentIconification == UNINITIALIZED) { sendSwitchIconificationCommand(APP_IN_BACKGROUND) } @@ -117,7 +123,8 @@ class CpuPerformanceSnapshotter( is CommandMessage.LogSnapshotDiff -> { performanceMetricsLogger.logCpuUsage( message.screenName, - message.relativeCpuUsage + message.relativeCpuUsage, + oppiaClock.getCurrentTimeMs() ) } } @@ -227,6 +234,6 @@ class CpuPerformanceSnapshotter( private fun AppIconification.getDelay(): Long = when (this) { APP_IN_BACKGROUND -> backgroundCpuLoggingTimePeriodMillis APP_IN_FOREGROUND -> foregroundCpuLoggingTimePeriodMillis - UNINITIALIZED -> initialIconificationCutOffTimePeriodMillis + UNINITIALIZED -> initialIconCutOffTimePeriodMillis } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotterModule.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotterModule.kt index feb8d01eddc..db868d09240 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotterModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/CpuPerformanceSnapshotterModule.kt @@ -2,40 +2,11 @@ package org.oppia.android.domain.oppialogger.analytics import dagger.Module import dagger.Provides -import kotlinx.coroutines.CoroutineDispatcher -import org.oppia.android.util.logging.ConsoleLogger -import org.oppia.android.util.logging.ExceptionLogger -import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor -import org.oppia.android.util.threading.BackgroundDispatcher import java.util.concurrent.TimeUnit -import javax.inject.Singleton /** Provides dependencies that are needed for logging CPU usage. */ @Module class CpuPerformanceSnapshotterModule { - - @Singleton - @Provides - fun providesCpuPerformanceSnapshotter( - performanceMetricsLogger: PerformanceMetricsLogger, - consoleLogger: ConsoleLogger, - exceptionLogger: ExceptionLogger, - performanceMetricsAssessor: PerformanceMetricsAssessor, - @BackgroundDispatcher backgroundCoroutineDispatcher: CoroutineDispatcher, - @ForegroundCpuLoggingTimePeriodMillis foregroundCpuLoggingTimePeriodMillis: Long, - @BackgroundCpuLoggingTimePeriodMillis backgroundCpuLoggingTimePeriodMillis: Long, - @InitialIconificationCutOffTimePeriodMillis initialIconificationCutOffTimePeriodMillis: Long - ): CpuPerformanceSnapshotter = CpuPerformanceSnapshotter( - backgroundCoroutineDispatcher, - performanceMetricsLogger, - consoleLogger, - exceptionLogger, - performanceMetricsAssessor, - foregroundCpuLoggingTimePeriodMillis, - backgroundCpuLoggingTimePeriodMillis, - initialIconificationCutOffTimePeriodMillis - ) - @Provides @ForegroundCpuLoggingTimePeriodMillis fun provideForegroundCpuLoggingTimePeriodMillis(): Long = TimeUnit.MINUTES.toMillis(5) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt index f5320bcf1b1..d2d53668e98 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/FeatureFlagsLogger.kt @@ -46,8 +46,9 @@ class FeatureFlagsLogger @Inject constructor( * loaded (i.e. not too early in the application lifecycle). * * @param appSessionId denotes the id of the current appInForeground session + * @param timestamp the timestamp to associate with the log */ - fun logAllFeatureFlags(appSessionId: String) { + fun logAllFeatureFlags(appSessionId: String, timestamp: Long) { // TODO(#5341): Set the UUID value for this context val featureFlagContext = FeatureFlagListContext.newBuilder() .setAppSessionId(appSessionId) @@ -58,7 +59,8 @@ class FeatureFlagsLogger @Inject constructor( EventLog.Context.newBuilder() .setFeatureFlagListContext(featureFlagContext) .build(), - profileId = null + profileId = null, + timestamp ) } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt index 4c7d672cc9b..9cf87261e14 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt @@ -22,6 +22,7 @@ import org.oppia.android.domain.oppialogger.LoggingIdentifierController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.oppialogger.analytics.LearnerAnalyticsLogger.BaseLogger.Companion.maybeLogEvent import org.oppia.android.util.math.toAnswerString +import org.oppia.android.util.system.OppiaClock import javax.inject.Inject import javax.inject.Singleton import org.oppia.android.app.model.EventLog.Context as EventContext @@ -42,7 +43,8 @@ import org.oppia.android.app.model.EventLog.Context.Builder as EventBuilder class LearnerAnalyticsLogger @Inject constructor( private val oppiaLogger: OppiaLogger, private val analyticsController: AnalyticsController, - private val loggingIdentifierController: LoggingIdentifierController + private val loggingIdentifierController: LoggingIdentifierController, + private val oppiaClock: OppiaClock ) { /** * The [ExplorationAnalyticsLogger] corresponding to the current play session, or ``null`` if @@ -91,7 +93,8 @@ class LearnerAnalyticsLogger @Inject constructor( exploration.version, oppiaLogger, analyticsController, - loggingIdentifierController + loggingIdentifierController, + oppiaClock ).also { if (!mutableExpAnalyticsLogger.compareAndSet(expect = null, update = it)) { oppiaLogger.w( @@ -126,14 +129,21 @@ class LearnerAnalyticsLogger @Inject constructor( * @param profileId the ID of the Oppia profile currently logged in, or null if none * @param learnerId the personal profile/learner ID corresponding to the new session learner, or * null if not known (which may impact whether the event is logged) + * @param timestamp the timestamp to associate with this log */ - fun logAppInBackground(installationId: String?, profileId: ProfileId?, learnerId: String?) { + fun logAppInBackground( + installationId: String?, + profileId: ProfileId?, + learnerId: String?, + timestamp: Long + ) { val learnerDetailsContext = createLearnerDetailsContextWithIdsPresent(installationId, learnerId) analyticsController.maybeLogEvent( installationId, createAnalyticsEvent(learnerDetailsContext, EventBuilder::setAppInBackgroundContext), profileId, - oppiaLogger + oppiaLogger, + timestamp ) } @@ -145,14 +155,21 @@ class LearnerAnalyticsLogger @Inject constructor( * @param profileId the ID of the Oppia profile currently logged in, or null if none * @param learnerId the personal profile/learner ID corresponding to the new session learner, or * null if not known (which may impact whether the event is logged) + * @param timestamp the timestamp to associate with this log */ - fun logAppInForeground(installationId: String?, profileId: ProfileId?, learnerId: String?) { + fun logAppInForeground( + installationId: String?, + profileId: ProfileId?, + learnerId: String?, + timestamp: Long + ) { val learnerDetailsContext = createLearnerDetailsContextWithIdsPresent(installationId, learnerId) analyticsController.maybeLogEvent( installationId, createAnalyticsEvent(learnerDetailsContext, EventBuilder::setAppInForegroundContext), profileId, - oppiaLogger + oppiaLogger, + timestamp ) } @@ -173,7 +190,8 @@ class LearnerAnalyticsLogger @Inject constructor( installationId, createAnalyticsEvent(learnerDetailsContext, EventBuilder::setDeleteProfileContext), profileId, - oppiaLogger + oppiaLogger, + oppiaClock.getCurrentTimeMs() ) } @@ -195,7 +213,8 @@ class LearnerAnalyticsLogger @Inject constructor( explorationVersion: Int, private val oppiaLogger: OppiaLogger, private val analyticsController: AnalyticsController, - private val loggingIdentifierController: LoggingIdentifierController + private val loggingIdentifierController: LoggingIdentifierController, + private val oppiaClock: OppiaClock ) { /** * The [StateAnalyticsLogger] corresponding to the current, pending state, or null if there is @@ -206,7 +225,7 @@ class LearnerAnalyticsLogger @Inject constructor( private val mutableStateAnalyticsLogger = MutableStateFlow(null) private val baseLogger by lazy { - BaseLogger(oppiaLogger, analyticsController, profileId, installationId) + BaseLogger(oppiaLogger, analyticsController, oppiaClock, profileId, installationId) } private val learnerDetailsContext by lazy { createLearnerDetailsContext(installationId, learnerId) @@ -617,6 +636,7 @@ class LearnerAnalyticsLogger @Inject constructor( internal class BaseLogger internal constructor( private val oppiaLogger: OppiaLogger, private val analyticsController: AnalyticsController, + private val oppiaClock: OppiaClock, private val profileId: ProfileId, private val installationId: String? ) { @@ -640,8 +660,11 @@ class LearnerAnalyticsLogger @Inject constructor( } /** See [AnalyticsController.maybeLogEvent]. */ - fun maybeLogEvent(context: EventContext?) = - analyticsController.maybeLogEvent(installationId, context, profileId, oppiaLogger) + fun maybeLogEvent(context: EventContext?) { + analyticsController.maybeLogEvent( + installationId, context, profileId, oppiaLogger, oppiaClock.getCurrentTimeMs() + ) + } internal companion object { /** @@ -653,16 +676,17 @@ class LearnerAnalyticsLogger @Inject constructor( installId: String?, context: EventContext?, profileId: ProfileId?, - oppiaLogger: OppiaLogger + oppiaLogger: OppiaLogger, + timestamp: Long ) { if (context != null) { - logImportantEvent(context, profileId) + logImportantEvent(context, profileId, timestamp) } else { oppiaLogger.e( "LearnerAnalyticsLogger", "Event is being dropped due to incomplete event (or missing learner ID for profile)." ) - logImportantEvent(createFailedToLogLearnerAnalyticsEvent(installId), profileId) + logImportantEvent(createFailedToLogLearnerAnalyticsEvent(installId), profileId, timestamp) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/PerformanceMetricsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/PerformanceMetricsLogger.kt index 8592ad19a5f..754f9c37aea 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/PerformanceMetricsLogger.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/PerformanceMetricsLogger.kt @@ -5,7 +5,6 @@ import org.oppia.android.app.model.ScreenName import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessor import org.oppia.android.util.platformparameter.EnablePerformanceMetricsCollection import org.oppia.android.util.platformparameter.PlatformParameterValue -import org.oppia.android.util.system.OppiaClock import javax.inject.Inject import javax.inject.Singleton @@ -19,7 +18,6 @@ import javax.inject.Singleton class PerformanceMetricsLogger @Inject constructor( private val performanceMetricsController: PerformanceMetricsController, private val performanceMetricsAssessor: PerformanceMetricsAssessor, - private val oppiaClock: OppiaClock, @EnablePerformanceMetricsCollection private val enablePerformanceMetricsCollection: PlatformParameterValue ) { @@ -30,11 +28,12 @@ class PerformanceMetricsLogger @Inject constructor( * application instance. * * @param currentScreen denotes the application screen at which this metric has been logged + * @param timestamp the timestamp to associate with this log */ - fun logApkSize(currentScreen: ScreenName) { + fun logApkSize(currentScreen: ScreenName, timestamp: Long) { if (enablePerformanceMetricsCollection.value) { performanceMetricsController.logLowPriorityMetricEvent( - oppiaClock.getCurrentTimeMs(), + timestamp, currentScreen, createApkSizeLoggableMetric(performanceMetricsAssessor.getApkSize()) ) @@ -47,11 +46,12 @@ class PerformanceMetricsLogger @Inject constructor( * application instance. * * @param currentScreen denotes the application screen at which this metric has been logged + * @param timestamp the timestamp to associate with this log */ - fun logStorageUsage(currentScreen: ScreenName) { + fun logStorageUsage(currentScreen: ScreenName, timestamp: Long) { if (enablePerformanceMetricsCollection.value) { performanceMetricsController.logLowPriorityMetricEvent( - oppiaClock.getCurrentTimeMs(), + timestamp, currentScreen, createStorageUsageLoggableMetric(performanceMetricsAssessor.getUsedStorage()) ) @@ -64,11 +64,12 @@ class PerformanceMetricsLogger @Inject constructor( * * @param startupLatency denotes the startup latency value that'll be logged to Firebase * @param currentScreen denotes the application screen at which this metric has been logged + * @param timestamp the timestamp to associate with this log */ - fun logStartupLatency(startupLatency: Long, currentScreen: ScreenName) { + fun logStartupLatency(startupLatency: Long, currentScreen: ScreenName, timestamp: Long) { if (startupLatency >= 0 && enablePerformanceMetricsCollection.value) { performanceMetricsController.logLowPriorityMetricEvent( - oppiaClock.getCurrentTimeMs(), + timestamp, currentScreen, createStartupLatencyLoggableMetric(startupLatency) ) @@ -81,11 +82,12 @@ class PerformanceMetricsLogger @Inject constructor( * application instance. * * @param currentScreen denotes the application screen at which this metric has been logged + * @param timestamp the timestamp to associate with this log */ - fun logMemoryUsage(currentScreen: ScreenName) { + fun logMemoryUsage(currentScreen: ScreenName, timestamp: Long) { if (enablePerformanceMetricsCollection.value) { performanceMetricsController.logMediumPriorityMetricEvent( - oppiaClock.getCurrentTimeMs(), + timestamp, currentScreen, createMemoryUsageLoggableMetric(performanceMetricsAssessor.getTotalPssUsed()) ) @@ -98,11 +100,12 @@ class PerformanceMetricsLogger @Inject constructor( * application instance. * * @param currentScreen denotes the application screen at which this metric has been logged + * @param timestamp the timestamp to associate with this log */ - fun logNetworkUsage(currentScreen: ScreenName) { + fun logNetworkUsage(currentScreen: ScreenName, timestamp: Long) { if (enablePerformanceMetricsCollection.value) { performanceMetricsController.logHighPriorityMetricEvent( - oppiaClock.getCurrentTimeMs(), + timestamp, currentScreen, createNetworkUsageLoggableMetric( performanceMetricsAssessor.getTotalReceivedBytes(), @@ -119,12 +122,13 @@ class PerformanceMetricsLogger @Inject constructor( * * @param currentScreen denotes the application screen at which this metric has been logged * @param cpuUsage denotes the relative CPU usage of the application which is measured across two - * time-separated points in the application. + * time-separated points in the application + * @param timestamp the timestamp to associate with this log */ - fun logCpuUsage(currentScreen: ScreenName, cpuUsage: Double) { + fun logCpuUsage(currentScreen: ScreenName, cpuUsage: Double, timestamp: Long) { if (enablePerformanceMetricsCollection.value) { performanceMetricsController.logHighPriorityMetricEvent( - oppiaClock.getCurrentTimeMs(), + timestamp, currentScreen, createCpuUsageLoggableMetric(cpuUsage) ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing/BUILD.bazel deleted file mode 100644 index fc4d5c7a062..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing/BUILD.bazel +++ /dev/null @@ -1,19 +0,0 @@ -""" -Package for testing utilities for log uploading functionality. -""" - -load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") - -kt_android_library( - name = "fake_log_scheduler", - testonly = True, - srcs = [ - "FakeLogScheduler.kt", - ], - visibility = ["//:oppia_testing_visibility"], - deps = [ - "//third_party:javax_inject_javax_inject", - "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", - "//utility/src/main/java/org/oppia/android/util/logging:metric_log_scheduler", - ], -) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing/FakeLogScheduler.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing/FakeLogScheduler.kt deleted file mode 100644 index 12973e595fa..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing/FakeLogScheduler.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.oppia.android.domain.oppialogger.analytics.testing - -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.oppia.android.util.logging.MetricLogScheduler -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -/** A test specific fake for the log uploader. */ -@Singleton -class FakeLogScheduler @Inject constructor() : MetricLogScheduler { - private val schedulingStorageUsageMetricLoggingRequestIdList = mutableListOf() - private val schedulingPeriodicUiMetricLoggingRequestIdList = mutableListOf() - private val schedulingPeriodicBackgroundMetricsLoggingRequestIdList = mutableListOf() - - override fun enqueueWorkRequestForPeriodicBackgroundMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - schedulingPeriodicBackgroundMetricsLoggingRequestIdList.add(workRequest.id) - } - - override fun enqueueWorkRequestForStorageUsage( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - schedulingStorageUsageMetricLoggingRequestIdList.add(workRequest.id) - } - - override fun enqueueWorkRequestForPeriodicUiMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - schedulingPeriodicUiMetricLoggingRequestIdList.add(workRequest.id) - } - - /** - * Returns the most recent work request id that's stored in the - * [schedulingStorageUsageMetricLoggingRequestIdList]. - */ - fun getMostRecentStorageUsageMetricLoggingRequestId() = - schedulingStorageUsageMetricLoggingRequestIdList.last() - - /** - * Returns the most recent work request id that's stored in the - * [schedulingPeriodicUiMetricLoggingRequestIdList]. - */ - fun getMostRecentPeriodicUiMetricLoggingRequestId() = - schedulingPeriodicUiMetricLoggingRequestIdList.last() - - /** - * Returns the most recent work request id that's stored in the - * [schedulingPeriodicBackgroundMetricsLoggingRequestIdList]. - */ - fun getMostRecentPeriodicBackgroundMetricLoggingRequestId() = - schedulingPeriodicBackgroundMetricsLoggingRequestIdList.last() -} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListener.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListener.kt index bee8f18579a..13bec21a94c 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListener.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListener.kt @@ -3,25 +3,39 @@ package org.oppia.android.domain.oppialogger.exceptions import org.oppia.android.domain.oppialogger.ApplicationStartupListener import org.oppia.android.util.logging.ConsoleLogger import javax.inject.Inject +import javax.inject.Provider /** Handler for catching fatal exceptions before the defaultUncaughtExceptionHandler. */ class UncaughtExceptionLoggerStartupListener @Inject constructor( - private val exceptionsController: ExceptionsController, - private val consoleLogger: ConsoleLogger + private val exceptionsControllerProvider: Provider, + private val consoleLogger: ConsoleLogger // Should be safe for early app access. ) : Thread.UncaughtExceptionHandler, ApplicationStartupListener { private var existingUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null + private var canLogExceptions: Boolean = false - /** Sets up the uncaught exception handler to [UncaughtExceptionLoggerStartupListener]. */ - override fun onCreate() { + override fun onCreateStarted() { + // This should be set up immediately to try and capture exceptions that occur early, but it may + // not be safe to log them until after app initialization completes. existingUncaughtExceptionHandler = Thread.currentThread().uncaughtExceptionHandler Thread.currentThread().uncaughtExceptionHandler = this } - /** Logs an uncaught exception to the [exceptionsController]. */ + override fun onCompletedInitialization() { + canLogExceptions = true + } + override fun uncaughtException(thread: Thread, throwable: Throwable) { try { - exceptionsController.logFatalException(Exception(throwable)) + if (canLogExceptions) { + exceptionsControllerProvider.get().logFatalException(Exception(throwable)) + } else { + consoleLogger.e( + "OPPIA_EXCEPTION_HANDLER", + "Skipped logging exception due to app not being fully initialized yet.", + throwable + ) + } } catch (e: Exception) { consoleLogger.e("OPPIA_EXCEPTION_HANDLER", "Problem in logging exception", e) } finally { diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/BUILD.bazel index 84b5cafd1dc..a31252a7cde 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/BUILD.bazel @@ -5,58 +5,36 @@ Library for providing log scheduling functionality to the Oppia android app. load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") kt_android_library( - name = "performance_metrics_log_scheduler", - srcs = [ - "PerformanceMetricsLogScheduler.kt", - ], + name = "scheduler", + srcs = ["MetricLogSchedulingWorkerScheduler.kt"], deps = [ ":metric_log_scheduling_worker", - "//third_party:androidx_work_work-runtime-ktx", - "//utility/src/main/java/org/oppia/android/util/logging:metric_log_scheduler", + "//domain/src/main/java/org/oppia/android/domain/workmanager:startup_worker_schedule_readiness_listener", ], ) kt_android_library( name = "metric_log_scheduling_worker", - srcs = [ - "MetricLogSchedulingWorker.kt", - ], + srcs = ["MetricLogSchedulingWorker.kt"], visibility = ["//:oppia_api_visibility"], deps = [ - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:application_lifecycle_observer", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:application_lifecycle_logger", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:performance_metrics_logger", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", - "//domain/src/main/java/org/oppia/android/domain/util:extensions", + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", "//model/src/main/proto:screens_java_proto_lite", - "//third_party:androidx_work_work-runtime-ktx", - "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-guava", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", - "//utility/src/main/java/org/oppia/android/util/threading:annotations", - ], -) - -kt_android_library( - name = "metric_log_scheduling_worker_factory", - srcs = [ - "MetricLogSchedulingWorkerFactory.kt", - ], - visibility = ["//domain:__pkg__"], - deps = [ - ":metric_log_scheduling_worker", - "//third_party:androidx_work_work-runtime-ktx", ], ) kt_android_library( name = "metric_log_scheduler_module", - srcs = [ - "MetricLogSchedulerModule.kt", - ], + srcs = ["MetricLogSchedulerModule.kt"], visibility = ["//:oppia_prod_module_visibility"], deps = [ - ":performance_metrics_log_scheduler", + ":scheduler", "//:dagger", - "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", - "//utility/src/main/java/org/oppia/android/util/logging:metric_log_scheduler", + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", + "//domain/src/main/java/org/oppia/android/domain/workmanager:startup_worker_schedule_readiness_listener", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulerModule.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulerModule.kt index 7594d6460ec..d181a871c34 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulerModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulerModule.kt @@ -2,13 +2,23 @@ package org.oppia.android.domain.oppialogger.logscheduler import dagger.Binds import dagger.Module -import org.oppia.android.util.logging.MetricLogScheduler +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.workmanager.OppiaWorker +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener /** Provides metric log scheduler related dependencies. */ @Module -abstract class MetricLogSchedulerModule { +interface MetricLogSchedulerModule { @Binds - abstract fun provideMetricLogScheduler( - performanceMetricLogScheduler: PerformanceMetricsLogScheduler - ): MetricLogScheduler + fun bindMetricLogSchedulingWorkerScheduler( + scheduler: MetricLogSchedulingWorkerScheduler + ): StartupWorkerScheduleReadinessListener + + @Binds + @IntoMap + @StringKey(MetricLogSchedulingWorker.WORKER_NAME) + fun bindLogUploadWorkerFactoryProvider( + factory: MetricLogSchedulingWorker.Factory + ): OppiaWorker.Factory<*> } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorker.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorker.kt index 3902ebb74d0..a38d66743c3 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorker.kt @@ -1,19 +1,11 @@ package org.oppia.android.domain.oppialogger.logscheduler -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerParameters -import com.google.common.util.concurrent.ListenableFuture -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.guava.asListenableFuture import org.oppia.android.app.model.ScreenName.BACKGROUND_SCREEN -import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleObserver +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleLogger import org.oppia.android.domain.oppialogger.analytics.PerformanceMetricsLogger -import org.oppia.android.domain.util.getStringFromData +import org.oppia.android.domain.workmanager.OppiaWorker import org.oppia.android.util.logging.ConsoleLogger -import org.oppia.android.util.threading.BackgroundDispatcher +import org.oppia.android.util.system.OppiaClock import javax.inject.Inject /** @@ -21,80 +13,40 @@ import javax.inject.Inject * and then stores it in in device cache. */ class MetricLogSchedulingWorker private constructor( - context: Context, - params: WorkerParameters, private val consoleLogger: ConsoleLogger, private val performanceMetricsLogger: PerformanceMetricsLogger, - private val applicationLifecycleObserver: ApplicationLifecycleObserver, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher -) : ListenableWorker(context, params) { + private val applicationLifecycleLogger: ApplicationLifecycleLogger, + private val oppiaClock: OppiaClock +) : OppiaWorker { companion object { - private const val TAG = "MetricLogSchedulingWorker" - /** - * The key for an input key-value pair for [MetricLogSchedulingWorker] where one of - * [PERIODIC_BACKGROUND_METRIC_WORKER], [PERIODIC_UI_METRIC_WORKER] and [STORAGE_USAGE_WORKER] indicates what - * kind of work to perform. - */ - const val WORKER_CASE_KEY = "metric_log_scheduling_worker_case_key" - /** - * Indicates to [MetricLogSchedulingWorker] that it should schedule logging for periodic - * performance metrics. - */ - const val PERIODIC_BACKGROUND_METRIC_WORKER = "periodic_background_metric_worker" - /** - * Indicates to [MetricLogSchedulingWorker] that it should schedule logging for storage usage - * performance metrics. - */ - const val STORAGE_USAGE_WORKER = "storage_usage_worker" - /** - * Indicates to [MetricLogSchedulingWorker] that it should schedule logging for ui-related - * memory usage performance metrics. - */ - const val PERIODIC_UI_METRIC_WORKER = "periodic_ui_metric_worker" + const val WORKER_NAME = "MetricLogSchedulingWorker" } - override fun startWork(): ListenableFuture { - val backgroundScope = CoroutineScope(backgroundDispatcher) - // TODO(#4463): Add withTimeout() to avoid potential hanging. - return backgroundScope.async { - when (inputData.getStringFromData(WORKER_CASE_KEY)) { - PERIODIC_BACKGROUND_METRIC_WORKER -> schedulePeriodicBackgroundMetricLogging() - STORAGE_USAGE_WORKER -> scheduleStorageUsageMetricLogging() - PERIODIC_UI_METRIC_WORKER -> schedulePeriodicUiMetricLogging() - else -> Result.failure() - } - }.asListenableFuture() + enum class Operation(override val persistentName: String) : OppiaWorker.TaskType { + SCHEDULE_LOG_PERIODIC_BACKGROUND_METRICS("schedule_log_periodic_background_metrics"), + SCHEDULE_LOG_PERIODIC_UI_METRICS("schedule_log_periodic_ui_metrics"), + SCHEDULE_LOG_STORAGE_USAGE_METRICS("schedule_log_storage_usage_metrics") } - private fun schedulePeriodicBackgroundMetricLogging(): Result { + override suspend fun doWork(taskType: Operation): OppiaWorker.Result { + val timestamp = oppiaClock.getCurrentTimeMs() return try { - performanceMetricsLogger.logNetworkUsage(BACKGROUND_SCREEN) - Result.success() - } catch (e: Exception) { - consoleLogger.e(TAG, e.toString(), e) - return Result.failure() - } - } - - private fun scheduleStorageUsageMetricLogging(): Result { - return try { - performanceMetricsLogger.logStorageUsage(BACKGROUND_SCREEN) - Result.success() - } catch (e: Exception) { - consoleLogger.e(TAG, e.toString(), e) - return Result.failure() - } - } - - private fun schedulePeriodicUiMetricLogging(): Result { - return try { - val currentScreen = applicationLifecycleObserver.getCurrentScreen() - performanceMetricsLogger.logMemoryUsage(currentScreen) - Result.success() + when (taskType) { + Operation.SCHEDULE_LOG_PERIODIC_BACKGROUND_METRICS -> + performanceMetricsLogger.logNetworkUsage(BACKGROUND_SCREEN, timestamp) + Operation.SCHEDULE_LOG_STORAGE_USAGE_METRICS -> + performanceMetricsLogger.logStorageUsage(BACKGROUND_SCREEN, timestamp) + Operation.SCHEDULE_LOG_PERIODIC_UI_METRICS -> { + performanceMetricsLogger.logMemoryUsage( + applicationLifecycleLogger.getCurrentScreen(), timestamp + ) + } + } + OppiaWorker.Result.SUCCESS } catch (e: Exception) { - consoleLogger.e(TAG, e.toString(), e) - return Result.failure() + consoleLogger.e(WORKER_NAME, "Failed operation: ${taskType.persistentName}.", e) + OppiaWorker.Result.FAILURE } } @@ -102,23 +54,17 @@ class MetricLogSchedulingWorker private constructor( class Factory @Inject constructor( private val consoleLogger: ConsoleLogger, private val performanceMetricsLogger: PerformanceMetricsLogger, - private val applicationLifecycleObserver: ApplicationLifecycleObserver, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher - ) { - /** - * Returns a new [MetricLogSchedulingWorker]. - * - * This [MetricLogSchedulingWorker] implements the [ListenableWorker] for facilitating metric - * log scheduling. - */ - fun create(context: Context, params: WorkerParameters): ListenableWorker { + private val applicationLifecycleLogger: ApplicationLifecycleLogger, + private val oppiaClock: OppiaClock + ) : OppiaWorker.Factory { + override val supportedTaskTypes: List = Operation.values().toList() + + override fun createWorker(): OppiaWorker { return MetricLogSchedulingWorker( - context, - params, consoleLogger, performanceMetricsLogger, - applicationLifecycleObserver, - backgroundDispatcher + applicationLifecycleLogger, + oppiaClock ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerFactory.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerFactory.kt deleted file mode 100644 index c8dae69942e..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.oppia.android.domain.oppialogger.logscheduler - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import javax.inject.Inject - -/** Custom [WorkerFactory] for the [MetricLogSchedulingWorker]. */ -class MetricLogSchedulingWorkerFactory @Inject constructor( - private val workerFactory: MetricLogSchedulingWorker.Factory -) : WorkerFactory() { - - /** Returns a new [MetricLogSchedulingWorker] for the given context and parameters. */ - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker { - return workerFactory.create(appContext, workerParameters) - } -} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerScheduler.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerScheduler.kt new file mode 100644 index 00000000000..c3d9ea16932 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerScheduler.kt @@ -0,0 +1,37 @@ +package org.oppia.android.domain.oppialogger.logscheduler + +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener +import org.oppia.android.domain.workmanager.WorkManagerScheduler +import org.oppia.android.util.platformparameter.PerformanceMetricsCollectionHighFrequencyTimeIntervalInMinutes +import org.oppia.android.util.platformparameter.PerformanceMetricsCollectionLowFrequencyTimeIntervalInMinutes +import org.oppia.android.util.platformparameter.PlatformParameterValue +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class MetricLogSchedulingWorkerScheduler @Inject constructor( + @PerformanceMetricsCollectionHighFrequencyTimeIntervalInMinutes + private val performanceMetricsCollectionHighFrequencyTimeInterval: PlatformParameterValue, + @PerformanceMetricsCollectionLowFrequencyTimeIntervalInMinutes + private val performanceMetricCollectionLowFrequencyTimeInterval: PlatformParameterValue +) : StartupWorkerScheduleReadinessListener { + override fun scheduleWork(workManagerScheduler: WorkManagerScheduler) { + workManagerScheduler.schedulePeriodicWorker( + MetricLogSchedulingWorker.WORKER_NAME, + MetricLogSchedulingWorker.Operation.SCHEDULE_LOG_PERIODIC_BACKGROUND_METRICS, + performanceMetricsCollectionHighFrequencyTimeInterval.value.toLong(), + TimeUnit.MINUTES + ) + workManagerScheduler.schedulePeriodicWorker( + MetricLogSchedulingWorker.WORKER_NAME, + MetricLogSchedulingWorker.Operation.SCHEDULE_LOG_PERIODIC_UI_METRICS, + performanceMetricsCollectionHighFrequencyTimeInterval.value.toLong(), + TimeUnit.MINUTES + ) + workManagerScheduler.schedulePeriodicWorker( + MetricLogSchedulingWorker.WORKER_NAME, + MetricLogSchedulingWorker.Operation.SCHEDULE_LOG_STORAGE_USAGE_METRICS, + performanceMetricCollectionLowFrequencyTimeInterval.value.toLong(), + TimeUnit.MINUTES + ) + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogScheduler.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogScheduler.kt deleted file mode 100644 index de82dc42dfa..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogScheduler.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.oppia.android.domain.oppialogger.logscheduler - -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.oppia.android.util.logging.MetricLogScheduler -import javax.inject.Inject - -private const val OPPIA_PERIODIC_METRIC_WORK = "OPPIA_PERIODIC_METRIC_WORK" -private const val OPPIA_STORAGE_USAGE_WORK = "OPPIA_STORAGE_USAGE_WORK" -private const val OPPIA_MEMORY_USAGE_WORK = "OPPIA_MEMORY_USAGE_WORK" - -/** - * Enqueues work requests for generating metric log reports for gaining an insight regarding into - * the performance of the application. - */ -class PerformanceMetricsLogScheduler @Inject constructor() : MetricLogScheduler { - override fun enqueueWorkRequestForPeriodicBackgroundMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_PERIODIC_METRIC_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } - - override fun enqueueWorkRequestForStorageUsage( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_STORAGE_USAGE_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } - - override fun enqueueWorkRequestForPeriodicUiMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_MEMORY_USAGE_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } -} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel index e240d31b87f..d6203d92662 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel @@ -5,72 +5,40 @@ Library for providing log uploading functionality to the Oppia android app. load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") kt_android_library( - name = "initializer", - srcs = [ - "LogReportWorkManagerInitializer.kt", - ], - visibility = [ - "//domain:domain_testing_visibility", - ], + name = "scheduler", + srcs = ["LogReportWorkerScheduler.kt"], + visibility = ["//domain:domain_testing_visibility"], deps = [ ":worker", - "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:analytics_startup_listener", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler:metric_log_scheduling_worker", - "//third_party:androidx_work_work-runtime-ktx", - "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", - "//utility/src/main/java/org/oppia/android/util/logging:metric_log_scheduler", + "//domain/src/main/java/org/oppia/android/domain/workmanager:startup_worker_schedule_readiness_listener", ], ) kt_android_library( name = "worker", - srcs = [ - "LogUploadWorker.kt", - ], - visibility = [ - "//domain:domain_testing_visibility", - ], + srcs = ["LogUploadWorker.kt"], + visibility = ["//domain:domain_testing_visibility"], deps = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:data_controller", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:performance_metrics_controller", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", - "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//third_party:androidx_work_work-runtime-ktx", - "//third_party:com_google_guava_guava", - "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-guava", - "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", - "//utility/src/main/java/org/oppia/android/util/extensions:iterable_extensions", + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:exception_logger", "//utility/src/main/java/org/oppia/android/util/logging/performancemetrics:performance_metrics_event_logger", - "//utility/src/main/java/org/oppia/android/util/threading:annotations", - ], -) - -kt_android_library( - name = "worker_factory", - srcs = [ - "LogUploadWorkerFactory.kt", - ], - visibility = ["//domain:__pkg__"], - deps = [ - ":worker", - "//third_party:androidx_work_work-runtime-ktx", ], ) kt_android_library( name = "worker_module", - srcs = [ - "LogReportWorkerModule.kt", - ], + srcs = ["LogReportWorkerModule.kt"], visibility = ["//:oppia_prod_module_visibility"], deps = [ - ":initializer", + ":scheduler", "//:dagger", "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt deleted file mode 100644 index ef31a5ab9dc..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializer.kt +++ /dev/null @@ -1,241 +0,0 @@ -package org.oppia.android.domain.oppialogger.loguploader - -import androidx.work.Constraints -import androidx.work.Data -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.oppia.android.domain.oppialogger.analytics.AnalyticsStartupListener -import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulingWorker -import org.oppia.android.util.logging.LogUploader -import org.oppia.android.util.logging.MetricLogScheduler -import org.oppia.android.util.platformparameter.PerformanceMetricsCollectionHighFrequencyTimeIntervalInMinutes -import org.oppia.android.util.platformparameter.PerformanceMetricsCollectionLowFrequencyTimeIntervalInMinutes -import org.oppia.android.util.platformparameter.PlatformParameterValue -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -/** - * Enqueues unique periodic work requests for uploading events and exceptions to the remote service - * on application creation. - */ -class LogReportWorkManagerInitializer @Inject constructor( - private val logUploader: LogUploader, - private val metricLogScheduler: MetricLogScheduler, - @PerformanceMetricsCollectionHighFrequencyTimeIntervalInMinutes - performanceMetricsCollectionHighFrequencyTimeInterval: PlatformParameterValue, - @PerformanceMetricsCollectionLowFrequencyTimeIntervalInMinutes - performanceMetricCollectionLowFrequencyTimeInterval: PlatformParameterValue -) : AnalyticsStartupListener { - - private val logReportWorkerConstraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .build() - - private val workerCaseForUploadingEvents: Data = Data.Builder() - .putString( - LogUploadWorker.WORKER_CASE_KEY, - LogUploadWorker.EVENT_WORKER - ) - .build() - - private val workerCaseForUploadingExceptions: Data = Data.Builder() - .putString( - LogUploadWorker.WORKER_CASE_KEY, - LogUploadWorker.EXCEPTION_WORKER - ) - .build() - - private val workerCaseForUploadingPerformanceMetrics: Data = Data.Builder() - .putString( - LogUploadWorker.WORKER_CASE_KEY, - LogUploadWorker.PERFORMANCE_METRICS_WORKER - ) - .build() - - private val workerCaseForUploadingFirestoreData: Data = Data.Builder() - .putString( - LogUploadWorker.WORKER_CASE_KEY, - LogUploadWorker.FIRESTORE_WORKER - ) - .build() - - private val workerCaseForSchedulingPeriodicBackgroundMetricLogs: Data = Data.Builder() - .putString( - MetricLogSchedulingWorker.WORKER_CASE_KEY, - MetricLogSchedulingWorker.PERIODIC_BACKGROUND_METRIC_WORKER - ) - .build() - - private val workerCaseForSchedulingStorageUsageMetricLogs: Data = Data.Builder() - .putString( - MetricLogSchedulingWorker.WORKER_CASE_KEY, - MetricLogSchedulingWorker.STORAGE_USAGE_WORKER - ) - .build() - - private val workerCaseForSchedulingPeriodicUiMetricLogs: Data = Data.Builder() - .putString( - MetricLogSchedulingWorker.WORKER_CASE_KEY, - MetricLogSchedulingWorker.PERIODIC_UI_METRIC_WORKER - ) - .build() - - private val workRequestForUploadingEvents: PeriodicWorkRequest = PeriodicWorkRequest - .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) - .setInputData(workerCaseForUploadingEvents) - .setConstraints(logReportWorkerConstraints) - .build() - - private val workRequestForUploadingExceptions: PeriodicWorkRequest = PeriodicWorkRequest - .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) - .setInputData(workerCaseForUploadingExceptions) - .setConstraints(logReportWorkerConstraints) - .build() - - private val workRequestForUploadingPerformanceMetrics: PeriodicWorkRequest = PeriodicWorkRequest - .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) - .setInputData(workerCaseForUploadingPerformanceMetrics) - .setConstraints(logReportWorkerConstraints) - .build() - - private val workRequestForSchedulingPeriodicBackgroundMetricLogs: PeriodicWorkRequest = - PeriodicWorkRequest.Builder( - MetricLogSchedulingWorker::class.java, - performanceMetricsCollectionHighFrequencyTimeInterval.value.toLong(), - TimeUnit.MINUTES - ) - .setInputData(workerCaseForSchedulingPeriodicBackgroundMetricLogs) - .setConstraints(logReportWorkerConstraints) - .build() - - private val workRequestForSchedulingStorageUsageMetricLogs: PeriodicWorkRequest = - PeriodicWorkRequest.Builder( - MetricLogSchedulingWorker::class.java, - performanceMetricCollectionLowFrequencyTimeInterval.value.toLong(), - TimeUnit.MINUTES - ) - .setInputData(workerCaseForSchedulingStorageUsageMetricLogs) - .setConstraints(logReportWorkerConstraints) - .build() - - private val workRequestForSchedulingPeriodicUiMetricLogs: PeriodicWorkRequest = - PeriodicWorkRequest.Builder( - MetricLogSchedulingWorker::class.java, - performanceMetricsCollectionHighFrequencyTimeInterval.value.toLong(), - TimeUnit.MINUTES - ) - .setInputData(workerCaseForSchedulingPeriodicUiMetricLogs) - .setConstraints(logReportWorkerConstraints) - .build() - - private val workRequestForUploadingFireStoreData: PeriodicWorkRequest = - PeriodicWorkRequest.Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) - .setInputData(workerCaseForUploadingFirestoreData) - .setConstraints(logReportWorkerConstraints) - .build() - - override fun onCreate(workManager: WorkManager) { - logUploader.enqueueWorkRequestForEvents(workManager, workRequestForUploadingEvents) - logUploader.enqueueWorkRequestForExceptions(workManager, workRequestForUploadingExceptions) - logUploader.enqueueWorkRequestForPerformanceMetrics( - workManager, - workRequestForUploadingPerformanceMetrics - ) - logUploader.enqueueWorkRequestForFirestore( - workManager, - workRequestForUploadingFireStoreData - ) - metricLogScheduler.enqueueWorkRequestForPeriodicBackgroundMetrics( - workManager, - workRequestForSchedulingPeriodicBackgroundMetricLogs - ) - metricLogScheduler.enqueueWorkRequestForStorageUsage( - workManager, - workRequestForSchedulingStorageUsageMetricLogs - ) - metricLogScheduler.enqueueWorkRequestForPeriodicUiMetrics( - workManager, - workRequestForSchedulingPeriodicUiMetricLogs - ) - } - - /** Returns the worker constraints set for the log reporting work requests. */ - fun getLogReportWorkerConstraints(): Constraints = logReportWorkerConstraints - - /** Returns the [UUID] of the work request that is enqueued for uploading event logs. */ - fun getWorkRequestForEventsId(): UUID = workRequestForUploadingEvents.id - - /** Returns the [UUID] of the work request that is enqueued for uploading exception logs. */ - fun getWorkRequestForExceptionsId(): UUID = workRequestForUploadingExceptions.id - - /** Returns the [UUID] of the work request that is enqueued for uploading performance metrics logs. */ - fun getWorkRequestForPerformanceMetricsId(): UUID = workRequestForUploadingPerformanceMetrics.id - - /** - * Returns the [UUID] of the work request that is enqueued for scheduling memory usage - * performance metrics collection. - */ - fun getWorkRequestForSchedulingPeriodicUiMetricLogsId(): UUID = - workRequestForSchedulingPeriodicUiMetricLogs.id - - /** - * Returns the [UUID] of the work request that is enqueued for scheduling storage usage - * performance metrics collection. - */ - fun getWorkRequestForSchedulingStorageUsageMetricLogsId(): UUID = - workRequestForSchedulingStorageUsageMetricLogs.id - - /** - * Returns the [UUID] of the work request that is enqueued for scheduling periodic performance - * metrics collection. - */ - fun getWorkRequestForSchedulingPeriodicBackgroundPerformanceMetricLogsId(): UUID = - workRequestForSchedulingPeriodicBackgroundMetricLogs.id - - /** Returns the [UUID] of the work request that is enqueued for uploading firestore data. */ - fun getWorkRequestForFirestoreId(): UUID = workRequestForUploadingFireStoreData.id - - /** - * Returns the [Data] that goes into the work request that is enqueued for uploading event logs. - */ - fun getWorkRequestDataForEvents(): Data = workerCaseForUploadingEvents - - /** - * Returns the [Data] that goes into the work request that is enqueued for uploading exception - * logs. - */ - fun getWorkRequestDataForExceptions(): Data = workerCaseForUploadingExceptions - - /** Returns the [Data] that goes into the work request that is enqueued for uploading performance metric logs. */ - fun getWorkRequestDataForPerformanceMetrics(): Data = workerCaseForUploadingPerformanceMetrics - - /** - * Returns the [Data] that goes into the work request that is enqueued for scheduling storage - * usage performance metrics collection. - */ - fun getWorkRequestDataForSchedulingStorageUsageMetricLogs(): Data = - workerCaseForSchedulingStorageUsageMetricLogs - - /** - * Returns the [Data] that goes into the work request that is enqueued for scheduling memory - * usage performance metrics collection. - */ - fun getWorkRequestDataForSchedulingPeriodicUiMetricLogs(): Data = - workerCaseForSchedulingPeriodicUiMetricLogs - - /** - * Returns the [Data] that goes into the work request that is enqueued for scheduling periodic - * performance metrics collection. - */ - fun getWorkRequestDataForSchedulingPeriodicBackgroundPerformanceMetricLogs(): Data = - workerCaseForSchedulingPeriodicBackgroundMetricLogs - - /** - * Returns the [Data] that goes into the work request that is enqueued for uploading firestore - * data. - */ - fun getWorkRequestDataForFirestore(): Data = workerCaseForUploadingFirestoreData -} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerModule.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerModule.kt index 32725e43266..8c65be9bdab 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerModule.kt @@ -2,16 +2,23 @@ package org.oppia.android.domain.oppialogger.loguploader import dagger.Binds import dagger.Module +import dagger.multibindings.IntoMap import dagger.multibindings.IntoSet -import org.oppia.android.domain.oppialogger.analytics.AnalyticsStartupListener +import dagger.multibindings.StringKey +import org.oppia.android.domain.workmanager.OppiaWorker +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener /** Provides [LogUploadWorker] related dependencies. */ @Module interface LogReportWorkerModule { - @Binds @IntoSet - fun bindLogReportWorkRequest( - logReportWorkManagerInitializer: LogReportWorkManagerInitializer - ): AnalyticsStartupListener + fun bindLogReportWorkerScheduler( + scheduler: LogReportWorkerScheduler + ): StartupWorkerScheduleReadinessListener + + @Binds + @IntoMap + @StringKey(LogUploadWorker.WORKER_NAME) + fun bindLogUploadWorkerFactoryProvider(factory: LogUploadWorker.Factory): OppiaWorker.Factory<*> } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerScheduler.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerScheduler.kt new file mode 100644 index 00000000000..93a6b7e2ada --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerScheduler.kt @@ -0,0 +1,20 @@ +package org.oppia.android.domain.oppialogger.loguploader + +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener +import org.oppia.android.domain.workmanager.WorkManagerScheduler +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Enqueues unique periodic work requests for uploading events and exceptions to the remote service + * on application creation. + */ +class LogReportWorkerScheduler @Inject constructor() : StartupWorkerScheduleReadinessListener { + override fun scheduleWork(workManagerScheduler: WorkManagerScheduler) { + for (operation in LogUploadWorker.Operation.values()) { + workManagerScheduler.schedulePeriodicWorker( + LogUploadWorker.WORKER_NAME, operation, 6, TimeUnit.HOURS + ) + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt index ea8bb8cf2ce..bca055e806e 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt @@ -1,31 +1,19 @@ package org.oppia.android.domain.oppialogger.loguploader -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerParameters -import com.google.common.util.concurrent.ListenableFuture -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.guava.asListenableFuture import org.oppia.android.domain.oppialogger.analytics.AnalyticsController import org.oppia.android.domain.oppialogger.analytics.FirestoreDataController import org.oppia.android.domain.oppialogger.analytics.PerformanceMetricsController import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.oppialogger.exceptions.toException -import org.oppia.android.domain.util.getStringFromData -import org.oppia.android.util.extensions.safeForEach +import org.oppia.android.domain.workmanager.OppiaWorker import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.logging.ExceptionLogger import org.oppia.android.util.logging.SyncStatusManager import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsEventLogger -import org.oppia.android.util.threading.BackgroundDispatcher import javax.inject.Inject /** Worker class that extracts log reports from the cache store and logs them to the remote service. */ class LogUploadWorker private constructor( - context: Context, - params: WorkerParameters, private val analyticsController: AnalyticsController, private val exceptionsController: ExceptionsController, private val performanceMetricsController: PerformanceMetricsController, @@ -33,85 +21,41 @@ class LogUploadWorker private constructor( private val dataController: FirestoreDataController, private val performanceMetricsEventLogger: PerformanceMetricsEventLogger, private val consoleLogger: ConsoleLogger, - private val syncStatusManager: SyncStatusManager, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher -) : ListenableWorker(context, params) { - + private val syncStatusManager: SyncStatusManager +) : OppiaWorker { companion object { - const val WORKER_CASE_KEY = "worker_case_key" - const val TAG = "LogUploadWorker.tag" - const val EVENT_WORKER = "event_worker" - const val EXCEPTION_WORKER = "exception_worker" - const val PERFORMANCE_METRICS_WORKER = "performance_metrics_worker" - const val FIRESTORE_WORKER = "firestore_worker" + const val WORKER_NAME = "LogUploadWorker" } - override fun startWork(): ListenableFuture { - val backgroundScope = CoroutineScope(backgroundDispatcher) - // TODO(#4463): Add withTimeout() to avoid potential hanging. - return backgroundScope.async { - when (inputData.getStringFromData(WORKER_CASE_KEY)) { - EVENT_WORKER -> uploadEvents() - EXCEPTION_WORKER -> uploadExceptions() - PERFORMANCE_METRICS_WORKER -> uploadPerformanceMetrics() - FIRESTORE_WORKER -> uploadFirestoreData() - else -> Result.failure() - } - }.asListenableFuture() + enum class Operation(override val persistentName: String) : OppiaWorker.TaskType { + UPLOAD_EVENTS("upload_events"), + UPLOAD_EXCEPTIONS("upload_exceptions"), + UPLOAD_PERFORMANCE_METRICS("upload_performance_metrics"), + UPLOAD_FIRESTORE_DATA("upload_firestore_data") } - /** Extracts exception logs from the cache store and logs them to the remote service. */ - private suspend fun uploadExceptions(): Result { + override suspend fun doWork(taskType: Operation): OppiaWorker.Result { return try { - val exceptionLogs = exceptionsController.getExceptionLogStoreList() - exceptionLogs.let { - for (exceptionLog in it) { - exceptionLogger.logException(exceptionLog.toException()) - exceptionsController.removeFirstExceptionLogFromStore() + when (taskType) { + Operation.UPLOAD_EVENTS -> analyticsController.uploadEventLogsAndWait() + Operation.UPLOAD_EXCEPTIONS -> { + for (exceptionLog in exceptionsController.getExceptionLogStoreList()) { + exceptionLogger.logException(exceptionLog.toException()) + exceptionsController.removeFirstExceptionLogFromStore() + } } + Operation.UPLOAD_PERFORMANCE_METRICS -> { + for (performanceMetricsLog in performanceMetricsController.getMetricLogStoreList()) { + performanceMetricsEventLogger.logPerformanceMetric(performanceMetricsLog) + performanceMetricsController.removeFirstMetricLogFromStore() + } + } + Operation.UPLOAD_FIRESTORE_DATA -> dataController.uploadData() } - Result.success() - } catch (e: Exception) { - consoleLogger.e(TAG, e.toString(), e) - Result.failure() - } - } - - /** Extracts event logs from the cache store and logs them to the remote service. */ - private suspend fun uploadEvents(): Result { - return try { - analyticsController.uploadEventLogsAndWait() - Result.success() - } catch (e: Exception) { - syncStatusManager.reportUploadError() - consoleLogger.e(TAG, "Failed to upload events", e) - Result.failure() - } - } - - /** Extracts performance metric logs from the cache store and logs them to the remote service. */ - private suspend fun uploadPerformanceMetrics(): Result { - return try { - val performanceMetricsLogs = performanceMetricsController.getMetricLogStoreList() - performanceMetricsLogs.safeForEach { performanceMetricsLog -> - performanceMetricsEventLogger.logPerformanceMetric(performanceMetricsLog) - performanceMetricsController.removeFirstMetricLogFromStore() - } - Result.success() - } catch (e: Exception) { - consoleLogger.e(TAG, e.toString(), e) - Result.failure() - } - } - - /** Extracts data from offline storage and logs them to the remote service. */ - private suspend fun uploadFirestoreData(): Result { - return try { - dataController.uploadData() - Result.success() + OppiaWorker.Result.SUCCESS } catch (e: Exception) { - consoleLogger.e(TAG, e.toString(), e) - Result.failure() + consoleLogger.e(WORKER_NAME, "Failed operation: ${taskType.persistentName}.", e) + OppiaWorker.Result.FAILURE } } @@ -124,13 +68,12 @@ class LogUploadWorker private constructor( private val dataController: FirestoreDataController, private val performanceMetricsEventLogger: PerformanceMetricsEventLogger, private val consoleLogger: ConsoleLogger, - private val syncStatusManager: SyncStatusManager, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher - ) { - fun create(context: Context, params: WorkerParameters): ListenableWorker { + private val syncStatusManager: SyncStatusManager + ) : OppiaWorker.Factory { + override val supportedTaskTypes: List = Operation.values().toList() + + override fun createWorker(): OppiaWorker { return LogUploadWorker( - context, - params, analyticsController, exceptionsController, performanceMetricsController, @@ -138,8 +81,7 @@ class LogUploadWorker private constructor( dataController, performanceMetricsEventLogger, consoleLogger, - syncStatusManager, - backgroundDispatcher + syncStatusManager ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt deleted file mode 100644 index 1308e0bf7f5..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.oppia.android.domain.oppialogger.loguploader - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import javax.inject.Inject - -/** Custom [WorkerFactory] for the [LogUploadWorker]. */ -class LogUploadWorkerFactory @Inject constructor( - private val workerFactory: LogUploadWorker.Factory -) : WorkerFactory() { - - /** Returns a new [LogUploadWorker] for the given context and parameters. */ - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? { - return workerFactory.create(appContext, workerParameters) - } -} diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/survey/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/survey/BUILD.bazel index 215f85d2212..81e8a490d74 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/survey/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/survey/BUILD.bazel @@ -10,7 +10,8 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//domain", - "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:data_controller", "//model/src/main/proto:survey_java_proto_lite", "//third_party:javax_inject_javax_inject", ], diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/BUILD.bazel index 195399bd015..3c72852f8ec 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/BUILD.bazel @@ -7,21 +7,19 @@ load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") kt_android_library( name = "syncup", srcs = [ - "PlatformParameterSyncUpWorkManagerInitializer.kt", "PlatformParameterSyncUpWorker.kt", - "PlatformParameterSyncUpWorkerFactory.kt", "PlatformParameterSyncUpWorkerModule.kt", + "PlatformParameterSyncUpWorkerScheduler.kt", ], visibility = ["//:oppia_api_visibility"], deps = [ "//:dagger", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:analytics_startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", "//domain/src/main/java/org/oppia/android/domain/platformparameter:controller", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-guava", + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", + "//domain/src/main/java/org/oppia/android/domain/workmanager:startup_worker_schedule_readiness_listener", "//utility", - "//utility/src/main/java/org/oppia/android/util/threading:annotations", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializer.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializer.kt deleted file mode 100644 index c0e4c0002cb..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializer.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.oppia.android.domain.platformparameter.syncup - -import android.annotation.SuppressLint -import androidx.annotation.VisibleForTesting -import androidx.work.Constraints -import androidx.work.Data -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.oppia.android.domain.oppialogger.analytics.AnalyticsStartupListener -import org.oppia.android.util.platformparameter.PlatformParameterValue -import org.oppia.android.util.platformparameter.SyncUpWorkerTimePeriodHours -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -/** - * Enqueues unique periodic work requests for fetching and caching latest platform parameter values - * from the remote service on application creation. - */ -class PlatformParameterSyncUpWorkManagerInitializer @Inject constructor( - @SyncUpWorkerTimePeriodHours private val workRequestRepeatInterval: PlatformParameterValue -) : AnalyticsStartupListener { - - private val OPPIA_PLATFORM_PARAMETER_WORK_REQUEST_NAME = "OPPIA_PLATFORM_PARAMETER_WORK_REQUEST" - - /** [Constraints] for platform parameter sync up work request. */ - private val platformParameterSyncUpWorkerConstraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .build() - - /** [Data] for platform parameter sync up work request. */ - private val workerTypeForSyncingPlatformParameters: Data = Data.Builder() - .putString( - PlatformParameterSyncUpWorker.WORKER_TYPE_KEY, - PlatformParameterSyncUpWorker.PLATFORM_PARAMETER_WORKER - ) - .build() - - /** [PeriodicWorkRequest] for platform parameter sync up worker. */ - private val workRequestForSyncingPlatformParameters = - PeriodicWorkRequest.Builder( - PlatformParameterSyncUpWorker::class.java, - workRequestRepeatInterval.value.toLong(), - TimeUnit.HOURS - ) - .addTag(PlatformParameterSyncUpWorker.TAG) - .setInputData(workerTypeForSyncingPlatformParameters) - .setConstraints(platformParameterSyncUpWorkerConstraints) - .build() - - override fun onCreate(workManager: WorkManager) { - workManager.enqueueUniquePeriodicWork( - OPPIA_PLATFORM_PARAMETER_WORK_REQUEST_NAME, - ExistingPeriodicWorkPolicy.KEEP, - workRequestForSyncingPlatformParameters - ) - } - - /** Returns the [UUID] of the work request that is enqueued to sync-up platform parameters. */ - @VisibleForTesting - fun getSyncUpWorkRequestId(): UUID { - return workRequestForSyncingPlatformParameters.id - } - - /** Returns the [Data] that goes into the work request enqueued to sync-up platform parameters. */ - @VisibleForTesting - fun getSyncUpWorkRequestData(): Data { - return workerTypeForSyncingPlatformParameters - } - - /** Returns the time interval of periodic work request enqueued to sync-up platform parameters. */ - @SuppressLint("RestrictedApi") // getWorkSpec is restricted; suppression is fine for tests. - @VisibleForTesting - fun getSyncUpWorkerTimePeriod(): Long { - return workRequestForSyncingPlatformParameters.workSpec.intervalDuration - } - - /** Returns the Worker [Constraints] set for the platform parameter sync-up work requests. */ - fun getSyncUpWorkerConstraints(): Constraints { - return platformParameterSyncUpWorkerConstraints - } -} diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt index 1a0a35e38b4..03ecda3a74f 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt @@ -1,71 +1,48 @@ package org.oppia.android.domain.platformparameter.syncup -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerParameters -import com.google.common.util.concurrent.ListenableFuture -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.guava.asListenableFuture import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.platformparameter.PlatformParameterController -import org.oppia.android.domain.util.getStringFromData +import org.oppia.android.domain.workmanager.OppiaWorker import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.threading.BackgroundDispatcher import javax.inject.Inject /** Worker class that fetches and caches the latest platform parameters from the remote service. */ class PlatformParameterSyncUpWorker private constructor( - context: Context, - params: WorkerParameters, private val platformParameterController: PlatformParameterController, private val oppiaLogger: OppiaLogger, - private val exceptionsController: ExceptionsController, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher -) : ListenableWorker(context, params) { + private val exceptionsController: ExceptionsController +) : OppiaWorker { companion object { - /** A Tag for the logs that are associated with PlatformParameterSyncUpWorker. */ - const val TAG = "PlatformParameterWorker.tag" - - /** Value of worker-type associated with this PlatformParameterSyncUpWorker. */ - const val PLATFORM_PARAMETER_WORKER = "platform_parameter_worker" - - /** Key for passing the worker-type as a parameter to PlatformParameterSyncUpWorker. */ - const val WORKER_TYPE_KEY = "worker_type_key" + const val WORKER_NAME = "PlatformParameterSyncUpWorker" } - - override fun startWork(): ListenableFuture { - val backgroundScope = CoroutineScope(backgroundDispatcher) - // TODO(#4463): Add withTimeout() to avoid potential hanging. - return backgroundScope.async { - when (inputData.getStringFromData(WORKER_TYPE_KEY)) { - PLATFORM_PARAMETER_WORKER -> refreshPlatformParameters() - else -> Result.failure() - } - }.asListenableFuture() + enum class Operation(override val persistentName: String) : OppiaWorker.TaskType { + REFRESH_PLATFORM_PARAMETERS("refresh_platform_parameters") } - /** Extracts platform parameters from the remote service and stores them in the cache store. */ - private suspend fun refreshPlatformParameters(): Result { - // This is valid to do per the contract of the returned DataProvider (there will only ever be - // one result from the provider). - val result = platformParameterController.downloadRemoteParameters().retrieveData() - return when (result) { - is AsyncResult.Pending -> { - oppiaLogger.e(TAG, "Unexpected pending state when downloading remote parameters.") - Result.failure() - } - is AsyncResult.Failure -> { - oppiaLogger.e(TAG, "Failed to fetch platform parameters", result.error) - exceptionsController.logNonFatalException( - IllegalStateException("Failed to fetch platform parameters", result.error) - ) - Result.failure() + override suspend fun doWork(taskType: Operation): OppiaWorker.Result { + return when (taskType) { + Operation.REFRESH_PLATFORM_PARAMETERS -> { + // This is valid to do per the contract of the returned DataProvider (there will only ever + // be one result from the provider). + when (val result = platformParameterController.downloadRemoteParameters().retrieveData()) { + is AsyncResult.Pending -> { + oppiaLogger.e( + WORKER_NAME, "Unexpected pending state when downloading remote parameters." + ) + OppiaWorker.Result.FAILURE + } + is AsyncResult.Failure -> { + oppiaLogger.e(WORKER_NAME, "Failed to fetch platform parameters", result.error) + exceptionsController.logNonFatalException( + IllegalStateException("Failed to fetch platform parameters", result.error) + ) + OppiaWorker.Result.FAILURE + } + is AsyncResult.Success -> OppiaWorker.Result.SUCCESS + } } - is AsyncResult.Success -> Result.Success() } } @@ -73,18 +50,15 @@ class PlatformParameterSyncUpWorker private constructor( class Factory @Inject constructor( private val platformParameterController: PlatformParameterController, private val oppiaLogger: OppiaLogger, - private val exceptionsController: ExceptionsController, - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher - ) { - /** Returns new instances of [PlatformParameterSyncUpWorker]. */ - fun create(context: Context, params: WorkerParameters): ListenableWorker { + private val exceptionsController: ExceptionsController + ) : OppiaWorker.Factory { + override val supportedTaskTypes: List = Operation.values().toList() + + override fun createWorker(): OppiaWorker { return PlatformParameterSyncUpWorker( - context, - params, platformParameterController, oppiaLogger, - exceptionsController, - backgroundDispatcher + exceptionsController ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerFactory.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerFactory.kt deleted file mode 100644 index b0da28ae48d..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerFactory.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.oppia.android.domain.platformparameter.syncup - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import javax.inject.Inject - -/** Custom [WorkerFactory] for the [PlatformParameterSyncUpWorker]. */ -class PlatformParameterSyncUpWorkerFactory @Inject constructor( - private val platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorker.Factory -) : WorkerFactory() { - override fun createWorker( - context: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? { - return platformParameterSyncUpWorkerFactory.create(context, workerParameters) - } -} diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerModule.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerModule.kt index ad99b3c3ddd..9ad30a8f09e 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerModule.kt @@ -2,16 +2,25 @@ package org.oppia.android.domain.platformparameter.syncup import dagger.Binds import dagger.Module +import dagger.multibindings.IntoMap import dagger.multibindings.IntoSet -import org.oppia.android.domain.oppialogger.analytics.AnalyticsStartupListener +import dagger.multibindings.StringKey +import org.oppia.android.domain.workmanager.OppiaWorker +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener /** Provides [PlatformParameterSyncUpWorker] related dependencies. */ @Module interface PlatformParameterSyncUpWorkerModule { - @Binds @IntoSet - fun bindLogUploadWorkRequest( - platformParameterSyncUpWorkManagerInitializer: PlatformParameterSyncUpWorkManagerInitializer - ): AnalyticsStartupListener + fun bindPlatformParameterSyncUpWorkerScheduler( + scheduler: PlatformParameterSyncUpWorkerScheduler + ): StartupWorkerScheduleReadinessListener + + @Binds + @IntoMap + @StringKey(PlatformParameterSyncUpWorker.WORKER_NAME) + fun bindLogUploadWorkerFactoryProvider( + factory: PlatformParameterSyncUpWorker.Factory + ): OppiaWorker.Factory<*> } diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerScheduler.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerScheduler.kt new file mode 100644 index 00000000000..26ce5c4ad47 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerScheduler.kt @@ -0,0 +1,31 @@ +package org.oppia.android.domain.platformparameter.syncup + +import androidx.work.Constraints +import androidx.work.NetworkType +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener +import org.oppia.android.domain.workmanager.WorkManagerScheduler +import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.platformparameter.SyncUpWorkerTimePeriodHours +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Enqueues unique periodic work requests for fetching and caching latest platform parameter values + * from the remote service on application creation. + */ +class PlatformParameterSyncUpWorkerScheduler @Inject constructor( + @SyncUpWorkerTimePeriodHours private val workRequestRepeatInterval: PlatformParameterValue +) : StartupWorkerScheduleReadinessListener { + override fun scheduleWork(workManagerScheduler: WorkManagerScheduler) { + workManagerScheduler.schedulePeriodicWorker( + PlatformParameterSyncUpWorker.WORKER_NAME, + PlatformParameterSyncUpWorker.Operation.REFRESH_PLATFORM_PARAMETERS, + workRequestRepeatInterval.value.toLong(), + TimeUnit.HOURS, + constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + ) + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/BUILD.bazel deleted file mode 100644 index c2714676669..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/BUILD.bazel +++ /dev/null @@ -1,18 +0,0 @@ -""" -Package for testing utilities for log uploading functionality. -""" - -load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") - -kt_android_library( - name = "fake_log_uploader", - testonly = True, - srcs = [ - "FakeLogUploader.kt", - ], - visibility = ["//:oppia_testing_visibility"], - deps = [ - "//third_party:javax_inject_javax_inject", - "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", - ], -) diff --git a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt b/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt deleted file mode 100644 index a6a3aae43b1..00000000000 --- a/domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.oppia.android.domain.testing.oppialogger.loguploader - -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.oppia.android.util.logging.LogUploader -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -/** A test specific fake for the log uploader. */ -@Singleton -class FakeLogUploader @Inject constructor() : LogUploader { - private val eventRequestIdList = mutableListOf() - private val exceptionRequestIdList = mutableListOf() - private val performanceMetricsRequestIdList = mutableListOf() - private val firestoreRequestIdList = mutableListOf() - - override fun enqueueWorkRequestForEvents( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - eventRequestIdList.add(workRequest.id) - } - - override fun enqueueWorkRequestForExceptions( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - exceptionRequestIdList.add(workRequest.id) - } - - override fun enqueueWorkRequestForPerformanceMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - performanceMetricsRequestIdList.add(workRequest.id) - } - - override fun enqueueWorkRequestForFirestore( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - firestoreRequestIdList.add(workRequest.id) - } - - /** Returns the most recent work request id that's stored in the [eventRequestIdList]. */ - fun getMostRecentEventRequestId() = eventRequestIdList.last() - - /** Returns the most recent work request id that's stored in the [exceptionRequestIdList]. */ - fun getMostRecentExceptionRequestId() = exceptionRequestIdList.last() - - /** Returns the most recent work request id that's stored in the [performanceMetricsRequestIdList]. */ - fun getMostRecentPerformanceMetricsRequestId() = performanceMetricsRequestIdList.last() - - /** Returns the most recent work request id that's stored in the [firestoreRequestIdList]. */ - fun getMostRecentFirestoreRequestId() = firestoreRequestIdList.last() -} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/workmanager/BUILD.bazel new file mode 100644 index 00000000000..fdec8bb1d48 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/BUILD.bazel @@ -0,0 +1,71 @@ +""" +Package for supporting Dagger bindings for work manager worker factories. +""" + +load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") + +kt_android_library( + name = "oppia_worker", + srcs = ["OppiaWorker.kt"], + visibility = ["//:oppia_api_visibility"], + deps = ["//third_party:androidx_work_work-runtime"], +) + +kt_android_library( + name = "startup_worker_schedule_readiness_listener", + srcs = ["StartupWorkerScheduleReadinessListener.kt"], + visibility = ["//:oppia_api_visibility"], + deps = [":work_manager_scheduler"], +) + +kt_android_library( + name = "startup_worker_schedule_readiness_monitor", + srcs = ["StartupWorkerScheduleReadinessMonitor.kt"], + deps = [ + ":startup_worker_schedule_readiness_listener", + ":work_manager_scheduler", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + ], +) + +kt_android_library( + name = "bootstrap_oppia_worker", + srcs = ["BootstrapOppiaWorker.kt"], + deps = [ + ":oppia_worker", + "//domain/src/main/java/org/oppia/android/domain/platformparameter:controller", + "//domain/src/main/java/org/oppia/android/domain/platformparameter:platform_parameter_controller_injector", + "//domain/src/main/java/org/oppia/android/domain/platformparameter:platform_parameter_controller_injector_provider", + "//domain/src/main/java/org/oppia/android/domain/util:extensions", + "//third_party:androidx_work_work-runtime", + "//third_party:com_google_guava_guava", + "//third_party:javax_inject_javax_inject", + "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-guava", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector_provider", + ], +) + +kt_android_library( + name = "work_manager_configuration_module", + srcs = ["WorkManagerConfigurationModule.kt"], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":bootstrap_oppia_worker", + ":oppia_worker", + ":startup_worker_schedule_readiness_monitor", + "//:dagger", + ], +) + +kt_android_library( + name = "work_manager_scheduler", + srcs = ["WorkManagerScheduler.kt"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":bootstrap_oppia_worker", + ":oppia_worker", + "//third_party:androidx_work_work-runtime", + ], +) diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/BootstrapOppiaWorker.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/BootstrapOppiaWorker.kt new file mode 100644 index 00000000000..68c717753dd --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/BootstrapOppiaWorker.kt @@ -0,0 +1,130 @@ +package org.oppia.android.domain.workmanager + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.google.common.util.concurrent.ListenableFuture +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.guava.asListenableFuture +import org.oppia.android.domain.platformparameter.PlatformParameterController +import org.oppia.android.domain.platformparameter.PlatformParameterControllerInjectorProvider +import org.oppia.android.domain.util.getStringFromData +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.threading.DispatcherInjectorProvider +import javax.inject.Inject +import javax.inject.Provider + +// TODO: Mention that the private constructor is intentional to prevent WM from trying to construct the class. +// TODO: Mention that the strategy for periodic jobs should be just to reschedule since this aggressively cancels them when renamed or changed. +class BootstrapOppiaWorker private constructor( + private val appContext: Context, + private val requestedWorkerName: String, + workerParams: WorkerParameters, + private val consoleLogger: ConsoleLogger, + private val oppiaWorkerFactories: Map>> +) : ListenableWorker(appContext, workerParams) { + override fun startWork(): ListenableFuture { + // TODO(#4463): Add withTimeout() to avoid potential hanging. + return CoroutineScope(getBackgroundDispatcher()).async { + startWorkInBackground() + }.asListenableFuture() + } + + private suspend fun startWorkInBackground(): Result { + // Validate if this is even the correct worker. If this fails it's likely due to a custom worker + // that isn't using the bootstrapper or a request to run an old worker that should now be + // migrated to use the bootstrap worker. + if (requestedWorkerName != javaClass.name) { + consoleLogger.e( + "BootstrapOppiaWorker", + "Attempting to bootstrap old or invalid worker class: $requestedWorkerName." + + " Cancelling worker to prevent repeated failures." + ) + WorkManager.getInstance(appContext).cancelWorkById(id) + return Result.failure() + } + + // This may be the very beginning of the app starting up so platform parameters must be fully + // initialized before a worker can even be created to do work (technically even its factory + // cannot be created since a worker's factory directly depends on the worker's required + // dependencies which may transitively require platform parameters to be loaded). + getPlatformParameterController().loadParametersAsync().await() + + // Retrieve the actual worker name being started and the corresponding delegate factory + // provider. These should always be present unless there's an old bootstrap-compatible worker + // that was previously scheduled, but has since been removed or renamed. + val delegatedWorkerName = inputData.getStringFromData(DELEGATED_WORKER_NAME_INPUT_KEY) + val oppiaWorkerFactoryProvider = oppiaWorkerFactories[delegatedWorkerName] + if (delegatedWorkerName == null || oppiaWorkerFactoryProvider == null) { + consoleLogger.e( + "BootstrapOppiaWorker", + "Attempting to bootstrap for an invalid worker delegate: $delegatedWorkerName (no" + + " provider found). Cancelling worker to prevent repeated failures." + ) + WorkManager.getInstance(appContext).cancelWorkById(id) + return Result.failure() + } + + // Construct the factory first, then try to fetch the type of task attempting to be run. + val factory = oppiaWorkerFactoryProvider.get() + val taskTypeNameKey = constructTaskTypeKey(delegatedWorkerName) + val taskTypeName = inputData.getStringFromData(taskTypeNameKey) + + // Delegate execution to a factory helper to simplify type safety. + val result = taskTypeName?.let { factory.doWorkForTaskName(it) } + if (result == null) { + // If the task type has an incompatibility then err on the side of canceling the task in case + // there was some sort if incompatible change in the worker's API. Since the bootstrap + // worker's contract is to encourage rescheduling, any valid worker that reaches a failure= + // here should hopefully self-correct itself with a future reschedule. + consoleLogger.e( + "BootstrapOppiaWorker", + "Encountered invalid task type when trying to prepare worker $delegatedWorkerName:" + + " $taskTypeName. Cancelling worker to prevent repeated failures." + ) + WorkManager.getInstance(appContext).cancelWorkById(id) + return Result.failure() + } + + return when (result) { + OppiaWorker.Result.SUCCESS -> Result.success() + OppiaWorker.Result.FAILURE -> Result.failure() + } + } + + private fun getPlatformParameterController(): PlatformParameterController { + val injectorProvider = appContext as PlatformParameterControllerInjectorProvider + val injector = injectorProvider.getPlatformParameterControllerInjector() + return injector.getPlatformParameterController() + } + + private fun getBackgroundDispatcher(): CoroutineDispatcher { + val injectorProvider = appContext as DispatcherInjectorProvider + val injector = injectorProvider.getDispatcherInjector() + return injector.getBackgroundDispatcher() + } + + companion object { + const val DELEGATED_WORKER_NAME_INPUT_KEY = "BootstrapOppiaWorker.delegated_worker_name" + + fun constructTaskTypeKey(workerName: String): String = "$workerName.TASK_TYPE_KEY" + } + + class Factory @Inject constructor( + private val context: Context, + private val consoleLogger: ConsoleLogger, + private val workerFactories: Map>> + ) { + fun createBootstrapWorker( + requestedWorkerName: String, + workerParams: WorkerParameters + ): BootstrapOppiaWorker { + return BootstrapOppiaWorker( + context, requestedWorkerName, workerParams, consoleLogger, workerFactories + ) + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/OppiaWorker.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/OppiaWorker.kt new file mode 100644 index 00000000000..0dac98e9fdf --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/OppiaWorker.kt @@ -0,0 +1,26 @@ +package org.oppia.android.domain.workmanager + +interface OppiaWorker { + suspend fun doWork(taskType: T): Result + + enum class Result { + SUCCESS, + FAILURE + } + + interface TaskType { + // TODO: Document that this needs to survive across different obfuscation sessions. It doesn't necessarily need to be stable across job runs, though. + val persistentName: String + } + + interface Factory { + val supportedTaskTypes: List + + fun createWorker(): OppiaWorker + + suspend fun doWorkForTaskName(taskName: String): Result? { + val taskType = supportedTaskTypes.find { taskName == it.persistentName } ?: return null + return createWorker().doWork(taskType) + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessListener.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessListener.kt new file mode 100644 index 00000000000..19a8e18099d --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessListener.kt @@ -0,0 +1,17 @@ +package org.oppia.android.domain.workmanager + +/** Listeners which will receive a prompt to start scheduling workers for periodic execution. */ +interface StartupWorkerScheduleReadinessListener { + /** + * Called early in application startup to allow periodic work to be scheduled. + * + * Note that no guarantees can be called about when in the application initialization this is + * called, but it is guaranteed to be called shortly after initialization. Between that and the + * nature of `WorkManager` and Android OS job behaviors, no work that actually synchronizes with + * application startup state should ever be scheduled through this method (or via `WorkManager` at + * all). + * + * This method is guaranteed to be called exactly once per application instance. + */ + fun scheduleWork(workManagerScheduler: WorkManagerScheduler) +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessMonitor.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessMonitor.kt new file mode 100644 index 00000000000..31c18dacf18 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessMonitor.kt @@ -0,0 +1,26 @@ +package org.oppia.android.domain.workmanager + +import org.oppia.android.domain.oppialogger.ApplicationStartupListener +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +class StartupWorkerScheduleReadinessMonitor @Inject constructor( + private val workManagerScheduler: WorkManagerScheduler, + private val listsProv: Provider> +) : ApplicationStartupListener { + override fun onCreateStarted() { + // Do nothing. It's not yet safe to initialize the startup listeners and schedule workers since + // the schedulers themselves may transitively depend on platform parameters. It's also fine to + // wait to schedule these until full initialization since there should be no cross-syncing + // happening. + } + + override fun onCompletedInitialization() { + // Now it's okay to initialize the listener instances and allow them to start scheduling work. + for (startupWorkerScheduleReadinessListener in listsProv.get()) { + startupWorkerScheduleReadinessListener.scheduleWork(workManagerScheduler) + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerConfigurationModule.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerConfigurationModule.kt index 11fa8e5f82f..bc042207233 100644 --- a/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerConfigurationModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerConfigurationModule.kt @@ -1,29 +1,71 @@ package org.oppia.android.domain.workmanager +import android.content.Context import androidx.work.Configuration -import androidx.work.DelegatingWorkerFactory +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import dagger.Binds import dagger.Module import dagger.Provides -import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulingWorkerFactory -import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerFactory -import org.oppia.android.domain.platformparameter.syncup.PlatformParameterSyncUpWorkerFactory +import dagger.multibindings.IntoSet +import dagger.multibindings.Multibinds +import org.oppia.android.domain.oppialogger.ApplicationStartupListener import javax.inject.Singleton /** Provides [Configuration] for the work manager. */ @Module -class WorkManagerConfigurationModule { +interface WorkManagerConfigurationModule { + @Multibinds + fun bindOppiaWorkerFactories(): Map> - @Singleton - @Provides - fun provideWorkManagerConfiguration( - logUploadWorkerFactory: LogUploadWorkerFactory, - platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory, - metricLogSchedulingWorkerFactory: MetricLogSchedulingWorkerFactory - ): Configuration { - val delegatingWorkerFactory = DelegatingWorkerFactory() - delegatingWorkerFactory.addFactory(logUploadWorkerFactory) - delegatingWorkerFactory.addFactory(platformParameterSyncUpWorkerFactory) - delegatingWorkerFactory.addFactory(metricLogSchedulingWorkerFactory) - return Configuration.Builder().setWorkerFactory(delegatingWorkerFactory).build() + @Multibinds + fun bindStartupWorkerScheduleReadinessListeners(): Set + + @Binds + @IntoSet + fun bindStartupWorkerScheduleReadinessMonitorAsStartupListener( + monitor: StartupWorkerScheduleReadinessMonitor + ): ApplicationStartupListener + + companion object ConfigurationCreationModule { + @Provides + @Singleton + fun provideWorkManagerConfiguration( + bootstrapWorkerFactory: BootstrapOppiaWorker.Factory + ): Configuration { + // The app uses only one worker factory since all work is bootstrapped. + val workerFactory = object : WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + // The only worker the app uses is the bootstrap one, and so all work requests should be + // directed to it even if it's not the worker being requested. This inconsistency can + // happen for old queued jobs before the worker was created and the worker is designed to + // check to make sure the correct class is being requested before proceeding. However, + // there's one set of counter cases: WorkManager's own internal workers route through this + // factory. There's no reliable way to check for those other than validating the class is + // a real class loadable in the app and that it's a ListenableFuture. This combined with a + // regex content check should prevent any Oppia workers from being ListenableWorker + // directly. + val workerExistsInApk = try { + ListenableWorker::class.java.isAssignableFrom(Class.forName(workerClassName)) + } catch (e: ClassNotFoundException) { false } + if (workerExistsInApk && workerClassName != BootstrapOppiaWorker::class.java.name) { + // Existing workers that aren't the bootstrap worker must be handled through reflection + // since they're almost certainly WorkManager internal workers. Note that the + // ListenableWorker check is necessary since there may be worker classes that previously + // were scheduled as ListenableWorkers but are now run through the bootstrap worker (and + // haven't been renamed). + return null + } + + return bootstrapWorkerFactory.createBootstrapWorker(workerClassName, workerParameters) + } + } + return Configuration.Builder().setWorkerFactory(workerFactory).build() + } } } diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerScheduler.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerScheduler.kt new file mode 100644 index 00000000000..3aa7e937be8 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerScheduler.kt @@ -0,0 +1,41 @@ +package org.oppia.android.domain.workmanager + +import android.content.Context +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class WorkManagerScheduler @Inject constructor(context: Context) { + private val workManager: WorkManager by lazy { WorkManager.getInstance(context) } + + fun schedulePeriodicWorker( + workerName: String, + taskType: OppiaWorker.TaskType, + repeatInterval: Long, + intervalUnit: TimeUnit, + constraints: Constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build(), + existingPeriodicWorkPolicy: ExistingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP + ) { + val workName = "$workerName.${taskType.persistentName}" + val taskTypeKey = BootstrapOppiaWorker.constructTaskTypeKey(workerName) + val inputData = Data.Builder().apply { + putString(BootstrapOppiaWorker.DELEGATED_WORKER_NAME_INPUT_KEY, workerName) + putString(taskTypeKey, taskType.persistentName) + }.build() + val request = PeriodicWorkRequest.Builder( + BootstrapOppiaWorker::class.java, repeatInterval, intervalUnit + ).addTag(workName) + .setConstraints(constraints) + .setInputData(inputData) + .build() + workManager.enqueueUniquePeriodicWork(workName, existingPeriodicWorkPolicy, request) + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/debug/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/BUILD.bazel new file mode 100644 index 00000000000..083e9b3c376 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/BUILD.bazel @@ -0,0 +1,38 @@ +""" +Library for providing a debug work manager worker that can be used for validating the work manager +configuration in the app. +""" + +load("@io_bazel_rules_kotlin//kotlin:android.bzl", "kt_android_library") + +kt_android_library( + name = "scheduler", + srcs = ["DebugWorkerScheduler.kt"], + deps = [ + ":worker", + "//domain/src/main/java/org/oppia/android/domain/workmanager:startup_worker_schedule_readiness_listener", + ], +) + +kt_android_library( + name = "worker", + srcs = ["DebugWorker.kt"], + deps = [ + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", + "//third_party:androidx_work_work-runtime-ktx", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + ], +) + +kt_android_library( + name = "debug_module", + srcs = ["DebugWorkerDebugModule.kt"], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":scheduler", + ":worker", + "//:dagger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", + "//domain/src/main/java/org/oppia/android/domain/workmanager:oppia_worker", + ], +) diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorker.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorker.kt new file mode 100644 index 00000000000..c69b57ae571 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorker.kt @@ -0,0 +1,47 @@ +package org.oppia.android.domain.workmanager.debug + +import org.oppia.android.domain.workmanager.OppiaWorker +import org.oppia.android.util.logging.ConsoleLogger +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class DebugWorker private constructor( + private val consoleLogger: ConsoleLogger +) : OppiaWorker { + override suspend fun doWork(taskType: Operation): OppiaWorker.Result { + consoleLogger.d(WORKER_NAME, "Debug worker ran with config: $taskType.") + return OppiaWorker.Result.SUCCESS + } + + enum class Operation( + val period: Long, + val periodUnit: TimeUnit, + val requireConnectivity: Boolean + ) : OppiaWorker.TaskType { + RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY( + period = 15, periodUnit = TimeUnit.MINUTES, requireConnectivity = true + ), + RUN_EVERY_TWENTY_MINUTES_WITH_OR_WITHOUT_CONNECTIVITY( + period = 20, periodUnit = TimeUnit.MINUTES, requireConnectivity = false + ), + RUN_EVERY_SIX_HOURS_WITH_OR_WITHOUT_CONNECTIVITY( + period = 6, periodUnit = TimeUnit.HOURS, requireConnectivity = false + ); + + // It's safe for this to just be the enum's name because it won't ever be minified since this + // worker only goes in debug builds. + override val persistentName = name + } + + class Factory @Inject constructor( + private val consoleLogger: ConsoleLogger + ) : OppiaWorker.Factory { + override val supportedTaskTypes: List = Operation.values().toList() + + override fun createWorker(): OppiaWorker = DebugWorker(consoleLogger) + } + + companion object { + const val WORKER_NAME = "DebugWorker" + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorkerDebugModule.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorkerDebugModule.kt new file mode 100644 index 00000000000..3a160b75414 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorkerDebugModule.kt @@ -0,0 +1,23 @@ +package org.oppia.android.domain.workmanager.debug + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet +import dagger.multibindings.StringKey +import org.oppia.android.domain.workmanager.OppiaWorker +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener + +@Module +interface DebugWorkerDebugModule { + @Binds + @IntoSet + fun bindDebugWorkerScheduler( + scheduler: DebugWorkerScheduler + ): StartupWorkerScheduleReadinessListener + + @Binds + @IntoMap + @StringKey(DebugWorker.WORKER_NAME) + fun bindDebugWorkerFactoryProvider(factory: DebugWorker.Factory): OppiaWorker.Factory<*> +} diff --git a/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorkerScheduler.kt b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorkerScheduler.kt new file mode 100644 index 00000000000..b3bdb38ac83 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/workmanager/debug/DebugWorkerScheduler.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.workmanager.debug + +import androidx.work.Constraints +import androidx.work.NetworkType +import org.oppia.android.domain.workmanager.StartupWorkerScheduleReadinessListener +import org.oppia.android.domain.workmanager.WorkManagerScheduler +import org.oppia.android.domain.workmanager.debug.DebugWorker.Companion.WORKER_NAME +import javax.inject.Inject + +class DebugWorkerScheduler @Inject constructor() : StartupWorkerScheduleReadinessListener { + override fun scheduleWork(workManagerScheduler: WorkManagerScheduler) { + for (operation in DebugWorker.Operation.values()) { + workManagerScheduler.schedulePeriodicWorker( + WORKER_NAME, + operation, + operation.period, + operation.periodUnit, + constraints = if (operation.requireConnectivity) { + REQUIRE_CONNECTIVITY_CONSTRAINT + } else UNREQUIRED_CONNECTIVITY_CONSTRAINT + ) + } + } + + private companion object { + private val UNREQUIRED_CONNECTIVITY_CONSTRAINT = Constraints.Builder() + .setRequiredNetworkType(NetworkType.NOT_REQUIRED) + .setRequiresBatteryNotLow(true) + .build() + + private val REQUIRE_CONNECTIVITY_CONSTRAINT = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/testing/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/testing/BUILD.bazel deleted file mode 100644 index 533770e2c80..00000000000 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/testing/BUILD.bazel +++ /dev/null @@ -1,38 +0,0 @@ -""" -Tests for test-only app analytics logging support components. -""" - -load("//:oppia_android_test.bzl", "oppia_android_test") - -oppia_android_test( - name = "FakeLogSchedulerTest", - srcs = ["FakeLogSchedulerTest.kt"], - custom_package = "org.oppia.android.domain.oppialogger.analytics.testing", - test_class = "org.oppia.android.domain.oppialogger.analytics.testing.FakeLogSchedulerTest", - test_manifest = "//domain:test_manifest", - deps = [ - "//:dagger", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:cpu_module", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing:fake_log_scheduler", - "//testing", - "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", - "//testing/src/main/java/org/oppia/android/testing/platformparameter:test_module", - "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", - "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", - "//testing/src/main/java/org/oppia/android/testing/threading:test_module", - "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", - "//third_party:androidx_work_work-testing", - "//third_party:com_google_truth_truth", - "//third_party:junit_junit", - "//third_party:org_mockito_mockito-core", - "//third_party:org_robolectric_robolectric", - "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", - "//utility/src/main/java/org/oppia/android/util/locale:prod_module", - "//utility/src/main/java/org/oppia/android/util/logging/performancemetrics:performance_metrics_configurations_module", - "//utility/src/main/java/org/oppia/android/util/networking:debug_module", - "//utility/src/main/java/org/oppia/android/util/networking:prod_module", - ], -) diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/testing/FakeLogSchedulerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/testing/FakeLogSchedulerTest.kt deleted file mode 100644 index 216221a986a..00000000000 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/testing/FakeLogSchedulerTest.kt +++ /dev/null @@ -1,222 +0,0 @@ -package org.oppia.android.domain.oppialogger.analytics.testing - -import android.app.Application -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.work.Configuration -import androidx.work.Data -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.testing.SynchronousExecutor -import androidx.work.testing.WorkManagerTestInitHelper -import com.google.common.truth.Truth.assertThat -import dagger.Binds -import dagger.BindsInstance -import dagger.Component -import dagger.Module -import dagger.Provides -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize -import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize -import org.oppia.android.domain.oppialogger.LoggingIdentifierModule -import org.oppia.android.domain.oppialogger.PerformanceMetricsLogStorageCacheSize -import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule -import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule -import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulingWorker -import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulingWorkerFactory -import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorker -import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.testing.TestLogReportingModule -import org.oppia.android.testing.platformparameter.TestPlatformParameterModule -import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.data.DataProvidersInjector -import org.oppia.android.util.data.DataProvidersInjectorProvider -import org.oppia.android.util.locale.LocaleProdModule -import org.oppia.android.util.logging.LoggerModule -import org.oppia.android.util.logging.MetricLogScheduler -import org.oppia.android.util.logging.SyncStatusModule -import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule -import org.oppia.android.util.networking.NetworkConnectionUtilProdModule -import org.oppia.android.util.system.OppiaClockModule -import org.robolectric.annotation.Config -import org.robolectric.annotation.LooperMode -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton - -/** Tests for [FakeLogScheduler]. */ -// FunctionName: test names are conventionally named with underscores. -@Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -@Config(application = FakeLogSchedulerTest.TestApplication::class) -class FakeLogSchedulerTest { - - @Inject - lateinit var fakeLogScheduler: FakeLogScheduler - - @Inject - lateinit var workerFactory: MetricLogSchedulingWorkerFactory - - @Inject - lateinit var context: Context - - @Before - fun setUp() { - setUpTestApplicationComponent() - val config = Configuration.Builder() - .setExecutor(SynchronousExecutor()) - .setWorkerFactory(workerFactory) - .build() - WorkManagerTestInitHelper.initializeTestWorkManager(context, config) - } - - @Test - fun testFakeScheduler_scheduleStorageLogging_verifyScheduling() { - val workManager = WorkManager.getInstance(context) - - val inputData = Data.Builder().putString( - MetricLogSchedulingWorker.WORKER_CASE_KEY, - MetricLogSchedulingWorker.STORAGE_USAGE_WORKER - ).build() - - val request = PeriodicWorkRequestBuilder(10, TimeUnit.SECONDS) - .setInputData(inputData) - .build() - - fakeLogScheduler.enqueueWorkRequestForStorageUsage( - workManager, - request - ) - - assertThat(fakeLogScheduler.getMostRecentStorageUsageMetricLoggingRequestId()) - .isEqualTo(request.id) - } - - @Test - fun testFakeScheduler_schedulePeriodicBackgroundMetricsLogging_verifyScheduling() { - val workManager = WorkManager.getInstance(context) - - val inputData = Data.Builder().putString( - MetricLogSchedulingWorker.WORKER_CASE_KEY, - MetricLogSchedulingWorker.PERIODIC_BACKGROUND_METRIC_WORKER - ).build() - - val request = PeriodicWorkRequestBuilder(10, TimeUnit.SECONDS) - .setInputData(inputData) - .build() - - fakeLogScheduler.enqueueWorkRequestForPeriodicBackgroundMetrics( - workManager, - request - ) - - assertThat(fakeLogScheduler.getMostRecentPeriodicBackgroundMetricLoggingRequestId()) - .isEqualTo(request.id) - } - - @Test - fun testFakeScheduler_schedulePeriodicUiMetricsLogging_verifyScheduling() { - val workManager = WorkManager.getInstance(context) - - val inputData = Data.Builder().putString( - MetricLogSchedulingWorker.WORKER_CASE_KEY, - MetricLogSchedulingWorker.PERIODIC_UI_METRIC_WORKER - ).build() - - val request = PeriodicWorkRequestBuilder(10, TimeUnit.SECONDS) - .setInputData(inputData) - .build() - - fakeLogScheduler.enqueueWorkRequestForPeriodicUiMetrics( - workManager, - request - ) - - assertThat(fakeLogScheduler.getMostRecentPeriodicUiMetricLoggingRequestId()) - .isEqualTo(request.id) - } - - private fun setUpTestApplicationComponent() { - ApplicationProvider.getApplicationContext().inject(this) - } - - // TODO(#89): Move this to a common test application component. - @Module - interface TestModule { - @Binds - fun provideContext(application: Application): Context - - @Binds - fun bindMetricLogScheduler(fakeLogScheduler: FakeLogScheduler): MetricLogScheduler - } - - @Module - class TestLogStorageModule { - - @Provides - @EventLogStorageCacheSize - fun provideEventLogStorageCacheSize(): Int = 2 - - @Provides - @ExceptionLogStorageCacheSize - fun provideExceptionLogStorageSize(): Int = 2 - - @Provides - @PerformanceMetricsLogStorageCacheSize - fun providePerformanceMetricsLogStorageCacheSize(): Int = 2 - } - - // TODO(#89): Move this to a common test application component. - @Singleton - @Component( - modules = [ - ApplicationLifecycleModule::class, - AssetModule::class, - CpuPerformanceSnapshotterModule::class, - LocaleProdModule::class, - LoggerModule::class, - LoggingIdentifierModule::class, - NetworkConnectionUtilProdModule::class, - OppiaClockModule::class, - PerformanceMetricsConfigurationsModule::class, - PlatformParameterSingletonModule::class, - RobolectricModule::class, - SyncStatusModule::class, - TestDispatcherModule::class, - TestLogReportingModule::class, - TestLogStorageModule::class, - TestModule::class, - TestPlatformParameterModule::class - ] - ) - interface TestApplicationComponent : DataProvidersInjector { - @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder - fun build(): TestApplicationComponent - } - - fun inject(test: FakeLogSchedulerTest) - } - - class TestApplication : Application(), DataProvidersInjectorProvider { - private val component: TestApplicationComponent by lazy { - DaggerFakeLogSchedulerTest_TestApplicationComponent.builder() - .setApplication(this) - .build() - } - - fun inject(test: FakeLogSchedulerTest) { - component.inject(test) - } - - override fun getDataProvidersInjector(): DataProvidersInjector = component - } -} diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogSchedulerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerSchedulerTest.kt similarity index 88% rename from domain/src/test/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogSchedulerTest.kt rename to domain/src/test/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerSchedulerTest.kt index 96815794e31..101180e8f2c 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogSchedulerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerSchedulerTest.kt @@ -45,16 +45,16 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton -/** Tests for [PerformanceMetricsLogScheduler]. */ +/** Tests for [MetricLogSchedulingWorkerScheduler]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(application = PerformanceMetricsLogSchedulerTest.TestApplication::class) -class PerformanceMetricsLogSchedulerTest { +@Config(application = MetricLogSchedulingWorkerSchedulerTest.TestApplication::class) +class MetricLogSchedulingWorkerSchedulerTest { @Inject - lateinit var performanceMetricsLogScheduler: PerformanceMetricsLogScheduler + lateinit var metricLogSchedulingWorkerScheduler: MetricLogSchedulingWorkerScheduler @Inject lateinit var metricLogSchedulingWorkerFactory: MetricLogSchedulingWorkerFactory @@ -106,7 +106,7 @@ class PerformanceMetricsLogSchedulerTest { .setInputData(workerCaseForSchedulingPeriodicBackgroundMetricLogs) .build() - performanceMetricsLogScheduler.enqueueWorkRequestForPeriodicBackgroundMetrics( + metricLogSchedulingWorkerScheduler.enqueueWorkRequestForPeriodicBackgroundMetrics( workManager, request ) @@ -125,7 +125,7 @@ class PerformanceMetricsLogSchedulerTest { .setInputData(workerCaseForSchedulingPeriodicUiMetricLogs) .build() - performanceMetricsLogScheduler.enqueueWorkRequestForPeriodicUiMetrics( + metricLogSchedulingWorkerScheduler.enqueueWorkRequestForPeriodicUiMetrics( workManager, request ) @@ -144,7 +144,7 @@ class PerformanceMetricsLogSchedulerTest { .setInputData(workerCaseForSchedulingStorageUsageMetricLogs) .build() - performanceMetricsLogScheduler.enqueueWorkRequestForStorageUsage( + metricLogSchedulingWorkerScheduler.enqueueWorkRequestForStorageUsage( workManager, request ) @@ -155,7 +155,7 @@ class PerformanceMetricsLogSchedulerTest { } private fun setUpTestApplicationComponent() { - DaggerPerformanceMetricsLogSchedulerTest_TestApplicationComponent.builder() + DaggerMetricLogSchedulingWorkerSchedulerTest_TestApplicationComponent.builder() .setApplication(ApplicationProvider.getApplicationContext()) .build() .inject(this) @@ -202,18 +202,18 @@ class PerformanceMetricsLogSchedulerTest { fun build(): TestApplicationComponent } - fun inject(performanceMetricsLogSchedulerTest: PerformanceMetricsLogSchedulerTest) + fun inject(metricLogSchedulingWorkerSchedulerTest: MetricLogSchedulingWorkerSchedulerTest) } class TestApplication : Application(), DataProvidersInjectorProvider { private val component: TestApplicationComponent by lazy { - DaggerPerformanceMetricsLogSchedulerTest_TestApplicationComponent.builder() + DaggerMetricLogSchedulingWorkerSchedulerTest_TestApplicationComponent.builder() .setApplication(this) .build() } - fun inject(performanceMetricsLogSchedulerTest: PerformanceMetricsLogSchedulerTest) { - component.inject(performanceMetricsLogSchedulerTest) + fun inject(metricLogSchedulingWorkerSchedulerTest: MetricLogSchedulingWorkerSchedulerTest) { + component.inject(metricLogSchedulingWorkerSchedulerTest) } override fun getDataProvidersInjector(): DataProvidersInjector = component diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel index 23ec337da67..3cbc693d999 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/BUILD.bazel @@ -6,7 +6,7 @@ load("//:oppia_android_test.bzl", "oppia_android_test") oppia_android_test( name = "LogReportWorkManagerInitializerTest", - srcs = ["LogReportWorkManagerInitializerTest.kt"], + srcs = ["LogReportWorkerSchedulerTest.kt"], custom_package = "org.oppia.android.domain.oppialogger.loguploader", test_class = "org.oppia.android.domain.oppialogger.loguploader.LogReportWorkManagerInitializerTest", test_manifest = "//domain:test_manifest", @@ -15,11 +15,9 @@ oppia_android_test( "//domain", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:cpu_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/testing:fake_log_scheduler", - "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:initializer", + "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:scheduler", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader:fake_log_uploader", "//testing", "//testing/src/main/java/org/oppia/android/testing/platformparameter:test_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", @@ -33,7 +31,6 @@ oppia_android_test( "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/locale:prod_module", - "//utility/src/main/java/org/oppia/android/util/logging:metric_log_scheduler", "//utility/src/main/java/org/oppia/android/util/networking:debug_module", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_debug_util", ], @@ -51,7 +48,6 @@ oppia_android_test( "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader:fake_log_uploader", "//testing", "//testing/src/main/java/org/oppia/android/testing/logging:sync_status_test_module", "//testing/src/main/java/org/oppia/android/testing/logging:test_sync_status_manager", diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerSchedulerTest.kt similarity index 87% rename from domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt rename to domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerSchedulerTest.kt index ef92acaf13c..989b2b4997a 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerSchedulerTest.kt @@ -62,8 +62,8 @@ import javax.inject.Singleton @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(application = LogReportWorkManagerInitializerTest.TestApplication::class) -class LogReportWorkManagerInitializerTest { +@Config(application = LogReportWorkerSchedulerTest.TestApplication::class) +class LogReportWorkerSchedulerTest { @Inject lateinit var logUploadWorkerFactory: LogUploadWorkerFactory @@ -72,7 +72,7 @@ class LogReportWorkManagerInitializerTest { lateinit var metricLogSchedulingWorkerFactory: MetricLogSchedulingWorkerFactory @Inject - lateinit var logReportWorkManagerInitializer: LogReportWorkManagerInitializer + lateinit var logReportWorkerScheduler: LogReportWorkerScheduler @Inject lateinit var exceptionsController: ExceptionsController @@ -118,23 +118,23 @@ class LogReportWorkManagerInitializerTest { @Test fun testWorkRequest_onCreate_enqueuesRequest_verifyRequestId() { - logReportWorkManagerInitializer.onCreate(WorkManager.getInstance(context)) + logReportWorkerScheduler.scheduleWork(WorkManager.getInstance(context)) testCoroutineDispatchers.runCurrent() - val enqueuedEventWorkRequestId = logReportWorkManagerInitializer.getWorkRequestForEventsId() + val enqueuedEventWorkRequestId = logReportWorkerScheduler.getWorkRequestForEventsId() val enqueuedExceptionWorkRequestId = - logReportWorkManagerInitializer.getWorkRequestForExceptionsId() + logReportWorkerScheduler.getWorkRequestForExceptionsId() val enqueuedPerformanceMetricsWorkRequestId = - logReportWorkManagerInitializer.getWorkRequestForPerformanceMetricsId() + logReportWorkerScheduler.getWorkRequestForPerformanceMetricsId() val enqueuedSchedulingStorageUsageMetricWorkRequestId = - logReportWorkManagerInitializer.getWorkRequestForSchedulingStorageUsageMetricLogsId() + logReportWorkerScheduler.getWorkRequestForSchedulingStorageUsageMetricLogsId() val enqueuedSchedulingPeriodicUiMetricWorkRequestId = - logReportWorkManagerInitializer.getWorkRequestForSchedulingPeriodicUiMetricLogsId() + logReportWorkerScheduler.getWorkRequestForSchedulingPeriodicUiMetricLogsId() val enqueuedSchedulingPeriodicBackgroundPerformanceMetricWorkRequestId = - logReportWorkManagerInitializer + logReportWorkerScheduler .getWorkRequestForSchedulingPeriodicBackgroundPerformanceMetricLogsId() val enqueuedFirestoreWorkRequestId = - logReportWorkManagerInitializer.getWorkRequestForFirestoreId() + logReportWorkerScheduler.getWorkRequestForFirestoreId() assertThat(fakeLogUploader.getMostRecentEventRequestId()).isEqualTo(enqueuedEventWorkRequestId) assertThat(fakeLogUploader.getMostRecentExceptionRequestId()).isEqualTo( @@ -165,7 +165,7 @@ class LogReportWorkManagerInitializerTest { .build() val logUploadingWorkRequestConstraints = - logReportWorkManagerInitializer.getLogReportWorkerConstraints() + logReportWorkerScheduler.getLogReportWorkerConstraints() assertThat(logUploadingWorkRequestConstraints).isEqualTo(workerConstraints) } @@ -179,7 +179,7 @@ class LogReportWorkManagerInitializerTest { ) .build() - assertThat(logReportWorkManagerInitializer.getWorkRequestDataForEvents()).isEqualTo( + assertThat(logReportWorkerScheduler.getWorkRequestDataForEvents()).isEqualTo( workerCaseForUploadingEvents ) } @@ -194,7 +194,7 @@ class LogReportWorkManagerInitializerTest { .build() assertThat( - logReportWorkManagerInitializer.getWorkRequestDataForExceptions() + logReportWorkerScheduler.getWorkRequestDataForExceptions() ).isEqualTo(workerCaseForUploadingExceptions) } @@ -207,7 +207,7 @@ class LogReportWorkManagerInitializerTest { ) .build() - assertThat(logReportWorkManagerInitializer.getWorkRequestDataForPerformanceMetrics()).isEqualTo( + assertThat(logReportWorkerScheduler.getWorkRequestDataForPerformanceMetrics()).isEqualTo( workerCaseForUploadingPerformanceMetrics ) } @@ -222,7 +222,7 @@ class LogReportWorkManagerInitializerTest { .build() assertThat( - logReportWorkManagerInitializer.getWorkRequestDataForSchedulingStorageUsageMetricLogs() + logReportWorkerScheduler.getWorkRequestDataForSchedulingStorageUsageMetricLogs() ).isEqualTo(workerCaseForSchedulingStorageUsageMetricLogs) } @@ -236,7 +236,7 @@ class LogReportWorkManagerInitializerTest { .build() assertThat( - logReportWorkManagerInitializer + logReportWorkerScheduler .getWorkRequestDataForSchedulingPeriodicBackgroundPerformanceMetricLogs() ).isEqualTo(workerCaseForSchedulingPeriodicPerformanceMetricLogs) } @@ -251,7 +251,7 @@ class LogReportWorkManagerInitializerTest { .build() assertThat( - logReportWorkManagerInitializer.getWorkRequestDataForSchedulingPeriodicUiMetricLogs() + logReportWorkerScheduler.getWorkRequestDataForSchedulingPeriodicUiMetricLogs() ).isEqualTo(workerCaseForSchedulingMemoryUsageMetricLogs) } @@ -265,7 +265,7 @@ class LogReportWorkManagerInitializerTest { .build() assertThat( - logReportWorkManagerInitializer.getWorkRequestDataForFirestore() + logReportWorkerScheduler.getWorkRequestDataForFirestore() ).isEqualTo(workerCaseForUploadingFirestoreData) } @@ -346,17 +346,17 @@ class LogReportWorkManagerInitializerTest { fun build(): TestApplicationComponent } - fun inject(logUploadWorkRequestTest: LogReportWorkManagerInitializerTest) + fun inject(logUploadWorkRequestTest: LogReportWorkerSchedulerTest) } class TestApplication : Application(), DataProvidersInjectorProvider { private val component: TestApplicationComponent by lazy { - DaggerLogReportWorkManagerInitializerTest_TestApplicationComponent.builder() + DaggerLogReportWorkerSchedulerTest_TestApplicationComponent.builder() .setApplication(this) .build() } - fun inject(test: LogReportWorkManagerInitializerTest) { + fun inject(test: LogReportWorkerSchedulerTest) { component.inject(test) } diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerSchedulerTest.kt similarity index 87% rename from domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializerTest.kt rename to domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerSchedulerTest.kt index b9598206c8f..e95786fad25 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerSchedulerTest.kt @@ -48,14 +48,14 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton -/** Tests for [PlatformParameterSyncUpWorkManagerInitializer]. */ +/** Tests for [PlatformParameterSyncUpWorkerScheduler]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) -class PlatformParameterSyncUpWorkManagerInitializerTest { +class PlatformParameterSyncUpWorkerSchedulerTest { @Inject - lateinit var syncUpWorkManagerInitializer: PlatformParameterSyncUpWorkManagerInitializer + lateinit var platformParameterSyncUpWorkerScheduler: PlatformParameterSyncUpWorkerScheduler @Inject lateinit var platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory @@ -79,17 +79,18 @@ class PlatformParameterSyncUpWorkManagerInitializerTest { @Test fun testWorkRequest_onCreate_enqueuesRequest_verifyRequestId() { val workManager = WorkManager.getInstance(context) - syncUpWorkManagerInitializer.onCreate(workManager) + platformParameterSyncUpWorkerScheduler.scheduleWork(workManager) testCoroutineDispatchers.runCurrent() - val enqueuedSyncUpWorkRequestId = syncUpWorkManagerInitializer.getSyncUpWorkRequestId() + val enqueuedSyncUpWorkRequestId = + platformParameterSyncUpWorkerScheduler.getSyncUpWorkRequestId() // Get all the WorkRequestInfo which have been tagged with "PlatformParameterSyncUpWorker.TAG" val workInfoList = workManager.getWorkInfosByTag(PlatformParameterSyncUpWorker.TAG).get() // There should be only one such work request having "PlatformParameterSyncUpWorker.TAG" tag assertThat(workInfoList.size).isEqualTo(1) // Match the ID of this work request with the ID of another work request which was enqueued by - // PlatformParameterSyncUpWorkManagerInitializer + // PlatformParameterSyncUpWorkerScheduler. assertThat(enqueuedSyncUpWorkRequestId).isEqualTo(workInfoList[0].id) } @@ -100,7 +101,8 @@ class PlatformParameterSyncUpWorkManagerInitializerTest { .setRequiresBatteryNotLow(true) .build() - val syncUpWorkRequestConstraints = syncUpWorkManagerInitializer.getSyncUpWorkerConstraints() + val syncUpWorkRequestConstraints = + platformParameterSyncUpWorkerScheduler.getSyncUpWorkerConstraints() assertThat(syncUpWorkRequestConstraints).isEqualTo(workerConstraints) } @@ -111,17 +113,18 @@ class PlatformParameterSyncUpWorkManagerInitializerTest { PlatformParameterSyncUpWorker.PLATFORM_PARAMETER_WORKER ).build() - val syncUpWorkRequestData = syncUpWorkManagerInitializer.getSyncUpWorkRequestData() + val syncUpWorkRequestData = platformParameterSyncUpWorkerScheduler.getSyncUpWorkRequestData() assertThat(syncUpWorkRequestData).isEqualTo(workerTypeForSyncingUpParameters) } @Test fun testWorkRequest_verifyWorkRequestPeriodicity() { - syncUpWorkManagerInitializer.onCreate(WorkManager.getInstance(context)) + platformParameterSyncUpWorkerScheduler.scheduleWork(WorkManager.getInstance(context)) testCoroutineDispatchers.runCurrent() - val syncUpWorkerTimePeriodInMs = syncUpWorkManagerInitializer.getSyncUpWorkerTimePeriod() + val syncUpWorkerTimePeriodInMs = + platformParameterSyncUpWorkerScheduler.getSyncUpWorkerTimePeriod() val syncUpWorkerTimePeriodInHours = TimeUnit.MILLISECONDS.toHours(syncUpWorkerTimePeriodInMs) assertThat(syncUpWorkerTimePeriodInHours).isEqualTo( @@ -130,7 +133,7 @@ class PlatformParameterSyncUpWorkManagerInitializerTest { } private fun setUpTestApplicationComponent() { - DaggerPlatformParameterSyncUpWorkManagerInitializerTest_TestApplicationComponent.builder() + DaggerPlatformParameterSyncUpWorkerSchedulerTest_TestApplicationComponent.builder() .setApplication(ApplicationProvider.getApplicationContext()) .build() .inject(this) @@ -192,6 +195,6 @@ class PlatformParameterSyncUpWorkManagerInitializerTest { fun build(): TestApplicationComponent } - fun inject(platformParameterSyncUpWorkerTest: PlatformParameterSyncUpWorkManagerInitializerTest) + fun inject(platformParameterSyncUpWorkerTest: PlatformParameterSyncUpWorkerSchedulerTest) } } diff --git a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt index 5f07d83d33f..accb4d7c0e2 100644 --- a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt +++ b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt @@ -53,7 +53,6 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.oppia.android.util.logging.SyncStatusModule import org.oppia.android.util.logging.firebase.DebugLogReportingModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsAssessorModule import org.oppia.android.util.logging.performancemetrics.PerformanceMetricsConfigurationsModule import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule @@ -88,7 +87,7 @@ import javax.inject.Singleton UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, LogReportWorkerModule::class, WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, - FirebaseLogUploaderModule::class, RetrofitModule::class, RetrofitServiceModule::class, + RetrofitModule::class, RetrofitServiceModule::class, PlatformParameterModule::class, PlatformParameterSingletonModule::class, ExplorationStorageModule::class, DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 9695f4036d2..5c8af6fec47 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -448,10 +448,18 @@ file_content_checks { file_content_checks { file_path_regex: ".+?\\.kt$" prohibited_content_regex: "WorkManager.getInstance" - failure_message: "Use AnalyticsStartupListener to retrieve an instance of WorkManager rather than fetching one using getInstance (as the latter may create a WorkManager if one isn't already present, and the application may intend to disable it)." - exempted_file_name: "app/src/main/java/org/oppia/android/app/application/AbstractOppiaApplication.kt" + failure_message: "Generally WorkManager never needs to be retrieved directly. Use WorkManagerScheduler to schedule new work." + exempted_file_name: "domain/src/main/java/org/oppia/android/domain/workmanager/BootstrapOppiaWorker.kt" + exempted_file_name: "domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerScheduler.kt" exempted_file_patterns: ".+?Test\\.kt" } +file_content_checks { + file_path_regex: ".+?\\.kt$" + prohibited_content_regex: "androidx.work.ListenableWorker" + failure_message: "Never use ListenableWorker directly. Instead, create a new OppiaWorker and rely on BootstrapOppiaWorker for direct interaction with WorkManager. See WorkManager wiki page for more." + exempted_file_name: "domain/src/main/java/org/oppia/android/domain/workmanager/BootstrapOppiaWorker.kt" + exempted_file_name: "domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerConfigurationModule.kt" +} file_content_checks { file_path_regex: ".+?\\.kt$" prohibited_content_regex: ".+?\\.(post|postDelayed)[\\s]*(\\(|\\{).*?" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 16cb1cfba17..5534bc7d79c 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -3848,7 +3848,7 @@ test_file_exemption { override_min_coverage_percent_required: 68 } test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsStartupListener.kt" + exempted_file_path: "domain/src/main/java/org/oppia/android/domain/workmanager/StartupWorkerScheduleReadinessListener.kt" test_file_not_required: true } test_file_exemption { @@ -3896,21 +3896,13 @@ test_file_exemption { source_file_is_incompatible_with_code_coverage: true } test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerFactory.kt" + exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/MetricLogSchedulingWorkerScheduler.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/logscheduler/PerformanceMetricsLogScheduler.kt" - source_file_is_incompatible_with_code_coverage: true -} test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkerModule.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt" - test_file_not_required: true -} test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/survey/SurveyEventsLogger.kt" source_file_is_incompatible_with_code_coverage: true @@ -3987,18 +3979,10 @@ test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/platformparameter/testing/TestPlatformParameterConfigRetriever.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializer.kt" - source_file_is_incompatible_with_code_coverage: true -} test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt" source_file_is_incompatible_with_code_coverage: true } -test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerFactory.kt" - test_file_not_required: true -} test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerModule.kt" test_file_not_required: true @@ -4067,10 +4051,6 @@ test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/survey/SurveyQuestionGraph.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader/FakeLogUploader.kt" - test_file_not_required: true -} test_file_exemption { exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/ConceptCardRetriever.kt" test_file_not_required: true @@ -4112,7 +4092,7 @@ test_file_exemption { test_file_not_required: true } test_file_exemption { - exempted_file_path: "domain/src/main/java/org/oppia/android/domain/workmanager/WorkManagerConfigurationModule.kt" + exempted_file_path: "domain/src/main/java/org/oppia/android/domain/workmanager/OppiaWorker.kt" test_file_not_required: true } test_file_exemption { @@ -4551,10 +4531,6 @@ test_file_exemption { exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LogLevel.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt" - test_file_not_required: true -} test_file_exemption { exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LoggerModule.kt" test_file_not_required: true @@ -4563,10 +4539,6 @@ test_file_exemption { exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LoggingAnnotations.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/MetricLogScheduler.kt" - test_file_not_required: true -} test_file_exemption { exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/SyncStatusManager.kt" test_file_not_required: true @@ -4591,14 +4563,6 @@ test_file_exemption { exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseExceptionLogger.kt" test_file_not_required: true } -test_file_exemption { - exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt" - test_file_not_required: true -} -test_file_exemption { - exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploaderModule.kt" - test_file_not_required: true -} test_file_exemption { exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirestoreEventLogger.kt" test_file_not_required: true diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 717b024d05c..1e4da0a2701 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -179,10 +179,13 @@ class RegexPatternValidationCheckTest { private val doesNotReferenceColorFromComponentColorInKotlinFiles = "Only colors from component_colors.xml may be used in Kotlin Files (Activities, Fragments, " + "Views and Presenters)." - private val doesNotUseWorkManagerGetInstance = - "Use AnalyticsStartupListener to retrieve an instance of WorkManager rather than fetching one" + - " using getInstance (as the latter may create a WorkManager if one isn't already present, " + - "and the application may intend to disable it)." + private val doNotUseWorkManagerGetInstance = + "Generally WorkManager never needs to be retrieved directly. Use WorkManagerScheduler to" + + " schedule new work." + private val doNotUseListenableWorker = + "Never use ListenableWorker directly. Instead, create a new OppiaWorker and rely on" + + " BootstrapOppiaWorker for direct interaction with WorkManager. See WorkManager wiki page" + + " for more." private val doesNotUsePostOrPostDelayed = "Prefer avoiding post() and postDelayed() methods as they can can lead to subtle and " + "difficult-to-debug crashes. Prefer using LifecycleSafeTimerFactory for most cases when " + @@ -2391,7 +2394,7 @@ class RegexPatternValidationCheckTest { } @Test - fun testFileContent_referenceGetInstance_fileContentIsNotCorrect() { + fun testFileContent_referenceWorkManagerGetInstance_fileContentIsNotCorrect() { val prohibitedContent = """ val workManager = WorkManager.getInstance(context) @@ -2400,14 +2403,38 @@ class RegexPatternValidationCheckTest { val stringFilePath = "app/src/main/java/org/oppia/android/SomeInitializer.kt" tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) - val exception = assertThrows() { runScript() } + val exception = assertThrows { runScript() } + + // Verify that all patterns are properly detected & prohibited. + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $doNotUseWorkManagerGetInstance + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_referenceListenableWorker_fileContentIsNotCorrect() { + val prohibitedContent = + """ + import androidx.work.ListenableWorker + class ProhibitedWorker: ListenableWorker() + """.trimIndent() + tempFolder.newFolder("testfiles", "app", "src", "main", "java", "org", "oppia", "android") + val stringFilePath = "app/src/main/java/org/oppia/android/SomeInitializer.kt" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows { runScript() } // Verify that all patterns are properly detected & prohibited. assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) assertThat(outContent.toString().trim()) .isEqualTo( """ - $stringFilePath:1: $doesNotUseWorkManagerGetInstance + $stringFilePath:1: $doNotUseListenableWorker $wikiReferenceNote """.trimIndent() ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index de80cc82f38..a81643a56b1 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -137,30 +137,6 @@ kt_android_library( ], ) -kt_android_library( - name = "log_uploader", - srcs = [ - "LogUploader.kt", - ], - visibility = ["//:oppia_api_visibility"], - deps = [ - "//third_party:androidx_work_work-runtime", - "//third_party:androidx_work_work-runtime-ktx", - ], -) - -kt_android_library( - name = "metric_log_scheduler", - srcs = [ - "MetricLogScheduler.kt", - ], - visibility = ["//:oppia_api_visibility"], - deps = [ - "//third_party:androidx_work_work-runtime", - "//third_party:androidx_work_work-runtime-ktx", - ], -) - kt_android_library( name = "sync_status_manager", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt b/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt deleted file mode 100644 index 0c337ca71a6..00000000000 --- a/utility/src/main/java/org/oppia/android/util/logging/LogUploader.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.oppia.android.util.logging - -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager - -/** Uploader for uploading events and exceptions to the remote service. */ -interface LogUploader { - - /** Enqueues a [workRequest] using the [workManager] for uploading event logs that are stored in the cache store. */ - fun enqueueWorkRequestForEvents(workManager: WorkManager, workRequest: PeriodicWorkRequest) - - /** Enqueues a [workRequest] using the [workManager] for uploading exception logs that are stored in the cache store. */ - fun enqueueWorkRequestForExceptions(workManager: WorkManager, workRequest: PeriodicWorkRequest) - - /** Enqueues a [workRequest] using the [workManager] for uploading performance metrics logs that are stored in the cache store. */ - fun enqueueWorkRequestForPerformanceMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) - - /** Enqueues a [workRequest] using the [workManager] for uploading event logs that are meant for Firestore. */ - fun enqueueWorkRequestForFirestore( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) -} diff --git a/utility/src/main/java/org/oppia/android/util/logging/MetricLogScheduler.kt b/utility/src/main/java/org/oppia/android/util/logging/MetricLogScheduler.kt deleted file mode 100644 index 463179cf15a..00000000000 --- a/utility/src/main/java/org/oppia/android/util/logging/MetricLogScheduler.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.oppia.android.util.logging - -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager - -/** Scheduler for scheduling metric log reports related to the performance of the application. */ -interface MetricLogScheduler { - /** - * Enqueues a [workRequest] using the [workManager] for scheduling metric collection of periodic - * metrics like network and cpu usage. - */ - fun enqueueWorkRequestForPeriodicBackgroundMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) - - /** - * Enqueues a [workRequest] using the [workManager] for scheduling metric collection of storage - * usage of the application on the current device. - */ - fun enqueueWorkRequestForStorageUsage(workManager: WorkManager, workRequest: PeriodicWorkRequest) - - /** - * Enqueues a [workRequest] using the [workManager] for scheduling metric collection of periodic - * ui metrics like memory usage. - */ - fun enqueueWorkRequestForPeriodicUiMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) -} diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel index cb3df472c2e..40641500c6e 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel @@ -19,7 +19,6 @@ kt_android_library( name = "prod_impl", srcs = [ "FirebaseAnalyticsEventLogger.kt", - "FirebaseLogUploader.kt", ], deps = [ "//model/src/main/proto:event_logger_java_proto_lite", @@ -30,7 +29,6 @@ kt_android_library( "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/logging:event_bundle_creator", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", - "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/logging:sync_status_manager", "//utility/src/main/java/org/oppia/android/util/logging/performancemetrics:performance_metrics_event_logger", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", @@ -40,7 +38,6 @@ kt_android_library( kt_android_library( name = "prod_module", srcs = [ - "FirebaseLogUploaderModule.kt", "LogReportingModule.kt", ], visibility = ["//:oppia_prod_module_visibility"], @@ -54,7 +51,6 @@ kt_android_library( "//third_party:com_google_firebase_firebase-crashlytics", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/logging:event_bundle_creator", - "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt b/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt deleted file mode 100644 index 40316ac0397..00000000000 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.oppia.android.util.logging.firebase - -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import org.oppia.android.util.logging.LogUploader -import javax.inject.Inject - -private const val OPPIA_EVENT_WORK = "OPPIA_EVENT_WORK_REQUEST" -private const val OPPIA_EXCEPTION_WORK = "OPPIA_EXCEPTION_WORK_REQUEST" -private const val OPPIA_PERFORMANCE_METRICS_WORK = "OPPIA_PERFORMANCE_METRICS_WORK" -private const val OPPIA_FIRESTORE_WORK = "OPPIA_FIRESTORE_WORK_REQUEST" - -/** Enqueues work requests for uploading stored event/exception logs to the remote service. */ -class FirebaseLogUploader @Inject constructor() : - LogUploader { - - override fun enqueueWorkRequestForEvents( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_EVENT_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } - - override fun enqueueWorkRequestForExceptions( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_EXCEPTION_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } - - override fun enqueueWorkRequestForPerformanceMetrics( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_PERFORMANCE_METRICS_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } - - override fun enqueueWorkRequestForFirestore( - workManager: WorkManager, - workRequest: PeriodicWorkRequest - ) { - workManager.enqueueUniquePeriodicWork( - OPPIA_FIRESTORE_WORK, - ExistingPeriodicWorkPolicy.KEEP, - workRequest - ) - } -} diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploaderModule.kt b/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploaderModule.kt deleted file mode 100644 index 108b6d8ca9e..00000000000 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploaderModule.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.oppia.android.util.logging.firebase - -import dagger.Binds -import dagger.Module -import org.oppia.android.util.logging.LogUploader - -/** Provides Log Uploader related dependencies. */ -@Module -interface FirebaseLogUploaderModule { - @Binds - fun bindFirebaseLogUploader(firebaseLogUploader: FirebaseLogUploader): LogUploader -} diff --git a/wiki/Work-Manager.md b/wiki/Work-Manager.md index ce9628d217f..770f519e744 100644 --- a/wiki/Work-Manager.md +++ b/wiki/Work-Manager.md @@ -1,170 +1,120 @@ ## Table of Contents -- [What is WorkManager?](#what-is-workmanager) -- [Its Usage in Oppia Android](#its-usage-in-oppia-android) -- [How to use WorkManager](#how-to-use-workmanager) -- [Writing tests with WorkManager](#writing-tests-with-workmanager) +- [What is `WorkManager`?](#what-is-workmanager) +- [When to use `WorkManager`](#when-to-use-workmanager) +- [How to use `WorkManager`](#how-to-use-workmanager) +- [Writing tests with `WorkManager`](#writing-tests-with-workmanager) +- [Debugging `WorkManager`](#debugging-workmanager) # What is WorkManager? -WorkManager is part of Android Jetpack and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app. +`WorkManager` is part of Android Jetpack and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that `WorkManager` will do your background work as soon as it can. Guaranteed execution means that `WorkManager` will take care of the logic to start your work under a variety of situations, even if you navigate away from your app. -WorkManager is an incredibly flexible library that has many additional benefits. These include: +`WorkManager` is an incredibly flexible library that has many additional benefits. These include: - Support for both asynchronous one-off and periodic tasks - Support for constraints such as network conditions, storage space, and charging status - Chaining of complex work requests, including running work in parallel - Output from one work request used as input for the next -- Handling API level compatibility back to API level 14 (see note) +- Handling API level compatibility back to API level 14 - Working with or without Google Play services - Following system health best practices -- LiveData support to easily display work request state in UI -The WorkManager library is a good choice for tasks that are useful to complete, even if the user navigates away from the particular screen or your app. Some examples of tasks that are a good use of WorkManager: +The `WorkManager` library is a good choice for tasks that are useful to complete, even if the user navigates away from the particular screen or your app. Some examples of tasks that are a good use of `WorkManager`: - Uploading logs -- Applying filters to images and saving the image - Periodically syncing local data with the network -WorkManager offers guaranteed execution, and not all tasks require that. As such, it is not a catch-all for running every task off of the main thread. +`WorkManager` offers guaranteed execution, and not all tasks require that. As such, it is not a catch-all for running every task off of the main thread. -# Its Usage in Oppia Android -There are a few WorkManager classes you need to know about: +# When to use WorkManager +`WorkManager` is used for long-running periodic tasks such as collecting or uploading analytics, and synchronizing platform parameters with the Oppia backend. These tasks can't be performed in any other way because they need to be able to run even when the user isn't actively using the device (particularly in the case of platform parameters which we want to synchronize for the next app open). Thus, only work that needs to be able to happen even when the app is closed, or under very specific network situations (like the device connecting to cellular or wifi triggering the app needing to do something) should use `WorkManager`. -- `Worker`: This is where you put the code for the actual work you want to perform in the background. You'll extend this class and override the doWork() method. -- `WorkRequest`: This represents a request to do some work. You'll pass in your Worker as part of creating your WorkRequest. When making the WorkRequest you can also specify things like Constraints on when the Worker should run. There are two types of work supported by WorkManager: OneTimeWorkRequest and PeriodicWorkRequest. -- `WorkManager`: This class actually schedules your WorkRequest and makes it run. It schedules WorkRequests in a way that spreads out the load on system resources, while honoring the constraints you specify. +Other types of background tasks (those are, tasks done off the main thread) are all handled using a background coroutine dispatcher and are generally coerced into a specific data flow pattern called `DataProvider`s. For more context on those, see the corresponding [wiki page](https://github.com/oppia/oppia-android/wiki/DataProvider-&-LiveData). +# How to use WorkManager +The Oppia Android app implements exactly one worker: `BootstrapOppiaWorker`. This worker is responsible for several things: +- Determining if an invalid worker is trying to be run (such as an old worker that's no longer valid) and, if so, cancelling it. +- Identifying the specific type of work a worker wants to do. +- Deferring worker construction until platform parameters are guaranteed to be loaded (so that both workers and their factories can depend on platform parameters either directly or via their other dependencies). +- Delegating work to an actual defined `OppiaWorker` implementation. -In Oppia we are using WorkManager in two scenarios : -- To upload cached Logs (for Analytics) over FirebaseAnalytics whenever data connection and battery requirements are met. This was implemented by @Sarthak2601 during GSoC'20, for more details you can go through the [proposal idea](https://github.com/oppia/oppia-web-developer-docs/blob/develop/pdfs/GSoC2020SarthakAgarwal.pdf) -- To sync up the PlatformParameters from OppiaBackend whenever the app starts and the data + battery requirements are met. This was implemented by @ARJUPTA during GSoC'21, for more details you can go through the [proposal idea](https://github.com/oppia/oppia-web-developer-docs/blob/develop/pdfs/GSoC2021ArjunGupta.pdf) +With this architecture in place, you'll never need to directly implement a `ListenableWorker`. Instead, you must do the following: +- Introduce a new `OppiaWorker` implementation (along with its factory and task type). +- Introduce a scheduler class that can automatically schedule the new worker for periodic work. +- Introduce a module that creates the necessary Dagger bindings for both the worker and schedule to automatically be used. Note that this includes adding the new module to the corresponding application component classes (e.g. `DeveloperApplicationComponent`) so that the bindings are enabled. -# How to use WorkManager -If you want to introduce a new feature or any change to the existing WorkManager implementation in oppia-android, here is the basic structure of files you need to keep in mind : - -1. Start with creating a Worker class (we have used `ListenableWorker` till now everywhere) for eg - MyFeatureWorker. - - ``` - class LogUploadWorker private constructor( - context: Context, - params: WorkerParameters, - ... - @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher - ) : ListenableWorker(context, params) { - override fun startWork(): ListenableFuture { - val backgroundScope = CoroutineScope(backgroundDispatcher) - val result = backgroundScope.async {...} - return if(checkWorkDone(result)) Result.success() else Result.failure() - } - } - ``` - -2. Then after implementing all the functionality in MyFeatureWorker, create a custom WorkerFactory class (for eg- MyFeatureWorkerFactory) so that we can provide any extra parameters if needed. - - ``` - class LogUploadWorkerFactory @Inject constructor( - private val workerFactory: LogUploadWorker.Factory - ) : WorkerFactory() { - - /** Returns a new [LogUploadWorker] for the given context and parameters. */ - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? { - return workerFactory.create(appContext, workerParameters) - } - } - ``` - -3. Provide an instance of this WorkerFactory class in the `WorkManagerConfigurationModule` so that a singular work manager configuration can be made for the entire app. - - ``` - @Module - class WorkManagerConfigurationModule { - - @Singleton - @Provides - fun provideWorkManagerConfiguration( - logUploadWorkerFactory: LogUploadWorkerFactory, - platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory - ): Configuration { - val delegatingWorkerFactory = DelegatingWorkerFactory() - delegatingWorkerFactory.addFactory(logUploadWorkerFactory) - delegatingWorkerFactory.addFactory(platformParameterSyncUpWorkerFactory) - return Configuration.Builder().setWorkerFactory(delegatingWorkerFactory).build() - } - } - ``` - -4. After all these steps create an Initializer class (for eg- MyFeatureWorkerInitializer) that will prepare and enqueue a WorkRequest for you at the time when app starts. - - ``` - @Singleton - class LogUploadWorkManagerInitializer @Inject constructor( - private val context: Context, - private val logUploader: LogUploader - ) : ApplicationStartupListener { - override fun onCreate() { - val workManager = WorkManager.getInstance(context) - logUploader.enqueueWorkRequestForEvents(workManager, workRequestForUploadingEvents) - logUploader.enqueueWorkRequestForExceptions(workManager, workRequestForUploadingExceptions) - } - } - ``` - -**Note** - All the parts of WorkManager implementation entirely lie in the domain layer, but there are few functionalities that you may need to acquire from other layers for eg- if you need to make a network request you would probably need to interact with data layer also. +That's it! From there you can customize which tasks the job can do and adapt the work and scheduling accordingly. It's recommended to look at existing implementations for how they're set up, but the simplest is the debug worker which is designed to specifically analyze `WorkManager` behavior and can be seen in the [`org.oppia.android.domain.workmanager.debug` package](https://github.com/oppia/oppia-android/tree/develop/domain/src/main/java/org/oppia/android/domain/workmanager/debug). # Writing tests with WorkManager -For writing any test with WorkManager you will need to interact with -- *WorkManagerTestInitHelper* so that you can emulate the enquing and running of WorkRequests. - - ``` - @Before - fun setUp() { - setUpTestApplicationComponent() - context = InstrumentationRegistry.getInstrumentation().targetContext - val config = Configuration.Builder() - .setExecutor(SynchronousExecutor()) - .setWorkerFactory(logUploadWorkerFactory) - .build() - WorkManagerTestInitHelper.initializeTestWorkManager(context, config) - } - ``` -- *TestCoroutinesDispatcher* so that you can block the code execution up untill WorkRequest(s) are running. (ie. working with suspend functions) -- You might also need to introduce some fakes so that you can make sure the entities (object, classes, varaibles & constants etc.) over which you MyFeatureWorker depends doesn't have any bugs. - -Here is an exemplar test that is using WorkManager to enqueue a WorkRequest with any inputData (if needed). After we enqueue a request, the next step is to wait until its execution is completed and for that we are using testCoroutineDispatchers - + +TODO: Re-write this section once the new testing strategy is understood. + +# Debugging WorkManager + +Verifying that `WorkManager` is behaving correctly can be difficult since it relies heavily on OS constraints and thus cannot necessarily guarantee certain behaviors, especially for periodic tasks. Here are some important aspects to note when trying to determine if a worker is running correctly: +- `WorkManager` won't allow periodic tasks to run more frequently than 15 minutes unless they are explicitly initiated with a one-time work request. +- Periodic tasks will run immediately upon opening the app if their constraints are met and they haven't yet run within their configured time period. +- Tasks will run even if the app is closed and this can be validated by force closing the app using: `adb shell am force-stop org.oppia.android` +- There's a debug worker that's configured to run every 15 minutes, 20 minutes, and 6 hours (to align with other long periodic workers). This only runs in `//:oppia_dev` builds but simply opening the developer app at least once and watching ADB (for debug logs) should provide a basis to verify that `WorkManager` is actually running. +- You can use tests to verify that your worker will run when expected. See `DebugWorkerTest` for an example of how to test periodic workers. +- Finally, you can actually force a periodic task to run through ADB (see below). + +**Important**: Many of the following commands may require using a root ADB shell. You can enable that using `adb root` but it may not be available on all Android devices. It should always be available for emulators so long as they're running a developer version of the Android OS (make sure you aren't using an image that has Google Play, but you may need Google APIs for testing certain jobs like Firebase analytics). + +To force a specific worker to run we must first figure out what's currently enqueued. To do that run the following ADB command: + +```sh +adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" org.oppia.android -f 32 ``` - @Test - fun testWorker_logEvent_withoutNetwork_enqueueRequest_verifySuccess() { - networkConnectionUtil.setCurrentConnectionStatus(NONE) - analyticsController.logTransitionEvent( - eventLogTopicContext.timestamp, - eventLogTopicContext.actionName, - oppiaLogger.createTopicContext(TEST_TOPIC_ID) - ) - - val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext()) - - val inputData = Data.Builder().putString( - LogUploadWorker.WORKER_CASE_KEY, - LogUploadWorker.EVENT_WORKER - ).build() - - val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder() - .setInputData(inputData) - .build() - - workManager.enqueue(request) - testCoroutineDispatchers.runCurrent() - val workInfo = workManager.getWorkInfoById(request.id) - - assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) - assertThat(fakeEventLogger.getMostRecentEvent()).isEqualTo(eventLogTopicContext) - } + +(Note that the `-f 32` will allow the diagnostic request to wake the app if it was force stopped or never opened). + +View logcat for info logs. You should see something like the following: + +``` +Enqueued work: +Id Class Name Job Id State Unique Name Tags +8126cd00-2c35-4704-8239-77abda3dd12e org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2109 ENQUEUED DebugWorker.RUN_EVERY_SIX_HOURS_WITH_OR_WITHOUT_CONNECTIVITY DebugWorker.RUN_EVERY_SIX_HOURS_WITH_OR_WITHOUT_CONNECTIVITY,org.oppia.android.domain.workmanager.BootstrapOppiaWorker +e7b9b00c-e742-4688-b004-14cf07698405 org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2110 ENQUEUED PlatformParameterSyncUpWorker.refresh_platform_parameters org.oppia.android.domain.workmanager.BootstrapOppiaWorker,PlatformParameterSyncUpWorker.refresh_platform_parameters +240ffbc1-3001-478b-947b-3549f9e91007 org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2112 ENQUEUED LogUploadWorker.upload_performance_metrics org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_performance_metrics +21048af0-8b1b-4c85-899e-c252ecd4c2fa org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2113 ENQUEUED LogUploadWorker.upload_exceptions org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_exceptions +88be6627-bc44-45ab-993f-9dcb9b6a756d org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2114 ENQUEUED LogUploadWorker.upload_firestore_data org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_firestore_data +9aecbfd4-405c-4f29-9467-a6e434350ded org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2115 ENQUEUED LogUploadWorker.upload_events org.oppia.android.domain.workmanager.BootstrapOppiaWorker,LogUploadWorker.upload_events +9de8273f-6c12-4684-94a3-a75b4b36aa0b org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2123 ENQUEUED DebugWorker.RUN_EVERY_TWENTY_MINUTES_WITH_OR_WITHOUT_CONNECTIVITY org.oppia.android.domain.workmanager.BootstrapOppiaWorker,DebugWorker.RUN_EVERY_TWENTY_MINUTES_WITH_OR_WITHOUT_CONNECTIVITY +9e070daa-c323-499f-b45f-00d2932f8809 org.oppia.android.domain.workmanager.BootstrapOppiaWorker 2131 ENQUEUED DebugWorker.RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY org.oppia.android.domain.workmanager.BootstrapOppiaWorker,DebugWorker.RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY ``` -In Oppia we write tests for both the Worker and its Initializer class. You can take a reference for the same from these files: +This forces `WorkManager` to reconcile its internal state which will automatically kick off stale jobs. We can leverage that to force a job that's run within its recent time period to run again by making `WorkManager` forget about it. This even bypasses its own internal 15 minute limit. The following command can be used to achieve this: -Worker Tests - *[PlatformParameterSyncUpWorkerTest](https://github.com/oppia/oppia-android/blob/develop/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt) OR [LogUploadWorkerTest](https://github.com/oppia/oppia-android/blob/develop/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorkerTest.kt)* +```sh +adb shell "run-as org.oppia.android sqlite3 no_backup/androidx.work.workdb \"UPDATE WorkSpec SET period_start_time = 0 WHERE id = ''; DELETE FROM SystemIdInfo WHERE work_spec_id = '';\"" +``` + +`` is updated using one of the IDs in the diagnostics table above, for example `9e070daa-c323-499f-b45f-00d2932f8809` would correspond to the periodic 15-minute `DebugWorker` job. These IDs are long-lived unlike the job IDs above. + +Once this command finishes you can re-run the diagnostics command above and the corresponding job should run. + +For simplicity, here are Bash functions that take either a parameter of the worker's unique name (e.g. 'DebugWorker.RUN_EVERY_FIFTEEN_MINUTES_WITH_CONNECTIVITY') or the worker's ID performs all of the necessary commands: + +```bash +function PrintOppiaWorkerDiagnostics() { + adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" org.oppia.android -f 32 +} + +function ForceRunOppiaJobById() { + echo "Attempting to force run Oppia worker with ID: $1" + local worker_id="$1" + adb shell "run-as org.oppia.android sqlite3 no_backup/androidx.work.workdb \"UPDATE WorkSpec SET period_start_time = 0 WHERE id = '$worker_id'; DELETE FROM SystemIdInfo WHERE work_spec_id = '$worker_id';\"" + PrintOppiaWorkerDiagnostics +} + +function ForceRunOppiaJobWithUniqueName() { + local unique_name="$1" + echo "Attempting to find an Oppia worker with unique name: $unique_name" + PrintOppiaWorkerDiagnostics + local app_pid=$(adb shell pidof -s org.oppia.android) + local worker_id=$(adb logcat --pid=$app_pid -d | grep "WM-DiagnosticsWrkr" | grep "$unique_name" | tail -1 | awk '{for(i=1;i<=NF;i++) if($i=="WM-DiagnosticsWrkr:") {print $(i+1); exit}}') + echo "Worker has unique ID: $worker_id" + ForceRunOppiaJobById $worker_id +} +``` -Initializer Tests - *[PlatformParameterSyncUpWorkManagerInitializerTest](https://github.com/oppia/oppia-android/blob/develop/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkManagerInitializerTest.kt) OR [LogUploadWorkManagerInitializerTest](https://github.com/oppia/oppia-android/blob/develop/domain/src/test/java/org/oppia/android/domain/oppialogger/loguploader/LogReportWorkManagerInitializerTest.kt)* \ No newline at end of file +Caveat: these may only run successfully if there's exactly one device available.