// Copyright 2016 The Chromium Authors
// 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.webapps;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import org.json.JSONArray;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.android.util.concurrent.RoboExecutorService;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPackageManager;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.FakeTimeTestRule;
import org.chromium.base.PathUtils;
import org.chromium.base.TimeUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.test.BackgroundShadowAsyncTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.JniMocker;
import org.chromium.blink.mojom.DisplayMode;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.background_task_scheduler.ChromeBackgroundTaskFactory;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebApkExtras;
import org.chromium.chrome.browser.browserservices.intents.WebApkShareTarget;
import org.chromium.chrome.browser.browserservices.intents.WebappConstants;
import org.chromium.chrome.browser.browserservices.intents.WebappIcon;
import org.chromium.chrome.browser.browserservices.intents.WebappInfo;
import org.chromium.chrome.browser.browserservices.intents.WebappIntentUtils;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.embedder_support.util.ShadowUrlUtilities;
import org.chromium.components.webapk.lib.common.WebApkMetaDataKeys;
import org.chromium.components.webapps.WebApkDistributor;
import org.chromium.components.webapps.WebApkInstallResult;
import org.chromium.device.mojom.ScreenOrientationLockType;
import org.chromium.ui.modaldialog.DialogDismissalCause;
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.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/** Unit tests for WebApkUpdateManager. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
manifest = Config.NONE,
shadows = {ShadowUrlUtilities.class, BackgroundShadowAsyncTask.class})
@LooperMode(LooperMode.Mode.LEGACY)
public class WebApkUpdateManagerUnitTest {
@Mock private Activity mActivityMock;
@Rule public FakeTimeTestRule mClockRule = new FakeTimeTestRule();
@Rule public JniMocker mJniMocker = new JniMocker();
private static final String WEBAPK_PACKAGE_NAME = "org.chromium.webapk.test_package";
private static final String UNBOUND_WEBAPK_PACKAGE_NAME = "com.webapk.test_package";
private static final int REQUEST_UPDATE_FOR_SHELL_APK_VERSION = 100;
/** Web Manifest URL */
private static final String WEB_MANIFEST_URL = "manifest.json";
private static final String START_URL = "/start_url.html";
private static final String SCOPE_URL = "/";
private static final String NAME = "Long Name";
private static final String SHORT_NAME = "Short Name";
private static final String MANIFEST_ID = "manifestId";
private static final String PRIMARY_ICON_URL = "/icon.png";
private static final String PRIMARY_ICON_MURMUR2_HASH = "3";
private static final @DisplayMode.EnumType int DISPLAY_MODE = DisplayMode.UNDEFINED;
private static final int ORIENTATION = ScreenOrientationLockType.DEFAULT;
private static final long THEME_COLOR = 1L;
private static final long BACKGROUND_COLOR = 2L;
private static final long DARK_THEME_COLOR = 3L;
private static final long DARK_BACKGROUND_COLOR = 4L;
private static final int DEFAULT_BACKGROUND_COLOR = 3;
private static final String SHARE_TARGET_ACTION = "/share_action.html";
private static final String SHARE_TARGET_PARAM_TITLE = "share_params_title";
private static final String SHARE_TARGET_METHOD_GET = "GET";
private static final String SHARE_TARGET_METHOD_POST = "POST";
private static final String SHARE_TARGET_ENC_TYPE_MULTIPART = "multipart/form-data";
private static final String[] SHARE_TARGET_FILE_NAMES = new String[] {"file_1", "file_2"};
private static final String[][] SHARE_TARGET_ACCEPTS =
new String[][] {
new String[] {"file_1_accept_1", "file_1_accept_2"},
new String[] {"file_2_accept_2", "file_2_accept_2"}
};
/** Different values than the ones used in {@link defaultManifestData()}. */
private static final String DIFFERENT_NAME = "Different Name";
private static final int DIFFERENT_BACKGROUND_COLOR = 42;
/** The histograms involved in showing the App Identity update dialog. */
private static final String HISTOGRAM_NOT_SHOWING = "Webapp.AppIdentityDialog.NotShowing";
private static final String HISTOGRAM_SHOWING = "Webapp.AppIdentityDialog.Showing";
private static final String HISTOGRAM_PRE_APPROVED = "Webapp.AppIdentityDialog.AlreadyApproved";
/** Mock {@link WebApkUpdateDataFetcher}. */
private static class TestWebApkUpdateDataFetcher extends WebApkUpdateDataFetcher {
private boolean mStarted;
public boolean wasStarted() {
return mStarted;
}
@Override
public boolean start(Tab tab, WebappInfo oldInfo, Observer observer) {
mStarted = true;
return true;
}
}
private static class TestWebApkUpdateManagerJni implements WebApkUpdateManager.Natives {
private static WebApkUpdateManager.WebApkUpdateCallback sUpdateCallback;
public static WebApkUpdateManager.WebApkUpdateCallback getUpdateCallback() {
return sUpdateCallback;
}
@Override
public void storeWebApkUpdateRequestToFile(
String updateRequestPath,
String startUrl,
String scope,
String name,
String shortName,
boolean hasCustomName,
String manifestId,
String appKey,
String primaryIconUrl,
byte[] primaryIconData,
boolean isPrimaryIconMaskable,
String splashIconUrl,
byte[] splashIconData,
boolean isSplashIconMaskable,
String[] iconUrls,
String[] iconHashes,
@DisplayMode.EnumType int displayMode,
int orientation,
long themeColor,
long backgroundColor,
long darkThemeColor,
long darkBackgroundColor,
String shareTargetAction,
String shareTargetParamTitle,
String shareTargetParamText,
boolean shareTargetParamIsMethodPost,
boolean shareTargetParamIsEncTypeMultipart,
String[] shareTargetParamFileNames,
Object[] shareTargetParamAccepts,
String[][] shortcuts,
byte[][] shortcutIconData,
String manifestUrl,
String webApkPackage,
int webApkVersion,
boolean isManifestStale,
boolean isAppIdentityUpdateSupported,
int[] updateReasons,
Callback<Boolean> callback) {}
@Override
public void updateWebApkFromFile(
String updateRequestPath, WebApkUpdateManager.WebApkUpdateCallback callback) {
sUpdateCallback = callback;
}
@Override
public int getWebApkTargetShellVersion() {
return REQUEST_UPDATE_FOR_SHELL_APK_VERSION;
}
}
private static class TestWebApkUpdateManager extends WebApkUpdateManager {
private Callback<Boolean> mStoreUpdateRequestCallback;
private TestWebApkUpdateDataFetcher mFetcher;
private String mUpdateName;
private String mAppKey;
private boolean mDestroyedFetcher;
/**
* Whether App Identity updates should be enabled. If either of those is true when the tests
* run, all App Identity update dialogs will be pre-approved (without showing).
*/
private boolean mNameUpdatesEnabled;
private boolean mIconUpdatesEnabled;
public TestWebApkUpdateManager(Activity activity) {
this(activity, /* nameUpdatesEnabled= */ false, /* iconUpdatesEnabled= */ false);
}
public TestWebApkUpdateManager(
Activity activity, boolean nameUpdatesEnabled, boolean iconUpdatesEnabled) {
this(activity, buildMockTabProvider(), Mockito.mock(ActivityLifecycleDispatcher.class));
mNameUpdatesEnabled = nameUpdatesEnabled;
mIconUpdatesEnabled = iconUpdatesEnabled;
}
private static ActivityTabProvider buildMockTabProvider() {
Tab mockTab = Mockito.mock(Tab.class);
ActivityTabProvider tabProvider = Mockito.mock(ActivityTabProvider.class);
Mockito.when(tabProvider.get()).thenReturn(mockTab);
return tabProvider;
}
private TestWebApkUpdateManager(
Activity activity,
ActivityTabProvider tabProvider,
ActivityLifecycleDispatcher activityLifecycleDispatcher) {
super(activity, tabProvider, activityLifecycleDispatcher);
}
/** Returns whether the is-update-needed check has been triggered. */
public boolean updateCheckStarted() {
return mFetcher != null && mFetcher.wasStarted();
}
/** Returns whether an update has been requested. */
public boolean updateRequested() {
return mStoreUpdateRequestCallback != null;
}
/** Returns the "name" from the requested update. Null if an update has not been requested. */
public String requestedUpdateName() {
return mUpdateName;
}
/**
* Returns the "app_key" from the requested update. Null if an update has not been
* requested.
*/
public String requestedAppKey() {
return mAppKey;
}
public boolean destroyedFetcher() {
return mDestroyedFetcher;
}
public Callback<Boolean> getStoreUpdateRequestCallback() {
return mStoreUpdateRequestCallback;
}
@Override
protected boolean iconUpdateDialogEnabled() {
return mIconUpdatesEnabled;
}
@Override
protected boolean nameUpdateDialogEnabled() {
return mNameUpdatesEnabled;
}
@Override
protected void showIconOrNameUpdateDialog(
boolean iconChanging, boolean shortNameChanging, boolean nameChanging) {
// This function is overridden because the parent class can't show the dialog (since
// WindowAndroid is null in this test) so there not much to do besides auto-approving
// the update (if the change is expected).
boolean expectNameChange = mNameUpdatesEnabled && (shortNameChanging || nameChanging);
boolean expectIconChange = mIconUpdatesEnabled && iconChanging;
super.onUserApprovedUpdate(
expectNameChange || expectIconChange
? DialogDismissalCause.POSITIVE_BUTTON_CLICKED
: DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
}
@Override
protected void encodeIconsInBackground(
String updateRequestPath,
WebappInfo info,
String primaryIconUrl,
String splashIconUrl,
boolean isManifestStale,
boolean isAppIdentityUpdateSupported,
List<Integer> updateReasons,
Callback<Boolean> callback) {
storeWebApkUpdateRequestToFile(
updateRequestPath,
info,
primaryIconUrl,
new byte[] {},
splashIconUrl,
new byte[] {},
isManifestStale,
isAppIdentityUpdateSupported,
updateReasons,
callback);
}
@Override
protected WebApkUpdateDataFetcher buildFetcher() {
mFetcher = new TestWebApkUpdateDataFetcher();
return mFetcher;
}
@Override
protected void storeWebApkUpdateRequestToFile(
String updateRequestPath,
WebappInfo info,
String primaryIconUrl,
byte[] primaryIconData,
String splashIconUrl,
byte[] splashIconData,
boolean isManifestStale,
boolean isAppIdentityUpdateSupported,
List<Integer> updateReasons,
Callback<Boolean> callback) {
mStoreUpdateRequestCallback = callback;
mUpdateName = info.name();
mAppKey = info.appKey();
writeRandomTextToFile(updateRequestPath);
}
@Override
protected void destroyFetcher() {
mFetcher = null;
mDestroyedFetcher = true;
}
}
private static class ManifestData {
public String startUrl;
public String scopeUrl;
public String name;
public String shortName;
public boolean hasCustomName;
public String id;
public String appKey;
public Map<String, String> iconUrlToMurmur2HashMap;
public String primaryIconUrl;
public Bitmap primaryIcon;
public @DisplayMode.EnumType int displayMode;
public int orientation;
public long themeColor;
public long backgroundColor;
public long darkThemeColor;
public long darkBackgroundColor;
public int defaultBackgroundColor;
public String shareTargetAction;
public String shareTargetParamTitle;
public String shareTargetMethod;
public String shareTargetEncType;
public String[] shareTargetFileNames;
public String[][] shareTargetFileAccepts;
// Injected directly into the resulting WebAPKInfo.
public List<WebApkExtras.ShortcutItem> shortcuts = new ArrayList<>();
}
private static class FakeDefaultBackgroundColorResource extends Resources {
private static final int ID = 10;
private int mColorValue;
public FakeDefaultBackgroundColorResource(int colorValue) {
super(new AssetManager(), null, null);
mColorValue = colorValue;
}
@Override
public int getColor(int id, Resources.Theme theme) {
if (id != ID) {
throw new Resources.NotFoundException("id 0x" + Integer.toHexString(id));
}
return mColorValue;
}
}
private void registerStorageForWebApkPackage(String webApkPackageName) throws Exception {
try {
CallbackHelper helper = new CallbackHelper();
WebappRegistry.getInstance()
.register(
WebappIntentUtils.getIdForWebApkPackage(webApkPackageName),
new WebappRegistry.FetchWebappDataStorageCallback() {
@Override
public void onWebappDataStorageRetrieved(
WebappDataStorage storage) {
helper.notifyCalled();
}
});
BackgroundShadowAsyncTask.runBackgroundTasks();
ShadowLooper.runUiThreadTasks();
helper.waitForOnly();
} catch (TimeoutException e) {
fail();
}
}
private static WebappDataStorage getStorage(String packageName) {
return WebappRegistry.getInstance()
.getWebappDataStorage(WebappIntentUtils.getIdForWebApkPackage(packageName));
}
/**
* Create a WebAPK metadata bundle from ManifestData.
*
* @param manifestData <meta-data> values for WebAPK's Android Manifest.
* @param shellApkVersionCode WebAPK's version of the //chrome/android/webapk/shell_apk code.
*/
private Bundle createWebApkMetadata(ManifestData manifestData, int shellApkVersionCode) {
Bundle metaData = new Bundle();
metaData.putInt(WebApkMetaDataKeys.SHELL_APK_VERSION, shellApkVersionCode);
metaData.putString(WebApkMetaDataKeys.START_URL, manifestData.startUrl);
metaData.putString(WebApkMetaDataKeys.SCOPE, manifestData.scopeUrl);
metaData.putString(WebApkMetaDataKeys.NAME, manifestData.name);
metaData.putString(WebApkMetaDataKeys.SHORT_NAME, manifestData.shortName);
metaData.putString(WebApkMetaDataKeys.THEME_COLOR, manifestData.themeColor + "L");
metaData.putString(WebApkMetaDataKeys.BACKGROUND_COLOR, manifestData.backgroundColor + "L");
metaData.putString(WebApkMetaDataKeys.DARK_THEME_COLOR, manifestData.darkThemeColor + "L");
metaData.putString(
WebApkMetaDataKeys.DARK_BACKGROUND_COLOR, manifestData.darkBackgroundColor + "L");
metaData.putInt(
WebApkMetaDataKeys.DEFAULT_BACKGROUND_COLOR_ID,
FakeDefaultBackgroundColorResource.ID);
metaData.putString(WebApkMetaDataKeys.WEB_MANIFEST_URL, WEB_MANIFEST_URL);
metaData.putString(WebApkMetaDataKeys.WEB_MANIFEST_ID, manifestData.id);
metaData.putString(WebApkMetaDataKeys.APP_KEY, manifestData.appKey);
String iconUrlsAndIconMurmur2Hashes = "";
for (Map.Entry<String, String> mapEntry : manifestData.iconUrlToMurmur2HashMap.entrySet()) {
String murmur2Hash = mapEntry.getValue();
if (murmur2Hash == null) {
murmur2Hash = "0";
}
iconUrlsAndIconMurmur2Hashes += " " + mapEntry.getKey() + " " + murmur2Hash;
}
iconUrlsAndIconMurmur2Hashes = iconUrlsAndIconMurmur2Hashes.trim();
metaData.putString(
WebApkMetaDataKeys.ICON_URLS_AND_ICON_MURMUR2_HASHES, iconUrlsAndIconMurmur2Hashes);
return metaData;
}
/**
* Registers WebAPK with default package name. Overwrites previous registrations.
*
* @param packageName Package name for which to register the WebApk.
* @param manifestData <meta-data> values for WebAPK's Android Manifest.
* @param shellApkVersionCode WebAPK's version of the //chrome/android/webapk/shell_apk code.
*/
private void registerWebApk(
String packageName, ManifestData manifestData, int shellApkVersionCode) {
Bundle metadata = createWebApkMetadata(manifestData, shellApkVersionCode);
Bundle shareTargetMetaData = new Bundle();
shareTargetMetaData.putString(
WebApkMetaDataKeys.SHARE_ACTION, manifestData.shareTargetAction);
shareTargetMetaData.putString(
WebApkMetaDataKeys.SHARE_PARAM_TITLE, manifestData.shareTargetParamTitle);
shareTargetMetaData.putString(
WebApkMetaDataKeys.SHARE_METHOD, manifestData.shareTargetMethod);
shareTargetMetaData.putString(
WebApkMetaDataKeys.SHARE_ENCTYPE, manifestData.shareTargetEncType);
shareTargetMetaData.remove(WebApkMetaDataKeys.SHARE_PARAM_NAMES);
if (manifestData.shareTargetFileNames != null) {
JSONArray fileNamesJson =
new JSONArray(Arrays.asList(manifestData.shareTargetFileNames));
shareTargetMetaData.putString(
WebApkMetaDataKeys.SHARE_PARAM_NAMES, fileNamesJson.toString());
}
shareTargetMetaData.remove(WebApkMetaDataKeys.SHARE_PARAM_ACCEPTS);
if (manifestData.shareTargetFileAccepts != null) {
JSONArray acceptJson = new JSONArray();
for (String[] acceptArr : manifestData.shareTargetFileAccepts) {
acceptJson.put(new JSONArray(Arrays.asList(acceptArr)));
}
shareTargetMetaData.putString(
WebApkMetaDataKeys.SHARE_PARAM_ACCEPTS, acceptJson.toString());
}
WebApkTestHelper.registerWebApkWithMetaData(
packageName, metadata, new Bundle[] {shareTargetMetaData});
WebApkTestHelper.setResource(
packageName,
new FakeDefaultBackgroundColorResource(manifestData.defaultBackgroundColor));
}
private static ManifestData defaultManifestData() {
ManifestData manifestData = new ManifestData();
manifestData.startUrl = START_URL;
manifestData.scopeUrl = SCOPE_URL;
manifestData.name = NAME;
manifestData.shortName = SHORT_NAME;
manifestData.hasCustomName = false;
manifestData.id = MANIFEST_ID;
manifestData.appKey = MANIFEST_ID;
manifestData.iconUrlToMurmur2HashMap = new HashMap<>();
manifestData.iconUrlToMurmur2HashMap.put(PRIMARY_ICON_URL, PRIMARY_ICON_MURMUR2_HASH);
manifestData.primaryIconUrl = PRIMARY_ICON_URL;
manifestData.primaryIcon = createBitmap(Color.GREEN);
manifestData.displayMode = DISPLAY_MODE;
manifestData.orientation = ORIENTATION;
manifestData.themeColor = THEME_COLOR;
manifestData.backgroundColor = BACKGROUND_COLOR;
manifestData.darkThemeColor = DARK_THEME_COLOR;
manifestData.darkBackgroundColor = DARK_BACKGROUND_COLOR;
manifestData.defaultBackgroundColor = DEFAULT_BACKGROUND_COLOR;
manifestData.shareTargetAction = SHARE_TARGET_ACTION;
manifestData.shareTargetParamTitle = SHARE_TARGET_PARAM_TITLE;
manifestData.shareTargetMethod = SHARE_TARGET_METHOD_GET;
manifestData.shareTargetEncType = SHARE_TARGET_ENC_TYPE_MULTIPART;
manifestData.shareTargetFileNames = SHARE_TARGET_FILE_NAMES.clone();
manifestData.shareTargetFileAccepts =
Arrays.stream(SHARE_TARGET_ACCEPTS)
.map(strings -> strings.clone())
.toArray(i -> new String[i][]);
manifestData.shortcuts = new ArrayList<>();
return manifestData;
}
private static BrowserServicesIntentDataProvider intentDataProviderFromManifestData(
ManifestData manifestData) {
if (manifestData == null) return null;
final String kPackageName = "org.random.webapk";
WebApkShareTarget shareTarget =
TextUtils.isEmpty(manifestData.shareTargetAction)
? null
: new WebApkShareTarget(
manifestData.shareTargetAction,
manifestData.shareTargetParamTitle,
null,
manifestData.shareTargetMethod != null
&& manifestData.shareTargetMethod.equals(
SHARE_TARGET_METHOD_POST),
manifestData.shareTargetEncType != null
&& manifestData.shareTargetEncType.equals(
SHARE_TARGET_ENC_TYPE_MULTIPART),
manifestData.shareTargetFileNames,
manifestData.shareTargetFileAccepts);
return WebApkIntentDataProviderFactory.create(
new Intent(),
"",
manifestData.scopeUrl,
new WebappIcon(manifestData.primaryIcon),
null,
manifestData.name,
manifestData.shortName,
manifestData.hasCustomName,
manifestData.displayMode,
manifestData.orientation,
-1,
manifestData.themeColor,
manifestData.backgroundColor,
manifestData.darkThemeColor,
manifestData.darkBackgroundColor,
manifestData.defaultBackgroundColor,
/* isPrimaryIconMaskable= */ false,
/* isSplashIconMaskable= */ false,
kPackageName,
-1,
WEB_MANIFEST_URL,
manifestData.startUrl,
manifestData.id,
manifestData.appKey,
WebApkDistributor.BROWSER,
manifestData.iconUrlToMurmur2HashMap,
shareTarget,
/* forceNavigation= */ false,
/* isSplashProvidedByWebApk= */ false,
/* shareData= */ null,
/* shortcutItems= */ manifestData.shortcuts,
/* webApkVersionCode= */ 1,
/* lastUpdateTime= */ TimeUtils.currentTimeMillis());
}
/**
* Creates 1x1 bitmap.
* @param color The bitmap color.
*/
private static Bitmap createBitmap(int color) {
int[] colors = {color};
return Bitmap.createBitmap(colors, 1, 1, Bitmap.Config.ALPHA_8);
}
private static void updateIfNeeded(
String packageName,
WebApkUpdateManager updateManager,
List<WebApkExtras.ShortcutItem> shortcuts) {
// Use the intent version of {@link WebApkInfo#create()} in order to test default values
// set by the intent version of {@link WebApkInfo#create()}.
Intent intent = new Intent();
intent.putExtra(WebappConstants.EXTRA_URL, "");
intent.putExtra(WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME, packageName);
BrowserServicesIntentDataProvider intentDataProvider =
WebApkIntentDataProviderFactory.create(intent);
intentDataProvider.getWebApkExtras().shortcutItems.clear();
intentDataProvider.getWebApkExtras().shortcutItems.addAll(shortcuts);
updateManager.updateIfNeeded(getStorage(packageName), intentDataProvider);
}
private static void updateIfNeeded(String packageName, WebApkUpdateManager updateManager) {
updateIfNeeded(packageName, updateManager, new ArrayList<>());
}
private static void onGotUnchangedWebManifestData(WebApkUpdateManager updateManager) {
onGotManifestData(updateManager, defaultManifestData());
}
private static void onGotDifferentData(WebApkUpdateManager updateManager) {
ManifestData manifestData = defaultManifestData();
// Note: Avoid using name/icon changes just to trigger updates, as there are special
// considerations involved in updating them (App Identity changes).
manifestData.backgroundColor = DIFFERENT_BACKGROUND_COLOR;
onGotManifestData(updateManager, manifestData);
}
private static void onGotManifestData(
WebApkUpdateManager updateManager, ManifestData fetchedManifestData) {
String primaryIconUrl = randomIconUrl(fetchedManifestData);
String splashIconUrl = randomIconUrl(fetchedManifestData);
updateManager.onGotManifestData(
intentDataProviderFromManifestData(fetchedManifestData),
primaryIconUrl,
splashIconUrl);
}
/**
* Tries to complete update request.
* @param updateManager
* @param result The result of the update task. Emulates the proto creation as always
* succeeding.
*/
private static void tryCompletingUpdate(
TestWebApkUpdateManager updateManager,
WebappDataStorage storage,
@WebApkInstallResult int result) {
// Emulate proto creation as always succeeding.
Callback<Boolean> storeUpdateRequestCallback =
updateManager.getStoreUpdateRequestCallback();
if (storeUpdateRequestCallback == null) return;
storeUpdateRequestCallback.onResult(true);
WebApkUpdateManager.updateWhileNotRunning(storage, Mockito.mock(Runnable.class));
WebApkUpdateManager.WebApkUpdateCallback updateCallback =
TestWebApkUpdateManagerJni.getUpdateCallback();
if (updateCallback == null) return;
updateCallback.onResultFromNative(result, /* relaxUpdates= */ false);
}
private static void writeRandomTextToFile(String path) {
File file = new File(path);
new File(file.getParent()).mkdirs();
try (FileOutputStream out = new FileOutputStream(file)) {
out.write(ApiCompatibilityUtils.getBytesUtf8("something"));
} catch (Exception e) {
}
}
private static String randomIconUrl(ManifestData fetchedManifestData) {
if (fetchedManifestData == null || fetchedManifestData.iconUrlToMurmur2HashMap.isEmpty()) {
return null;
}
return fetchedManifestData.iconUrlToMurmur2HashMap.keySet().iterator().next();
}
private boolean checkUpdateNeededForFetchedManifest(
ManifestData androidManifestData, ManifestData fetchedManifestData) {
return checkUpdateNeededForFetchedManifest(
androidManifestData,
fetchedManifestData,
/* nameUpdatesEnabled= */ false,
/* iconUpdatesEnabled= */ false);
}
/**
* Checks whether the WebAPK is updated given data from the WebAPK's Android Manifest and data
* from the fetched Web Manifest.
*/
private boolean checkUpdateNeededForFetchedManifest(
ManifestData androidManifestData,
ManifestData fetchedManifestData,
boolean nameUpdatesEnabled,
boolean iconUpdatesEnabled) {
registerWebApk(
WEBAPK_PACKAGE_NAME, androidManifestData, REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager =
new TestWebApkUpdateManager(mActivityMock, nameUpdatesEnabled, iconUpdatesEnabled);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager, androidManifestData.shortcuts);
assertTrue(updateManager.updateCheckStarted());
updateManager.onGotManifestData(
intentDataProviderFromManifestData(fetchedManifestData),
fetchedManifestData.primaryIconUrl,
null);
return updateManager.updateRequested();
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
PathUtils.setPrivateDataDirectorySuffix("chrome");
PostTask.setPrenativeThreadPoolExecutorForTesting(new RoboExecutorService());
mJniMocker.mock(WebApkUpdateManagerJni.TEST_HOOKS, new TestWebApkUpdateManagerJni());
WebappRegistry.refreshSharedPrefsForTesting();
registerWebApk(
WEBAPK_PACKAGE_NAME, defaultManifestData(), REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
registerStorageForWebApkPackage(WEBAPK_PACKAGE_NAME);
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
storage.updateTimeOfLastCheckForUpdatedWebManifest();
storage.updateTimeOfLastWebApkUpdateRequestCompletion();
storage.updateDidLastWebApkUpdateRequestSucceed(true);
ChromeBackgroundTaskFactory.setAsDefault();
}
/**
* Test that the is-update-needed check is tried the next time that the WebAPK is launched if
* Chrome is killed prior to the initial URL finishing loading.
*/
@Test
public void testCheckOnNextLaunchIfClosePriorToFirstPageLoad() {
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
{
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
}
// Chrome is killed. {@link WebApkUpdateManager#OnGotManifestData()} is not called.
{
// Relaunching the WebAPK should do an is-update-needed check.
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotUnchangedWebManifestData(updateManager);
}
{
// Relaunching the WebAPK should not do an is-update-needed-check.
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertFalse(updateManager.updateCheckStarted());
}
}
/**
* Test that the completion time of the previous WebAPK update is not modified if:
* - The previous WebAPK update succeeded.
* AND
* - A WebAPK update is not required.
*/
@Test
public void testUpdateNotNeeded() {
long initialTime = TimeUtils.currentTimeMillis();
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotUnchangedWebManifestData(updateManager);
assertFalse(updateManager.updateRequested());
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
assertTrue(storage.getDidLastWebApkUpdateRequestSucceed());
assertEquals(initialTime, storage.getLastWebApkUpdateRequestCompletionTimeMs());
}
/**
* Test that the last WebAPK update is marked as having succeeded if:
* - The previous WebAPK update failed.
* AND
* - A WebAPK update is no longer required.
*/
@Test
public void testMarkUpdateAsSucceededIfUpdateNoLongerNeeded() {
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
storage.updateDidLastWebApkUpdateRequestSucceed(false);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotUnchangedWebManifestData(updateManager);
assertFalse(updateManager.updateRequested());
assertTrue(storage.getDidLastWebApkUpdateRequestSucceed());
assertEquals(
TimeUtils.currentTimeMillis(),
storage.getLastWebApkUpdateRequestCompletionTimeMs());
}
/**
* Test that the WebAPK update is marked as having failed if Chrome is killed prior to the
* WebAPK update completing.
*/
@Test
public void testMarkUpdateAsFailedIfClosePriorToUpdateCompleting() {
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
// Chrome is killed. {@link WebApkUpdateCallback#onResultFromNative} is never called.
// Check {@link WebappDataStorage} state.
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
assertFalse(storage.getDidLastWebApkUpdateRequestSucceed());
assertEquals(
TimeUtils.currentTimeMillis(),
storage.getLastWebApkUpdateRequestCompletionTimeMs());
}
/**
* Test that the pending update file is deleted after update completes regardless of whether
* update succeeded.
*/
@Test
public void testPendingUpdateFileDeletedAfterUpdateCompletion() {
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
String updateRequestPath = storage.getPendingUpdateRequestPath();
assertNotNull(updateRequestPath);
assertTrue(new File(updateRequestPath).exists());
tryCompletingUpdate(updateManager, storage, WebApkInstallResult.FAILURE);
assertNull(storage.getPendingUpdateRequestPath());
assertFalse(new File(updateRequestPath).exists());
}
/**
* Test that the pending update file is deleted if
* {@link WebApkUpdateManager#nativeStoreWebApkUpdateRequestToFile} creates the pending update
* file but fails.
*/
@Test
public void testFileDeletedIfStoreWebApkUpdateRequestToFileFails() {
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
String updateRequestPath = storage.getPendingUpdateRequestPath();
assertNotNull(updateRequestPath);
assertTrue(new File(updateRequestPath).exists());
updateManager.getStoreUpdateRequestCallback().onResult(false);
assertNull(storage.getPendingUpdateRequestPath());
assertFalse(new File(updateRequestPath).exists());
}
/**
* Test that an update with data from the WebAPK's Android manifest is done if:
* - WebAPK's code is out of date
* AND
* - WebAPK's start_url does not refer to a Web Manifest.
*
* It is good to minimize the number of users with out of date WebAPKs. We try to keep WebAPKs
* up to date even if the web developer has removed the Web Manifest from their site.
*/
@Test
public void testShellApkOutOfDateNoWebManifest() {
registerWebApk(
WEBAPK_PACKAGE_NAME,
defaultManifestData(),
REQUEST_UPDATE_FOR_SHELL_APK_VERSION - 1);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertTrue(updateManager.updateRequested());
assertEquals(NAME, updateManager.requestedUpdateName());
assertEquals(MANIFEST_ID, updateManager.requestedAppKey());
// Check that the {@link WebApkUpdateDataFetcher} has been destroyed. This prevents
// {@link #onGotManifestData()} from getting called.
assertTrue(updateManager.destroyedFetcher());
}
/**
* Test that an update is not done if:
* - WebAPK's code is out of date
* AND
* - WebApkUpdateManager has been destroyed.
*/
@Test
public void testDontRequestUpdateAfterManagerDestroyed() {
registerWebApk(
WEBAPK_PACKAGE_NAME,
defaultManifestData(),
REQUEST_UPDATE_FOR_SHELL_APK_VERSION - 1);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
updateManager.onDestroy();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertFalse(updateManager.updateRequested());
}
/**
* Test that an update with data from the fetched Web Manifest is done if the WebAPK's code is
* out of date and the WebAPK's start_url refers to a Web Manifest.
*/
@Test
public void testShellApkOutOfDateStillHasWebManifest() {
registerWebApk(
WEBAPK_PACKAGE_NAME,
defaultManifestData(),
REQUEST_UPDATE_FOR_SHELL_APK_VERSION - 1);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, defaultManifestData());
assertTrue(updateManager.updateRequested());
assertEquals(NAME, updateManager.requestedUpdateName());
assertEquals(MANIFEST_ID, updateManager.requestedAppKey());
assertTrue(updateManager.destroyedFetcher());
}
/**
* Test that an update is requested if:
* - start_url does not refer to a Web Manifest.
* AND
* - The user eventually navigates to a page pointing to a Web Manifest with the correct URL.
* AND
* - The Web Manifest has changed.
*/
@Test
public void testStartUrlRedirectsToPageWithUpdatedWebManifest() {
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager =
new TestWebApkUpdateManager(
mActivityMock,
/* nameUpdatesEnabled= */ true,
/* iconUpdatesEnabled= */ false);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
// start_url does not have a Web Manifest. {@link #onGotManifestData} is called as a result
// of update manager timing out. No update should be requested.
updateManager.onGotManifestData(null, null, null);
assertFalse(updateManager.updateRequested());
// {@link WebApkUpdateDataFetcher} should still be alive so that it can get
// {@link #onGotManifestData} when page with the Web Manifest finishes loading.
assertFalse(updateManager.destroyedFetcher());
// User eventually navigates to page with Web Manifest.
ManifestData manifestData = defaultManifestData();
manifestData.name = DIFFERENT_NAME;
onGotManifestData(updateManager, manifestData);
assertTrue(updateManager.updateRequested());
assertEquals(DIFFERENT_NAME, updateManager.requestedUpdateName());
assertTrue(updateManager.destroyedFetcher());
}
/**
* Test that an update is not requested if:
* - start_url does not refer to a Web Manifest.
* AND
* - The user eventually navigates to a page pointing to a Web Manifest with the correct URL.
* AND
* - The Web Manifest has not changed.
*/
@Test
public void testStartUrlRedirectsToPageWithUnchangedWebManifest() {
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
// Update manager times out.
updateManager.onGotManifestData(null, null, null);
onGotManifestData(updateManager, defaultManifestData());
assertFalse(updateManager.updateRequested());
// We got the Web Manifest. The {@link WebApkUpdateDataFetcher} should be destroyed to stop
// it from fetching the Web Manifest for subsequent page loads.
assertTrue(updateManager.destroyedFetcher());
}
@Test
public void testManifestDoesNotUpgrade() {
assertFalse(
checkUpdateNeededForFetchedManifest(defaultManifestData(), defaultManifestData()));
}
/** Test that a webapk with an unexpected package name does not request updates. */
@Test
public void testUnboundWebApkDoesNotUpgrade() {
ManifestData androidManifestData = defaultManifestData();
registerWebApk(
UNBOUND_WEBAPK_PACKAGE_NAME,
androidManifestData,
REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(UNBOUND_WEBAPK_PACKAGE_NAME, updateManager);
assertFalse(updateManager.updateCheckStarted());
assertFalse(updateManager.updateRequested());
}
/**
* Test that an upgrade is requested when the share target specified in the Web Manifest
* changes.
*/
@Test
public void testShareTargetChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
ManifestData fetchedData = defaultManifestData();
fetchedData.shareTargetAction = "/action2.html";
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
@Test
public void testShareTargetV2ChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
ManifestData fetchedData1 = defaultManifestData();
fetchedData1.shareTargetFileNames[0] = "changed";
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData1));
ManifestData fetchedData2 = defaultManifestData();
fetchedData2.shareTargetFileAccepts[1] = new String[] {};
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData2));
ManifestData fetchedData3 = defaultManifestData();
fetchedData3.shareTargetFileAccepts[1][1] = "changed";
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData3));
}
@Test
public void testShareTargetV2UpgradeFromV1() {
ManifestData oldNoShareTarget = defaultManifestData();
oldNoShareTarget.shareTargetAction = null;
oldNoShareTarget.shareTargetParamTitle = null;
oldNoShareTarget.shareTargetMethod = null;
oldNoShareTarget.shareTargetEncType = null;
oldNoShareTarget.shareTargetFileNames = null;
oldNoShareTarget.shareTargetFileAccepts = null;
ManifestData fetchedNoShareTarget2 = defaultManifestData();
fetchedNoShareTarget2.shareTargetAction = null;
fetchedNoShareTarget2.shareTargetParamTitle = null;
fetchedNoShareTarget2.shareTargetMethod = null;
fetchedNoShareTarget2.shareTargetEncType = null;
fetchedNoShareTarget2.shareTargetFileNames = null;
fetchedNoShareTarget2.shareTargetFileAccepts = null;
assertFalse(checkUpdateNeededForFetchedManifest(oldNoShareTarget, fetchedNoShareTarget2));
ManifestData oldV1ShareTarget = defaultManifestData();
oldV1ShareTarget.shareTargetMethod = null;
oldV1ShareTarget.shareTargetEncType = null;
oldV1ShareTarget.shareTargetFileNames = null;
oldV1ShareTarget.shareTargetFileAccepts = null;
ManifestData fetchedV1ShareTarget = defaultManifestData();
fetchedV1ShareTarget.shareTargetMethod = null;
fetchedV1ShareTarget.shareTargetEncType = null;
fetchedV1ShareTarget.shareTargetFileNames = null;
fetchedV1ShareTarget.shareTargetFileAccepts = null;
assertFalse(checkUpdateNeededForFetchedManifest(oldV1ShareTarget, fetchedV1ShareTarget));
ManifestData oldV2ShareTarget = defaultManifestData();
ManifestData fetchedV2ShareTarget = defaultManifestData();
assertFalse(checkUpdateNeededForFetchedManifest(oldV2ShareTarget, fetchedV2ShareTarget));
assertTrue(checkUpdateNeededForFetchedManifest(oldNoShareTarget, fetchedV1ShareTarget));
assertTrue(checkUpdateNeededForFetchedManifest(oldNoShareTarget, fetchedV2ShareTarget));
assertTrue(checkUpdateNeededForFetchedManifest(oldV1ShareTarget, fetchedV2ShareTarget));
assertTrue(checkUpdateNeededForFetchedManifest(fetchedV2ShareTarget, fetchedV1ShareTarget));
assertTrue(checkUpdateNeededForFetchedManifest(fetchedV2ShareTarget, oldNoShareTarget));
assertTrue(checkUpdateNeededForFetchedManifest(fetchedV1ShareTarget, oldNoShareTarget));
}
/** Test that an upgrade is requested when the Web Manifest 'scope' changes. */
@Test
public void testManifestScopeChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
// webapk_installer.cc sets the scope to the default scope if the scope is empty.
oldData.scopeUrl = "/scope1/";
ManifestData fetchedData = defaultManifestData();
fetchedData.scopeUrl = "/scope2/";
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/**
* Test that an upgrade is not requested when the Web Manifest did not change and the Web
* Manifest scope is empty. TODO(crbug.com/40827678): Re-enable test.
*/
@Ignore
@Test
public void testManifestEmptyScopeShouldNotUpgrade() {
ManifestData oldData = defaultManifestData();
// webapk_installer.cc sets the scope to the default scope if the scope is empty.
oldData.scopeUrl = ShortcutHelper.getScopeFromUrl(oldData.startUrl);
ManifestData fetchedData = defaultManifestData();
fetchedData.scopeUrl = "";
assertTrue(!oldData.scopeUrl.equals(fetchedData.scopeUrl));
assertFalse(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/**
* Test that an upgrade is requested when the Web Manifest is updated from using a non-empty
* scope to an empty scope.
*/
@Test
public void testManifestNonEmptyScopeToEmptyScopeShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.startUrl = "/fancy/scope/special/snowflake.html";
oldData.scopeUrl = "/fancy/scope/";
assertTrue(!oldData.scopeUrl.equals(ShortcutHelper.getScopeFromUrl(oldData.startUrl)));
ManifestData fetchedData = defaultManifestData();
fetchedData.startUrl = "/fancy/scope/special/snowflake.html";
fetchedData.scopeUrl = "";
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/**
* Test that an upgrade is requested when:
* - WebAPK was generated using icon at {@link PRIMARY_ICON_URL} from Web Manifest.
* - Bitmap at {@link PRIMARY_ICON_URL} has changed.
*/
@Test
public void testPrimaryIconChangeShouldUpgrade() {
ManifestData fetchedData = defaultManifestData();
fetchedData.iconUrlToMurmur2HashMap.put(
fetchedData.primaryIconUrl, PRIMARY_ICON_MURMUR2_HASH + "1");
fetchedData.primaryIcon = createBitmap(Color.BLUE);
assertTrue(
checkUpdateNeededForFetchedManifest(
defaultManifestData(),
fetchedData,
/* nameUpdatesEnabled= */ false,
/* iconUpdatesEnabled= */ true));
}
/**
* Test that an upgrade is requested when:
* - WebAPK is generated using icon at {@link PRIMARY_ICON_URL} from Web Manifest.
* - A new icon URL is added to the Web Manifest. And InstallableManager selects the new icon as
* the primary icon.
*/
@Test
public void testPrimaryIconUrlChangeShouldUpgrade() {
ManifestData fetchedData = defaultManifestData();
fetchedData.iconUrlToMurmur2HashMap.put("/icon2.png", "22");
fetchedData.primaryIconUrl = "/icon2.png";
assertTrue(
checkUpdateNeededForFetchedManifest(
defaultManifestData(),
fetchedData,
/* nameUpdatesEnabled= */ false,
/* iconUpdatesEnabled= */ true));
}
/**
* Test that an upgrade is not requested if:
* - icon URL is added to the Web Manifest
* AND
* - "best" icon URL for the primary icon did not change.
* AND
* - "best" icon URL for the monochrome icon did not change.
*/
@Test
public void
testIconUrlsChangeShouldNotUpgradeIfPrimaryIconUrlAndMonochromeIconUrlDoNotChange() {
ManifestData fetchedData = defaultManifestData();
fetchedData.iconUrlToMurmur2HashMap.put("/icon2.png", null);
assertFalse(checkUpdateNeededForFetchedManifest(defaultManifestData(), fetchedData));
}
/**
* Test that an upgrade is not requested if:
* - the WebAPK's meta data has murmur2 hashes for all of the icons.
* AND
* - the Web Manifest has not changed
* AND
* - the computed best icon URLs are different from the one stored in the WebAPK's meta data.
*/
@Test
public void testWebManifestSameButBestIconUrlChangedShouldNotUpgrade() {
String iconUrl1 = "/icon1.png";
String iconUrl2 = "/icon2.png";
String monochromeUrl1 = "/monochrome1.png";
String monochromeUrl2 = "/monochrome2.png";
String hash1 = "11";
String hash2 = "22";
String hash3 = "33";
String hash4 = "44";
ManifestData androidManifestData = defaultManifestData();
androidManifestData.primaryIconUrl = iconUrl1;
androidManifestData.iconUrlToMurmur2HashMap.clear();
androidManifestData.iconUrlToMurmur2HashMap.put(iconUrl1, hash1);
androidManifestData.iconUrlToMurmur2HashMap.put(iconUrl2, hash2);
androidManifestData.iconUrlToMurmur2HashMap.put(monochromeUrl1, hash3);
androidManifestData.iconUrlToMurmur2HashMap.put(monochromeUrl2, hash4);
ManifestData fetchedManifestData = defaultManifestData();
fetchedManifestData.primaryIconUrl = iconUrl2;
fetchedManifestData.iconUrlToMurmur2HashMap.clear();
fetchedManifestData.iconUrlToMurmur2HashMap.put(iconUrl1, null);
fetchedManifestData.iconUrlToMurmur2HashMap.put(iconUrl2, hash2);
fetchedManifestData.iconUrlToMurmur2HashMap.put(monochromeUrl1, null);
fetchedManifestData.iconUrlToMurmur2HashMap.put(monochromeUrl2, hash4);
assertFalse(checkUpdateNeededForFetchedManifest(androidManifestData, fetchedManifestData));
}
/** Test that an upgrade is requested when the Web Manifest 'short_name' changes. */
@Test
public void testManifestShortNameChangedShouldUpgrade() {
ManifestData fetchedData = defaultManifestData();
fetchedData.shortName = SHORT_NAME + "2";
assertTrue(
checkUpdateNeededForFetchedManifest(
defaultManifestData(),
fetchedData,
/* nameUpdatesEnabled= */ true,
/* iconUpdatesEnabled= */ false));
}
/** Test that an upgrade is requested when the Web Manifest 'name' changes. */
@Test
public void testManifestNameChangedShouldUpgrade() {
ManifestData fetchedData = defaultManifestData();
fetchedData.name = NAME + "2";
assertTrue(
checkUpdateNeededForFetchedManifest(
defaultManifestData(),
fetchedData,
/* nameUpdatesEnabled= */ true,
/* iconUpdatesEnabled= */ false));
}
/** Test that an upgrade is requested when the Web Manifest 'display' changes. */
@Test
public void testManifestDisplayModeChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.displayMode = DisplayMode.STANDALONE;
ManifestData fetchedData = defaultManifestData();
fetchedData.displayMode = DisplayMode.FULLSCREEN;
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/** Test that an upgrade is requested when the Web Manifest 'orientation' changes. */
@Test
public void testManifestOrientationChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.orientation = ScreenOrientationLockType.LANDSCAPE;
ManifestData fetchedData = defaultManifestData();
fetchedData.orientation = ScreenOrientationLockType.PORTRAIT;
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/** Test that an upgrade is requested when the Web Manifest 'theme_color' changes. */
@Test
public void testManifestThemeColorChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.themeColor = 1L;
ManifestData fetchedData = defaultManifestData();
fetchedData.themeColor = 2L;
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/** Test that an upgrade is requested when the Web Manifest 'background_color' changes. */
@Test
public void testManifestBackgroundColorChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.backgroundColor = 1L;
ManifestData fetchedData = defaultManifestData();
fetchedData.backgroundColor = 2L;
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/** Test that an upgrade is requested when the Web Manifest 'dark_theme_color' changes. */
@Test
public void testManifestDarkThemeColorChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.themeColor = 3L;
ManifestData fetchedData = defaultManifestData();
fetchedData.themeColor = 4L;
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/** Test that an upgrade is requested when the Web Manifest 'dark_background_color' changes. */
@Test
public void testManifestDarkBackgroundColorChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.backgroundColor = 4L;
ManifestData fetchedData = defaultManifestData();
fetchedData.backgroundColor = 5L;
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/**
* Test that an upgrade is not requested if the AndroidManifest does not have a valid background
* color and the default background color in the WebAPK's resources is different than
* {@link SplashLayout#getDefaultBackgroundColor()} (due to a change in the return value of
* {@link SplashLayout#getDefaultBackgroundColor()} in a new Chrome version).
*/
@Test
public void testDefaultBackgroundColorHasChangedShouldNotUpgrade() {
int oldDefaultBackgroundColor = 3;
int splashLayoutDefaultBackgroundColor =
SplashLayout.getDefaultBackgroundColor(RuntimeEnvironment.application);
assertNotEquals(oldDefaultBackgroundColor, splashLayoutDefaultBackgroundColor);
ManifestData androidManifestData = defaultManifestData();
androidManifestData.backgroundColor = ColorUtils.INVALID_COLOR;
androidManifestData.defaultBackgroundColor = oldDefaultBackgroundColor;
ManifestData fetchedManifestData = defaultManifestData();
fetchedManifestData.backgroundColor = ColorUtils.INVALID_COLOR;
fetchedManifestData.defaultBackgroundColor = splashLayoutDefaultBackgroundColor;
assertFalse(checkUpdateNeededForFetchedManifest(androidManifestData, fetchedManifestData));
}
/** Test that an upgrade is requested when the Web Manifest 'start_url' changes. */
@Test
public void testManifestStartUrlChangedShouldUpgrade() {
ManifestData oldData = defaultManifestData();
oldData.startUrl = "/old_start_url.html";
ManifestData fetchedData = defaultManifestData();
fetchedData.startUrl = "/new_start_url.html";
assertTrue(checkUpdateNeededForFetchedManifest(oldData, fetchedData));
}
/**
* Tests that a WebAPK update is requested immediately if:
* the Shell APK is out of date,
* AND
* there wasn't a previous request for this ShellAPK version.
*/
@Test
public void testShellApkOutOfDate() {
registerWebApk(
WEBAPK_PACKAGE_NAME,
defaultManifestData(),
REQUEST_UPDATE_FOR_SHELL_APK_VERSION - 1);
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
// There have not been any update requests for the current ShellAPK version. A WebAPK update
// should be requested immediately.
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, defaultManifestData());
assertTrue(updateManager.updateRequested());
tryCompletingUpdate(updateManager, storage, WebApkInstallResult.FAILURE);
mClockRule.advanceMillis(1);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertFalse(updateManager.updateCheckStarted());
// A previous update request was made for the current ShellAPK version. A WebAPK update
// should be requested after the regular delay.
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL - 1);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, defaultManifestData());
assertTrue(updateManager.updateRequested());
}
/**
* Tests that a WebAPK update is requested if the Shell APK hasn't been updated for a long time
* even if is is newer than the requested version.
*/
@Test
public void testShellApkOutOfDateInterval() {
ShadowPackageManager pm =
Shadows.shadowOf(RuntimeEnvironment.getApplication().getPackageManager());
PackageInfo packageInfo =
WebApkTestHelper.newPackageInfo(
WEBAPK_PACKAGE_NAME,
createWebApkMetadata(
defaultManifestData(), REQUEST_UPDATE_FOR_SHELL_APK_VERSION),
null,
null);
packageInfo.lastUpdateTime = TimeUtils.currentTimeMillis();
pm.addPackage(packageInfo);
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
// Update is not needed when no manifest change and shell version is the same as
// REQUEST_UPDATE_FOR_SHELL_APK_VERSION.
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, defaultManifestData());
assertFalse(updateManager.updateRequested());
tryCompletingUpdate(updateManager, storage, WebApkInstallResult.FAILURE);
// Update is requested when a shell hasn't been updated for a long time.
mClockRule.advanceMillis(WebApkUpdateManager.OLD_SHELL_NEEDS_UPDATE_INTERVAL);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, defaultManifestData());
assertTrue(updateManager.updateRequested());
}
/**
* Tests that a forced update is requested and performed immediately if there is a material
* change to the manifest.
*/
@Test
public void testForcedUpdateSuccess() {
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
storage.setShouldForceUpdate(true);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
tryCompletingUpdate(updateManager, storage, WebApkInstallResult.SUCCESS);
assertFalse(storage.shouldForceUpdate());
}
/**
* Tests that a forced update is requested, but not performed if there is no material change to
* the manifest.
*/
@Test
public void testForcedUpdateNotNeeded() {
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
storage.setShouldForceUpdate(true);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, defaultManifestData());
assertFalse(updateManager.updateRequested());
assertFalse(storage.shouldForceUpdate());
}
/** Tests that a forced update handles failure gracefully. */
@Test
public void testForcedUpdateFailure() {
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
storage.setShouldForceUpdate(true);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
tryCompletingUpdate(updateManager, storage, WebApkInstallResult.FAILURE);
assertFalse(storage.shouldForceUpdate());
}
/** Tests that a forced update handles failing to retrieve the manifest. */
@Test
public void testForcedUpdateManifestNotRetrieved() {
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
storage.setShouldForceUpdate(true);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotManifestData(updateManager, null);
assertFalse(updateManager.updateRequested());
assertFalse(storage.shouldForceUpdate());
}
/** Test that WebappDataStorage#setShouldForceUpdate() is a no-op for unbound WebAPKs. */
@Test
public void testForceUpdateUnboundWebApk() throws Exception {
registerWebApk(
UNBOUND_WEBAPK_PACKAGE_NAME,
defaultManifestData(),
REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
registerStorageForWebApkPackage(UNBOUND_WEBAPK_PACKAGE_NAME);
WebappDataStorage storage = getStorage(UNBOUND_WEBAPK_PACKAGE_NAME);
storage.updateWebApkPackageNameForTests(UNBOUND_WEBAPK_PACKAGE_NAME);
// Should no-op for an unbound WebAPK.
storage.setShouldForceUpdate(true);
assertFalse(storage.shouldForceUpdate());
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(UNBOUND_WEBAPK_PACKAGE_NAME, updateManager);
assertFalse(updateManager.updateCheckStarted());
assertFalse(updateManager.updateRequested());
}
/** Test that an update is required if a shortcut has been added. */
@Test
public void testUpdateIfShortcutIsAdded() {
ManifestData fetchedData = defaultManifestData();
fetchedData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name", "shortName", "launchUrl", "iconUrl", "iconHash", new WebappIcon()));
assertTrue(checkUpdateNeededForFetchedManifest(defaultManifestData(), fetchedData));
}
/** Test that an update is required if a shortcut name has been changed. */
@Test
public void testUpdateIfShortcutHasChangedName() {
ManifestData androidData = defaultManifestData();
androidData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name1",
"shortName",
"launchUrl",
"iconUrl",
"iconHash",
new WebappIcon("appName", 42)));
ManifestData fetchedData = defaultManifestData();
fetchedData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name2",
"shortName",
"launchUrl",
"iconUrl",
"iconHash",
new WebappIcon()));
assertTrue(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
}
/** Test that an update is required if a shortcut short name has been changed. */
@Test
public void testUpdateIfShortcutHasChangedShortName() {
ManifestData androidData = defaultManifestData();
androidData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName1",
"launchUrl",
"iconUrl",
"iconHash",
new WebappIcon("appName", 42)));
ManifestData fetchedData = defaultManifestData();
fetchedData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName2",
"launchUrl",
"iconUrl",
"iconHash",
new WebappIcon()));
assertTrue(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
}
/** Test that an update is required if a shortcut launch URL has been changed. */
@Test
public void testUpdateIfShortcutHasChangedLaunchUrl() {
ManifestData androidData = defaultManifestData();
androidData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName",
"launchUrl1",
"iconUrl",
"iconHash",
new WebappIcon("appName", 42)));
ManifestData fetchedData = defaultManifestData();
fetchedData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName",
"launchUrl2",
"iconUrl",
"iconHash",
new WebappIcon()));
assertTrue(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
}
/** Test that an update is required if a shortcut icon hash has been changed. */
@Test
public void testUpdateIfShortcutHasChangedIconHash() {
ManifestData androidData = defaultManifestData();
androidData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName",
"launchUrl",
"iconUrl",
"iconHash1",
new WebappIcon("appName", 42)));
ManifestData fetchedData = defaultManifestData();
fetchedData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName",
"launchUrl",
"iconUrl",
"iconHash2",
new WebappIcon()));
assertTrue(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
}
/** Test that an update is not required if a shortcut url has changed but the hash hasn't. */
@Test
public void testNoUpdateIfShortcutHasOnlyIconUrlChanges() {
ManifestData androidData = defaultManifestData();
androidData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName",
"launchUrl",
"iconUrl1",
"iconHash",
new WebappIcon("appName", 42)));
ManifestData fetchedData = defaultManifestData();
fetchedData.shortcuts.add(
new WebApkExtras.ShortcutItem(
"name",
"shortName",
"launchUrl",
"iconUrl2",
"iconHash",
new WebappIcon()));
assertFalse(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
}
private void verifyHistograms(String name, int expectedCallCount) {
assertEquals(
"Histogram record count doesn't match.",
expectedCallCount,
RecordHistogram.getHistogramTotalCountForTesting(name));
}
@Test
public void testDialogSuppressed() {
WebappDataStorage storage = getStorage(WEBAPK_PACKAGE_NAME);
ManifestData androidData = defaultManifestData();
ManifestData fetchedData = defaultManifestData();
boolean iconUpdatesEnabled = false;
boolean nameUpdatesEnabled = false;
// Try with unchanged manifest data.
assertFalse(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 0);
// Change the app name (and pre-approve the update), but don't enable
// naming updates (then the old name will be used and no update detected).
fetchedData.name = "foo";
String hash = "foo|Short Name|3|NotAdaptive";
storage.updateLastWebApkUpdateHashAccepted(hash);
assertFalse(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 0);
nameUpdatesEnabled = true;
// Now try again, but with naming updates allowed.
hash = "foo|Short Name|3|NotAdaptive";
storage.updateLastWebApkUpdateHashAccepted(hash);
assertTrue(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 1);
// Revert the name change.
fetchedData.name = NAME;
// Now change the short name (and pre-approve the update).
fetchedData.shortName = "bar";
hash = "Long Name|bar|3|NotAdaptive";
storage.updateLastWebApkUpdateHashAccepted(hash);
assertTrue(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 2);
// Revert the shortName change.
fetchedData.shortName = SHORT_NAME;
// Also change the icon hash (and pre-approve the update), but don't allow updates of the
// icon (which will cause the old info to be used).
fetchedData.iconUrlToMurmur2HashMap.put(PRIMARY_ICON_URL, "42");
hash = "Long Name|Short Name|3|NotAdaptive";
storage.updateLastWebApkUpdateHashAccepted(hash);
assertFalse(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 2);
iconUpdatesEnabled = true;
// Now try again, but with icon updates allowed.
hash = "Long Name|Short Name|42|NotAdaptive";
storage.updateLastWebApkUpdateHashAccepted(hash);
assertTrue(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 3);
// Now try with both name and icon updates.
fetchedData.name = "foo";
fetchedData.shortName = "bar";
hash = "foo|bar|42|NotAdaptive";
storage.updateLastWebApkUpdateHashAccepted(hash);
assertTrue(
checkUpdateNeededForFetchedManifest(
androidData, fetchedData, nameUpdatesEnabled, iconUpdatesEnabled));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 0);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 4);
}
@Test
public void testEmptyAppIdentityHash() {
// Setup the test to trigger a null mFetchedInfo within WebApkUpdateManager.
registerWebApk(
WEBAPK_PACKAGE_NAME,
defaultManifestData(),
REQUEST_UPDATE_FOR_SHELL_APK_VERSION - 1);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertTrue(updateManager.updateRequested());
assertEquals(NAME, updateManager.requestedUpdateName());
// Given a null mFetchedInfo (see above) this path should exercise
// WebApkUpdateManager#getAppIdentityHash returning a blank hash, which should not be
// treated as a trigger for the pre-approved path when it comes to AppIdentity updates.
ManifestData androidData = defaultManifestData();
ManifestData fetchedData = defaultManifestData();
assertFalse(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
verifyHistograms(HISTOGRAM_NOT_SHOWING, 1);
verifyHistograms(HISTOGRAM_SHOWING, 0);
verifyHistograms(HISTOGRAM_PRE_APPROVED, 0);
}
/** Test for crashing when IntentDataProvider is null, as per https://crbug.com/1342066. */
@Test
public void testDoesntCrashWithNullProvider() {
ManifestData androidManifestData = defaultManifestData();
registerWebApk(
WEBAPK_PACKAGE_NAME, androidManifestData, REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager, androidManifestData.shortcuts);
assertTrue(updateManager.updateCheckStarted());
updateManager.onGotManifestData(
/* fetchedIntentDataProvider= */ null,
/* primaryIconUrl= */ null,
/* splashIconUrl= */ null);
}
@Test
public void testManifestIdChangeShouldNotUpdate() {
ManifestData androidData = defaultManifestData();
ManifestData fetchedData = defaultManifestData();
fetchedData.id = MANIFEST_ID + "1";
assertFalse(checkUpdateNeededForFetchedManifest(androidData, fetchedData));
}
@Test
public void testAppKeyNotChangeWhenUpdate() {
ManifestData androidData = defaultManifestData();
androidData.appKey = WEB_MANIFEST_URL;
registerWebApk(WEBAPK_PACKAGE_NAME, androidData, REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
ManifestData fetchedData = defaultManifestData();
fetchedData.appKey = "another id";
// Set a different backgroundColor to trigger an update.
fetchedData.backgroundColor = DIFFERENT_BACKGROUND_COLOR;
onGotManifestData(updateManager, fetchedData);
assertTrue(updateManager.updateRequested());
assertEquals(WEB_MANIFEST_URL, updateManager.requestedAppKey());
}
/**
* Tests that WebAPK updates keeps the default appKey when no value specified from the WebAPK's
* Android Manifest <meta-data>.
*/
@Test
public void testEmptyManifestAppKeyHasDefault() {
ManifestData androidData = defaultManifestData();
androidData.id = null;
androidData.appKey = null;
registerWebApk(WEBAPK_PACKAGE_NAME, androidData, REQUEST_UPDATE_FOR_SHELL_APK_VERSION);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
assertEquals(WEB_MANIFEST_URL, updateManager.requestedAppKey());
}
/**
* Tests that WebAPK updates keeps the default appKey when no value specified from the WebAPK's
* Android Manifest <meta-data>.
*/
@Test
public void testUpdateWithCustomName() {
ManifestData androidData = defaultManifestData();
androidData.name = "custom name";
androidData.hasCustomName = true;
registerWebApk(WEBAPK_PACKAGE_NAME, androidData, REQUEST_UPDATE_FOR_SHELL_APK_VERSION - 1);
mClockRule.advanceMillis(WebappDataStorage.UPDATE_INTERVAL);
TestWebApkUpdateManager updateManager = new TestWebApkUpdateManager(mActivityMock);
updateIfNeeded(WEBAPK_PACKAGE_NAME, updateManager);
assertTrue(updateManager.updateCheckStarted());
onGotDifferentData(updateManager);
assertTrue(updateManager.updateRequested());
assertEquals(androidData.name, updateManager.requestedUpdateName());
}
}