'use strict'; /* global ApplicationSource */ /* global BookmarkSource */ /* global configurator */ /* global dispatchEvent */ /* global GaiaGrid */ (function(exports) { const DB_VERSION = 1; const DB_NAME = 'verticalhome'; const DB_ITEM_STORE = 'items'; const DB_SV_APP_STORE_NAME = 'svAppsInstalled'; const DATASTORE_NAME = 'verticalhome_items_migration'; const PREFS_STORE = 'vertical_preferences_store'; var db; var datastore; function sort(entries, order) { if (!order || !order.length) { return entries; } var newEntries = []; function isEqual(lookFor, compareWith) { if (!compareWith.detail || !lookFor) { return false; } if (compareWith instanceof GaiaGrid.Mozapp) { if (!lookFor.manifestURL || !compareWith.detail.manifestURL) { return false; } if (compareWith.detail.entryPoint) { return lookFor.manifestURL === compareWith.detail.manifestURL && lookFor.entry_point === compareWith.detail.entryPoint; } else { return lookFor.manifestURL === compareWith.detail.manifestURL; } } else if (compareWith instanceof GaiaGrid.Bookmark) { if (!lookFor.id || !compareWith.detail.id) { return false; } return lookFor.id === compareWith.detail.id; } } for (var i = 0, iLen = order.length; i < iLen; i++) { // Add all entries of current section for (var j = 0, jLen = order[i].length; j < jLen; j++) { var ind = entries.findIndex( isEqual.bind(null, order[i][j])); if (ind >= 0) { newEntries.push(entries.splice(ind,1)[0]); } } // If we have more sections add a divider if (i < iLen - 1) { newEntries.push(new GaiaGrid.Divider()); } } // If entries is not empty yet, they could be unordered apps (added at end) // or SingleVariant apps, installed before vertical started, which we need // insert it in their correct position (ever if there was a first run with // SIM). if (entries.length > 0) { // We only reorder sv apps if SIM was present on first boot! if (configurator.isSimPresentOnFirstBoot) { sortUnclassifiedApps(newEntries, entries); } else { newEntries.push(new GaiaGrid.Divider()); newEntries = newEntries.concat(entries); } } for (i = 0, iLen = newEntries.length; i < iLen; i++) { if (newEntries[i].detail) { newEntries[i].detail.index = i; } } return newEntries; } function sortUnclassifiedApps(dstEntries, orgEntries) { function compareApps(appA, appB) { var svAppA = null; var svAppB = null; if (appA) { svAppA = configurator.getSingleVariantApp(appA.detail.manifestURL); } if (appB) { svAppB = configurator.getSingleVariantApp(appB.detail.manifestURL); } var locA = svAppA && svAppA.location !== undefined && svAppA.location || Number.MAX_VALUE; var locB = svAppB && svAppB.location !== undefined && svAppB.location || Number.MAX_VALUE; var relPos = locA - locB; return relPos ? (relPos > 0 ? 1 : -1) : 0; } // To simplify the process, we order the apps first (SV apps in relative // order and at the beginning of the array) orgEntries.sort(compareApps); // After the sorting process, if the first element is not a SV app then // we only need to add them at the end of dstEntries if (!configurator.getSingleVariantApp(orgEntries[0].detail.manifestURL)) { dstEntries.push(new GaiaGrid.Divider()); dstEntries = dstEntries.concat(orgEntries); } else { var sepAdded = false; for (var i = 0, iLen = orgEntries.length; i < iLen; i++) { var app = orgEntries[i]; var svApp = configurator.getSingleVariantApp(app.detail.manifestURL); if (!svApp) { if (!sepAdded) { dstEntries.push(new GaiaGrid.Divider()); sepAdded = true; } dstEntries.push(app); } else { if (svApp.location !== undefined) { dstEntries.splice(svApp.location, 0, app); } else { dstEntries.push(app); } } } } } function loadTable(table, indexName, iterator, aNext) { newTxn(table, 'readonly', function(txn, store) { var index = store.index(indexName); index.openCursor().onsuccess = function onsuccess(event) { var cursor = event.target.result; if (!cursor) { return; } iterator(cursor.value); cursor.continue(); }; }, aNext); } function newTxn(storeName, txnType, withTxnAndStore, successCb) { var txn = db.transaction([storeName], txnType); var store = txn.objectStore(storeName); txn.oncomplete = function(event) { if (successCb) { successCb(event); } }; txn.onerror = function(event) { console.warn('Error during transaction.'); }; withTxnAndStore(txn, store); } function ItemStore(onsuccess) { var self = this; this.applicationSource = new ApplicationSource(this); this.bookmarkSource = new BookmarkSource(this); this.sources = [this.applicationSource, this.bookmarkSource]; this.ready = false; var isEmpty = false; self.gridOrder = null; var request = window.indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = function _onupgradeneeded(event) { var db = event.target.result; var oldVersion = event.oldVersion || 0; switch (oldVersion) { case 0: // Create the item store var objectStore = db.createObjectStore(DB_ITEM_STORE, { keyPath: 'index'}); objectStore.createIndex('index', 'index', { unique: true }); isEmpty = true; var objectSV = db.createObjectStore(DB_SV_APP_STORE_NAME, { keyPath: 'manifestURL' }); objectSV.createIndex('indexSV', 'indexSV', { unique: true }); } }; request.onsuccess = function _onsuccess() { onsuccess && onsuccess(isEmpty); db = request.result; var cb = self.fetch.bind(self, self.synchronize.bind(self)); if (isEmpty) { window.addEventListener('configuration-ready', function onReady() { window.removeEventListener('configuration-ready', onReady); self.gridOrder = configurator.getGrid(); self.populate(cb); }); } else { self.initSources(cb); } // Handle migration navigator.mozSetMessageHandler('connection', self.handleIAC.bind(self)); }; window.addEventListener('gaiagrid-cached-icons-rendered', this); } ItemStore.prototype = { /** * Whether or not the grid has been rendered. We use this as an * indicator as to whether or not we should save items to indexedDB. * If we get items to save before this is true, we wait until the * cached-icons-rendered event to save. */ _renderedOnce: false, /** * The saved arguments for the last deferred save call. */ _deferredSaveArgs: [], /** * A list of all items. These are item objects (App, Bookmark, Divider) */ _allItems: [], /** * Maintains the current index of the last grid item. */ nextPosition: 0, /** * Fetches a list of all items in the store. */ all: function(success) { if (!this.ready) { window.addEventListener('databaseready', this.all.bind(this, success)); return; } success(this._allItems); }, saveTable: function(table, objArr, column, checkPersist, aNext) { newTxn(table, 'readwrite', function(txn, store) { store.clear(); for (var i = 0, iLen = objArr.length; i < iLen; i++) { if (!checkPersist || (checkPersist && objArr[i].persistToDB)) { store.put(column?objArr[i][column]:objArr[i]); } } if (typeof aNext === 'function') { aNext(); } }); }, saveMigrationData: function(objArr, column, checkPersist) { return datastore.clear().then(() => { var chain = []; objArr.forEach((object) => { if (!checkPersist || object.persistToDB) { var data = column ? object[column] : object; chain.push(datastore.put(data, data.index + 1)); } }); return Promise.all(chain); }); }, /** * @param {Object} object a single object to update. * @param {Function} callback fires when transaction finishes. */ saveItem: function(object, callback) { // intentional use of == meaning null or undefined. if (object.index == null) { console.error('Attempting to save object without `index`'); return; } newTxn( DB_ITEM_STORE, 'readwrite', function(txn, store) { store.put(object); }, callback ); }, /** * Defers the save of items until after the grid is ready. */ deferredSave: function() { var args = Array.slice(arguments); if (this._renderedOnce) { return this.save.apply(this, args); } this._deferredSaveArgs = args; }, /** * Saves all icons to the database. */ save: function(entries, aNext) { entries = sort(entries, this.gridOrder); this.gridOrder = null; // The initial config is simply the list of apps this.saveTable(DB_ITEM_STORE, entries, 'detail', true, aNext); }, /** * Save reference to SingleVariant app previously installed */ savePrevInstalledSvApp: function(svApps, aNext) { this.saveTable(DB_SV_APP_STORE_NAME, svApps, null, false, aNext); }, /** * Fetches items from the database. * @param {Function} callback A function to call after fetching all items. */ fetch: function(callback) { var cached = {}; var collected = []; function iterator(value) { collected.push(value); } function iteratorSV(value) { /* jshint validthis: true */ this.applicationSource.addPreviouslyInstalledSvApp(value.manifestURL); } loadTable(DB_SV_APP_STORE_NAME, 'indexSV', iteratorSV.bind(this)); loadTable(DB_ITEM_STORE, 'index', iterator, finish.bind(this)); /** * Add to _allItems if the record is unique by comparing the identifier. */ function addIfUnique(item) { /* jshint validthis: true */ if (!cached[item.identifier]) { cached[item.identifier] = true; this._allItems.push(item); } } function finish() { /* jshint validthis: true */ // Transforms DB results into item classes for (var i = 0, iLen = collected.length; i < iLen; i++) { var thisItem = collected[i]; if (thisItem.type === 'app') { var itemObj = this.applicationSource.mapToApp(thisItem); addIfUnique.call(this, itemObj); } else if (thisItem.type === 'divider') { var divider = new GaiaGrid.Divider(thisItem); this._allItems.push(divider); } else if (thisItem.type === 'bookmark') { var bookmark = new GaiaGrid.Bookmark(thisItem); addIfUnique.call(this, bookmark); } } this.notifyReady(); if (callback && typeof callback === 'function') { callback(); } } }, /** * We have fetched data from our local database and displayed it, * but data inside of our application or bookmark store may be outdated. * We need to synchronize each source and delete/add records. */ synchronize: function() { this.sources.forEach(function eachSource(source) { source.synchronize(); }); }, /** * Initializes all sources. * @param {Function} callback The callback to fire after all sources init. */ initSources: function(callback) { var pending = this.sources.length; var allEntries = []; var self = this; var current = 0; function handleSource() { var source = self.sources[current]; current++; source.populate(next); } handleSource(); function next(entries) { allEntries = allEntries.concat(entries); if (!(--pending)) { callback(allEntries); } else { handleSource(); } } }, /** * Populates the database with the initial data. * @param {Function} callback Callback after database is populated. */ populate: function(callback) { this.initSources(function(entries) { this.deferredSave(entries, callback); }.bind(this)); }, /** * Notifies consumers that the database is ready for queries to be makde. */ notifyReady: function() { this.ready = true; dispatchEvent(new CustomEvent('databaseready')); }, /** * Gets the next available position in the grid */ getNextPosition: function() { var nextPosition = this.nextPosition; this.nextPosition++; return nextPosition; }, handleIAC: function(connectionRequest) { if (connectionRequest.keyword !== 'verticalhome-migrate') { return; } var port = connectionRequest.port; port.onmessage = (event) => { if (event.data.action !== 'migrate') { console.error('Unhandled migration action requested:', event.data.action); return; } this.migrate(event.data.manifestURL); }; port.start(); }, migrate: function(manifestURL) { this.migratingTo = manifestURL; console.info('verticalhome migrating to ' + this.migratingTo); if (this.migrating) { return; } this.migrating = true; (datastore ? Promise.resolve() : navigator.getDataStores(DATASTORE_NAME). then(stores => { datastore = stores[0]; return Promise.resolve(); }, (e) => { return Promise.reject(e); })). then(() => { this.all(entries => { this.saveMigrationData(entries, 'detail', true).then(() => { this.migrating = false; window.navigator.mozSettings.createLock().set({ 'homescreen.manifestURL': this.migratingTo }); navigator.getDataStores(PREFS_STORE).then( stores => { if (stores.length < 1) { console.error('Error opening verticalhome prefs datastore'); return; } stores[0].put('migrated', true).then(() => { window.close(); }, e => { console.error('Error storing migration pref', e); window.close(); }); }); }); }); }, e => { console.error('Error retrieving datastore', e); }); }, handleEvent: function(e) { switch(e.type) { case 'gaiagrid-cached-icons-rendered': this._renderedOnce = true; window.removeEventListener('gaiagrid-cached-icons-rendered', this); if (this._deferredSaveArgs.length) { this.save.apply(this, this._deferredSaveArgs); } break; } } }; exports.ItemStore = ItemStore; }(window));