Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ the legacy Support Library should remain on an ARouter 1.x release until the app
migrated to AndroidX. The namespace change is a source and binary compatibility boundary, so
its release will be handled as a major-version change.

ARouter's runtime keeps `minSdkVersion=14`. The demo application and its Gson-based
`module-java` use `minSdkVersion=21`; Gson is not an ARouter runtime dependency.

1. Adding dependencies and configurations
``` gradle
android {
Expand Down
3 changes: 3 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
AndroidX 迁移后再升级。命名空间变化构成源码和二进制兼容边界,因此正式发布时会按大版本
变更处理。

ARouter 运行库仍保持 `minSdkVersion=14`。示例应用及其中使用 Gson 的 `module-java`
采用 `minSdkVersion=21`;Gson 不是 ARouter 运行库的依赖。

1. 添加依赖和配置
``` gradle
android {
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android {
}

defaultConfig {
minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
minSdkVersion Integer.parseInt(DEMO_MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
versionName "0.0.1"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,12 @@ public void onInterrupt(Throwable exception) {
assertEquals(0, continued.get());
assertEquals(navigationCount, interrupted.get());

Warehouse.interceptors.clear();
Warehouse.interceptors.add(new CountingInterceptor());
Warehouse.clear();
Warehouse.interceptorsIndex.put(1, CountingInterceptor.class);
InterceptorServiceImpl subsequentService = new InterceptorServiceImpl();
subsequentService.init(InstrumentationRegistry.getInstrumentation().getTargetContext());
RecordingCallback subsequentNavigation = new RecordingCallback();
service.doInterceptions(new Postcard("/test/subsequent", "test").setTimeout(2), subsequentNavigation);
subsequentService.doInterceptions(new Postcard("/test/subsequent", "test").setTimeout(2), subsequentNavigation);

assertTrue(subsequentNavigation.await(1, TimeUnit.SECONDS));
assertEquals(1, subsequentNavigation.continueCount.get());
Expand Down Expand Up @@ -478,7 +480,7 @@ public void init(Context context) {
}
}

private static final class CountingInterceptor implements IInterceptor {
public static final class CountingInterceptor implements IInterceptor {
private final AtomicInteger processCount = new AtomicInteger();

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,87 @@ public void successfulInitializationRunsTheCompleteInterceptorChain() throws Exc
assertEquals(2, PassingInterceptor.processCount.get());
}

@Test
public void retiredInitializationDoesNotModifyTheReplacementService() throws Exception {
ThreadPoolExecutor retiringExecutor = new ThreadPoolExecutor(
1, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()
);
try {
Warehouse.interceptorsIndex.put(1, BlockingInitInterceptor.class);
LogisticsCenter.executor = retiringExecutor;
new InterceptorServiceImpl().init(targetContext());
assertTrue(BlockingInitInterceptor.awaitInitializationStart());

// Debug destroy/re-init clears the registry while the old initializer can still run.
Warehouse.clear();
Warehouse.interceptorsIndex.put(1, PassingInterceptor.class);
LogisticsCenter.executor = testExecutor;
InterceptorServiceImpl replacement = new InterceptorServiceImpl();
replacement.init(targetContext());

RecordingCallback firstCallback = new RecordingCallback();
replacement.doInterceptions(new Postcard("/test/replacement/first", "test"), firstCallback);
assertTrue(firstCallback.await());
assertNull(firstCallback.interrupted.get());
assertEquals(1, PassingInterceptor.processCount.get());

BlockingInitInterceptor.releaseInitialization();
retiringExecutor.shutdown();
assertTrue(retiringExecutor.awaitTermination(5, TimeUnit.SECONDS));

PassingInterceptor.reset();
RecordingCallback secondCallback = new RecordingCallback();
Postcard postcard = new Postcard("/test/replacement/second", "test");
replacement.doInterceptions(postcard, secondCallback);
assertTrue(secondCallback.await());
assertSame(postcard, secondCallback.continued.get());
assertNull(secondCallback.interrupted.get());
assertEquals(1, PassingInterceptor.processCount.get());
assertEquals(0, BlockingInitInterceptor.processCount.get());
} finally {
BlockingInitInterceptor.releaseInitialization();
retiringExecutor.shutdownNow();
LogisticsCenter.executor = testExecutor;
assertTrue(retiringExecutor.awaitTermination(5, TimeUnit.SECONDS));
}
}

@Test
public void registryResetDoesNotSkipTheInitializingServicesInterceptors() throws Exception {
Warehouse.interceptorsIndex.put(1, BlockingInitInterceptor.class);
Warehouse.interceptorsIndex.put(2, PassingInterceptor.class);
InterceptorServiceImpl service = new InterceptorServiceImpl();
service.init(targetContext());
assertTrue(BlockingInitInterceptor.awaitInitializationStart());

Warehouse.clear();
BlockingInitInterceptor.releaseInitialization();
RecordingCallback callback = new RecordingCallback();
Postcard postcard = new Postcard("/test/registry-reset", "test");
service.doInterceptions(postcard, callback);

assertTrue(callback.await());
assertSame(postcard, callback.continued.get());
assertNull(callback.interrupted.get());
assertEquals(1, BlockingInitInterceptor.processCount.get());
assertEquals(1, PassingInterceptor.processCount.get());
}

@Test
public void emptyRegistryContinuesWithoutSchedulingWork() {
testExecutor.shutdown();
InterceptorServiceImpl service = new InterceptorServiceImpl();
service.init(targetContext());
RecordingCallback callback = new RecordingCallback();
Postcard postcard = new Postcard("/test/no-interceptors", "test");

service.doInterceptions(postcard, callback);

assertSame(postcard, callback.continued.get());
assertNull(callback.interrupted.get());
assertEquals(0, testExecutor.getTaskCount());
}

@Test
public void initializationFailureInterruptsNavigationWithTheOriginalCause() throws Exception {
Warehouse.interceptorsIndex.put(1, FailingInitInterceptor.class);
Expand Down Expand Up @@ -269,10 +350,12 @@ public void init(Context context) {
}

public static class BlockingInitInterceptor implements IInterceptor {
static final AtomicInteger processCount = new AtomicInteger();
private static CountDownLatch initializationStarted;
private static CountDownLatch initializationRelease;

static void reset() {
processCount.set(0);
initializationStarted = new CountDownLatch(1);
initializationRelease = new CountDownLatch(1);
}
Expand All @@ -287,6 +370,7 @@ static void releaseInitialization() {

@Override
public void process(Postcard postcard, InterceptorCallback callback) {
processCount.incrementAndGet();
callback.onContinue(postcard);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import com.alibaba.android.arouter.facade.callback.InterceptorCallback;
import com.alibaba.android.arouter.facade.service.InterceptorService;
import com.alibaba.android.arouter.facade.template.IInterceptor;
import com.alibaba.android.arouter.utils.MapUtils;

import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.alibaba.android.arouter.launcher.ARouter.logger;
Expand All @@ -27,30 +27,31 @@
public class InterceptorServiceImpl implements InterceptorService {
private static final long INTERCEPTOR_INIT_TIMEOUT_SECONDS = 10;
private final InterceptorInitState interceptorInitState = new InterceptorInitState();
private final List<IInterceptor> interceptors = new ArrayList<>();

@Override
public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
InterceptorInitState.Result initResult;
try {
initResult = interceptorInitState.await(INTERCEPTOR_INIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
HandlerException interruption = new HandlerException("Interceptor initialization was interrupted.");
interruption.initCause(ex);
callback.onInterrupt(interruption);
return;
}
InterceptorInitState.Result initResult;
try {
initResult = interceptorInitState.await(INTERCEPTOR_INIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
HandlerException interruption = new HandlerException("Interceptor initialization was interrupted.");
interruption.initCause(ex);
callback.onInterrupt(interruption);
return;
}

if (initResult.getOutcome() == InterceptorInitState.Outcome.TIMEOUT) {
callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time."));
return;
} else if (initResult.getOutcome() == InterceptorInitState.Outcome.FAILURE) {
callback.onInterrupt(initResult.getFailure());
return;
}
if (initResult.getOutcome() == InterceptorInitState.Outcome.TIMEOUT) {
callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time."));
return;
} else if (initResult.getOutcome() == InterceptorInitState.Outcome.FAILURE) {
callback.onInterrupt(initResult.getFailure());
return;
}

InterceptorChain interceptorChain = new InterceptorChain(Warehouse.interceptors, postcard, callback);
if (!interceptors.isEmpty()) {
InterceptorChain interceptorChain = new InterceptorChain(interceptors, postcard, callback);
try {
interceptorChain.scheduleTimeout(postcard.getTimeout(), TimeUnit.SECONDS);
LogisticsCenter.executor.execute(interceptorChain);
Expand All @@ -65,30 +66,32 @@ public void doInterceptions(final Postcard postcard, final InterceptorCallback c
@Override
public void init(final Context context) {
interceptorInitState.start();
// An old async initializer must not read or modify a later debug re-init's registry.
final List<Class<? extends IInterceptor>> interceptorClasses =
new ArrayList<>(Warehouse.interceptorsIndex.values());
if (interceptorClasses.isEmpty()) {
interceptorInitState.succeed();
return;
}
try {
LogisticsCenter.executor.execute(new Runnable() {
@Override
public void run() {
if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
Class<? extends IInterceptor> interceptorClass = entry.getValue();
try {
IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
iInterceptor.init(context);
Warehouse.interceptors.add(iInterceptor);
} catch (Exception ex) {
HandlerException failure = interceptorInitFailure(interceptorClass, ex);
interceptorInitState.fail(failure);
logger.error(TAG, failure.getMessage(), ex);
return;
}
for (Class<? extends IInterceptor> interceptorClass : interceptorClasses) {
try {
IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
iInterceptor.init(context);
interceptors.add(iInterceptor);
} catch (Exception ex) {
HandlerException failure = interceptorInitFailure(interceptorClass, ex);
interceptorInitState.fail(failure);
logger.error(TAG, failure.getMessage(), ex);
return;
}

interceptorInitState.succeed();
logger.info(TAG, "ARouter interceptors init over.");
} else {
interceptorInitState.succeed();
}

interceptorInitState.succeed();
logger.info(TAG, "ARouter interceptors init over.");
}
});
} catch (RuntimeException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
import com.alibaba.android.arouter.facade.template.IProvider;
import com.alibaba.android.arouter.facade.template.IRouteGroup;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
Expand All @@ -29,14 +27,12 @@ class Warehouse {

// Cache interceptor
static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
static List<IInterceptor> interceptors = new ArrayList<>();

static void clear() {
routes.clear();
groupsIndex.clear();
providers.clear();
providersIndex.clear();
interceptors.clear();
interceptorsIndex.clear();
}
}
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.1.3
ANDROIDX_TEST_RUNNER_VERSION=1.3.0
ANDROIDX_TEST_EXT_JUNIT_VERSION=1.1.2
MIN_SDK_VERSION=14
# The Gson-based demo has a higher minimum than the ARouter runtime.
DEMO_MIN_SDK_VERSION=21
TARGET_SDK_VERSION=28

# ARouter is compiled and tested as a native AndroidX library. Jetifier stays
Expand Down
36 changes: 34 additions & 2 deletions gradle/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

These checks validate development artifacts, not a release or publication.
ARouter's published minimum dependencies and `minSdkVersion=14` are unchanged.
Device coverage starts at API 21; it does not establish runtime coverage for
every API level between 14 and 20.
The complete demo CI matrix runs on API 21 and API 34. Minimum-API framework
and consumer checks are separate; they do not establish coverage for every
intermediate API level.

## Complete framework device suite

Expand Down Expand Up @@ -32,6 +33,37 @@ The verifier requires fresh, nonempty JUnit reports with no failures, errors,
or skipped tests. Local copies are retained by API and build type under
`build/reports/device-tests/`, so later runs do not erase earlier matrix results.

## Minimum API 14 runtime checks

Connect exactly one booted API 14 emulator and verify its API level before
running the standalone framework tests with JDK 8:

```sh
"${ANDROID_HOME}/platform-tools/adb" shell getprop ro.build.version.sdk
./gradlew :arouter-api:connectedDebugAndroidTest
```

For end-to-end routing and Release/R8, run the published-artifact consumer
below on the same emulator with the minimum dependencies and `AROUTER_MIN_SDK=14`.
This exercises both daily/online Debug/Release variants without test-only keep
rules. Preserve the fixture and check its fresh JUnit reports, not just APK
assembly. The consumer covers Activity and provider routing, Fragment and
DialogFragment creation, arguments and injection. `withOptionsCompat` requires
API 16 and is deliberately not exercised on API 14.

The demo's Gson 2.14 dependency requires [API 21 or newer](https://github.com/google/gson#minimum-android-api-level).
The demo application and its Gson-based `module-java` therefore use
`DEMO_MIN_SDK_VERSION=21`, separately from the runtime's `MIN_SDK_VERSION=14`.
Do not use the demo as proof of the runtime library's minimum API. Gson is a
JVM compiler/demo dependency, not an ARouter runtime dependency.

The official API 14 ARM image uses the old `kernel-qemu`/classic engine.
Emulator 36.1.9 cannot boot it. An isolated SDK Tools 25.2.5 classic engine
was validated on macOS through Rosetta; the [official macOS archive](https://dl.google.com/android/repository/tools_r25.2.5-macosx.zip)
can be extracted separately without replacing a working SDK emulator.
See the [SDK Tools release history](https://developer.android.com/tools/releases/sdk-tools)
and keep the chosen engine version and system-image checksum with test results.

## Published-artifact consumer

The dependency verifier requires `rg` (ripgrep), `unzip`, and `strings`
Expand Down
4 changes: 2 additions & 2 deletions module-java/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ android {
buildToolsVersion BUILDTOOLS_VERSION

defaultConfig {
minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
minSdkVersion Integer.parseInt(DEMO_MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)

javaCompileOptions {
Expand All @@ -29,7 +29,7 @@ android {

compileOptions {
// Gson 2.14 contains Java 8 default interface methods. Enable Android's
// desugaring pipeline so this minSdk 14 demo module can package tests.
// desugaring pipeline for the demo; Gson's runtime minimum is API 21.
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
Expand Down
Loading