// 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.webapps; import android.graphics.Bitmap; import android.os.Bundle; import android.text.TextUtils; import org.chromium.base.Log; import org.chromium.chrome.browser.ShortcutHelper; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.util.IntentUtils; import org.chromium.chrome.browser.util.UrlUtilities; import org.chromium.webapk.lib.common.WebApkMetaDataKeys; /** * This class checks whether the WebAPK needs to be re-installed and sends a request to re-install * the WebAPK if it needs to be re-installed. */ public class ManifestUpgradeDetector implements ManifestUpgradeDetectorFetcher.Callback { /** * Called when the process of checking Web Manifest update is complete. */ public interface Callback { public void onUpgradeNeededCheckFinished(boolean needsUpgrade, FetchedManifestData data); } private static final String TAG = "cr_UpgradeDetector"; /** * Fetched Web Manifest data. */ public static class FetchedManifestData { public String startUrl; public String scopeUrl; public String name; public String shortName; public String iconUrl; // Hash of untransformed icon bytes. The hash should have been taken prior to any // encoding/decoding. public String iconMurmur2Hash; public Bitmap icon; public int displayMode; public int orientation; public long themeColor; public long backgroundColor; } /** The WebAPK's tab. */ private final Tab mTab; /** * Web Manifest data at time that the WebAPK was generated. */ private WebappInfo mWebappInfo; private String mManifestUrl; private String mStartUrl; private String mIconUrl; private String mIconMurmur2Hash; /** * Fetches the WebAPK's Web Manifest from the web. */ private ManifestUpgradeDetectorFetcher mFetcher; private Callback mCallback; /** * Gets the Murmur2 hash from a Bundle. Returns an empty string if the value could not be * parsed. */ private static String getMurmur2HashFromBundle(Bundle bundle) { String value = bundle.getString(WebApkMetaDataKeys.ICON_MURMUR2_HASH); // The Murmur2 hash should be terminated with 'L' to force the value to be a string. // According to https://developer.android.com/guide/topics/manifest/meta-data-element.html // numeric <meta-data> values can only be retrieved via {@link Bundle#getInt()} and // {@link Bundle#getFloat()}. We cannot use {@link Bundle#getFloat()} due to loss of // precision. if (value == null || !value.endsWith("L")) { return ""; } return value.substring(0, value.length() - 1); } /** * Creates an instance of {@link ManifestUpgradeDetector}. * * @param tab WebAPK's tab. * @param webappInfo Parameters used for generating the WebAPK. Extracted from WebAPK's Android * manifest. * @param metadata Metadata from WebAPK's Android Manifest. * @param callback Called once it has been determined whether the WebAPK needs to be upgraded. */ public ManifestUpgradeDetector(Tab tab, WebappInfo info, Bundle metadata, Callback callback) { mTab = tab; mWebappInfo = info; mCallback = callback; parseMetaData(metadata); } public String getManifestUrl() { return mManifestUrl; } public String getWebApkPackageName() { return mWebappInfo.webApkPackageName(); } /** * Starts fetching the web manifest resources. */ public boolean start() { if (mFetcher != null) return false; if (TextUtils.isEmpty(mManifestUrl)) { return false; } mFetcher = createFetcher(mTab, mWebappInfo.scopeUri().toString(), mManifestUrl); mFetcher.start(this); return true; } /** * Creates ManifestUpgradeDataFetcher. */ protected ManifestUpgradeDetectorFetcher createFetcher(Tab tab, String scopeUrl, String manifestUrl) { return new ManifestUpgradeDetectorFetcher(tab, scopeUrl, manifestUrl); } private void parseMetaData(Bundle metadata) { mManifestUrl = IntentUtils.safeGetString(metadata, WebApkMetaDataKeys.WEB_MANIFEST_URL); mStartUrl = IntentUtils.safeGetString(metadata, WebApkMetaDataKeys.START_URL); mIconUrl = IntentUtils.safeGetString(metadata, WebApkMetaDataKeys.ICON_URL); mIconMurmur2Hash = getMurmur2HashFromBundle(metadata); } /** * Puts the object in a state where it is safe to be destroyed. */ public void destroy() { if (mFetcher != null) { mFetcher.destroy(); } mFetcher = null; } /** * Called when the updated Web Manifest has been fetched. */ @Override public void onGotManifestData(String startUrl, String scopeUrl, String name, String shortName, String iconUrl, String iconMurmur2Hash, Bitmap iconBitmap, int displayMode, int orientation, long themeColor, long backgroundColor) { mFetcher.destroy(); mFetcher = null; if (TextUtils.isEmpty(scopeUrl)) { scopeUrl = ShortcutHelper.getScopeFromUrl(startUrl); } FetchedManifestData fetchedData = new FetchedManifestData(); fetchedData.startUrl = startUrl; fetchedData.scopeUrl = scopeUrl; fetchedData.name = name; fetchedData.shortName = shortName; fetchedData.iconUrl = iconUrl; fetchedData.iconMurmur2Hash = iconMurmur2Hash; fetchedData.icon = iconBitmap; fetchedData.displayMode = displayMode; fetchedData.orientation = orientation; fetchedData.themeColor = themeColor; fetchedData.backgroundColor = backgroundColor; // TODO(hanxi): crbug.com/627824. Validate whether the new fetched data is // WebAPK-compatible. boolean upgrade = needsUpgrade(fetchedData); mCallback.onUpgradeNeededCheckFinished(upgrade, fetchedData); } /** * Checks whether the WebAPK needs to be upgraded provided the fetched manifest data. */ private boolean needsUpgrade(FetchedManifestData fetchedData) { if (!urlsMatchIgnoringFragments(mIconUrl, fetchedData.iconUrl) || !mIconMurmur2Hash.equals(fetchedData.iconMurmur2Hash)) { return true; } if (!urlsMatchIgnoringFragments(mWebappInfo.scopeUri().toString(), fetchedData.scopeUrl)) { // Sometimes the scope doesn't match due to a missing "/" at the end of the scope URL. // Print log to find such cases. Log.d(TAG, "Needs to request update since the scope from WebappInfo (%s) doesn't match" + "the one fetched from Web Manifest(%s).", mWebappInfo.scopeUri().toString(), fetchedData.scopeUrl); return true; } if (!urlsMatchIgnoringFragments(mStartUrl, fetchedData.startUrl) || !TextUtils.equals(mWebappInfo.shortName(), fetchedData.shortName) || !TextUtils.equals(mWebappInfo.name(), fetchedData.name) || mWebappInfo.backgroundColor() != fetchedData.backgroundColor || mWebappInfo.themeColor() != fetchedData.themeColor || mWebappInfo.orientation() != fetchedData.orientation || mWebappInfo.displayMode() != fetchedData.displayMode) { return true; } return false; } /** * Returns whether the urls match ignoring fragments. Canonicalizes the URLs prior to doing the * comparison. */ protected boolean urlsMatchIgnoringFragments(String url1, String url2) { return UrlUtilities.urlsMatchIgnoringFragments(url1, url2); } }