Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
393 lines
11 KiB
JavaScript
Executable file
393 lines
11 KiB
JavaScript
Executable file
/* global IDBKeyRange, indexedDB, Promise */
|
|
'use strict';
|
|
|
|
/*
|
|
* customRingtones is an object that provides access to the user-created
|
|
* ringtones on the device. It has the following methods:
|
|
*
|
|
* add():
|
|
* Add a new ringtone to the database.
|
|
*
|
|
* @param {Object} info An object describing the new ringtone. Should contain
|
|
* the following properties: name, subtitle (optional), and blob.
|
|
* @return {Promise} A promise returning the created CustomRingtone.
|
|
*
|
|
* remove():
|
|
* Remove a ringtone from the database.
|
|
*
|
|
* @param {String} id The ID of the ringtone to delete.
|
|
* @return {Promise} A promise signalling completion of the removal.
|
|
*
|
|
* clear():
|
|
* Remove all ringtones from the database. This function is primarily meant
|
|
* for unit tests.
|
|
*
|
|
* @return {Promise} A promise signalling completion of the removal.
|
|
*
|
|
* list():
|
|
* Lists all the custom ringtones.
|
|
*
|
|
* @param {String} toneType The type of tone to list ('ringtone' or
|
|
* 'alerttone').
|
|
* @return {Promise} A promise returning an array of CustomRingtones
|
|
* representing each tone.
|
|
*
|
|
* get():
|
|
* Gets a ringtone with the specified id.
|
|
*
|
|
* @param {String} id The ID of the ringtone to get.
|
|
* @return {Promise} A promise returning the requested ringtone.
|
|
*/
|
|
window.customRingtones = (function() {
|
|
var ID_PREFIX = 'custom:';
|
|
|
|
/**
|
|
* Create a new custom ringtone object.
|
|
*
|
|
* @param {Object} info An object containing info about the ringtone. Contains
|
|
* the following members:
|
|
* {String} name The name of the tone.
|
|
* {String} subtitle (optional) The subtitle for the tone (e.g. the artist
|
|
* of a song).
|
|
* {Number} uniqueNum The "unique" number for the tone, to disambiguate it
|
|
* from other tones with the same name and subtitle.
|
|
* {Boolean} isExplicitNum True if the "unique" number was explicitly
|
|
* specified in the original name of the tone, false if it was inferred
|
|
* for de-duplication purposes.
|
|
* @param {Number} dbKey The database key of the tone.
|
|
*/
|
|
function CustomRingtone(info, dbKey) {
|
|
this._name = info.name;
|
|
if (info.uniqueNum !== 0 || info.isExplicitNum) {
|
|
this._name += ' (' + info.uniqueNum + ')';
|
|
}
|
|
this._subtitle = info.subtitle || null;
|
|
this._id = ID_PREFIX + dbKey;
|
|
this._blob = info.blob;
|
|
}
|
|
|
|
CustomRingtone.prototype = {
|
|
/**
|
|
* @return {String} The name of the ringtone.
|
|
*/
|
|
get name() {
|
|
return this._name;
|
|
},
|
|
|
|
/**
|
|
* @return {String} The filename of the ringtone.
|
|
*/
|
|
get filename() {
|
|
// XXX: If we ever start supporting Blobs that aren't really files, we'll
|
|
// need to figure out a way to make a fallback filename!
|
|
return this._blob.name;
|
|
},
|
|
|
|
/**
|
|
* @return {String} The subtitle of the ringtone (e.g. the artist of the
|
|
* song), or null if there is no subtitle.
|
|
*/
|
|
get subtitle() {
|
|
return this._subtitle;
|
|
},
|
|
|
|
/**
|
|
* @return {String} A unique ID for the ringtone.
|
|
*/
|
|
get id() {
|
|
return this._id;
|
|
},
|
|
|
|
/**
|
|
* @return {String} The type of the ringtone. XXX: Currently always
|
|
* "ringtone".
|
|
*/
|
|
get type() {
|
|
return 'ringtone';
|
|
},
|
|
|
|
/**
|
|
* @return {String} A URL pointing to the ringtone's audio data.
|
|
*/
|
|
get url() {
|
|
// Lazily get the URL for the blob, but once we've done it once, we don't
|
|
// need to use the getter again. XXX: We could be smarter here and have a
|
|
// way of revoking URLs when we're done with them.
|
|
return this._url || (this._url = URL.createObjectURL(this._blob));
|
|
},
|
|
|
|
/**
|
|
* @return {Boolean} Whether this ringtone is shareable (always true).
|
|
*/
|
|
get shareable() {
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* @return {Boolean} Whether this ringtone is deletable (always true).
|
|
*/
|
|
get deletable() {
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Delete this ringtone from the database.
|
|
*
|
|
* @return A promise returning upon completion of the deletion.
|
|
*/
|
|
remove: function() {
|
|
return remove(this.id);
|
|
},
|
|
|
|
/**
|
|
* Gets a blob for this ringtone.
|
|
*
|
|
* @return {Promise} A promise returning a Blob of the audio data.
|
|
*/
|
|
getBlob: function() {
|
|
return Promise.resolve(this._blob);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convert a CustomRingtone ID to its corresponding DB key.
|
|
*
|
|
* @param {String} id The ID.
|
|
* @return {Number} The DB key.
|
|
*/
|
|
function idToDBKey(id) {
|
|
if (id.indexOf(ID_PREFIX) !== 0) {
|
|
throw new Error('invalid id: ' + id);
|
|
}
|
|
return parseInt(id.substr(ID_PREFIX.length));
|
|
}
|
|
|
|
var DBNAME = 'customTones';
|
|
var DBVERSION = 1;
|
|
// Eventually, we'll probably want a separate store for alert tones.
|
|
var STORENAME = 'ringtones';
|
|
var db = null;
|
|
|
|
function withStore(type, callback) {
|
|
if (db) {
|
|
var transaction = db.transaction(STORENAME, type);
|
|
callback(
|
|
null, transaction.objectStore(STORENAME), transaction
|
|
);
|
|
} else {
|
|
var openreq = indexedDB.open(DBNAME, DBVERSION);
|
|
openreq.onerror = function() {
|
|
console.error('customRingtones: can\'t open database:',
|
|
openreq.error.name);
|
|
callback(openreq.error);
|
|
};
|
|
openreq.onupgradeneeded = function() {
|
|
var objStore = openreq.result.createObjectStore(
|
|
STORENAME, { autoIncrement: true }
|
|
);
|
|
objStore.createIndex(
|
|
'fullname', ['name', 'subtitle', 'uniqueNum'], { unique: false }
|
|
);
|
|
};
|
|
openreq.onsuccess = function() {
|
|
db = openreq.result;
|
|
var transaction = db.transaction(STORENAME, type);
|
|
callback(
|
|
null, transaction.objectStore(STORENAME), transaction
|
|
);
|
|
};
|
|
}
|
|
}
|
|
|
|
// This function gives us a unique number for our ringtones in case the name
|
|
// and subtitle are the same. We try to be a bit "magical", and if the name
|
|
// we got had an explicit number in parens at the end, we'll try incrementing
|
|
// that number instead of adding a second number. See also the beginning of
|
|
// add() below.
|
|
function uniqueify(info) {
|
|
return new Promise(function(resolve, reject) {
|
|
withStore('readonly', function(err, store) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
var nextUniqueNum = info.uniqueNum || 0;
|
|
var index = store.index('fullname');
|
|
|
|
var req = index.openCursor(
|
|
IDBKeyRange.bound(
|
|
[info.name, info.subtitle, 0],
|
|
[info.name, info.subtitle, Infinity]
|
|
)
|
|
);
|
|
req.onsuccess = function(event) {
|
|
var cursor = event.target.result;
|
|
if (!cursor || nextUniqueNum < cursor.value.uniqueNum) {
|
|
resolve(nextUniqueNum);
|
|
return;
|
|
}
|
|
nextUniqueNum = cursor.value.uniqueNum + 1;
|
|
cursor.continue();
|
|
};
|
|
req.onerror = function() {
|
|
console.error(
|
|
'Error in customRingtones.uniqueify(): ', req.error.name
|
|
);
|
|
reject(req.error);
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
function add(info) {
|
|
var cleanedInfo = {
|
|
name: info.name || '',
|
|
subtitle: info.subtitle || '',
|
|
blob: info.blob,
|
|
uniqueNum: 0
|
|
};
|
|
var m = /^(.*) \((\d+)\)$/.exec(cleanedInfo.name);
|
|
if (m) {
|
|
cleanedInfo.name = m[1];
|
|
cleanedInfo.uniqueNum = m[2];
|
|
cleanedInfo.isExplicitNum = true;
|
|
}
|
|
|
|
return uniqueify(cleanedInfo).then(function(uniqueNum) {
|
|
return new Promise(function(resolve, reject) {
|
|
withStore('readwrite', function(err, store, transaction) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
cleanedInfo.uniqueNum = uniqueNum;
|
|
var tone;
|
|
var req = store.add(cleanedInfo);
|
|
req.onsuccess = function(event) {
|
|
tone = new CustomRingtone(cleanedInfo, event.target.result);
|
|
};
|
|
transaction.oncomplete = function(event) {
|
|
resolve(tone);
|
|
};
|
|
transaction.onabort = transaction.onerror = function() {
|
|
console.error('Error in customRingtones.add(): ',
|
|
transaction.error.name);
|
|
reject(req.error);
|
|
};
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function remove(id) {
|
|
return new Promise(function(resolve, reject) {
|
|
withStore('readwrite', function(err, store, transaction) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
var req = store.delete(idToDBKey(id));
|
|
transaction.oncomplete = function(event) {
|
|
resolve();
|
|
};
|
|
transaction.onabort = transaction.onerror = function() {
|
|
console.error('Error in customRingtones.remove(): ',
|
|
transaction.error.name);
|
|
reject(req.error);
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
function clear() {
|
|
return new Promise(function(resolve, reject) {
|
|
withStore('readwrite', function(err, store, transaction) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
var req = store.clear();
|
|
transaction.oncomplete = function(event) {
|
|
resolve();
|
|
};
|
|
transaction.onabort = transaction.onerror = function() {
|
|
console.error('Error in customRingtones.clear(): ',
|
|
transaction.error.name);
|
|
reject(req.error);
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
function get(id) {
|
|
return new Promise(function(resolve, reject) {
|
|
withStore('readonly', function(err, store) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
var key = idToDBKey(id);
|
|
var req = store.get(key);
|
|
req.onsuccess = function(event) {
|
|
var data = event.target.result;
|
|
if (!data) {
|
|
resolve(null);
|
|
} else {
|
|
resolve(new CustomRingtone(data, key));
|
|
}
|
|
};
|
|
req.onerror = function() {
|
|
console.error('Error in customRingtones.get(): ', req.error.name);
|
|
reject(req.error);
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
function list(toneType) {
|
|
return new Promise(function(resolve, reject) {
|
|
// XXX: We should actually handle alert tones eventually!
|
|
if (toneType === 'alerttone') {
|
|
resolve([]);
|
|
return;
|
|
}
|
|
|
|
withStore('readonly', function(err, store) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
var req = store.openCursor();
|
|
var results = [];
|
|
req.onsuccess = function listOnSuccess(event) {
|
|
var cursor = event.target.result;
|
|
if (cursor) {
|
|
var value = cursor.value;
|
|
results.push(new CustomRingtone(value, cursor.key));
|
|
cursor.continue();
|
|
} else {
|
|
resolve(results);
|
|
}
|
|
};
|
|
req.onerror = function listOnError() {
|
|
console.error('Error in customRingtones.list(): ', req.error.name);
|
|
reject(req.error);
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
return {
|
|
add: add,
|
|
remove: remove,
|
|
clear: clear,
|
|
get: get,
|
|
list: list
|
|
};
|
|
})();
|