Skip to content

Commit

Permalink
Add AppComponentFactoryRule
Browse files Browse the repository at this point in the history
- Adds support for custom `AppComponentFactory` during tests.
- Designed to be used together with `ActivityScenarioRule`.
- Supports `MonitoringInstrumentation`.
- In order to avoid breaking open source, support for `RoboMonitoringInstrumentation` will be done later, once `AppComponentFactoryRegistry` is available in a published `androidx.test:monitor` release.

PiperOrigin-RevId: 551196131
  • Loading branch information
copybara-androidxtest committed Aug 15, 2023
1 parent f5a9b66 commit 8d7d142
Show file tree
Hide file tree
Showing 7 changed files with 519 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.ext.junit.rules

import android.app.AppComponentFactory
import android.os.Build
import androidx.test.annotation.ExperimentalTestApi
import androidx.test.platform.app.AppComponentFactoryRegistry
import org.junit.rules.ExternalResource

/**
* [AppComponentFactory] let you define a [AppComponentFactory] before the tests starts and clean-up
* the factory after the test.
*
* This rule is designed to be used with [ActivityScenarioRule].
*
* Example:
*
* ```kotlin
* // Set-up `AppComponentFactoryRule` with your custom `AppComponentFactory`.
* private val factoryRule = AppComponentFactoryRule(MyAppComponentFactory())
*
* // Set-up `ActivityScenarioRule` with your custom `Activity`.
* private val activityRule = ActivityScenarioRule<MyActivity>()
*
* // Creates a `RuleChain` for ordering the test rules above. We need to ensure the
* // `AppComponentFactoryRule` will always run BEFORE the `ActivityScenarioRule` so that
* // your custom `AppComponentFactory` is available when the activity is launched.
* @[Rule JvmField] val ruleChain = RuleChain.outerRule(factoryRule).around(activityRule)
* ```
*
* @see <a href="https://junit.org/junit4/javadoc/4.12/org/junit/rules/RuleChain.html">RuleChain</a>
*/
@ExperimentalTestApi
class AppComponentFactoryRule(private val factory: AppComponentFactory) : ExternalResource() {

override fun before() {
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
"AppComponentFactoryRule is not supported on 'VERSION.SDK_INT < VERSION_CODES.P'"
}
AppComponentFactoryRegistry.appComponentFactory = factory
}

