1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
8 import { PromiseUtils } from "resource://gre/modules/PromiseUtils.sys.mjs";
10 import { Log } from "resource://gre/modules/Log.sys.mjs";
17 } from "resource://gre/modules/GMPUtils.sys.mjs";
19 import { ProductAddonChecker } from "resource://gre/modules/addons/ProductAddonChecker.sys.mjs";
23 ChromeUtils.defineESModuleGetters(lazy, {
24 CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
25 FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
26 ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
27 UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
30 function getScopedLogger(prefix) {
31 // `PARENT_LOGGER_ID.` being passed here effectively links this logger
32 // to the parentLogger.
33 return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " ");
36 const LOCAL_GMP_SOURCES = [
38 id: "gmp-gmpopenh264",
39 src: "chrome://global/content/gmp-sources/openh264.json",
40 installByDefault: true,
43 id: "gmp-widevinecdm",
44 src: "chrome://global/content/gmp-sources/widevinecdm.json",
45 installByDefault: true,
48 id: "gmp-widevinecdm-l1",
49 src: "chrome://global/content/gmp-sources/widevinecdm_l1.json",
50 installByDefault: false,
54 function downloadJSON(uri) {
55 let log = getScopedLogger("GMPInstallManager.checkForAddons");
56 log.info("fetching config from: " + uri);
57 return new Promise((resolve, reject) => {
58 let xmlHttp = new lazy.ServiceRequest({ mozAnon: true });
60 xmlHttp.onload = function (aResponse) {
61 resolve(JSON.parse(this.responseText));
64 xmlHttp.onerror = function (e) {
65 reject("Fetching " + uri + " results in error code: " + e.target.status);
68 xmlHttp.open("GET", uri);
69 xmlHttp.overrideMimeType("application/json");
75 * If downloading from the network fails (AUS server is down),
76 * load the sources from local build configuration.
78 function downloadLocalConfig(sources) {
79 if (!sources.length) {
80 return Promise.resolve({ addons: [] });
83 let log = getScopedLogger("GMPInstallManager.downloadLocalConfig");
86 return downloadJSON(conf.src).then(addons => {
87 let platforms = addons.vendors[conf.id].platforms;
88 let target = Services.appinfo.OS + "_" + lazy.UpdateUtils.ABI;
92 if (!(target in platforms)) {
93 // There was no matching platform so return false, this addon
94 // will be filtered from the results below
95 log.info("no details found for: " + target);
98 // Field either has the details of the binary or is an alias
99 // to another build target key that does
100 if (platforms[target].alias) {
101 target = platforms[target].alias;
103 details = platforms[target];
107 log.info("found plugin: " + conf.id);
110 URL: details.fileUrl,
111 hashFunction: addons.hashFunction,
112 hashValue: details.hashValue,
113 version: addons.vendors[conf.id].version,
114 size: details.filesize,
120 // Some filters may not match this platform so
122 return { addons: addons.filter(x => x !== false) };
127 * Provides an easy API for downloading and installing GMP Addons
129 export function GMPInstallManager() {}
132 * Temp file name used for downloading
134 GMPInstallManager.prototype = {
136 * Obtains a URL with replacement of vars
139 let log = getScopedLogger("GMPInstallManager._getURL");
140 // Use the override URL if it is specified. The override URL is just like
141 // the normal URL but it does not check the cert.
142 let url = GMPPrefs.getString(GMPPrefs.KEY_URL_OVERRIDE, "");
144 log.info("Using override url: " + url);
146 url = GMPPrefs.getString(GMPPrefs.KEY_URL);
147 log.info("Using url: " + url);
150 url = await lazy.UpdateUtils.formatUpdateURL(url);
152 log.info("Using url (with replacement): " + url);
157 * Records telemetry results on if fetching update.xml from Balrog succeeded
158 * when content signature was used to verify the response from Balrog.
159 * @param didGetAddonList
160 * A boolean indicating if an update.xml containing the addon list was
161 * successfully fetched (true) or not (false).
163 * The error that was thrown (if it exists) for the failure case. This
164 * is expected to have a addonCheckerErr member which provides further
165 * information on why the addon checker failed.
167 recordUpdateXmlTelemetryForContentSignature(didGetAddonList, err = null) {
168 let log = getScopedLogger(
169 "GMPInstallManager.recordUpdateXmlTelemetryForContentSignature"
172 let updateResultHistogram = Services.telemetry.getHistogramById(
173 "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
176 // The non-glean telemetry used here will be removed in future and just
177 // the glean data will be gathered.
178 if (didGetAddonList) {
179 updateResultHistogram.add("content_sig_ok");
180 Glean.gmp.updateXmlFetchResult.content_sig_success.add(1);
183 // All remaining cases are failure cases.
184 updateResultHistogram.add("content_sig_fail");
185 if (!err?.addonCheckerErr) {
186 // Unknown error case. If this is happening we should audit error paths
187 // to identify why we're not getting an error, or not getting it
189 Glean.gmp.updateXmlFetchResult.content_sig_unknown_error.add(1);
192 const errorToHistogramMap = {
193 [ProductAddonChecker.NETWORK_REQUEST_ERR]:
194 "content_sig_net_request_error",
195 [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "content_sig_net_timeout",
196 [ProductAddonChecker.ABORT_ERR]: "content_sig_abort",
197 [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
198 "content_sig_missing_data",
199 [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "content_sig_failed",
200 [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "content_sig_invalid",
201 [ProductAddonChecker.XML_PARSE_ERR]: "content_sig_xml_parse_error",
204 errorToHistogramMap[err.addonCheckerErr] ?? "content_sig_unknown_error";
205 let metric = Glean.gmp.updateXmlFetchResult[metricID];
208 // We don't expect this path to be hit, but we don't want telemetry
209 // failures to break GMP updates, so catch any issues here and let the
210 // update machinery continue.
212 `Failed to record telemetry result of getProductAddonList, got error: ${e}`
218 * Records telemetry results on if fetching update.xml from Balrog succeeded
219 * when cert pinning was used to verify the response from Balrog. This
220 * should be removed once we're no longer using cert pinning.
221 * @param didGetAddonList
222 * A boolean indicating if an update.xml containing the addon list was
223 * successfully fetched (true) or not (false).
225 * The error that was thrown (if it exists) for the failure case. This
226 * is expected to have a addonCheckerErr member which provides further
227 * information on why the addon checker failed.
229 recordUpdateXmlTelemetryForCertPinning(didGetAddonList, err = null) {
230 let log = getScopedLogger(
231 "GMPInstallManager.recordUpdateXmlTelemetryForCertPinning"
234 let updateResultHistogram = Services.telemetry.getHistogramById(
235 "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
238 // The non-glean telemetry used here will be removed in future and just
239 // the glean data will be gathered.
240 if (didGetAddonList) {
241 updateResultHistogram.add("cert_pinning_ok");
242 Glean.gmp.updateXmlFetchResult.cert_pin_success.add(1);
245 // All remaining cases are failure cases.
246 updateResultHistogram.add("cert_pinning_fail");
247 if (!err?.addonCheckerErr) {
248 // Unknown error case. If this is happening we should audit error paths
249 // to identify why we're not getting an error, or not getting it
251 Glean.gmp.updateXmlFetchResult.cert_pin_unknown_error.add(1);
254 const errorToHistogramMap = {
255 [ProductAddonChecker.NETWORK_REQUEST_ERR]: "cert_pin_net_request_error",
256 [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "cert_pin_net_timeout",
257 [ProductAddonChecker.ABORT_ERR]: "cert_pin_abort",
258 [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
259 "cert_pin_missing_data",
260 [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "cert_pin_failed",
261 [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "cert_pin_invalid",
262 [ProductAddonChecker.XML_PARSE_ERR]: "cert_pin_xml_parse_error",
265 errorToHistogramMap[err.addonCheckerErr] ?? "cert_pin_unknown_error";
266 let metric = Glean.gmp.updateXmlFetchResult[metricID];
269 // We don't expect this path to be hit, but we don't want telemetry
270 // failures to break GMP updates, so catch any issues here and let the
271 // update machinery continue.
273 `Failed to record telemetry result of getProductAddonList, got error: ${e}`
279 * Performs an addon check.
280 * @return a promise which will be resolved or rejected.
281 * The promise is resolved with an object with properties:
282 * addons: array of addons
283 * usedFallback: whether the data was collected from online or
284 * from fallback data within the build
285 * The promise is rejected with an object with properties:
286 * target: The XHR request object
287 * status: The HTTP status code
288 * type: Sometimes specifies type of rejection
290 async checkForAddons() {
291 let log = getScopedLogger("GMPInstallManager.checkForAddons");
292 if (this._deferred) {
293 log.error("checkForAddons already called");
294 return Promise.reject({ type: "alreadycalled" });
297 if (!GMPPrefs.getBool(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
298 log.info("Updates are disabled via media.gmp-manager.updateEnabled");
299 return { usedFallback: true, addons: [] };
302 this._deferred = PromiseUtils.defer();
303 let deferredPromise = this._deferred.promise;
305 // Should content signature checking of Balrog replies be used? If so this
306 // will be done instead of the older cert pinning method.
307 let checkContentSignature = GMPPrefs.getBool(
308 GMPPrefs.KEY_CHECK_CONTENT_SIGNATURE,
312 let allowNonBuiltIn = true;
314 // Only check certificates if we're not using a custom URL, and only if
315 // we're not checking a content signature.
317 !Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
318 !checkContentSignature
320 allowNonBuiltIn = !GMPPrefs.getString(
321 GMPPrefs.KEY_CERT_REQUIREBUILTIN,
324 if (GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
325 certs = lazy.CertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
329 let url = await this._getURL();
332 `Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}`
338 res = await ProductAddonChecker.getProductAddonList(
342 checkContentSignature
345 if (checkContentSignature) {
346 this.recordUpdateXmlTelemetryForContentSignature(true);
348 this.recordUpdateXmlTelemetryForCertPinning(true);
352 if (checkContentSignature) {
353 this.recordUpdateXmlTelemetryForContentSignature(false, err);
355 this.recordUpdateXmlTelemetryForCertPinning(false, err);
361 log.info("Falling back to local config");
362 let fallbackSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) {
363 return gmpSource.installByDefault;
365 res = await downloadLocalConfig(fallbackSources);
368 this._deferred.reject(err);
369 delete this._deferred;
370 return deferredPromise;
374 if (res && res.addons) {
375 addons = res.addons.map(a => new GMPAddon(a));
380 // We need to merge in the addons that are only available via fallback that
381 // the user has requested be forced installed regardless of our update
382 // server configuration.
384 let forcedSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) {
385 return GMPPrefs.getBool(
386 GMPPrefs.KEY_PLUGIN_FORCE_INSTALL,
392 let forcedConfigs = await downloadLocalConfig(
393 forcedSources.filter(function (gmpSource) {
394 return !addons.find(gmpAddon => gmpAddon.id == gmpSource.id);
398 let forcedAddons = forcedConfigs.addons.map(
399 config => new GMPAddon(config)
402 log.info("Forced " + forcedAddons.length + " addons.");
403 addons = addons.concat(forcedAddons);
405 log.info("Failed to force addons: " + err);
408 this._deferred.resolve({ addons });
409 delete this._deferred;
410 return deferredPromise;
413 * Installs the specified addon and calls a callback when done.
414 * @param gmpAddon The GMPAddon object to install
415 * @return a promise which will be resolved or rejected
416 * The promise will resolve with an array of paths that were extracted
417 * The promise will reject with an error object:
418 * target: The XHR request object
419 * status: The HTTP status code
420 * type: A string to represent the type of error
421 * downloaderr, verifyerr or previouserrorencountered
423 installAddon(gmpAddon) {
424 if (this._deferred) {
425 let log = getScopedLogger("GMPInstallManager.installAddon");
426 log.error("previous error encountered");
427 return Promise.reject({ type: "previouserrorencountered" });
429 this.gmpDownloader = new GMPDownloader(gmpAddon);
430 return this.gmpDownloader.start();
432 _getTimeSinceLastCheck() {
433 let now = Math.round(Date.now() / 1000);
434 // Default to 0 here because `now - 0` will be returned later if that case
435 // is hit. We want a large value so a check will occur.
436 let lastCheck = GMPPrefs.getInt(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
437 // Handle clock jumps, return now since we want it to represent
438 // a lot of time has passed since the last check.
439 if (now < lastCheck) {
442 return now - lastCheck;
444 get _isEMEEnabled() {
445 return GMPPrefs.getBool(GMPPrefs.KEY_EME_ENABLED, true);
447 _isAddonEnabled(aAddon) {
448 return GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon);
450 _isAddonUpdateEnabled(aAddon) {
452 this._isAddonEnabled(aAddon) &&
453 GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon)
457 let now = Math.round(Date.now() / 1000);
458 GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
460 _versionchangeOccurred() {
461 let savedBuildID = GMPPrefs.getString(GMPPrefs.KEY_BUILDID, "");
462 let buildID = Services.appinfo.platformBuildID || "";
463 if (savedBuildID == buildID) {
466 GMPPrefs.setString(GMPPrefs.KEY_BUILDID, buildID);
470 * Wrapper for checkForAddons and installAddon.
471 * Will only install if not already installed and will log the results.
472 * This will only install/update the OpenH264 and EME plugins
473 * @return a promise which will be resolved if all addons could be installed
474 * successfully, rejected otherwise.
476 async simpleCheckAndInstall() {
477 let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall");
479 if (this._versionchangeOccurred()) {
481 "A version change occurred. Ignoring " +
482 "media.gmp-manager.lastCheck to check immediately for " +
483 "new or updated GMPs."
486 let secondsBetweenChecks = GMPPrefs.getInt(
487 GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS,
488 DEFAULT_SECONDS_BETWEEN_CHECKS
490 let secondsSinceLast = this._getTimeSinceLastCheck();
494 " seconds ago, minimum seconds: " +
497 if (secondsBetweenChecks > secondsSinceLast) {
498 log.info("Will not check for updates.");
499 return { status: "too-frequent-no-check" };
504 let { addons } = await this.checkForAddons();
505 this._updateLastCheck();
506 log.info("Found " + addons.length + " addons advertised.");
507 let addonsToInstall = addons.filter(function (gmpAddon) {
508 log.info("Found addon: " + gmpAddon.toString());
510 if (!gmpAddon.isValid) {
511 log.info("Addon |" + gmpAddon.id + "| is invalid.");
515 if (GMPUtils.isPluginHidden(gmpAddon)) {
516 log.info("Addon |" + gmpAddon.id + "| has been hidden.");
520 if (gmpAddon.isInstalled) {
521 log.info("Addon |" + gmpAddon.id + "| already installed.");
525 // Do not install from fallback if already installed as it
526 // may be a downgrade
527 if (gmpAddon.usedFallback && gmpAddon.isUpdate) {
531 "| not installing updates based " +
537 let addonUpdateEnabled = false;
538 if (GMP_PLUGIN_IDS.includes(gmpAddon.id)) {
539 if (!this._isAddonEnabled(gmpAddon.id)) {
541 "GMP |" + gmpAddon.id + "| has been disabled; skipping check."
543 } else if (!this._isAddonUpdateEnabled(gmpAddon.id)) {
545 "Auto-update is off for " + gmpAddon.id + ", skipping check."
548 addonUpdateEnabled = true;
551 // Currently, we only support installs of OpenH264 and EME plugins.
553 "Auto-update is off for unknown plugin '" +
559 return addonUpdateEnabled;
562 if (!addonsToInstall.length) {
563 let now = Math.round(Date.now() / 1000);
564 GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_EMPTY_CHECK, now);
565 log.info("No new addons to install, returning");
566 return { status: "nothing-new-to-install" };
569 let installResults = [];
570 let failureEncountered = false;
571 for (let addon of addonsToInstall) {
573 await this.installAddon(addon);
574 installResults.push({
579 failureEncountered = true;
580 installResults.push({
586 if (failureEncountered) {
587 // eslint-disable-next-line no-throw-literal
588 throw { status: "failed", results: installResults };
590 return { status: "succeeded", results: installResults };
592 log.error("Could not check for addons", e);
598 * Makes sure everything is cleaned up
601 let log = getScopedLogger("GMPInstallManager.uninit");
603 log.info("Aborting request");
604 this._request.abort();
606 if (this._deferred) {
607 log.info("Rejecting deferred");
608 this._deferred.reject({ type: "uninitialized" });
610 log.info("Done cleanup");
614 * If set to true, specifies to leave the temporary downloaded zip file.
615 * This is useful for tests.
617 overrideLeaveDownloadedZip: false,
621 * Used to construct a single GMP addon
622 * GMPAddon objects are returns from GMPInstallManager.checkForAddons
623 * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
625 * @param addon The ProductAddonChecker `addon` object
627 export function GMPAddon(addon) {
628 let log = getScopedLogger("GMPAddon.constructor");
629 this.usedFallback = false;
630 for (let name of Object.keys(addon)) {
631 this[name] = addon[name];
633 log.info("Created new addon: " + this.toString());
636 GMPAddon.prototype = {
638 * Returns a string representation of the addon
652 (this.size !== undefined ? ", size: " + this.size : "") +
657 * If all the fields aren't specified don't consider this addon valid
658 * @return true if the addon is parsed and valid
673 GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) ===
675 GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "", this.id) ===
680 return this.id == WIDEVINE_L1_ID || this.id == WIDEVINE_L3_ID;
683 return this.id == "gmp-gmpopenh264";
686 * @return true if the addon has been previously installed and this is
687 * a new version, if this is a fresh install return false
692 GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id)
698 * Constructs a GMPExtractor object which is used to extract a GMP zip
699 * into the specified location.
700 * @param zipPath The path on disk of the zip file to extract
701 * @param relativePath The relative path components inside the profile directory
702 * to extract the zip to.
704 export function GMPExtractor(zipPath, relativeInstallPath) {
705 this.zipPath = zipPath;
706 this.relativeInstallPath = relativeInstallPath;
709 GMPExtractor.prototype = {
711 * Installs the this.zipPath contents into the directory used to store GMP
712 * addons for the current platform.
714 * @return a promise which will be resolved or rejected
715 * See GMPInstallManager.installAddon for resolve/rejected info
718 this._deferred = PromiseUtils.defer();
719 let deferredPromise = this._deferred;
720 let { zipPath, relativeInstallPath } = this;
721 // Escape the zip path since the worker will use it as a URI
722 let zipFile = new lazy.FileUtils.File(zipPath);
723 let zipURI = Services.io.newFileURI(zipFile).spec;
724 let worker = new ChromeWorker(
725 "resource://gre/modules/GMPExtractor.worker.js"
727 worker.onmessage = function (msg) {
728 let log = getScopedLogger("GMPExtractor");
730 if (msg.data.result != "success") {
731 log.error("Failed to extract zip file: " + zipURI);
732 log.error("Exception: " + msg.data.exception);
733 return deferredPromise.reject({
735 status: msg.data.exception,
739 log.info("Successfully extracted zip file: " + zipURI);
740 return deferredPromise.resolve(msg.data.extractedPaths);
742 worker.postMessage({ zipURI, relativeInstallPath });
743 return this._deferred.promise;
748 * Constructs an object which downloads and initiates an install of
749 * the specified GMPAddon object.
750 * @param gmpAddon The addon to install.
752 export function GMPDownloader(gmpAddon) {
753 this._gmpAddon = gmpAddon;
756 GMPDownloader.prototype = {
758 * Starts the download process for an addon.
759 * @return a promise which will be resolved or rejected
760 * See GMPInstallManager.installAddon for resolve/rejected info
763 let log = getScopedLogger("GMPDownloader");
764 let gmpAddon = this._gmpAddon;
765 let now = Math.round(Date.now() / 1000);
766 GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_INSTALL_START, now, gmpAddon.id);
768 if (!gmpAddon.isValid) {
769 log.info("gmpAddon is not valid, will not continue");
770 return Promise.reject({
775 // If the HTTPS-Only Mode is enabled, every insecure request gets upgraded
776 // by default. This upgrade has to be prevented for openh264 downloads since
777 // the server doesn't support https://
778 const downloadOptions = {
779 httpsOnlyNoUpgrade: gmpAddon.isOpenH264,
781 return ProductAddonChecker.downloadAddon(gmpAddon, downloadOptions).then(
783 let now = Math.round(Date.now() / 1000);
784 GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD, now, gmpAddon.id);
786 `install to directory path: ${gmpAddon.id}/${gmpAddon.version}`
788 let gmpInstaller = new GMPExtractor(zipPath, [
792 let installPromise = gmpInstaller.install();
793 return installPromise.then(
795 // Success, set the prefs
796 let now = Math.round(Date.now() / 1000);
797 GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
798 // Remember our ABI, so that if the profile is migrated to another
799 // platform or from 32 -> 64 bit, we notice and don't try to load the
800 // unexecutable plugin library.
801 let abi = GMPUtils._expectedABI(gmpAddon);
802 log.info("Setting ABI to '" + abi + "' for " + gmpAddon.id);
803 GMPPrefs.setString(GMPPrefs.KEY_PLUGIN_ABI, abi, gmpAddon.id);
804 // We use the combination of the hash and version to ensure we are
807 GMPPrefs.KEY_PLUGIN_HASHVALUE,
811 // Setting the version pref signals installation completion to consumers,
812 // if you need to set other prefs etc. do it before this.
814 GMPPrefs.KEY_PLUGIN_VERSION,
818 return extractedPaths;
822 GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAIL_REASON,
826 let now = Math.round(Date.now() / 1000);
828 GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAILED,
838 GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAIL_REASON,
842 let now = Math.round(Date.now() / 1000);
844 GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAILED,