urbanairship / android-library

Urban Airship Android SDK
Other
109 stars 123 forks source link

Airship errors with Hilt and UI Tests #221

Closed barry-irvine closed 1 year ago

barry-irvine commented 1 year ago

❗For how-to inquiries involving Airship functionality or use cases, please contact (support)[https://support.airship.com/].

Preliminary Info

What Airship dependencies are you using?

Airship 16.8.0 com.urbanairship.android:urbanairship-fcm com.urbanairship.android:urbanairship-automation com.urbanairship.android:urbanairship-message-center

What are the versions of any relevant development tools you are using?

Android Studio Electric Eel | 2022.1.1

Report

What unexpected behavior are you seeing?

When running UI Tests the logs are full of errors from the airship work manager. This is because we are using Hilt and workmanager ourselves and have to use an EntryPoint in our BaseApplication rather than directly injecting the HiltWorkerFactory. My own jobs (which subclass CoroutineWorker) do not error.

What is the expected behavior?

I don't see any job schedule failures in the logs.

What are the steps to reproduce the unexpected behavior?

Add Hilt and Airship to a project with work manager. The Manifest configuration should look like this:

  <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="com.urbanairship.AirshipInitializer"
                tools:node="remove" />
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
            <meta-data
                android:name="com.urbanairship.NoDependencyAirshipInitializer"
                android:value="androidx.startup" />
        </provider>

You should have a test application like this:

 * This interface will generate [HiltTestApplication_Application] to be used by [HiltTestRunner]
 */
@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

And BaseApplication should look something like this:

/**
 * To allow UI testing, no usage of @Inject is allowed in this class. Use EntryPointAccessors instead.
 */
open class BaseApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration =
        Configuration.Builder().setWorkerFactory(
            EntryPoints.get(this, WorkerFactoryEntryPoint::class.java).workerFactory()
        ).build()

    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface WorkerFactoryEntryPoint {
        fun workerFactory(): HiltWorkerFactory
    }
}

You need a UI Test that includes the Android test rule

@get:Rule
val hiltRule = HiltAndroidRule(this)

@Before
fun setup() {
    hiltRule.inject()
}

I don't particularly want to use Airship in my tests but despite having logic like this I still see the error

override fun createAirshipConfigOptions(context: Context): AirshipConfigOptions =
    AirshipConfigOptions.Builder()
        .applyDefaultProperties(context)
        .setInProduction(!BuildConfig.DEBUG)
        .setEnabledFeatures(if (ActivityManager.isRunningInUserTestHarness()) PrivacyManager.FEATURE_MESSAGE_CENTER else PrivacyManager.FEATURE_ALL)
        .build()

Do you have logging for the issue?

2023-02-07 11:50:37.575 29295-29295 UALib com.gocitypass.debug E The airship config options is missing URL allow list rules for SCOPE_OPEN. By default only Airship, YouTube, mailto, sms, and tel URLs will be allowed.To suppress this error, specify allow list rules by providing rules for urlAllowListScopeOpenUrl or urlAllowList. Alternatively you can suppress this error and keep the default rules by using the flag suppressAllowListError. For more information, see https://docs.airship.com/platform/android/getting-started/#url-allow-list. 2023-02-07 11:50:37.580 29295-29397 Go City - UALib com.gocitypass.debug I Airship taking off! 2023-02-07 11:50:37.580 29295-29397 Go City - UALib com.gocitypass.debug I Airship log level: 3 2023-02-07 11:50:38.010 29295-29397 Go City - UALib com.gocitypass.debug E Scheduler failed to schedule jobInfo com.urbanairship.job.SchedulerException: Failed to schedule job at com.urbanairship.job.WorkManagerScheduler.schedule(WorkManagerScheduler.java:31) at com.urbanairship.job.JobDispatcher.dispatch(JobDispatcher.java:122) at com.urbanairship.job.JobDispatcher.dispatch(JobDispatcher.java:116) at com.urbanairship.push.PushManager.dispatchUpdateJob(PushManager.java:413) at com.urbanairship.push.PushManager.updateManagerEnablement(PushManager.java:390) at com.urbanairship.push.PushManager.init(PushManager.java:346) at com.urbanairship.UAirship.init(UAirship.java:800) at com.urbanairship.UAirship.executeTakeOff(UAirship.java:422) at com.urbanairship.UAirship.access$000(UAirship.java:67) at com.urbanairship.UAirship$2.run(UAirship.java:381) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at com.urbanairship.util.AirshipThreadFactory$1.run(AirshipThreadFactory.java:50) at java.lang.Thread.run(Thread.java:764) Caused by: java.lang.IllegalStateException: The component was not created. Check that you have added the HiltAndroidRule. at dagger.hilt.internal.Preconditions.checkState(Preconditions.java:83) at dagger.hilt.android.internal.testing.TestApplicationComponentManager.generatedComponent(TestApplicationComponentManager.java:96) at com.gocitypass.HiltTestApplication_Application.generatedComponent(HiltTestApplication_Application.java:28) at dagger.hilt.EntryPoints.get(EntryPoints.java:59) at com.gocitypass.BaseApplication.getWorkManagerConfiguration(BaseApplication.kt:19) at androidx.work.impl.WorkManagerImpl.getInstance(WorkManagerImpl.java:155) at androidx.work.WorkManager.getInstance(WorkManager.java:184) at com.urbanairship.job.WorkManagerScheduler.schedule(WorkManagerScheduler.java:28) at com.urbanairship.job.JobDispatcher.dispatch(JobDispatcher.java:122)  at com.urbanairship.job.JobDispatcher.dispatch(JobDispatcher.java:116)  at com.urbanairship.push.PushManager.dispatchUpdateJob(PushManager.java:413)  at com.urbanairship.push.PushManager.updateManagerEnablement(PushManager.java:390)  at com.urbanairship.push.PushManager.init(PushManager.java:346)  at com.urbanairship.UAirship.init(UAirship.java:800)  at com.urbanairship.UAirship.executeTakeOff(UAirship.java:422)  at com.urbanairship.UAirship.access$000(UAirship.java:67)  at com.urbanairship.UAirship$2.run(UAirship.java:381)  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)  at com.urbanairship.util.AirshipThreadFactory$1.run(AirshipThreadFactory.java:50)  at java.lang.Thread.run(Thread.java:764) 

rlepinski commented 1 year ago

This looks like a similar issue to this - https://github.com/urbanairship/android-library/issues/212, you might need to provide your own factory and return null for default for Airships worker ID

barry-irvine commented 1 year ago

I tried this based on the other issue but I'm still getting the same error (although the line numbers have moved because the change in implementation:

open class BaseApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return Configuration.Builder().setWorkerFactory(
            CustomWorkerFactory(
                EntryPoints.get(this, WorkerFactoryEntryPoint::class.java).workerFactory
            )
        ).build()
    }

    class CustomWorkerFactory(private val hiltWorkerFactory: HiltWorkerFactory) : WorkerFactory() {
        override fun createWorker(
            appContext: Context,
            workerClassName: String,
            workerParameters: WorkerParameters
        ): ListenableWorker? {
            return when (workerClassName) {
                "com.urbanairship.job.AirshipWorker" -> AirshipWorker(appContext, workerParameters)
                else -> hiltWorkerFactory.createWorker(
                    appContext,
                    workerClassName,
                    workerParameters
                )
            }
        }

    }

    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface WorkerFactoryEntryPoint {
        val workerFactory: HiltWorkerFactory
    }
}
jyaganeh commented 1 year ago

Hi @barry-irvine, from the stack trace, it doesn't look like this is being caused by the Airship SDK:

Caused by: java.lang.IllegalStateException: The component was not created. Check that you have added the HiltAndroidRule.

I believe you also may need to use EarlyEntryPoint instead of the normal EntryPoint when installing/getting the worker factory, since SingletonComponent is created after Application.onCreate() is called in instrumentation tests (ref).

Would you be able to provide an example of one of your tests, with rules/setup?

barry-irvine commented 1 year ago

Hi @jyaganeh

I tried changing the original Base Application to use EarlyEntryPoint but still no dice. All our tests use a base test class which looks like this:

open class BaseTest {

    @get:Rule(order = 0)
    val disableAnimationsRule = DisableAnimationsRule()

    @get:Rule(order = 1)
    val hiltRule by lazy { HiltAndroidRule(this) }

    @get:Rule(order = 2)
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Inject
    lateinit var skipOnboarding: SkipOnboarding

    @Inject
    lateinit var languagePref: LanguagePref

    @Inject
    lateinit var workManager: WorkManager

    private val mockWebServer = GoCityMockWebServer()
    open var shouldSkipOnboarding = true

    @Before
    open fun setUp() {
        hiltRule.inject()
        if (shouldSkipOnboarding) {
            skipOnboarding()
        } else {
            runTest { languagePref.setLanguage(Locale.US) }
        }
        mockWebServer.start()
        mockWebServer.setupDefaultMocks()
        composeTestRule.waitForIdle()
    }

    @After
    open fun teardown() {
        mockWebServer.shutdown()
        workManager.cancelAllWork()
    }
}
barry-irvine commented 1 year ago

@jyaganeh Oops sorry. I missed the EarlyEntryPoints.get() aspect!

Thank you so much. The only error that I see in the logs now is:

AirshipChannel - Channel registration failed, will retry
com.urbanairship.http.RequestException: Unable to perform request: missing URL

For reference my BaseApplication now looks like this:

open class BaseApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return Configuration.Builder().setWorkerFactory(EarlyEntryPoints.get(applicationContext, WorkerFactoryEntryPoint::class.java).workerFactory).build()
    }

    @EarlyEntryPoint
    @InstallIn(SingletonComponent::class)
    interface WorkerFactoryEntryPoint {
        val workerFactory: HiltWorkerFactory
    }
}
rlepinski commented 1 year ago

That is an expected error message right now. Ill see if we can suppress for the 16.9 release.

rlepinski commented 1 year ago

The error should no longer be logged in 16.9. It just means we tried to do a channel registration before the url config could be fetched. The SDK will now wait to attempt channel creation until we have a URL.