/*jshint browser: true */ /*global performance, console, Notification */ 'use strict'; var _xstart = performance.timing.fetchStart - performance.timing.navigationStart; window.plog = function(msg) { console.log(msg + ' ' + (performance.now() - _xstart)); }; /** * Apparently the event catching done for the startup events from * performance_testing_helper record the last event received of that type as * the official time, instead of using the first. So unfortunately, we need a * global to make sure we do not emit the same events later. * @type {Boolean} */ window.startupCacheEventsSent = false; /** * Version number for cache, allows expiring cache. * Set by build process. Set as a global because it * is also used in html_cache.js. */ window.HTML_CACHE_VERSION = '2'; /** * A function callback is registered if the model needs to be loaded before * final view determination can be done. This should be a rare event, but can * happen, see localOnModelLoaded for more details. config.js looks for this * callback. */ window.startupOnModelLoaded = null; /** * Tracks if a mozSetMessageHandler has been dispatched to code. This only * exists to help us be efficient on closing the app in the case of notification * close events. If the notification system improved so that the app could tell * it not to notify us on close events, this could be removed. It is a global * so that cronsync-main can set it for alarm operations. This file does not * track alarm setMozMessageHandler messages directly, since they need to * acquire wake locks in the event turn the message is received. Since that is a * bit complicated, the back-end handles it, since it also knows more about the * sync details. */ window.appDispatchedMessage = false; (function() { // Holds on to the pending message type that is indicated by // mozHasPendingMessage. If it has a value, given by mozHasPendingMessage, // then startup is put on hold until it arrives. Once the mozSetMessageHandler // for the pending message type is received, this variable is cleared, // opening the way for other mozSetMessageHandler messages to be processed. // This is only important to track for messages that have UI impact, like // inserting cards. Data messages, like alarm are fine to pass through // unblocked. var pendingUiMessageType = null; // There are special tasks, like DOM injection from cache, and startup of the // main JS scripts, that should only be done during startup, but once the app // has started up, then this file just handles dispatching of messages // received via mozSetMessageHandler. var startingUp = true; // Holds the determination of the entry point type and view that should be // used for startup. var startupData = {}; /** * startupOnModelLoaded is set to this function if the * data_has_account localStorage value is not known. Called eventually by * config.js once the model module has been loaded. */ function localOnModelLoaded(model, callback) { model.latestOnce('acctsSlice', function(acctsSlice) { // At this point, model will have set up 'data_has_account', so the the // final view can be set and the rest of the world can start turning. // Note that alarm can get kicked off before this is all done, // since the worker will have started up and registered to get those // messages. It is OK though since not showing any UI. If this case is // triggered, it will result in a UI display in that case, but that is OK, // it only happens on an app update situation, from cookies to // localStorage or some kind of localStorage reset. So a rare event, and // does not cause harm, just a bit of extra work in those cases once. console.log('localOnModelLoaded called, hasAccount: ' + localStorage.getItem('data_has_account')); setDefaultView(); hydrateHtml(startupData.view); window.startupOnModelLoaded = null; callback(); }); } if (!localStorage.getItem('data_has_account')) { console.log('data_has_account unknown, asking for model load first'); window.startupOnModelLoaded = localOnModelLoaded; } function hasAccount() { // var _1 = performance.now(); var has = localStorage.getItem('data_has_account') === 'yes'; // console.log('@@@ LOCALSTORAGE GET data_has_account: ' + // (performance.now() - _1)); return has; } function setDefaultView() { if (hasAccount()) { if (!startupData.view) { startupData.view = 'message_list'; } } else { startupData.view = 'setup_account_info'; } } // Set up the default view, but if that is not possible to know yet, since // the status of hasAccount is unknown, wait for the callback to set it up. if (!window.startupOnModelLoaded) { setDefaultView(); } startupData.entry = 'default'; /** * Makes sure the message type is wanted, given pendingUiMessageType concerns, * and not coming in at a fast rate due to things like double clicks. Only * necessary to use if the message is something that would insert cards, and * fast entries could mess up card state. */ var lastEntryTime = 0; function isUiMessageTypeAllowedEntry(type) { // If startup is pending on a message, and this message type is not what is // wanted, skip it. if (pendingUiMessageType && pendingUiMessageType !== type) { console.log('Ignoring message of type: ' + type); return false; } var entryTime = Date.now(); // This is the right pending startup message, so proceed without checking // the entryTime as it is the first one allowed through. if (pendingUiMessageType) { pendingUiMessageType = null; } else { // Check for fast incoming messages, like from activity double taps, and // ignore them, to avoid messing up the UI startup from double-click taps // of activities/notifications that would insert new cards. Only one entry // per second. if (entryTime < lastEntryTime + 1000) { console.log('email entry gate blocked fast repeated action: ' + type); return false; } } lastEntryTime = entryTime; return true; } /** * Gets the HTML string from cache, as well as language direction. * This method assumes all cookie keys that have pattern * /htmlc(\d+)/ are part of the object value. This method could * throw given vagaries of cookie cookie storage and encodings. * Be prepared. */ function retrieve(id) { // var _1 = performance.now(); var value = localStorage.getItem('html_cache_' + id) || ''; // console.log('@@@ LOCALSTORAGE GET html_cache: ' + // (performance.now() - _1)); var index, version, langDir; // console.log('RETRIEVED: ' + 'html_cache_' + id + ': ' + value); index = value.indexOf(':'); if (index === -1) { value = ''; } else { version = value.substring(0, index); value = value.substring(index + 1); // Version is further subdivided to include lang direction. See email's // l10n.js for logic to reset the dir back to the actual language choice // if the language direction changes between email invocations. var versionParts = version.split(','); version = versionParts[0]; langDir = versionParts[1]; } if (version !== window.HTML_CACHE_VERSION) { console.log('Skipping html cache for ' + id + ', out of date. Expected ' + window.HTML_CACHE_VERSION + ' but found ' + version); value = ''; } return { langDir: langDir, contents: value }; } // The evt module is needed to register for 'notification' events that are // triggered by other code in the email app (not from the mozSetMessageHandler // pathway), but it can be done lazily once the rest of the app has started up // and has asked to listen for app messages. var evt; // Tracks the handlers that are registered via globalOnAppMessage. The // handlers are stored in slots that are named for the message types, like // 'activity', 'notification'. var handlers = {}; // Holds on to messages that come in via mozSetMessageHandler until there is a // handler that has been registred for that message type. var handlerQueues = { notification: [], activity: [] }; /** * Called by app code. Only expects one listener to be registered for each * handler type. This function also assumes that a `require` loader is * available to fetch the 'evt' module. This would not be needed if * evt.emit('notification') was not triggered by the email code. * @param {Object} listener Object whose keys are the handler type names and * values are functions that handle that type. */ window.globalOnAppMessage = function(listener) { Object.keys(listener).forEach(function(key) { var fn = handlers[key] = listener[key]; var queue = handlerQueues[key]; if (queue.length) { handlerQueues[key] = []; queue.forEach(function(argsArray) { fn.apply(undefined, argsArray); }); } }); // Only need to do this wiring once, but globalOnAppMessage could be called // multiple times. if (!evt) { require(['evt'], function(ev) { evt = ev; evt.on('notification', onNotification); }); } return startupData; }; // Attach the hasAccount so that other code can use it and always get the // freshest cached value. window.globalOnAppMessage.hasAccount = hasAccount; function dispatch(type, args) { window.appDispatchedMessage = true; if (handlers[type]) { return handlers[type].apply(undefined, args); } else { handlerQueues[type].push(args); } // On the very first dispatch when app open is triggered by a dispatchable // event, need to finish bootup now that full startup state is known. finishStartup(); } /** * Perform requested activity. * * @param {MozActivityRequestHandler} req activity invocation. */ function onActivityRequest(req) { console.log('mozSetMessageHandler: received an activity'); if (!isUiMessageTypeAllowedEntry('activity')) { return req.postError('cancelled'); } // Right now all activity entry points go to compose, but may need to // revisited if the activity entry points change. if (startingUp) { startupData.view = 'compose'; hydrateHtml(startupData.view); } dispatch('activity', [req]); } function onNotification(msg) { console.log('mozSetMessageHandler: received a notification'); // Skip notification events that are not from a notification "click". The // system app will also notify this method of any close events for // notifications, which are not at all interesting. if (!msg.clicked) { // If a alarm is waiting right behind this notification message, that sync // would will be lost when the application closes. It is an edge case // though, and recoverable on the next sync, where trying to be // accommodating to it here would add more code complexity, and it would // still have a failure window where the app just starts up with a // notification, but just after that, after startup is finished but before // this function is called, a sync message is queued up. Activities could // be dropped too in a similar situation, but we might already drop some // due to the fast click gate. The long term fix is to just get a // notification system that allows apps to tell it not to call it if just // closing a notification. // Only close if entry was a notification and no other messages, like a // alarm or a UI-based message, have been dispatched. if (startupData.entry === 'notification' && !window.appDispatchedMessage) { console.log('App only started for notification close, closing app.'); window.close(); } return; } // Bail early if notification is ignored. Do this before the notification // close() work, so that user has the opportunity to tap on the notification // later and still activate that notification flow. if (!isUiMessageTypeAllowedEntry('notification')) { return; } // Need to manually get all notifications and close the one that triggered // this event due to fallout from 890440 and 966481. if (typeof Notification !== 'undefined' && Notification.get) { Notification.get().then(function(notifications) { if (notifications) { notifications.some(function(notification) { // Compare tags, as the tag is based on the account ID and // we only have one notification per account. Plus, there // is no "id" field on the notification. if (notification.tag === msg.tag && notification.close) { notification.close(); return true; } }); } }); } // Adjust the startupData view as desired by the notification. For upgrade // cases where a previous notification from an older version of email // used the iconUrl, this just means we will got to the message_list instead // of the message_reader for the single email notification case, but that is // OK since it is a temporary upgrade issue, and the email will still be // seen since it should be top of the list in the message_list. var view = msg.data && msg.data.type; if (startingUp && view) { startupData.view = view; hydrateHtml(view); } // The notification infrastructure does not automatically bring the app to // the foreground, so if still hidden, show it now. Ideally this would not // use a setTimeout, but it was getting incorrect document.hidden values on // startup, where just a bit later the value does seem to be set correctly. // The other option was to do this work inside the notification handler in // mail_app, but the delay is long enough waiting for that point that the // user might be concerned they did not correctly tap the notification. setTimeout(function() { if (document.hidden && navigator.mozApps) { console.log('document was hidden, showing app via mozApps.getSelf'); navigator.mozApps.getSelf().onsuccess = function(event) { var app = event.target.result; app.launch(); }; } }, 300); dispatch('notification', [msg.data]); } var selfNode = document.querySelector('[data-loadsrc]'); function hydrateHtml(id) { var parsedResults = retrieve(id); if (parsedResults.langDir) { document.querySelector('html').setAttribute('dir', parsedResults.langDir); } var contents = parsedResults.contents; // Automatically restore the HTML as soon as module is executed. // ASSUMES card node is available (DOMContentLoaded or execution of // module after DOM node is in doc) var cardsNode = document.getElementById(selfNode.dataset.targetid); cardsNode.innerHTML = contents; window.startupCacheEventsSent = !!contents; if (contents) { console.log('Using HTML cache for ' + id); } if (window.startupCacheEventsSent) { window.performance.mark('navigationLoaded'); window.performance.mark('visuallyLoaded'); } } function finishStartup() { // This can be called multiple times due to pendingUiMessageType listeners // so only do the work once. if (!startingUp) { return; } var scriptNode = document.createElement('script'), loader = selfNode.dataset.loader, loadSrc = selfNode.dataset.loadsrc; if (loader) { scriptNode.setAttribute('data-main', loadSrc); scriptNode.src = loader; } else { scriptNode.src = loadSrc; } document.head.appendChild(scriptNode); startingUp = false; } // mozHasPendingMessage seems like it can only be called once per message // type, so only asking once. If we have both an activity or a notification // coming in, the activity should win since it is part of a larger user action // besides just email, and expects to get some callbacks. if (navigator.mozHasPendingMessage) { if (navigator.mozHasPendingMessage('activity')) { pendingUiMessageType = 'activity'; } else if (navigator.mozHasPendingMessage('notification')) { pendingUiMessageType = 'notification'; } if (pendingUiMessageType) { startupData.entry = pendingUiMessageType; } else if (navigator.mozHasPendingMessage('alarm')) { // While alarm is not important for the pendingUiMessageType // gateway, it still should be indicated that the entry point was not the // default entry point, so that the UI is not fully started if this is a // background sync. startupData.entry = 'alarm'; } else if (navigator.mozHasPendingMessage('request-sync')) { // Be nice and consume the message so that request-sync feels like it // completed its mission, but ignore the actual message. if (navigator.mozSetMessageHandler) { navigator.mozSetMessageHandler('request-sync', function (e) { console.log('Received legacy request-sync message, ignoring.'); }); } // Legacy request-sync trigger. Just clear all request-sync tasks, and // allow the UI to go to a normal view this time. When back end starts up // it will ensure the sync alarms are set properly for the next sync, so // in this legacy case we will just miss one sync pass. var navSync = navigator.sync; if (navSync) { navSync.registrations().then(function(regs) { regs.forEach(function(reg) { console.log('Unregistering legacy request sync...'); navSync.unregister(reg.task); console.log('Unregister done!'); }); }, function(err) { console.error('navigator.sync.registrations failed: ', err); }); } } } if ('mozSetMessageHandler' in navigator) { navigator.mozSetMessageHandler('notification', onNotification); navigator.mozSetMessageHandler('activity', onActivityRequest); } else { console.warn('mozSetMessageHandler not available. No notifications, ' + 'activities or syncs.'); } if (window.startupOnModelLoaded) { finishStartup(); } else if (startupData.entry === 'default' || startupData.entry === 'alarm') { hydrateHtml(startupData.view); finishStartup(); } }());