VulpesOS/gaia/apps/ringtones/js/custom_ringtones.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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
};
})();