override fun after() {
AppComponentFactoryRegistry.appComponentFactory = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.ext.junit.rules

import android.app.Activity
import android.app.AppComponentFactory
import android.app.Application
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
class AppComponentFactoryRuleTest {

private val appComponentFactoryRule = AppComponentFactoryRule(DummyAppComponentFactory())

private val activityScenarioRule = ActivityScenarioRule(DummyActivity::class.java)

@[JvmField Rule]
val ruleChain: RuleChain =
RuleChain.outerRule(appComponentFactoryRule).around(activityScenarioRule)

@Test
@Config(minSdk = 28)
fun shouldCreateNewAppComponentsUsingAppComponentFactorySet() {
activityScenarioRule.scenario.onActivity { activity: DummyActivity ->
assertThat(activity.text).isEqualTo("instantiateActivity")
assertThat(activity.application).isInstanceOf(DummyApplication::class.java)
assertThat((activity.application as DummyApplication).text)
.isEqualTo("instantiateApplication")
}
}
}

private class DummyAppComponentFactory : AppComponentFactory() {
override fun instantiateApplication(cl: ClassLoader, className: String): Application =
if (className == DummyApplication::class.java.name) {
DummyApplication(text = "instantiateApplication")
} else {
super.instantiateApplication(cl, className)
}

override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity =
if (className == DummyActivity::class.java.name) {
DummyActivity(text = "instantiateActivity")
} else {
super.instantiateActivity(cl, className, intent)
}
}

private class DummyApplication(val text: String) : Application()

private class DummyActivity(val text: String) : Activity()
12 changes: 12 additions & 0 deletions runner/monitor/java/androidx/test/api/current_internal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,18 @@ package androidx.test.internal.util {

}

package androidx.test.platform.app {

@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class AppComponentFactoryRegistry {
method public static android.app.AppComponentFactory? getAppComponentFactory();
method public static android.app.Activity? instantiateActivity(ClassLoader cl, String className, android.content.Intent? intent = null);
method public static android.app.Application? instantiateApplication(ClassLoader cl, String className);
method public static void setAppComponentFactory(android.app.AppComponentFactory?);
property public static final android.app.AppComponentFactory? appComponentFactory;
}

}

package androidx.test.platform.concurrent {

@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public enum DirectExecutor implements java.util.concurrent.Executor {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.platform.app

import android.app.Activity
import android.app.AppComponentFactory
import android.app.Application
import android.app.Instrumentation
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RestrictTo
import java.util.concurrent.atomic.AtomicReference

/**
* An exposed registry instance that holds a reference to an application [AppComponentFactory] which
* will be used by a test [Instrumentation].
*
* [AppComponentFactoryRegistry] is a low level APIs, and is used internally by Android testing
* frameworks. It is **NOT** designed for direct use by third party clients.
*
* TODO(b/275323224): In order to avoid breaking open source, support for
* `RoboMonitoringInstrumentation` will be done later, once `AppComponentFactoryRegistry` is
* available in a published `androidx.test:monitor` release.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object AppComponentFactoryRegistry {

private val factoryRef = AtomicReference<AppComponentFactory?>(/* initialValue= */ null)

/** [AppComponentFactory] to be used by the current test [Instrumentation]. */
@JvmStatic
var appComponentFactory: AppComponentFactory?
/**
* Set the [AppComponentFactory] to be used by the current test [Instrumentation].
*
* @throws [IllegalArgumentException] if called from an Android version smaller than 28.
*/
set(value) {
check(isVersionCodeAtLeastP()) {
"AppComponentFactoryRegistry is not supported on 'VERSION.SDK_INT < VERSION_CODES.P'"
}
factoryRef.set(value)
}
/** Returns the [AppComponentFactory] to be used by the current test [Instrumentation]. */
get() = factoryRef.get()

/**
* Returns an instance of an [Application] given a [className] using the registered
* [AppComponentFactory] as factory to instantiate it.
*
* It may return null if:
* - an instance of [AppComponentFactory] has not been registered with [appComponentFactory].
* - the registered [AppComponentFactory] can not create an instance of the requested [className].
* - the current Android [Build.VERSION.SDK_INT] is smaller than [Build.VERSION_CODES.P].
*
* This function is a shorthand for getting an instance of the registered factory or null.
*
* @see [AppComponentFactory.instantiateApplication]
*/
@JvmStatic
fun instantiateApplication(
cl: ClassLoader,
className: String,
): Application? =
if (isVersionCodeAtLeastP()) {
appComponentFactory?.instantiateApplication(cl, className)
} else {
null
}

/**
* Returns an instance of an [Activity] given a [className] using the registered
* [AppComponentFactory] as factory to instantiate it.
*
* It may return null if:
* - an instance of [AppComponentFactory] has not been registered with [appComponentFactory].
* - the registered [AppComponentFactory] can not create an instance of the requested [className].
* - the current Android [Build.VERSION.SDK_INT] is smaller than [Build.VERSION_CODES.P].
*
* This function is a shorthand for getting an instance of the registered factory or null.
*
* @see [AppComponentFactory.instantiateApplication]
*/
@JvmStatic
fun instantiateActivity(
cl: ClassLoader,
className: String,
intent: Intent? = null,
): Activity? =
if (isVersionCodeAtLeastP()) {
appComponentFactory?.instantiateActivity(cl, className, intent)
} else {
null
}

@ChecksSdkIntAtLeast(api = VERSION_CODES.P)
private fun isVersionCodeAtLeastP(): Boolean = VERSION.SDK_INT >= VERSION_CODES.P
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import androidx.test.internal.runner.lifecycle.ApplicationLifecycleMonitorImpl;
import androidx.test.internal.util.Checks;
import androidx.test.internal.util.ProcSummary;
import androidx.test.platform.app.AppComponentFactoryRegistry;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.intent.IntentMonitorRegistry;
import androidx.test.runner.intent.IntentStubberRegistry;
Expand Down Expand Up @@ -150,6 +151,12 @@ public Application newApplication(ClassLoader cl, String className, Context cont
// On API <= 15, initialization should have been called in #onCreate().
installMultidexAndExceptionHandler();
}

Application application = AppComponentFactoryRegistry.instantiateApplication(cl, className);
if (application != null) {
return application;
}

return super.newApplication(cl, className, context);
}

Expand Down Expand Up @@ -859,9 +866,16 @@ public Activity newActivity(
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return interceptingActivityFactory.shouldIntercept(cl, className, intent)
? interceptingActivityFactory.create(cl, className, intent)
: super.newActivity(cl, className, intent);
if (interceptingActivityFactory.shouldIntercept(cl, className, intent)) {
return interceptingActivityFactory.create(cl, className, intent);
}

Activity activity = AppComponentFactoryRegistry.instantiateActivity(cl, className, intent);
if (activity != null) {
return activity;
}

return super.newActivity(cl, className, intent);
}

/**
Expand Down
Loading

0 comments on commit 8d7d142

Please sign in to comment.