Commit 74c4a854 authored by Rayan Kanso's avatar Rayan Kanso Committed by Commit Bot

[Shortcuts] Populate WebApkInfo with shortcut values.

The information is extracted from the shortcuts.xml resource if present
in the WebAPK.

This is needed to diff with a new WebApkInfo generated from the web app
manifest for update decisions.

Bug: 1045588
Change-Id: Ibe5284d22799e5d5d5c37e53da3da95c6903a5ca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2027417
Commit-Queue: Rayan Kanso <rayankans@chromium.org>
Reviewed-by: default avatarPeter Kotwicz <pkotwicz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#737907}
parent 5bf17cd5
......@@ -7,6 +7,8 @@ package org.chromium.chrome.browser.webapps;
import org.chromium.chrome.browser.webapps.WebApkInfo.ShareData;
import org.chromium.chrome.browser.webapps.WebApkInfo.ShareTarget;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
......@@ -78,25 +80,46 @@ public class WebApkExtras {
*/
public final ShareData shareData;
/**
* The list of the WebAPK's shortcuts.
*/
public final List<ShortcutItem> shortcutItems;
/**
* WebAPK's version code.
*/
public final int webApkVersionCode;
/** A class that stores information from shortcut items. */
public static class ShortcutItem {
public String name;
public String shortName;
public String launchUrl;
public String iconHash;
public ShortcutItem(String name, String shortName, String launchUrl, String iconHash) {
this.name = name;
this.shortName = shortName;
this.launchUrl = launchUrl;
this.iconHash = iconHash;
}
}
public static WebApkExtras createEmpty() {
return new WebApkExtras(null /* webApkPackageName */, new WebappIcon(), new WebappIcon(),
false /* isSplashIconMaskable */, 0 /* shellApkVersion */, null /* manifestUrl */,
null /* manifestStartUrl */, WebApkDistributor.OTHER,
null /* iconUrlToMurmur2HashMap */, new ShareTarget(),
false /* isSplashProvidedByWebApk */, null /* shareData */,
0 /* webApkVersionCode */);
new ArrayList<>() /* shortcutItems */, 0 /* webApkVersionCode */);
}
public WebApkExtras(String webApkPackageName, WebappIcon badgeIcon, WebappIcon splashIcon,
boolean isSplashIconMaskable, int shellApkVersion, String manifestUrl,
String manifestStartUrl, @WebApkDistributor int distributor,
Map<String, String> iconUrlToMurmur2HashMap, ShareTarget shareTarget,
boolean isSplashProvidedByWebApk, ShareData shareData, int webApkVersionCode) {
boolean isSplashProvidedByWebApk, ShareData shareData, List<ShortcutItem> shortcutItems,
int webApkVersionCode) {
this.webApkPackageName = webApkPackageName;
this.badgeIcon = badgeIcon;
this.splashIcon = splashIcon;
......@@ -109,6 +132,7 @@ public class WebApkExtras {
this.shareTarget = shareTarget;
this.isSplashProvidedByWebApk = isSplashProvidedByWebApk;
this.shareData = shareData;
this.shortcutItems = shortcutItems;
this.webApkVersionCode = webApkVersionCode;
}
}
......@@ -12,10 +12,12 @@ import androidx.annotation.Nullable;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.webapps.WebApkExtras.ShortcutItem;
import org.chromium.webapk.lib.common.WebApkConstants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
......@@ -179,13 +181,14 @@ public class WebApkInfo extends WebappInfo {
String manifestUrl, String manifestStartUrl, @WebApkDistributor int distributor,
Map<String, String> iconUrlToMurmur2HashMap, ShareTarget shareTarget,
boolean forceNavigation, boolean isSplashProvidedByWebApk, ShareData shareData,
int webApkVersionCode) {
List<ShortcutItem> shortcutItems, int webApkVersionCode) {
return create(WebApkIntentDataProviderFactory.create(url, scope, primaryIcon, badgeIcon,
splashIcon, name, shortName, displayMode, orientation, source, themeColor,
backgroundColor, defaultBackgroundColor, isPrimaryIconMaskable,
isSplashIconMaskable, webApkPackageName, shellApkVersion, manifestUrl,
manifestStartUrl, distributor, iconUrlToMurmur2HashMap, shareTarget,
forceNavigation, isSplashProvidedByWebApk, shareData, webApkVersionCode));
forceNavigation, isSplashProvidedByWebApk, shareData, shortcutItems,
webApkVersionCode));
}
private static WebApkInfo create(@Nullable BrowserServicesIntentDataProvider provider) {
......@@ -265,6 +268,10 @@ public class WebApkInfo extends WebappInfo {
return getWebApkExtras().shareData;
}
public List<ShortcutItem> shortcutItems() {
return getWebApkExtras().shortcutItems;
}
private WebApkExtras getWebApkExtras() {
WebApkExtras extras = mProvider.getWebApkExtras();
assert extras != null;
......
......@@ -11,12 +11,15 @@ import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import org.xmlpull.v1.XmlPullParser;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
......@@ -26,6 +29,7 @@ import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.ShortcutSource;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.webapps.WebApkExtras.ShortcutItem;
import org.chromium.chrome.browser.webapps.WebApkInfo.ShareData;
import org.chromium.chrome.browser.webapps.WebApkInfo.ShareTarget;
import org.chromium.content_public.common.ScreenOrientationValues;
......@@ -47,7 +51,18 @@ import java.util.Map;
public class WebApkIntentDataProviderFactory {
public static final String RESOURCE_NAME = "name";
public static final String RESOURCE_SHORT_NAME = "short_name";
public static final String RESOURCE_SHORTCUTS = "shortcuts";
public static final String RESOURCE_STRING_TYPE = "string";
public static final String RESOURCE_XML_TYPE = "xml";
private static final String SHORTCUT_ATTRIBUTE_NAMESPACE =
"http://schemas.android.com/apk/res/android";
private static final String SHORTCUT_TAG_NAME = "shortcut";
private static final String SHORTCUT_INTENT_TAG_NAME = "intent";
private static final String SHORTCUT_NAME_ATTRIBUTE = "shortcutLongLabel";
private static final String SHORTCUT_SHORT_NAME_ATTRIBUTE = "shortcutShortLabel";
private static final String SHORTCUT_ICON_HASH_ATTRIBUTE = "iconHash";
private static final String SHORTCUT_INTENT_LAUNCH_URL_ATTRIBUTE = "data";
private static final String TAG = "WebApkInfo";
......@@ -128,6 +143,54 @@ public class WebApkIntentDataProviderFactory {
: WebApkDistributor.OTHER;
}
/**
* @param webApkPackageName
* @param resources
* @return A list of shortcut items derived from the parser.
*/
private static List<ShortcutItem> parseShortcutItems(String webApkPackageName, Resources res) {
int shortcutsResId =
res.getIdentifier(RESOURCE_SHORTCUTS, RESOURCE_XML_TYPE, webApkPackageName);
if (shortcutsResId == 0) {
return new ArrayList<>();
}
XmlResourceParser parser = res.getXml(shortcutsResId);
List<ShortcutItem> shortcuts = new ArrayList<>();
try {
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG
&& TextUtils.equals(parser.getName(), SHORTCUT_TAG_NAME)) {
int nameResId = parser.getAttributeResourceValue(
SHORTCUT_ATTRIBUTE_NAMESPACE, SHORTCUT_NAME_ATTRIBUTE, 0);
int shortNameResId = parser.getAttributeResourceValue(
SHORTCUT_ATTRIBUTE_NAMESPACE, SHORTCUT_SHORT_NAME_ATTRIBUTE, 0);
String iconHash = parser.getAttributeValue(null, SHORTCUT_ICON_HASH_ATTRIBUTE);
eventType = parser.next();
if (eventType != XmlPullParser.START_TAG
&& !TextUtils.equals(parser.getName(), SHORTCUT_INTENT_TAG_NAME)) {
// shortcuts.xml is malformed for some reason. Bail out.
return new ArrayList<>();
}
String launchUrl = parser.getAttributeValue(
SHORTCUT_ATTRIBUTE_NAMESPACE, SHORTCUT_INTENT_LAUNCH_URL_ATTRIBUTE);
shortcuts.add(new ShortcutItem(nameResId != 0 ? res.getString(nameResId) : null,
shortNameResId != 0 ? res.getString(shortNameResId) : null,
launchUrl != null ? launchUrl : null, iconHash));
}
eventType = parser.next();
}
} catch (Exception e) {
return new ArrayList<>();
}
return shortcuts;
}
/**
* Constructs a BrowserServicesIntentDataProvider from the passed in parameters and <meta-data>
* in the WebAPK's Android manifest.
......@@ -261,7 +324,8 @@ public class WebApkIntentDataProviderFactory {
orientation, source, themeColor, backgroundColor, defaultBackgroundColor,
isPrimaryIconMaskable, isSplashIconMaskable, webApkPackageName, shellApkVersion,
manifestUrl, manifestStartUrl, distributor, iconUrlToMurmur2HashMap, shareTarget,
forceNavigation, isSplashProvidedByWebApk, shareData, apkVersion);
forceNavigation, isSplashProvidedByWebApk, shareData,
parseShortcutItems(webApkPackageName, res), apkVersion);
}
/**
......@@ -298,6 +362,7 @@ public class WebApkIntentDataProviderFactory {
* display the splash screen and (2) has a content provider
* which provides a screenshot of the splash screen.
* @param shareData Shared information from the share intent.
* @param shortcutItems A list of shortcut items.
* @param webApkVersionCode WebAPK's version code.
*/
public static BrowserServicesIntentDataProvider create(String url, String scope,
......@@ -308,7 +373,7 @@ public class WebApkIntentDataProviderFactory {
int shellApkVersion, String manifestUrl, String manifestStartUrl,
@WebApkDistributor int distributor, Map<String, String> iconUrlToMurmur2HashMap,
ShareTarget shareTarget, boolean forceNavigation, boolean isSplashProvidedByWebApk,
ShareData shareData, int webApkVersionCode) {
ShareData shareData, List<ShortcutItem> shortcutItems, int webApkVersionCode) {
if (manifestStartUrl == null || webApkPackageName == null) {
Log.e(TAG, "Incomplete data provided: " + manifestStartUrl + ", " + webApkPackageName);
return null;
......@@ -349,7 +414,7 @@ public class WebApkIntentDataProviderFactory {
WebApkExtras webApkExtras = new WebApkExtras(webApkPackageName, badgeIcon, splashIcon,
isSplashIconMaskable, shellApkVersion, manifestUrl, manifestStartUrl, distributor,
iconUrlToMurmur2HashMap, shareTarget, isSplashProvidedByWebApk, shareData,
webApkVersionCode);
shortcutItems, webApkVersionCode);
boolean hasCustomToolbarColor = WebappIntentUtils.isLongColorValid(themeColor);
int toolbarColor = hasCustomToolbarColor
? (int) themeColor
......
......@@ -137,7 +137,7 @@ public class WebApkUpdateDataFetcher extends EmptyTabObserver {
mOldInfo.webApkPackageName(), mOldInfo.shellApkVersion(), mOldInfo.manifestUrl(),
manifestStartUrl, WebApkDistributor.BROWSER, iconUrlToMurmur2HashMap, shareTarget,
mOldInfo.shouldForceNavigation(), mOldInfo.isSplashProvidedByWebApk(), null,
mOldInfo.webApkVersionCode());
mOldInfo.shortcutItems(), mOldInfo.webApkVersionCode());
mObserver.onGotManifestData(info, primaryIconUrl, badgeIconUrl);
}
......
......@@ -33,6 +33,7 @@ import org.chromium.content_public.common.ScreenOrientationValues;
import org.chromium.net.test.EmbeddedTestServerRule;
import org.chromium.webapk.lib.client.WebApkVersion;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
......@@ -167,18 +168,16 @@ public class WebApkUpdateManagerTest {
TestThreadUtils.runOnUiThreadBlocking(() -> {
WebappDataStorage storage =
WebappRegistry.getInstance().getWebappDataStorage(WEBAPK_ID);
WebApkInfo info = WebApkInfo.create(
"", creationData.scope, null, null, null, creationData.name,
creationData.shortName, creationData.displayMode, creationData.orientation, 0,
creationData.themeColor, creationData.backgroundColor, 0,
creationData.isPrimaryIconMaskable, false /* isSplashIconMaskable */, "",
WebApkInfo info = WebApkInfo.create("", creationData.scope, null, null, null,
creationData.name, creationData.shortName, creationData.displayMode,
creationData.orientation, 0, creationData.themeColor,
creationData.backgroundColor, 0, creationData.isPrimaryIconMaskable,
false /* isSplashIconMaskable */, "",
WebApkVersion.REQUEST_UPDATE_FOR_SHELL_APK_VERSION, creationData.manifestUrl,
creationData.startUrl, WebApkDistributor.BROWSER,
creationData.iconUrlToMurmur2HashMap, null, false /* forceNavigation */,
false /* isSplashProvidedByWebApk */, null /* shareData */,
1 /* webApkVersionCode */
);
new ArrayList<>() /* shortcutItems */, 1 /* webApkVersionCode */);
updateManager.updateIfNeeded(storage, info);
});
waiter.waitForCallback(0);
......
......@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.webapps;
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;
......@@ -17,7 +18,10 @@ 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.w3c.dom.Document;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.ShortcutHelper;
......@@ -28,10 +32,14 @@ import org.chromium.webapk.lib.common.WebApkMetaDataKeys;
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 WebApkInfo.
*/
......@@ -60,6 +68,28 @@ public class WebApkInfoTest {
private static class FakeResources extends Resources {
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;
}
}
// 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
......@@ -91,9 +121,33 @@ public class WebApkInfoTest {
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);
}
......@@ -632,4 +686,92 @@ public class WebApkInfoTest {
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");
WebApkTestHelper.setResource(WEBAPK_PACKAGE_NAME, res);
Intent intent = createMinimalWebApkIntent(WEBAPK_PACKAGE_NAME, START_URL);
// No shortcuts case.
res.setShortcutsXmlContent(
"<shortcuts xmlns:android='http://schemas.android.com/apk/res/android'/>");
WebApkInfo info = WebApkInfo.create(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'"
+ " 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 = WebApkInfo.create(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.iconHash, "1234");
// 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'"
+ " 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'"
+ " 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 = WebApkInfo.create(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.iconHash, "2345");
}
}
......@@ -53,6 +53,7 @@ import org.chromium.webapk.test.WebApkTestHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
......@@ -393,7 +394,8 @@ public class WebApkUpdateManagerUnitTest {
SHARE_TARGET_ENC_TYPE_MULTIPART),
manifestData.shareTargetFileNames, manifestData.shareTargetFileAccepts),
false /* forceNavigation */, false /* isSplashProvidedByWebApk */,
null /* shareData */, 1 /* webApkVersionCode */);
null /* shareData */, new ArrayList<>() /* shortcutItems */,
1 /* webApkVersionCode */);
}
/**
......
......@@ -13,6 +13,7 @@ import org.chromium.chrome.browser.webapps.WebApkInfo;
import org.chromium.chrome.browser.webapps.WebDisplayMode;
import org.chromium.content_public.common.ScreenOrientationValues;
import java.util.ArrayList;
import java.util.HashMap;
/** Builder class for {@link WebApkInfo} objects. */
......@@ -58,6 +59,6 @@ public class WebApkInfoBuilder {
WebApkDistributor.BROWSER,
new HashMap<String, String>() /* iconUrlToMurmur2HashMap */, null,
false /* forceNavigation */, false /* isSplashProvidedByWebApk */, null,
mWebApkVersionCode);
new ArrayList<>() /* shortcutItems */, mWebApkVersionCode);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment