blob: 9539de55bc9a677179eb03c35b68baeeb6ec703b [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs.dynamicmodule;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.EXTRA_HIDE_CCT_HEADER_ON_MODULE_MANAGED_URLS;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.EXTRA_MODULE_CLASS_NAME;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.EXTRA_MODULE_MANAGED_URLS_HEADER_VALUE;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.EXTRA_MODULE_MANAGED_URLS_REGEX;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.EXTRA_MODULE_PACKAGE_NAME;
import static org.chromium.chrome.browser.customtabs.dynamicmodule.DynamicModuleConstants.ON_NAVIGATION_EVENT_MODULE_API_VERSION;
import static org.chromium.chrome.browser.customtabs.dynamicmodule.DynamicModuleConstants.ON_PAGE_LOAD_METRIC_API_VERSION;
import static org.chromium.chrome.browser.customtabs.dynamicmodule.DynamicModuleNavigationEventObserver.PENDING_URL_KEY;
import static org.chromium.chrome.browser.customtabs.dynamicmodule.DynamicModuleNavigationEventObserver.URL_KEY;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsCallback;
import android.support.test.InstrumentationRegistry;
import org.junit.Assert;
import org.chromium.base.annotations.UsedByReflection;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.AppHooksModule;
import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.metrics.PageLoadMetrics;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.concurrent.TimeoutException;
/**
* Utility class that contains fake CCT dynamic module classes and convenience calls
* related with CCT dynamic module testing.
*/
public class CustomTabsDynamicModuleTestUtils {
/* package */ final static String FAKE_MODULE_PACKAGE_NAME =
InstrumentationRegistry.getInstrumentation().getContext().getPackageName();
/* package */ final static String FAKE_MODULE_CLASS_NAME = FakeCCTDynamicModule.class.getName();
/* package */ final static ComponentName FAKE_MODULE_COMPONENT_NAME = new ComponentName(
FAKE_MODULE_PACKAGE_NAME, FAKE_MODULE_CLASS_NAME);
/**
* A asset name used to load {@link #FAKE_MODULE_DEX}.
*/
/* package */ final static String FAKE_MODULE_DEX_RESOURCE_ID = "42";
/**
* A fake "dex file" that consists of couple of bytes.
*/
/* package */ final static byte[] FAKE_MODULE_DEX = new byte[] {42, 42};
/**
* A fake {@link ModuleLoader.DexClassLoaderProvider} that provides the {@link ClassLoader} of
* {@link FakeCCTDynamicModule} which guarantees that it can always load the entry module.
*/
/* package */ final static ModuleLoader.DexClassLoaderProvider FAKE_CLASS_LOADER_PROVIDER =
dexFile -> FakeCCTDynamicModule.class.getClassLoader();
private static int sModuleVersion = 1;
public static void setModuleVersion(int version) {
sModuleVersion = version;
}
/**
* To load a fake module in tests we need to bypass a check if package name of module
* is Google-signed. This class overrides this check for testing.
*/
/* package */ static class AppHooksModuleForTest extends AppHooksModule {
@Override
public ExternalAuthUtils provideExternalAuthUtils() {
return new ExternalAuthUtils() {
@Override
public boolean isGoogleSigned(String packageName) {
return true;
}
};
}
}
/**
* This class is used to test CCT module loader.
*/
@UsedByReflection("dynamicmodule.ModuleLoader$LoadClassTask")
public static class FakeCCTDynamicModule extends BaseModuleEntryPoint {
@Override
public void init(IModuleHost moduleHost) {
}
@Override
public int getModuleVersion() {
return sModuleVersion;
}
@Override
public int getMinimumHostVersion() {
return 0;
}
@Override
public IActivityDelegate createActivityDelegate(IActivityHost activityHost) {
return new FakeCCTActivityDelegate();
}
@Override
public void onDestroy() {
}
}
/**
* Fake class to test CCT dynamic module.
*/
public static class FakeCCTActivityDelegate extends BaseActivityDelegate {
private final CallbackHelper mOnNavigationStarted = new CallbackHelper();
private final CallbackHelper mOnNavigationFinished = new CallbackHelper();
private final CallbackHelper mOnFirstContentfulPaint = new CallbackHelper();
public FakeCCTActivityDelegate() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
}
@Override
public void onResume() {
}
@Override
public void onPostCreate(Bundle savedInstanceState) {
}
@Override
public void onStart() {
}
@Override
public void onStop() {
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
}
@Override
public void onSaveInstanceState(Bundle outState) {
}
@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
}
@Override
public void onPause() {
}
@Override
public void onDestroy() {
}
@Override
public boolean onBackPressed() {
return false;
}
@Override
public void onBackPressedAsync(IObjectWrapper notHandledRunnable) {
}
@Override
public void onMessageChannelReady() {
}
@Override
public void onPostMessage(String message) {
}
@Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
if (sModuleVersion < ON_NAVIGATION_EVENT_MODULE_API_VERSION) {
Assert.fail(String.format(
"onNavigationEvent must not be used if module version less than %d",
ON_NAVIGATION_EVENT_MODULE_API_VERSION));
}
if (navigationEvent == CustomTabsCallback.NAVIGATION_STARTED) {
Assert.assertNotNull(extras.get(PENDING_URL_KEY));
mOnNavigationStarted.notifyCalled();
} else if (navigationEvent == CustomTabsCallback.NAVIGATION_FINISHED) {
Assert.assertNotNull(extras.get(URL_KEY));
mOnNavigationFinished.notifyCalled();
}
}
@Override
public void onPageMetricEvent(String metricName, long navigationStart,
long offset, long navigationId) {
if (sModuleVersion < ON_PAGE_LOAD_METRIC_API_VERSION) {
Assert.fail(String.format(
"onPageMetricEvent must not be used if module version less than %d",
ON_PAGE_LOAD_METRIC_API_VERSION));
}
long current = SystemClock.uptimeMillis();
Assert.assertTrue(navigationStart <= current);
Assert.assertTrue(offset <= (current - navigationStart));
if (PageLoadMetrics.FIRST_CONTENTFUL_PAINT.equals(metricName)) {
mOnFirstContentfulPaint.notifyCalled();
}
}
/**
* Waits for expected number of navigation events happen.
*/
/* package */ void waitForNavigationEvent(int navigationEvent, int currentCallCount,
int numberOfCallsToWaitFor) throws TimeoutException, InterruptedException {
if (sModuleVersion < ON_NAVIGATION_EVENT_MODULE_API_VERSION) return;
if (navigationEvent == CustomTabsCallback.NAVIGATION_STARTED) {
mOnNavigationStarted.waitForCallback(currentCallCount, numberOfCallsToWaitFor);
} else if (navigationEvent == CustomTabsCallback.NAVIGATION_FINISHED) {
mOnNavigationFinished.waitForCallback(currentCallCount, numberOfCallsToWaitFor);
}
}
/* package */ void waitForFirstContentfulPaint(int currentCallCount,
int numberOfCallsToWaitFor) throws TimeoutException, InterruptedException {
if (sModuleVersion < ON_PAGE_LOAD_METRIC_API_VERSION) return;
mOnFirstContentfulPaint.waitForCallback(currentCallCount, numberOfCallsToWaitFor);
}
}
/**
* Class to build an {@link Intent} to start a CCT with a dynamic module.
*
* By default the intent contains extras to load {@link FakeCCTDynamicModule}.
*/
/* package */ static class IntentBuilder {
private final Intent mIntent;
public Intent build() {
return mIntent;
}
IntentBuilder(String url) {
mIntent = CustomTabsTestUtils.createMinimalCustomTabIntent(
InstrumentationRegistry.getTargetContext(), url);
setModuleComponentName(FAKE_MODULE_COMPONENT_NAME);
}
IntentBuilder setModuleFailToLoadComponentName() {
setModulePackageName(FAKE_MODULE_PACKAGE_NAME);
setModuleClassName("ClassName");
return this;
}
IntentBuilder setModuleComponentName(ComponentName componentName) {
setModulePackageName(componentName.getPackageName());
setModuleClassName(componentName.getClassName());
return this;
}
IntentBuilder setModulePackageName(@Nullable String packageName) {
if (packageName == null) mIntent.removeExtra(EXTRA_MODULE_PACKAGE_NAME);
mIntent.putExtra(EXTRA_MODULE_PACKAGE_NAME, packageName);
return this;
}
IntentBuilder setModuleClassName(@Nullable String className) {
if (className == null) mIntent.removeExtra(EXTRA_MODULE_CLASS_NAME);
mIntent.putExtra(EXTRA_MODULE_CLASS_NAME, className);
return this;
}
IntentBuilder setModuleManagedUrlRegex(String urlRegex) {
mIntent.putExtra(EXTRA_MODULE_MANAGED_URLS_REGEX, urlRegex);
return this;
}
IntentBuilder setModuleManagedUrlHeaderValue(String headerValue) {
mIntent.putExtra(EXTRA_MODULE_MANAGED_URLS_HEADER_VALUE, headerValue);
return this;
}
IntentBuilder setHideCCTHeader(boolean isEnabled) {
mIntent.putExtra(EXTRA_HIDE_CCT_HEADER_ON_MODULE_MANAGED_URLS, isEnabled);
return this;
}
}
/**
* A fake version of {@link ModuleLoader.DexInputStreamProvider} that provides {@link
* #FAKE_MODULE_DEX}.
*/
/* package */ static class FakeDexInputStreamProvider
implements ModuleLoader.DexInputStreamProvider {
private int mCallCount;
@Override
public InputStream createInputStream(@Nullable String dexAssetName, Context moduleContext) {
if (!FAKE_MODULE_DEX_RESOURCE_ID.equals(dexAssetName)) {
throw new RuntimeException("Unknown resource ID: " + dexAssetName);
}
mCallCount++;
return new ByteArrayInputStream(FAKE_MODULE_DEX);
}
/* package */ int getCallCount() {
return mCallCount;
}
}
}