| // Copyright 2016 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.browserservices.intents; |
| |
| import android.content.Intent; |
| import android.content.res.AssetManager; |
| import android.content.res.Resources; |
| import android.content.res.XmlResourceParser; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.provider.Browser; |
| |
| import androidx.browser.trusted.sharing.ShareData; |
| |
| import org.hamcrest.Matchers; |
| import org.junit.Assert; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.android.XmlResourceParserImpl; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.res.ResourceTable; |
| import org.robolectric.util.ReflectionHelpers; |
| import org.w3c.dom.Document; |
| |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| import org.chromium.blink.mojom.DisplayMode; |
| import org.chromium.chrome.browser.ShortcutHelper; |
| import org.chromium.chrome.browser.webapps.WebApkIntentDataProviderFactory; |
| import org.chromium.components.webapk.lib.common.WebApkMetaDataKeys; |
| import org.chromium.components.webapps.ShortcutSource; |
| import org.chromium.components.webapps.WebApkDistributor; |
| import org.chromium.device.mojom.ScreenOrientationLockType; |
| import org.chromium.ui.util.ColorUtils; |
| import org.chromium.webapk.lib.common.WebApkConstants; |
| import org.chromium.webapk.lib.common.splash.SplashLayout; |
| import org.chromium.webapk.test.WebApkTestHelper; |
| |
| import java.io.ByteArrayInputStream; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| |
| /** |
| * Tests {@link WebappInfo} with WebAPKs. |
| */ |
| @RunWith(BaseRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE) |
| public class WebApkInfoTest { |
| private static final String WEBAPK_PACKAGE_NAME = "org.chromium.webapk.test_package"; |
| private static final String UNBOUND_WEBAPK_PACKAGE_NAME = "unbound.webapk"; |
| |
| // Android Manifest meta data for {@link PACKAGE_NAME}. |
| private static final String START_URL = "https://www.google.com/scope/a_is_for_apple"; |
| private static final String SCOPE = "https://www.google.com/scope"; |
| private static final String NAME = "name"; |
| private static final String SHORT_NAME = "short_name"; |
| private static final String DISPLAY_MODE = "minimal-ui"; |
| private static final String ORIENTATION = "portrait"; |
| private static final String THEME_COLOR = "1L"; |
| private static final String BACKGROUND_COLOR = "2L"; |
| private static final int SHELL_APK_VERSION = 3; |
| private static final String MANIFEST_URL = "https://www.google.com/alphabet.json"; |
| private static final String ICON_URL = "https://www.google.com/scope/worm.png"; |
| private static final String ICON_MURMUR2_HASH = "5"; |
| private static final int PRIMARY_ICON_ID = 12; |
| private static final int PRIMARY_MASKABLE_ICON_ID = 14; |
| private static final int SOURCE = ShortcutSource.NOTIFICATION; |
| |
| /** Fakes the Resources object, allowing lookup of String value. */ |
| private static class FakeResources extends Resources { |
| private static AssetManager sAssetManager = createAssetManager(); |
| private final Map<String, Integer> mStringIdMap; |
| private final Map<Integer, String> mIdValueMap; |
| private String mShortcutsXmlContents; |
| |
| private class MockXmlResourceParserImpl extends XmlResourceParserImpl { |
| String mPackageName; |
| |
| public MockXmlResourceParserImpl(Document document, String fileName, String packageName, |
| String applicationPackageName, ResourceTable resourceTable) { |
| super(document, fileName, packageName, applicationPackageName, resourceTable); |
| mPackageName = packageName; |
| } |
| |
| @Override |
| public int getAttributeResourceValue( |
| String namespace, String attribute, int defaultValue) { |
| // Remove the trailing '@'. |
| String attributeValue = getAttributeValue(namespace, attribute).substring(1); |
| if (mStringIdMap.containsKey(attributeValue)) { |
| return mStringIdMap.get(attributeValue); |
| } |
| return defaultValue; |
| } |
| } |
| |
| private static AssetManager createAssetManager() { |
| try { |
| return AssetManager.class.getConstructor().newInstance(); |
| } catch (Exception e) { |
| return null; |
| } |
| } |
| |
| // Do not warn about deprecated call to Resources(); the documentation says code is not |
| // supposed to create its own Resources object, but we are using it to fake out the |
| // Resources, and there is no other way to do that. |
| @SuppressWarnings("deprecation") |
| public FakeResources() { |
| super(sAssetManager, null, null); |
| mStringIdMap = new HashMap<>(); |
| mIdValueMap = new HashMap<>(); |
| } |
| |
| @Override |
| public int getIdentifier(String name, String defType, String defPackage) { |
| String key = getKey(name, defType, defPackage); |
| return mStringIdMap.containsKey(key) ? mStringIdMap.get(key) : 0; |
| } |
| |
| @Override |
| public String getString(int id) { |
| if (!mIdValueMap.containsKey(id)) { |
| throw new Resources.NotFoundException("id 0x" + Integer.toHexString(id)); |
| } |
| |
| return mIdValueMap.get(id); |
| } |
| |
| @Override |
| public int getColor(int id, Resources.Theme theme) { |
| return Integer.parseInt(getString(id)); |
| } |
| |
| @Override |
| public XmlResourceParser getXml(int id) { |
| try { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| factory.setIgnoringComments(true); |
| factory.setIgnoringElementContentWhitespace(true); |
| DocumentBuilder documentBuilder = factory.newDocumentBuilder(); |
| Document document = documentBuilder.parse( |
| new ByteArrayInputStream(mShortcutsXmlContents.getBytes())); |
| |
| return new MockXmlResourceParserImpl( |
| document, "file", WEBAPK_PACKAGE_NAME, WEBAPK_PACKAGE_NAME, null); |
| } catch (Exception e) { |
| Assert.fail("Failed to create XmlResourceParser"); |
| return null; |
| } |
| } |
| |
| void setShortcutsXmlContent(String content) { |
| mShortcutsXmlContents = content; |
| } |
| |
| public void addStringForTesting( |
| String name, String defType, String defPackage, int identifier, String value) { |
| String key = getKey(name, defType, defPackage); |
| |
| mStringIdMap.put(key, identifier); |
| mIdValueMap.put(identifier, value); |
| } |
| |
| public void addColorForTesting(String name, String defPackage, int identifier, int value) { |
| addStringForTesting(name, "color", defPackage, identifier, Integer.toString(value)); |
| } |
| |
| private String getKey(String name, String defType, String defPackage) { |
| return defPackage + ":" + defType + "/" + name; |
| } |
| } |
| |
| @Test |
| public void testSanity() { |
| // Test guidelines: |
| // - Stubbing out native calls in this test likely means that there is a bug. |
| // - For every WebappInfo boolean there should be a test which tests both values. |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.SCOPE, SCOPE); |
| bundle.putString(WebApkMetaDataKeys.NAME, NAME); |
| bundle.putString(WebApkMetaDataKeys.SHORT_NAME, SHORT_NAME); |
| bundle.putString(WebApkMetaDataKeys.DISPLAY_MODE, DISPLAY_MODE); |
| bundle.putString(WebApkMetaDataKeys.ORIENTATION, ORIENTATION); |
| bundle.putString(WebApkMetaDataKeys.THEME_COLOR, THEME_COLOR); |
| bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, BACKGROUND_COLOR); |
| bundle.putInt(WebApkMetaDataKeys.SHELL_APK_VERSION, SHELL_APK_VERSION); |
| bundle.putString(WebApkMetaDataKeys.WEB_MANIFEST_URL, MANIFEST_URL); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| bundle.putString(WebApkMetaDataKeys.ICON_URLS_AND_ICON_MURMUR2_HASHES, |
| ICON_URL + " " + ICON_MURMUR2_HASH); |
| bundle.putInt(WebApkMetaDataKeys.ICON_ID, PRIMARY_ICON_ID); |
| bundle.putInt(WebApkMetaDataKeys.MASKABLE_ICON_ID, PRIMARY_MASKABLE_ICON_ID); |
| |
| Bundle shareActivityBundle = new Bundle(); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ACTION, "action0"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_METHOD, "POST"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ENCTYPE, "multipart/form-data"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_PARAM_TITLE, "title0"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_PARAM_TEXT, "text0"); |
| shareActivityBundle.putString( |
| WebApkMetaDataKeys.SHARE_PARAM_NAMES, "[\"name1\", \"name2\"]"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_PARAM_ACCEPTS, |
| "[[\"text/plain\"], [\"image/png\", \"image/jpeg\"]]"); |
| |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, new Bundle[] {shareActivityBundle}); |
| |
| Intent intent = new Intent(); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME, WEBAPK_PACKAGE_NAME); |
| intent.putExtra(WebappConstants.EXTRA_FORCE_NAVIGATION, true); |
| intent.putExtra(WebappConstants.EXTRA_URL, START_URL); |
| intent.putExtra(WebappConstants.EXTRA_SOURCE, ShortcutSource.NOTIFICATION); |
| intent.putExtra(WebApkConstants.EXTRA_SPLASH_PROVIDED_BY_WEBAPK, true); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| |
| Assert.assertEquals(WebApkConstants.WEBAPK_ID_PREFIX + WEBAPK_PACKAGE_NAME, info.id()); |
| Assert.assertEquals(START_URL, info.url()); |
| Assert.assertTrue(info.shouldForceNavigation()); |
| Assert.assertEquals(SCOPE, info.scopeUrl()); |
| Assert.assertEquals(NAME, info.name()); |
| Assert.assertEquals(SHORT_NAME, info.shortName()); |
| Assert.assertEquals(DisplayMode.MINIMAL_UI, info.displayMode()); |
| Assert.assertEquals(ScreenOrientationLockType.PORTRAIT, info.orientation()); |
| Assert.assertTrue(info.hasValidToolbarColor()); |
| Assert.assertEquals(1L, info.toolbarColor()); |
| Assert.assertTrue(info.hasValidBackgroundColor()); |
| Assert.assertEquals(2L, info.backgroundColor()); |
| Assert.assertEquals(WEBAPK_PACKAGE_NAME, info.webApkPackageName()); |
| Assert.assertEquals(SHELL_APK_VERSION, info.shellApkVersion()); |
| Assert.assertEquals(MANIFEST_URL, info.manifestUrl()); |
| Assert.assertEquals(START_URL, info.manifestStartUrl()); |
| Assert.assertEquals(WebApkDistributor.BROWSER, info.distributor()); |
| |
| Assert.assertEquals(1, info.iconUrlToMurmur2HashMap().size()); |
| Assert.assertTrue(info.iconUrlToMurmur2HashMap().containsKey(ICON_URL)); |
| Assert.assertEquals(ICON_MURMUR2_HASH, info.iconUrlToMurmur2HashMap().get(ICON_URL)); |
| |
| Assert.assertEquals(SOURCE, info.source()); |
| Assert.assertEquals( |
| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M), info.isSplashProvidedByWebApk()); |
| |
| Assert.assertEquals(PRIMARY_MASKABLE_ICON_ID, info.icon().resourceIdForTesting()); |
| Assert.assertEquals(true, info.isIconAdaptive()); |
| Assert.assertEquals(null, info.splashIcon().bitmap()); |
| |
| WebApkShareTarget shareTarget = info.shareTarget(); |
| Assert.assertNotNull(shareTarget); |
| Assert.assertEquals("action0", shareTarget.getAction()); |
| Assert.assertTrue(shareTarget.isShareMethodPost()); |
| Assert.assertTrue(shareTarget.isShareEncTypeMultipart()); |
| Assert.assertEquals("title0", shareTarget.getParamTitle()); |
| Assert.assertEquals("text0", shareTarget.getParamText()); |
| Assert.assertEquals(new String[] {"name1", "name2"}, shareTarget.getFileNames()); |
| Assert.assertEquals(new String[][] {{"text/plain"}, {"image/png", "image/jpeg"}}, |
| shareTarget.getFileAccepts()); |
| } |
| |
| /** |
| * Test that {@link createWebApkInfo()} ignores the maskable icon on pre-Android-O |
| * Android OSes. |
| */ |
| @Test |
| public void testOsVersionDoesNotSupportAdaptive() { |
| ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.N); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putInt(WebApkMetaDataKeys.ICON_ID, PRIMARY_ICON_ID); |
| bundle.putInt(WebApkMetaDataKeys.MASKABLE_ICON_ID, PRIMARY_MASKABLE_ICON_ID); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(PRIMARY_ICON_ID, info.icon().resourceIdForTesting()); |
| Assert.assertEquals(false, info.isIconAdaptive()); |
| } |
| |
| /** |
| * Test that {@link createWebApkInfo()} selects {@link WebApkMetaDataKeys.ICON_ID} if no |
| * maskable icon is provided and that the icon is tagged as non-maskable. |
| */ |
| @Test |
| public void testNoMaskableIcon() { |
| Bundle bundle = new Bundle(); |
| bundle.putInt(WebApkMetaDataKeys.ICON_ID, PRIMARY_ICON_ID); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(PRIMARY_ICON_ID, info.icon().resourceIdForTesting()); |
| Assert.assertEquals(false, info.isIconAdaptive()); |
| } |
| |
| /** |
| * Test that {@link createWebApkInfo()} populates {@link WebappInfo#url()} with the start URL |
| * from the intent not the start URL in the WebAPK's meta data. When a WebAPK is launched via a |
| * deep link from a URL within the WebAPK's scope, the WebAPK should open at the URL it was deep |
| * linked from not the WebAPK's start URL. |
| */ |
| @Test |
| public void testUseStartUrlOverride() { |
| String intentStartUrl = "https://www.google.com/master_override"; |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = |
| WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, intentStartUrl); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(intentStartUrl, info.url()); |
| |
| // {@link WebappInfo#manifestStartUrl()} should contain the start URL from the Android |
| // Manifest. |
| Assert.assertEquals(START_URL, info.manifestStartUrl()); |
| } |
| |
| /** |
| * Test that if the scope is empty that the scope is computed from the "start URL specified from |
| * the Web Manifest" not the "URL the WebAPK initially navigated to". Deep links can open a |
| * WebAPK at an arbitrary URL. |
| */ |
| @Test |
| public void testDefaultScopeFromManifestStartUrl() { |
| String manifestStartUrl = START_URL; |
| String intentStartUrl = "https://www.google.com/a/b/c"; |
| |
| String scopeFromManifestStartUrl = ShortcutHelper.getScopeFromUrl(manifestStartUrl); |
| String scopeFromIntentStartUrl = ShortcutHelper.getScopeFromUrl(intentStartUrl); |
| Assert.assertNotEquals(scopeFromManifestStartUrl, scopeFromIntentStartUrl); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, manifestStartUrl); |
| bundle.putString(WebApkMetaDataKeys.SCOPE, ""); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = |
| WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, intentStartUrl); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(scopeFromManifestStartUrl, info.scopeUrl()); |
| } |
| |
| /** |
| * Test that {@link createWebApkInfo} can read multiple icon URLs and multiple icon murmur2 |
| * hashes from the WebAPK's meta data. |
| */ |
| @Test |
| public void testGetIconUrlAndMurmur2HashFromMetaData() { |
| String iconUrl1 = "/icon1.png"; |
| String murmur2Hash1 = "1"; |
| String iconUrl2 = "/icon2.png"; |
| String murmur2Hash2 = "2"; |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| bundle.putString(WebApkMetaDataKeys.ICON_URLS_AND_ICON_MURMUR2_HASHES, |
| iconUrl1 + " " + murmur2Hash1 + " " + iconUrl2 + " " + murmur2Hash2); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Map<String, String> iconUrlToMurmur2HashMap = info.iconUrlToMurmur2HashMap(); |
| Assert.assertEquals(2, iconUrlToMurmur2HashMap.size()); |
| Assert.assertEquals(murmur2Hash1, iconUrlToMurmur2HashMap.get(iconUrl1)); |
| Assert.assertEquals(murmur2Hash2, iconUrlToMurmur2HashMap.get(iconUrl2)); |
| } |
| |
| /** |
| * WebApkIconHasher generates hashes with values [0, 2^64-1]. 2^64-1 is greater than |
| * {@link Long#MAX_VALUE}. Test that {@link createWebApkInfo()} can read a hash with value |
| * 2^64 - 1. |
| */ |
| @Test |
| public void testGetIconMurmur2HashFromMetaData() { |
| String hash = "18446744073709551615"; // 2^64 - 1 |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| bundle.putString(WebApkMetaDataKeys.ICON_URLS_AND_ICON_MURMUR2_HASHES, "randomUrl " + hash); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Map<String, String> iconUrlToMurmur2HashMap = info.iconUrlToMurmur2HashMap(); |
| Assert.assertEquals(1, iconUrlToMurmur2HashMap.size()); |
| Assert.assertTrue(iconUrlToMurmur2HashMap.containsValue(hash)); |
| } |
| |
| /** |
| * Prior to SHELL_APK_VERSION 2, WebAPKs did not specify |
| * {@link WebappConstants#EXTRA_FORCE_NAVIGATION} in the intent. Test that |
| * {@link WebappInfo#shouldForceNavigation()} defaults to true when the intent extra is not |
| * specified. |
| */ |
| @Test |
| public void testForceNavigationNotSpecified() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertTrue(info.shouldForceNavigation()); |
| } |
| |
| /** |
| * Test that {@link WebappInfo#source()} returns {@link ShortcutSource#UNKNOWN} if the source |
| * in the launch intent > {@link ShortcutSource#COUNT}. This can occur if the user is using a |
| * new WebAPK and an old version of Chrome. |
| */ |
| @Test |
| public void testOutOfBoundsSource() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| intent.putExtra(WebappConstants.EXTRA_SOURCE, ShortcutSource.COUNT + 1); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(ShortcutSource.UNKNOWN, info.source()); |
| } |
| |
| /** |
| * Test that {@link WebappInfo#name()} and {@link WebappInfo#shortName()} return the name and |
| * short name from the meta data before they are moved to strings in resources. |
| */ |
| @Test |
| public void testNameAndShortNameFromMetadataWhenStringResourcesDoNotExist() { |
| String name = "WebAPK name"; |
| String shortName = "WebAPK short name"; |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| bundle.putString(WebApkMetaDataKeys.NAME, name); |
| bundle.putString(WebApkMetaDataKeys.SHORT_NAME, shortName); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(name, info.name()); |
| Assert.assertEquals(shortName, info.shortName()); |
| } |
| |
| /** |
| * Test that {@link WebappInfo#name()} and {@link WebappInfo#shortName()} return the string |
| * values from the WebAPK resources if exist. |
| */ |
| @Test |
| public void testNameAndShortNameFromWebApkStrings() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| String name = "WebAPK name"; |
| String shortName = "WebAPK short name"; |
| FakeResources res = new FakeResources(); |
| res.addStringForTesting(WebApkIntentDataProviderFactory.RESOURCE_NAME, |
| WebApkIntentDataProviderFactory.RESOURCE_STRING_TYPE, WEBAPK_PACKAGE_NAME, 1, name); |
| res.addStringForTesting(WebApkIntentDataProviderFactory.RESOURCE_SHORT_NAME, |
| WebApkIntentDataProviderFactory.RESOURCE_STRING_TYPE, WEBAPK_PACKAGE_NAME, 2, |
| shortName); |
| WebApkTestHelper.setResource(WEBAPK_PACKAGE_NAME, res); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(name, info.name()); |
| Assert.assertEquals(shortName, info.shortName()); |
| } |
| |
| /** |
| * Test that ShortcutSource#EXTERNAL_INTENT is rewritten to |
| * ShortcutSource#EXTERNAL_INTENT_FROM_CHROME if the WebAPK is launched from a |
| * browser with the same package name (e.g. web page link on Chrome Stable |
| * launches WebAPK whose host browser is Chrome Stable). |
| */ |
| @Test |
| public void testOverrideExternalIntentSourceIfLaunchedFromChrome() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| intent.putExtra(WebappConstants.EXTRA_SOURCE, ShortcutSource.EXTERNAL_INTENT); |
| intent.putExtra( |
| Browser.EXTRA_APPLICATION_ID, RuntimeEnvironment.application.getPackageName()); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(ShortcutSource.EXTERNAL_INTENT_FROM_CHROME, info.source()); |
| } |
| |
| /** |
| * Test that ShortcutSource#EXTERNAL_INTENT is not rewritten when the WebAPK is launched |
| * from a non-browser app. |
| */ |
| @Test |
| public void testOverrideExternalIntentSourceIfLaunchedFromNonChromeApp() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| intent.putExtra(WebappConstants.EXTRA_SOURCE, ShortcutSource.EXTERNAL_INTENT); |
| intent.putExtra(Browser.EXTRA_APPLICATION_ID, "com.google.android.talk"); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(ShortcutSource.EXTERNAL_INTENT, info.source()); |
| } |
| |
| /** |
| * Test that ShortcutSource#SHARE_TARGET is rewritten to |
| * ShortcutSource#WEBAPK_SHARE_TARGET_FILE if the WebAPK is launched as a result of user sharing |
| * a binary file. |
| */ |
| @Test |
| public void testOverrideShareTargetSourceIfLaunchedFromFileSharing() { |
| Bundle shareActivityBundle = new Bundle(); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ACTION, "/share.html"); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, new Bundle[] {shareActivityBundle}); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| intent.setAction(Intent.ACTION_SEND); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_SELECTED_SHARE_TARGET_ACTIVITY_CLASS_NAME, |
| "something"); |
| ArrayList<Uri> uris = new ArrayList<>(); |
| uris.add(Uri.parse("mock-uri-3")); |
| intent.putExtra(Intent.EXTRA_STREAM, uris); |
| intent.putExtra(WebappConstants.EXTRA_SOURCE, ShortcutSource.WEBAPK_SHARE_TARGET); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(ShortcutSource.WEBAPK_SHARE_TARGET_FILE, info.source()); |
| } |
| |
| /** |
| * Test when a distributor is not specified, the default distributor value for a WebAPK |
| * installed by Chrome is |WebApkDistributor.BROWSER|, while for an Unbound WebAPK is |
| * |WebApkDistributor.Other|. |
| */ |
| @Test |
| public void testWebApkDistributorDefaultValue() { |
| // Test Case: Bound WebAPK |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(WebApkDistributor.BROWSER, info.distributor()); |
| |
| // Test Case: Unbound WebAPK |
| WebApkTestHelper.registerWebApkWithMetaData( |
| UNBOUND_WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| intent = new Intent(); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME, UNBOUND_WEBAPK_PACKAGE_NAME); |
| intent.putExtra(WebappConstants.EXTRA_URL, START_URL); |
| info = createWebApkInfo(intent); |
| Assert.assertEquals(WebApkDistributor.OTHER, info.distributor()); |
| } |
| |
| /** |
| * Test that {@link WebappInfo#shareTarget()} returns a null object if the WebAPK does not |
| * handle share intents. |
| */ |
| @Test |
| public void testGetShareTargetNullIfDoesNotHandleShareIntents() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData(WEBAPK_PACKAGE_NAME, bundle, null); |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| WebappInfo info = createWebApkInfo(intent); |
| |
| Assert.assertNull(info.shareTarget()); |
| } |
| |
| /** |
| * Tests that {@link WebApkShareTarget#getFileNames()} returns an empty list when the |
| * {@link shareParamNames} <meta-data> tag is not a JSON array. |
| */ |
| @Test |
| public void testPostShareTargetInvalidParamNames() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| |
| Bundle shareActivityBundle = new Bundle(); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_METHOD, "POST"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ENCTYPE, "multipart/form-data"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_PARAM_NAMES, "not an array"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_PARAM_ACCEPTS, "[[\"image/*\"]]"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_PARAM_TEXT, "share-text"); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ACTION, "/share.html"); |
| |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, new Bundle[] {shareActivityBundle}); |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| WebappInfo info = createWebApkInfo(intent); |
| |
| WebApkShareTarget shareTarget = info.shareTarget(); |
| Assert.assertNotNull(shareTarget); |
| Assert.assertEquals(0, shareTarget.getFileNames().length); |
| } |
| |
| /** |
| * Tests building {@link ShareData} when {@link Intent.EXTRA_STREAM} has a Uri value. |
| */ |
| @Test |
| public void testShareDataUriString() { |
| Bundle shareActivityBundle = new Bundle(); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ACTION, "/share.html"); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, new Bundle[] {shareActivityBundle}); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| intent.setAction(Intent.ACTION_SEND); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_SELECTED_SHARE_TARGET_ACTIVITY_CLASS_NAME, |
| WebApkTestHelper.getGeneratedShareTargetActivityClassName(0)); |
| Uri sharedFileUri = Uri.parse("mock-uri-1"); |
| intent.putExtra(Intent.EXTRA_STREAM, sharedFileUri); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| ShareData shareData = info.shareData(); |
| Assert.assertNotNull(shareData); |
| Assert.assertNotNull(shareData.uris); |
| Assert.assertThat(shareData.uris, Matchers.contains(sharedFileUri)); |
| } |
| |
| /** |
| * Tests building {@link ShareData} when {@link Intent.EXTRA_STREAM} has an ArrayList |
| * value. |
| */ |
| @Test |
| public void testShareDataUriList() { |
| Bundle shareActivityBundle = new Bundle(); |
| shareActivityBundle.putString(WebApkMetaDataKeys.SHARE_ACTION, "/share.html"); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, new Bundle[] {shareActivityBundle}); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| intent.setAction(Intent.ACTION_SEND); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_SELECTED_SHARE_TARGET_ACTIVITY_CLASS_NAME, |
| WebApkTestHelper.getGeneratedShareTargetActivityClassName(0)); |
| Uri sharedFileUri = Uri.parse("mock-uri-1"); |
| ArrayList<Uri> uris = new ArrayList<>(); |
| uris.add(sharedFileUri); |
| intent.putExtra(Intent.EXTRA_STREAM, uris); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| ShareData shareData = info.shareData(); |
| Assert.assertNotNull(shareData); |
| Assert.assertNotNull(shareData.uris); |
| Assert.assertThat(shareData.uris, Matchers.contains(sharedFileUri)); |
| } |
| |
| /** |
| * Test that {@link WebappInfo#backgroundColorFallbackToDefault()} uses |
| * {@link SplashLayout#getDefaultBackgroundColor()} as the default background color if there is |
| * no default background color in the WebAPK's resources. |
| */ |
| @Test |
| public void testBackgroundColorFallbackToDefaultNoCustomDefault() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, ColorUtils.INVALID_COLOR + "L"); |
| WebApkTestHelper.registerWebApkWithMetaData(WEBAPK_PACKAGE_NAME, bundle, null); |
| |
| Intent intent = new Intent(); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME, WEBAPK_PACKAGE_NAME); |
| intent.putExtra(WebappConstants.EXTRA_URL, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(SplashLayout.getDefaultBackgroundColor(RuntimeEnvironment.application), |
| info.backgroundColorFallbackToDefault()); |
| } |
| |
| /** |
| * Test that {@link WebappInfo#backgroundColorFallbackToDefault()} uses the default |
| * background color from the WebAPK's resources if present. |
| */ |
| @Test |
| public void testBackgroundColorFallbackToDefaultWebApkHasCustomDefault() { |
| final int defaultBackgroundColorResourceId = 1; |
| final int defaultBackgroundColorInWebApk = 42; |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| bundle.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, ColorUtils.INVALID_COLOR + "L"); |
| bundle.putInt( |
| WebApkMetaDataKeys.DEFAULT_BACKGROUND_COLOR_ID, defaultBackgroundColorResourceId); |
| WebApkTestHelper.registerWebApkWithMetaData(WEBAPK_PACKAGE_NAME, bundle, null); |
| |
| FakeResources res = new FakeResources(); |
| res.addColorForTesting("mockResource", WEBAPK_PACKAGE_NAME, |
| defaultBackgroundColorResourceId, defaultBackgroundColorInWebApk); |
| WebApkTestHelper.setResource(WEBAPK_PACKAGE_NAME, res); |
| |
| Intent intent = new Intent(); |
| intent.putExtra(WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME, WEBAPK_PACKAGE_NAME); |
| intent.putExtra(WebappConstants.EXTRA_URL, START_URL); |
| |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals( |
| defaultBackgroundColorInWebApk, info.backgroundColorFallbackToDefault()); |
| } |
| |
| /** |
| * Test that shortcut items are properly parsed. |
| */ |
| @Test |
| public void testShortcutItemsFromWebApkStrings() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(WebApkMetaDataKeys.START_URL, START_URL); |
| WebApkTestHelper.registerWebApkWithMetaData( |
| WEBAPK_PACKAGE_NAME, bundle, null /* shareTargetMetaData */); |
| |
| FakeResources res = new FakeResources(); |
| res.addStringForTesting(WebApkIntentDataProviderFactory.RESOURCE_SHORTCUTS, |
| WebApkIntentDataProviderFactory.RESOURCE_XML_TYPE, WEBAPK_PACKAGE_NAME, 1, null); |
| res.addStringForTesting("shortcut_1_short_name", |
| WebApkIntentDataProviderFactory.RESOURCE_STRING_TYPE, WEBAPK_PACKAGE_NAME, 2, |
| "short name1"); |
| res.addStringForTesting("shortcut_1_name", |
| WebApkIntentDataProviderFactory.RESOURCE_STRING_TYPE, WEBAPK_PACKAGE_NAME, 3, |
| "name1"); |
| res.addStringForTesting("shortcut_2_short_name", |
| WebApkIntentDataProviderFactory.RESOURCE_STRING_TYPE, WEBAPK_PACKAGE_NAME, 4, |
| "short name2"); |
| res.addStringForTesting("shortcut_2_name", |
| WebApkIntentDataProviderFactory.RESOURCE_STRING_TYPE, WEBAPK_PACKAGE_NAME, 5, |
| "name2"); |
| res.addStringForTesting("shortcut_1_icon", "drawable", WEBAPK_PACKAGE_NAME, 6, null); |
| res.addStringForTesting("shortcut_2_icon", "drawable", WEBAPK_PACKAGE_NAME, 7, null); |
| WebApkTestHelper.setResource(WEBAPK_PACKAGE_NAME, res); |
| |
| Intent intent = WebApkTestHelper.createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL); |
| |
| // No shortcuts case. |
| res.setShortcutsXmlContent( |
| "<shortcuts xmlns:android='http://schemas.android.com/apk/res/android'/>"); |
| WebappInfo info = createWebApkInfo(intent); |
| Assert.assertEquals(info.shortcutItems().size(), 0); |
| |
| // One shortcut case. |
| String oneShortcut = |
| "<shortcuts xmlns:android='http://schemas.android.com/apk/res/android'>" |
| + " <shortcut" |
| + " android:shortcutId='shortcut_1'" |
| + " android:icon='@drawable/shortcut_1_icon'" |
| + " iconUrl='https://example.com/icon1.png'" |
| + " iconHash='1234'" |
| + " android:shortcutShortLabel='@string/shortcut_1_short_name'" |
| + " android:shortcutLongLabel='@string/shortcut_1_name'>" |
| + " <intent android:data='https://example.com/launch1' />" |
| + " </shortcut>" |
| + "</shortcuts>"; |
| |
| res.setShortcutsXmlContent(oneShortcut); |
| info = createWebApkInfo(intent); |
| Assert.assertEquals(info.shortcutItems().size(), 1); |
| WebApkExtras.ShortcutItem item = info.shortcutItems().get(0); |
| Assert.assertEquals(item.name, "name1"); |
| Assert.assertEquals(item.shortName, "short name1"); |
| Assert.assertEquals(item.launchUrl, "https://example.com/launch1"); |
| Assert.assertEquals(item.iconUrl, "https://example.com/icon1.png"); |
| Assert.assertEquals(item.iconHash, "1234"); |
| Assert.assertNotNull(item.icon); |
| Assert.assertEquals(item.icon.resourceIdForTesting(), 6); |
| |
| // Multiple shortcuts case. |
| String twoShortcuts = |
| "<shortcuts xmlns:android='http://schemas.android.com/apk/res/android'>" |
| + " <shortcut" |
| + " android:shortcutId='shortcut_1'" |
| + " android:icon='@drawable/shortcut_1_icon'" |
| + " iconUrl='https://example.con/icon1.png'" |
| + " iconHash='1234'" |
| + " android:shortcutShortLabel='@string/shortcut_1_short_name'" |
| + " android:shortcutLongLabel='@string/shortcut_1_name'>" |
| + " <intent android:data='https://example.com/launch1' />" |
| + " </shortcut>" |
| + " <shortcut" |
| + " android:shortcutId='shortcut_2'" |
| + " android:icon='@drawable/shortcut_2_icon'" |
| + " iconUrl='https://example.com/icon2.png'" |
| + " iconHash='2345'" |
| + " android:shortcutShortLabel='@string/shortcut_2_short_name'" |
| + " android:shortcutLongLabel='@string/shortcut_2_name'>" |
| + " <intent android:data='https://example.com/launch2' />" |
| + " </shortcut>" |
| + "</shortcuts>"; |
| |
| res.setShortcutsXmlContent(twoShortcuts); |
| info = createWebApkInfo(intent); |
| Assert.assertEquals(info.shortcutItems().size(), 2); |
| item = info.shortcutItems().get(1); |
| Assert.assertEquals(item.name, "name2"); |
| Assert.assertEquals(item.shortName, "short name2"); |
| Assert.assertEquals(item.launchUrl, "https://example.com/launch2"); |
| Assert.assertEquals(item.iconUrl, "https://example.com/icon2.png"); |
| Assert.assertEquals(item.iconHash, "2345"); |
| Assert.assertNotNull(item.icon); |
| Assert.assertEquals(item.icon.resourceIdForTesting(), 7); |
| } |
| |
| private WebappInfo createWebApkInfo(Intent intent) { |
| return WebappInfo.create(WebApkIntentDataProviderFactory.create(intent)); |
| } |
| } |