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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
97
gaia/apps/fl/download.html
Executable file
97
gaia/apps/fl/download.html
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8" />
|
||||
<title>Locked Content</title>
|
||||
<link rel="stylesheet" href="shared/style/confirm.css" type="text/css" />
|
||||
<link rel="stylesheet" href="shared/style/progress_activity.css" type="text/css" />
|
||||
<link rel="stylesheet" href="style/download.css" type="text/css" />
|
||||
<!-- Localization -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="locales/download.{locale}.properties">
|
||||
<!-- Shared code -->
|
||||
<script type="text/javascript" src="shared/js/l10n.js"></script>
|
||||
<script defer src="shared/js/omadrm/fl.js"></script>
|
||||
<script defer src="shared/js/device_storage/get_unused_filename.js"></script>
|
||||
<script defer src="shared/js/device_storage/get_storage_if_available.js"></script>
|
||||
<script defer src="shared/js/media/media_utils.js"></script>
|
||||
<!-- Specific code -->
|
||||
<script defer src="js/constants.js"></script>
|
||||
<script defer src="js/utils.js"></script>
|
||||
<script defer src="js/db.js"></script>
|
||||
<script defer src="js/binary_string_view.js"></script>
|
||||
<script defer src="js/download.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="direct-download" hidden>
|
||||
<form role="dialog" data-type="confirm">
|
||||
<section>
|
||||
<h1 data-l10n-id="download-progress-title"></h1>
|
||||
<p style="text-align: center;">
|
||||
<progress id="direct-download-progress" class="pack-activity" value="0"></progress>
|
||||
</p>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div id="download-confirmation" hidden>
|
||||
<form role="dialog" data-type="confirm">
|
||||
<section>
|
||||
<h1 id="download-confirmation-title">download file?</h1>
|
||||
<progress id="download-progress" value="0"></progress>
|
||||
<dl>
|
||||
<dt data-l10n-id="download-confirmation-name"></dt>
|
||||
<dd id="download-confirmation-name"></dd>
|
||||
<dt data-l10n-id="download-confirmation-type"></dt>
|
||||
<dd id="download-confirmation-type"></dd>
|
||||
<dt data-l10n-id="download-confirmation-size"></dt>
|
||||
<dd id="download-confirmation-size"></dd>
|
||||
<dt data-l10n-id="download-confirmation-description"></dt>
|
||||
<dd id="download-confirmation-description"></dd>
|
||||
</dl>
|
||||
<p>
|
||||
<img id="locked-content-advisory-icon" src="style/images/actionicon_lock.png">
|
||||
<small id="locked-content-advisory-text"></small>
|
||||
</p>
|
||||
</section>
|
||||
<menu>
|
||||
<button id="download-cancel" type="button"
|
||||
data-l10n-id="cancel-button">cancel</button>
|
||||
<button id="download-confirm" type="button" class="recommend"
|
||||
data-l10n-id="download-button">download</button>
|
||||
</menu>
|
||||
</form>
|
||||
</div>
|
||||
<div id="preview-image-screen" hidden>
|
||||
<form role="dialog" data-type="confirm">
|
||||
<section>
|
||||
<h1 data-l10n-id="preview-image-title">preview image</h1>
|
||||
<p><div id="preview-image"></div></p>
|
||||
</section>
|
||||
<menu>
|
||||
<button id="preview-image-cancel" type="button"
|
||||
data-l10n-id="cancel-button">cancel</button>
|
||||
<button id="preview-image-install" type="button" disabled
|
||||
class="recommend"
|
||||
data-l10n-id="install-button">install</button>
|
||||
</menu>
|
||||
</form>
|
||||
</div>
|
||||
<div id="preview-audio-screen" hidden>
|
||||
<form role="dialog" data-type="confirm">
|
||||
<section>
|
||||
<h1 data-l10n-id="preview-audio-title">preview audio</h1>
|
||||
<p><audio id="preview-audio" autoplay controls></audio></p>
|
||||
</section>
|
||||
<menu>
|
||||
<button id="preview-audio-cancel" type="button"
|
||||
data-l10n-id="cancel-button">cancel</button>
|
||||
<button id="preview-audio-install" type="button" disabled
|
||||
class="recommend"
|
||||
data-l10n-id="install-button">install</button>
|
||||
</menu>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
114
gaia/apps/fl/js/binary_string_view.js
Executable file
114
gaia/apps/fl/js/binary_string_view.js
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
* BinaryStringView.js: a view class that treats an Array buffer as a binary
|
||||
* string and implements String methods for it. Invoke the constructor
|
||||
* on an ArrayBuffer, a Uint8Array, or a string.
|
||||
*/
|
||||
function BinaryStringView(data) {
|
||||
// If the constructor is invoked on a string, assume it is a binary string
|
||||
// and copy it into an Array buffer
|
||||
if (typeof data === 'string') {
|
||||
this.bytes = new Uint8Array(data.length);
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
this.bytes[i] = (data.charCodeAt(i) & 0xFF);
|
||||
}
|
||||
} else if (data instanceof Uint8Array) {
|
||||
this.bytes = data;
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
this.bytes = new Uint8Array(data);
|
||||
} else {
|
||||
throw TypeError();
|
||||
}
|
||||
|
||||
this.length = this.bytes.length;
|
||||
this.buffer = this.bytes.buffer;
|
||||
this.byteOffset = this.bytes.byteOffset;
|
||||
this.byteLength = this.bytes.byteLength;
|
||||
}
|
||||
|
||||
BinaryStringView.prototype.toString = function toString() {
|
||||
var s = '';
|
||||
for (var i = 0; i < this.length; i++) {
|
||||
s += String.fromCharCode(this.bytes[i]);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
BinaryStringView.prototype.toArrayBuffer = function toArrayBuffer() {
|
||||
return this.buffer.slice(this.byteOffset, this.byteOffset + this.byteLength);
|
||||
};
|
||||
|
||||
|
||||
BinaryStringView.prototype.charAt = function(i) {
|
||||
return String.fromCharCode(this.bytes[i]);
|
||||
};
|
||||
|
||||
BinaryStringView.prototype.slice = function slice(from, to) {
|
||||
if (to === undefined) {
|
||||
return new BinaryStringView(this.bytes.subarray(from));
|
||||
} else {
|
||||
return new BinaryStringView(this.bytes.subarray(from, to));
|
||||
}
|
||||
};
|
||||
|
||||
BinaryStringView.prototype.trim = function() {
|
||||
var start = 0, end = this.length - 1;
|
||||
var WS = /\s/;
|
||||
while (start <= end && WS.test(this.charAt(start))) {
|
||||
start++;
|
||||
}
|
||||
while (end >= start && WS.test(this.charAt(end))) {
|
||||
end--;
|
||||
}
|
||||
return this.slice(start, end + 1);
|
||||
};
|
||||
|
||||
BinaryStringView.prototype.indexOf = function indexOf(target, start) {
|
||||
if (typeof target === 'string') {
|
||||
target = new BinaryStringView(target);
|
||||
}
|
||||
|
||||
start = Math.min(start || 0, this.length);
|
||||
if (target.length === 0) {
|
||||
return start;
|
||||
}
|
||||
|
||||
var end = this.length - target.length;
|
||||
|
||||
var i = start;
|
||||
while (i <= end) {
|
||||
var j = 0;
|
||||
while (this.bytes[i + j] === target.bytes[j]) {
|
||||
if (++j === target.length) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
BinaryStringView.prototype.lastIndexOf = function indexOf(target, start) {
|
||||
if (typeof target === 'string') {
|
||||
target = new BinaryStringView(target);
|
||||
}
|
||||
|
||||
if (target.length === 0) {
|
||||
return Math.min(start || this.length, this.length);
|
||||
}
|
||||
|
||||
start = start || this.length - 1;
|
||||
|
||||
var i = start;
|
||||
while (i >= 0) {
|
||||
var j = 0;
|
||||
while (this.bytes[i + j] === target.bytes[j]) {
|
||||
if (++j === target.length) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
i--;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
127
gaia/apps/fl/js/constants.js
Executable file
127
gaia/apps/fl/js/constants.js
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
'use strict';
|
||||
/* exported
|
||||
DEBUG,
|
||||
DOWNLOAD_ERROR,
|
||||
ERR_BAD_DESCRIPTOR,
|
||||
ERR_DESCRIPTOR_DOWNLOAD_FAILED,
|
||||
ERR_TOO_BIG,
|
||||
ERR_UNSUPPORTED_TYPE,
|
||||
ERR_BAD_TYPE,
|
||||
ERR_CONTENT_DOWNLOAD_FAILED,
|
||||
ERR_BAD_DRM_MESSAGE,
|
||||
ERR_BAD_IMAGE,
|
||||
ERR_BAD_AUDIO,
|
||||
ERR_NO_SPACE,
|
||||
ERR_NO_SDCARD,
|
||||
ERR_SDCARD_IN_USE,
|
||||
ERR_DB_STORE_FAILURE,
|
||||
ERR_DS_SAVE_FAILURE,
|
||||
INSTALL_ERROR,
|
||||
MAX_DOWNLOAD_SIZE,
|
||||
MimeTypeAliases,
|
||||
OMADownloadStatus,
|
||||
RINGTONE,
|
||||
RINGTONE_KEY,
|
||||
RINGTONE_NAME_KEY,
|
||||
RINGTONE_ID_KEY,
|
||||
SONG,
|
||||
SUCCESS_RINGTONE,
|
||||
SUCCESS_SONG,
|
||||
SUCCESS_WALLPAPER,
|
||||
SupportedImageTypes,
|
||||
SupportedAudioTypes,
|
||||
systemXHR,
|
||||
WALLPAPER,
|
||||
WALLPAPER_KEY
|
||||
*/
|
||||
const DEBUG = true;
|
||||
|
||||
//
|
||||
// These status codes are defined in section 5.2 of
|
||||
// http://technical.openmobilealliance.org/Technical/release_program/docs/
|
||||
// Download/V1_0-20040625-A/OMA-Download-OTA-V1_0-20040625-A.pdf
|
||||
//
|
||||
const OMADownloadStatus = Object.freeze({
|
||||
SUCCESS: '900 Success',
|
||||
INSUFFICIENT_MEMORY: '901 Insufficient memory',
|
||||
USER_CANCELLED: '902 User Cancelled',
|
||||
LOSS_OF_SERVICE: '903 Loss of Service',
|
||||
ATTRIBUTE_MISMATCH: '905 Attribute mismatch',
|
||||
INVALID_DESCRIPTOR: '906 Invalid descriptor',
|
||||
INVALID_DDVERSION: '951 Invalid DDVersion',
|
||||
DEVICE_ABORTED: '952 Device Aborted',
|
||||
NON_ACCEPTABLE_CONTENT: '953 Non-Acceptable Content',
|
||||
LOADER_ERROR: '954 Loader Error'
|
||||
});
|
||||
|
||||
// These constants are used in calls to reportError.
|
||||
// Each one defines a localization key.
|
||||
const DOWNLOAD_ERROR = 'download_error';
|
||||
const INSTALL_ERROR = 'install_error';
|
||||
const ERR_DESCRIPTOR_DOWNLOAD_FAILED = // httpStatus parameter
|
||||
'err_descriptor_download_failed';
|
||||
const ERR_BAD_DESCRIPTOR = 'err_bad_descriptor';
|
||||
const ERR_TOO_BIG = 'err_too_big';
|
||||
const ERR_UNSUPPORTED_TYPE = 'err_unsupported_type'; // type parameter
|
||||
const ERR_BAD_TYPE = 'err_bad_type';
|
||||
const ERR_CONTENT_DOWNLOAD_FAILED = // httpStatus parameter
|
||||
'err_content_download_failed';
|
||||
const ERR_BAD_DRM_MESSAGE = 'err_bad_drm_message';
|
||||
const ERR_BAD_IMAGE = 'err_bad_image';
|
||||
const ERR_BAD_AUDIO = 'err_bad_audio';
|
||||
const ERR_NO_SPACE = 'err_no_space';
|
||||
const ERR_NO_SDCARD = 'err_no_sdcard';
|
||||
const ERR_SDCARD_IN_USE = 'err_sdcard_in_use';
|
||||
const ERR_DB_STORE_FAILURE = 'err_db_store_failure';
|
||||
const ERR_DS_SAVE_FAILURE = 'err_ds_save_failure';
|
||||
|
||||
// Success codes that define localization keys
|
||||
const SUCCESS_SONG = 'success_song';
|
||||
const SUCCESS_RINGTONE = 'success_ringtone';
|
||||
const SUCCESS_WALLPAPER = 'success_wallpaper';
|
||||
|
||||
// We pass this object to the XMLHttpRequest() constructor so we can
|
||||
// load things cross-origin. This only works because we have the systemXHR
|
||||
// permission.
|
||||
const systemXHR = Object.freeze({ mozSystem: true });
|
||||
|
||||
const SupportedImageTypes = Object.freeze({
|
||||
'image/jpeg': true,
|
||||
'image/png': true,
|
||||
'image/gif': true,
|
||||
'image/bmp': true
|
||||
});
|
||||
|
||||
const SupportedAudioTypes = Object.freeze({
|
||||
'audio/mpeg': true,
|
||||
'audio/mp4': true,
|
||||
'audio/opus': true,
|
||||
'audio/ogg': true
|
||||
});
|
||||
|
||||
// If we see one of the mime types on the left convert it
|
||||
// to the corresponding type on the right.
|
||||
// See normalizeMimeType in download.js
|
||||
const MimeTypeAliases = Object.freeze({
|
||||
'audio/mp3': 'audio/mpeg'
|
||||
});
|
||||
|
||||
// SettingsDB keys for ringtones and wallpaper
|
||||
const RINGTONE_KEY = 'dialer.ringtone';
|
||||
const RINGTONE_NAME_KEY = 'dialer.ringtone.name';
|
||||
const RINGTONE_ID_KEY = 'dialer.ringtone.id';
|
||||
const WALLPAPER_KEY = 'wallpaper.image';
|
||||
|
||||
// These three constants define the possible uses of the media we download.
|
||||
// Different values require different handling after download. See the
|
||||
const SONG = 'song';
|
||||
const RINGTONE = 'ringtone';
|
||||
const WALLPAPER = 'wallpaper';
|
||||
|
||||
// In our current implementation we download the .dm file as a typed array and
|
||||
// hold the entire value in memory so that we can easily extract the content
|
||||
// from the multipart message headers. This means that we have to guard
|
||||
// against out-of-memory errors. We do that by limiting locked content to
|
||||
// 16mb, which should be big enough for just about any song that a carrier
|
||||
// might want to sell.
|
||||
const MAX_DOWNLOAD_SIZE = 16 * 1024 * 1024;
|
||||
54
gaia/apps/fl/js/db.js
Executable file
54
gaia/apps/fl/js/db.js
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
'use strict';
|
||||
/* global indexedDB */
|
||||
/* exported objectStore */
|
||||
|
||||
var objectStore = (function() {
|
||||
const DBNAME = 'LOCKEDCONTENT';
|
||||
const DBVERSION = 1;
|
||||
const STORENAMES = ['ringtones', 'wallpapers'];
|
||||
var db;
|
||||
|
||||
function getStore(access, storeName, callback) {
|
||||
if (STORENAMES.indexOf(storeName) === -1) {
|
||||
throw Error('unknown object store name:', storeName);
|
||||
}
|
||||
|
||||
if (db) {
|
||||
callback(db.transaction(storeName, access).objectStore(storeName));
|
||||
}
|
||||
else {
|
||||
initDB(function() { getStore(access, storeName, callback); });
|
||||
}
|
||||
}
|
||||
|
||||
function initDB(callback) {
|
||||
var open = indexedDB.open(DBNAME, DBVERSION);
|
||||
|
||||
open.onsuccess = function() {
|
||||
db = open.result;
|
||||
callback();
|
||||
};
|
||||
|
||||
open.onupgradeneeded = function() {
|
||||
STORENAMES.forEach(function(storeName) {
|
||||
open.result.createObjectStore(storeName, {
|
||||
autoIncrement: true,
|
||||
keyPath: 'id'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
open.onerror = function() {
|
||||
console.error('can not open database:', open.error.name);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
readonly: function readonly(storeName, callback) {
|
||||
getStore('readonly', storeName, callback);
|
||||
},
|
||||
readwrite: function readwrite(storeName, callback) {
|
||||
getStore('readwrite', storeName, callback);
|
||||
}
|
||||
};
|
||||
}());
|
||||
907
gaia/apps/fl/js/download.js
Executable file
907
gaia/apps/fl/js/download.js
Executable file
|
|
@ -0,0 +1,907 @@
|
|||
/*
|
||||
* This is an implementation of the OMA Download specification:
|
||||
* http://technical.openmobilealliance.org/Technical/release_program/docs/Download/V1_0-20040625-A/OMA-Download-OTA-V1_0-20040625-A.pdf
|
||||
*
|
||||
* It downloads and installs locked wallpaper, ringtones and songs
|
||||
* roughly following these steps:
|
||||
*
|
||||
* 1) Download a download descriptor from the specified URL
|
||||
* - onerror: display a message to the user and abort the activity
|
||||
*
|
||||
* 2) Validate and parse the download descriptor
|
||||
* - extract all the required and optional tags from the descriptor document
|
||||
* - if it doesn't contain the tags we expect, display an error message
|
||||
* and abort the activity.
|
||||
* - if the version number is wrong, display an error, report the failure,
|
||||
* and abort the activity.
|
||||
*
|
||||
* 3) Check types
|
||||
* - If we can't handle any of the types specified in the descriptor,
|
||||
* report a failure to the server, display an error to the user and abort.
|
||||
* - For audio files, ask the user if they want to use them as a ringtone
|
||||
* or as a song
|
||||
*
|
||||
* 4) Confirm download and use of media
|
||||
* - Display name, vendor, type, size, etc. to the user, remind them
|
||||
* that the content will be locked (and that they may be charged for
|
||||
* it) and ask them to confirm that they want to download.
|
||||
* - If the user cancels, report that to the server and abort the activity.
|
||||
*
|
||||
* 5) Download content
|
||||
* - Display a progress meter and cancel button
|
||||
* - If the user clicks cancel, report to server and abort the activity.
|
||||
*
|
||||
* 6) Extract the content, if necessary
|
||||
* - If the content type is a DRM message, then it is in multi-part
|
||||
* MIME format and we have to extract the first part, using the boundary
|
||||
* string from the response headers.
|
||||
* - If extracting the content fails, notify the server and user and abort
|
||||
*
|
||||
* 7) Allow the user to preview the downloaded files
|
||||
* - if the file cannot be loaded or played, abort with an error
|
||||
* - otherwise, show a small image, or play part of the audio
|
||||
* to the user and allow them to click Cancel or Install.
|
||||
*
|
||||
* 8) Install the content
|
||||
* - For wallpapers and ringtones, save to the database
|
||||
* - For music, encrypt the file and save to device storage
|
||||
* - If this fails, report errors and abort
|
||||
* - Report success to the server
|
||||
* - For ringtones and wallpaper, set them as the default
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
/* global
|
||||
_,
|
||||
$,
|
||||
BinaryStringView,
|
||||
debug,
|
||||
DOWNLOAD_ERROR,
|
||||
ERR_BAD_AUDIO,
|
||||
ERR_BAD_DESCRIPTOR,
|
||||
ERR_BAD_DRM_MESSAGE,
|
||||
ERR_BAD_IMAGE,
|
||||
ERR_BAD_TYPE,
|
||||
ERR_CONTENT_DOWNLOAD_FAILED,
|
||||
ERR_DS_SAVE_FAILURE,
|
||||
ERR_DB_STORE_FAILURE,
|
||||
ERR_DESCRIPTOR_DOWNLOAD_FAILED,
|
||||
ERR_NO_SDCARD,
|
||||
ERR_NO_SPACE,
|
||||
ERR_SDCARD_IN_USE,
|
||||
ERR_TOO_BIG,
|
||||
ERR_UNSUPPORTED_TYPE,
|
||||
ForwardLock,
|
||||
getStorageIfAvailable,
|
||||
getUnusedFilename,
|
||||
INSTALL_ERROR,
|
||||
MAX_DOWNLOAD_SIZE,
|
||||
MediaUtils,
|
||||
MimeTypeAliases,
|
||||
objectStore,
|
||||
OMADownloadStatus,
|
||||
RINGTONE,
|
||||
RINGTONE_KEY,
|
||||
RINGTONE_NAME_KEY,
|
||||
RINGTONE_ID_KEY,
|
||||
showDialog,
|
||||
SONG,
|
||||
SUCCESS_RINGTONE,
|
||||
SUCCESS_SONG,
|
||||
SUCCESS_WALLPAPER,
|
||||
SupportedAudioTypes,
|
||||
SupportedImageTypes,
|
||||
systemXHR,
|
||||
WALLPAPER,
|
||||
WALLPAPER_KEY
|
||||
*/
|
||||
|
||||
// Wait until we're loaded, localized, and get an activity request
|
||||
window.addEventListener('load', function() {
|
||||
navigator.mozL10n.once(function() {
|
||||
navigator.mozSetMessageHandler('activity', view);
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
// This function is invoked with:
|
||||
//
|
||||
// * a view activity for MIME type application/vnd.oma.dd+xml and
|
||||
// activity.source.data.url set to the url of the download descriptor ".dd"
|
||||
// file.
|
||||
//
|
||||
// or
|
||||
//
|
||||
// * a view activity for MIME type application/vnd.oma.drm.message and
|
||||
// activity.source.data.url set to the url of the download ".dm" file.
|
||||
//
|
||||
function view(activity) {
|
||||
debug('view() invoked for activity', activity.source.name,
|
||||
'with data', JSON.stringify(activity.source.data));
|
||||
|
||||
var descriptor = {}; // The parsed form of the download descriptor
|
||||
var mimeType; // Set by checkType() to the primary media type
|
||||
var isImage = false; // These are also set by checkType()
|
||||
var isAudio = false;
|
||||
var installType; // Wallpaper, ringtone or song.
|
||||
var hasDescriptor; // To acommodate direct download without a descriptor
|
||||
|
||||
//We start the proccess
|
||||
// First a little bit of cleanup. Sometimes we get a content-type like
|
||||
// Content-type: a/type; boundary=somethingelse
|
||||
// And we don't really want anything after the ;
|
||||
var activityType = normalizeMimeType(activity.source.data.type);
|
||||
if (activityType === 'application/vnd.oma.drm.message') {
|
||||
hasDescriptor = false;
|
||||
|
||||
var url = (activity.source.data.url || '');
|
||||
descriptor.name = getFileNameFromURL(url);
|
||||
descriptor.size = 0;
|
||||
descriptor.types = ['application/vnd.oma.drm.message'];
|
||||
descriptor.objectURI = url;
|
||||
|
||||
displayDownloadProgress();
|
||||
} else {
|
||||
hasDescriptor = true;
|
||||
// We start by downloading the url passed with the activity request
|
||||
downloadDescriptor(activity.source.data.url);
|
||||
}
|
||||
|
||||
// We call this function on all mime type values we read from .dm or .dd
|
||||
// files or we get in XHR headers. This strips off trailing parameters,
|
||||
// converts to lower case, and fixes incorrect types (like audio/mp3).
|
||||
function normalizeMimeType(type) {
|
||||
// Strip off any mime type parameters
|
||||
type = type.split(';')[0];
|
||||
|
||||
// convert to lower case
|
||||
type = type.toLowerCase();
|
||||
|
||||
// Map bad types like "audio/mp3" to the correct type like "audio/mpeg"
|
||||
if (type in MimeTypeAliases) {
|
||||
type = MimeTypeAliases[type];
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
function askSongOrRingtone(aCallback) {
|
||||
showDialog({
|
||||
message: _('ringtone-or-song-question'),
|
||||
okText: _('ringtone-response'),
|
||||
cancelText: _('song-response'),
|
||||
okCallback: aCallback.bind(undefined, RINGTONE),
|
||||
cancelCallback: aCallback.bind(undefined, SONG)
|
||||
});
|
||||
}
|
||||
|
||||
function getFileNameFromURL(aUri) {
|
||||
var name = '';
|
||||
if (aUri) {
|
||||
name = aUri.split('/').pop();
|
||||
// Slice off the .dm suffix
|
||||
if (name.endsWith('.dm')) {
|
||||
name = name.slice(0, -3);
|
||||
}
|
||||
if (name.length > 18) {
|
||||
name = name.substring(0, 15) + '\u2026';
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Gets the filename from a header.
|
||||
function getFileNameFromHeader(aHeader) {
|
||||
// This is not exactly RFC compliant since it will cut
|
||||
// quoted-strings with quoted-pairs by the first "
|
||||
var regexp = /filename="([^"]*)"/i;
|
||||
var match = regexp.exec(aHeader);
|
||||
var name = match && match[1];
|
||||
if (name) {
|
||||
name = getFileNameFromURL(name);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function reportError(type, errorID, downloadStatus, details) {
|
||||
// Report to the console
|
||||
console.error(type, errorID, downloadStatus, details);
|
||||
|
||||
// Report to the server, if necessary
|
||||
if (downloadStatus) {
|
||||
sendStatus(downloadStatus);
|
||||
}
|
||||
|
||||
details = '[' + (details ? errorID + ': ' + details : errorID) + ']';
|
||||
|
||||
// Display error to the user
|
||||
showDialog({
|
||||
title: _(type) || type,
|
||||
message: _(errorID),
|
||||
details: details,
|
||||
okCallback: function() {
|
||||
// Cancel the activity after the user has seen the error message.
|
||||
activity.postError(errorID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reportSuccess(successID) {
|
||||
// This is a "well-intentioned attempt" to send the success code.
|
||||
sendStatus(OMADownloadStatus.success);
|
||||
|
||||
showDialog({
|
||||
message: _(successID) || successID,
|
||||
okCallback: function() {
|
||||
// Return from the activity after the user has seen the error message.
|
||||
activity.postResult({ success: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendStatus(status, callback) {
|
||||
// If the descriptor contained an installNotifyURI, then we need to
|
||||
// report errors by posting to that URL
|
||||
if (descriptor.installNotifyURI) {
|
||||
var xhr = new XMLHttpRequest(systemXHR);
|
||||
xhr.open('POST', descriptor.installNotifyURI);
|
||||
xhr.timeout = 1000; // wait up to a second for a response
|
||||
xhr.send(status);
|
||||
// If there is a callback, call it on success, error or timeout
|
||||
if (callback) {
|
||||
xhr.onloadend = function() { callback(); };
|
||||
}
|
||||
} else if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: download the download descriptor
|
||||
function downloadDescriptor(descriptorURL) {
|
||||
debug('Step 1: downloading descriptor from', descriptorURL);
|
||||
var xhr = new XMLHttpRequest(systemXHR);
|
||||
xhr.open('GET', descriptorURL);
|
||||
xhr.responseType = 'document'; // Parse it as an XML document
|
||||
xhr.send();
|
||||
|
||||
xhr.onerror = function(e) {
|
||||
reportError(DOWNLOAD_ERROR, ERR_DESCRIPTOR_DOWNLOAD_FAILED, null,
|
||||
xhr.status + ' ' + xhr.statusText);
|
||||
};
|
||||
|
||||
xhr.onload = function() {
|
||||
// When we get the descriptor, go to step 2
|
||||
// The setTimeout() may not be necessary here. The intent is to ensure
|
||||
// that this XHR is complete before we begin downloading the .dm file.
|
||||
setTimeout(function() {
|
||||
parse(xhr.response);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Parse and validate the download descriptor document
|
||||
//
|
||||
// The format of the download descriptor is defined in
|
||||
// http://technical.openmobilealliance.org/Technical/release_program/docs/
|
||||
// Download/V1_0-20040625-A/OMA-Download-OTA-V1_0-20040625-A.pdf
|
||||
//
|
||||
function parse(document) {
|
||||
|
||||
// Get all the types listed for this content
|
||||
var types = document.getElementsByTagName('type');
|
||||
if (types) {
|
||||
descriptor.types = [];
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
descriptor.types.push(normalizeMimeType(types[i].textContent.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
// A utility function for parsing the rest of the descriptor
|
||||
function get(tag) {
|
||||
var elts = document.getElementsByTagName(tag);
|
||||
if (elts && elts.length) {
|
||||
descriptor[tag] = elts[0].textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the other properties, ignoring nextURL, infoURL, iconURI,
|
||||
// and installParam (which are all optional)
|
||||
get('size');
|
||||
get('objectURI');
|
||||
get('installNotifyURI');
|
||||
get('DDVersion');
|
||||
get('name');
|
||||
get('description');
|
||||
get('vendor');
|
||||
|
||||
debug('Step 2: parse descriptor:', JSON.stringify(descriptor));
|
||||
|
||||
// If any of the required attributes are missing, then report an error
|
||||
if (!descriptor.types || descriptor.types.length === 0 ||
|
||||
!descriptor.size || !descriptor.objectURI) {
|
||||
reportError(DOWNLOAD_ERROR, ERR_BAD_DESCRIPTOR,
|
||||
OMADownloadStatus.INVALID_DESCRIPTOR);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the size is too big, just abort now so we don't run out of
|
||||
// memory when we download the .dm file later
|
||||
if (descriptor.size > MAX_DOWNLOAD_SIZE) {
|
||||
MediaUtils.getLocalizedSize(descriptor.size).then((msg) => {
|
||||
reportError(DOWNLOAD_ERROR, ERR_TOO_BIG,
|
||||
OMADownloadStatus.INSUFFICIENT_MEMORY,
|
||||
msg);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If the descriptor did not include a name, derive one from the URL
|
||||
if (!descriptor.name) {
|
||||
descriptor.name = getFileNameFromURL(descriptor.objectURI);
|
||||
}
|
||||
|
||||
// If the version number is too large, abort
|
||||
if (descriptor.DDVersion && parseInt(descriptor.DDVersion) > 1) {
|
||||
reportError(DOWNLOAD_ERROR, ERR_BAD_DESCRIPTOR,
|
||||
OMADownloadStatus.INVALID_DDVERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, move on to step 3
|
||||
checkTypes();
|
||||
}
|
||||
|
||||
// Step 3: check that we can handle the types listed in the download
|
||||
// descriptor and abort with an error if we can't
|
||||
function checkTypes() {
|
||||
debug('Step 3: check types');
|
||||
var types = descriptor.types;
|
||||
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
var type = types[i];
|
||||
|
||||
if (type === 'application/vnd.oma.drm.message') {
|
||||
// This type just indicates that the download will be a multi-part
|
||||
// MIME message body rather than the content directly.
|
||||
// We support it.
|
||||
continue;
|
||||
}
|
||||
|
||||
mimeType = type;
|
||||
if (type in SupportedImageTypes) {
|
||||
isImage = true;
|
||||
} else if (type in SupportedAudioTypes) {
|
||||
isAudio = true;
|
||||
} else {
|
||||
mimeType = undefined;
|
||||
reportError(DOWNLOAD_ERROR, ERR_UNSUPPORTED_TYPE,
|
||||
OMADownloadStatus.NON_ACCEPTABLE_CONTENT,
|
||||
type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We expect one media type plus one option DRM message ".dm" MIME
|
||||
// type. If we didn't see a media type at all, that is an
|
||||
// error. And if we had more than two types, then the content is
|
||||
// some kind of composite thing that we don't know how to handle.
|
||||
if (types.length < 1 || types.length > 2 || // wrong number of types
|
||||
(isAudio && isImage) || // both audio and image
|
||||
(!isAudio && !isImage)) { // neither audio nor image
|
||||
reportError(DOWNLOAD_ERROR, ERR_BAD_TYPE,
|
||||
OMADownloadStatus.NON_ACCEPTABLE_CONTENT);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an image, then we know we're downloading wallpaper
|
||||
// and can move on to the confirmation screen. For audio, we need
|
||||
// to ask the user if they want a ringtone or a song
|
||||
if (isImage) {
|
||||
installType = WALLPAPER;
|
||||
confirmDownload();
|
||||
} else {
|
||||
askSongOrRingtone(function(response) {
|
||||
installType = response;
|
||||
confirmDownload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Let the user confirm or cancel the download
|
||||
function confirmDownload() {
|
||||
debug('Step 4: confirm download');
|
||||
|
||||
// Customize the dialog title based on the installType
|
||||
var titleElement = $('download-confirmation-title');
|
||||
titleElement.textContent = _('download-confirmation-title-' + installType);
|
||||
|
||||
// Display details from the descriptor
|
||||
$('download-confirmation-name').textContent = descriptor.name;
|
||||
$('download-confirmation-type').textContent = mimeType;
|
||||
MediaUtils.getLocalizedSizeTokens(descriptor.size).then((args) => {
|
||||
navigator.mozL10n.setAttributes(
|
||||
$('download-confirmation-size'), 'fileSize', args);
|
||||
});
|
||||
// XXX: this might be too big to fit on the screen.
|
||||
$('download-confirmation-description').textContent = descriptor.description;
|
||||
|
||||
// Display the Locked Content Advisory
|
||||
var lca;
|
||||
if (descriptor.vendor) {
|
||||
lca = _('locked-content-advisory', {
|
||||
vendor: descriptor.vendor
|
||||
});
|
||||
} else {
|
||||
lca = _('locked-content-advisory-generic');
|
||||
}
|
||||
$('locked-content-advisory-text').textContent = lca;
|
||||
|
||||
// Hook up the Download button: if the user clicks it, go on to step 5
|
||||
$('download-confirm').onclick = function(e) {
|
||||
e.target.disabled = true; // Don't let the user click twice
|
||||
displayDownloadProgress();
|
||||
};
|
||||
|
||||
// Hook up the Cancel button: if the user clicks it, report this to
|
||||
// the server and abort without displaying an error message to the user.
|
||||
$('download-cancel').onclick = function(e) {
|
||||
debug('download cancelled');
|
||||
$('download-confirmation').hidden = true;
|
||||
sendStatus(OMADownloadStatus.USER_CANCELLED, function() {
|
||||
activity.postError('cancelled');
|
||||
});
|
||||
};
|
||||
|
||||
// Finally, make the confirmation screen visible
|
||||
$('download-confirmation').hidden = false;
|
||||
}
|
||||
|
||||
// Step 5: download the content and display progress
|
||||
function displayDownloadProgress() {
|
||||
debug('Step 5: download content and display progress');
|
||||
|
||||
var bar;
|
||||
var contentLength;
|
||||
if (!hasDescriptor) {
|
||||
$('direct-download').hidden = false;
|
||||
bar = $('direct-download-progress');
|
||||
// We might get or not the Content-Length header. We defer the max size
|
||||
// setting until the first progress call.
|
||||
bar.max = 100;
|
||||
} else {
|
||||
// Change the dialog title from Download? to Downloading...
|
||||
var titleElement = $('download-confirmation-title');
|
||||
titleElement.textContent = _('downloading-' + installType);
|
||||
$('download-confirmation').hidden = false;
|
||||
bar = $('download-progress');
|
||||
bar.max = descriptor.size;
|
||||
}
|
||||
|
||||
bar.style.visibility = 'visible';
|
||||
bar.value = 0;
|
||||
|
||||
|
||||
debug('starting download for', descriptor.objectURI);
|
||||
|
||||
|
||||
var download = new XMLHttpRequest(systemXHR);
|
||||
download.open('GET', descriptor.objectURI);
|
||||
download.responseType = 'arraybuffer';
|
||||
download.send();
|
||||
|
||||
download.onprogress = function(e) {
|
||||
if (contentLength === undefined) {
|
||||
contentLength = download.getResponseHeader('Content-Length');
|
||||
bar.max = contentLength && parseInt(contentLength) || bar.max;
|
||||
}
|
||||
|
||||
if (descriptor.size || contentLength) {
|
||||
bar.value = e.loaded;
|
||||
} else {
|
||||
// We didn't get a download size, let's just do a percent increase
|
||||
bar.value = (bar.value + 10) % bar.max;
|
||||
}
|
||||
};
|
||||
|
||||
// XXX This onerror function is being called with status 0 and statusText OK
|
||||
// It only happens sometimes. A caching thing? A bug in gecko?
|
||||
// There is no response body when this happends
|
||||
download.onerror = function() {
|
||||
$('download-confirmation').hidden = true;
|
||||
reportError(DOWNLOAD_ERROR, ERR_CONTENT_DOWNLOAD_FAILED,
|
||||
OMADownloadStatus.LOADER_ERROR,
|
||||
download.status + ' ' + download.statusText);
|
||||
};
|
||||
|
||||
download.onload = function() {
|
||||
// We're done downloading, so we can't cancel it anymore
|
||||
$('download-cancel').disabled = true;
|
||||
$('download-confirmation').hidden = true;
|
||||
|
||||
var cdHeader =
|
||||
download.getResponseHeader('Content-Disposition');
|
||||
|
||||
if (cdHeader && !hasDescriptor) {
|
||||
descriptor.name = getFileNameFromHeader(cdHeader) || descriptor.name;
|
||||
}
|
||||
|
||||
// Move on to step 6
|
||||
var type = normalizeMimeType(download.getResponseHeader('Content-Type'));
|
||||
extractContent(type, download.response);
|
||||
};
|
||||
|
||||
// Add another cancel button handler to abort the download if the
|
||||
// user clicks cancel
|
||||
$('download-cancel').addEventListener('click', function() {
|
||||
download.abort();
|
||||
});
|
||||
}
|
||||
|
||||
// Step 6: extract the downloaded content from the array buffer
|
||||
function extractContent(type, buffer) {
|
||||
debug('Step 6a: extractContent', type, buffer.byteLength);
|
||||
|
||||
if (type !== 'application/vnd.oma.drm.message') {
|
||||
// In this case, we already have the content and don't need to
|
||||
// extract anything. Move on to step 7
|
||||
previewContent(type, buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
// View the array buffer as if it was a binary string
|
||||
var dataString = new BinaryStringView(buffer);
|
||||
|
||||
// If we're here, then dataString is a multipart message with a
|
||||
// boundary string at the start and end. There should be only one part
|
||||
// but we still must strip off the separators and extract the real
|
||||
// content type. The type argument should specify the boundary string
|
||||
// but in practice it seems like it doesn't so we find it by examining
|
||||
// the end of the string.
|
||||
|
||||
// Make sure there are no trailing newlines
|
||||
dataString = dataString.trim();
|
||||
|
||||
function malformed(why) {
|
||||
if (why) {
|
||||
debug('malformed drm message:', why);
|
||||
}
|
||||
reportError(DOWNLOAD_ERROR, ERR_BAD_DRM_MESSAGE,
|
||||
OMADownloadStatus.ATTRIBUTE_MISMATCH);
|
||||
}
|
||||
|
||||
// The last two characters should be "--". If they are not, then
|
||||
// the content is malformed
|
||||
if (dataString.slice(-2).toString() !== '--') {
|
||||
return malformed('missing trailing --');
|
||||
}
|
||||
|
||||
// Find the last newline.
|
||||
var end = dataString.lastIndexOf('\r\n');
|
||||
if (end === -1) {
|
||||
return malformed('missing final \r\n');
|
||||
}
|
||||
|
||||
// The boundary string begins right after that newline
|
||||
var boundaryString = dataString.slice(end + 2, -2).toString() + '\r\n';
|
||||
dataString = dataString.slice(0, end);
|
||||
|
||||
debug('Boundary string is', boundaryString);
|
||||
|
||||
// The first occurance of the boundary string is the start of
|
||||
// the content headers
|
||||
var headerStart =
|
||||
dataString.indexOf(boundaryString) + boundaryString.length;
|
||||
if (headerStart === -1) {
|
||||
return malformed('missing boundary string at start');
|
||||
}
|
||||
|
||||
// If there are any more occurrances of the boundary string then
|
||||
// this is probably a .dm object for "combined delivery" and we
|
||||
// must reject it since we support only forward lock.
|
||||
if (dataString.indexOf(boundaryString, headerStart) !== -1) {
|
||||
return malformed('more than one part');
|
||||
}
|
||||
|
||||
var contentStart = dataString.indexOf('\r\n\r\n', headerStart);
|
||||
if (contentStart === -1) {
|
||||
return malformed('missing header separator');
|
||||
}
|
||||
|
||||
var headers = dataString.slice(headerStart,
|
||||
contentStart).toString().split('\r\n');
|
||||
var content = dataString.slice(contentStart + 4);
|
||||
|
||||
debug('got first part headers:', JSON.stringify(headers));
|
||||
|
||||
// find the content-type header for the content
|
||||
var contentType;
|
||||
for (var i = 0; i < headers.length; i++) {
|
||||
var header = headers[i].toLowerCase();
|
||||
if (header.startsWith('content-type:')) {
|
||||
contentType = normalizeMimeType(header.substring(13).trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
return malformed('no content-type header found in first part');
|
||||
}
|
||||
|
||||
debug('Found content in drm message file', contentType, content.length);
|
||||
previewContent(contentType, content.toArrayBuffer());
|
||||
}
|
||||
|
||||
// Step 7
|
||||
function previewContent(type, bytes) {
|
||||
if (!hasDescriptor) {
|
||||
mimeType = type;
|
||||
if (type in SupportedImageTypes) {
|
||||
isImage = true;
|
||||
} else if (type in SupportedAudioTypes) {
|
||||
isAudio = true;
|
||||
} else {
|
||||
mimeType = undefined;
|
||||
reportError(DOWNLOAD_ERROR, ERR_UNSUPPORTED_TYPE,
|
||||
OMADownloadStatus.NON_ACCEPTABLE_CONTENT,
|
||||
type);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// If the content we found in the DRM message (.dm) file has a different
|
||||
// Content-Type than what we were expecting based on the .dd file
|
||||
// report an error.
|
||||
if (type !== mimeType) {
|
||||
reportError(DOWNLOAD_ERROR, ERR_BAD_DRM_MESSAGE,
|
||||
OMADownloadStatus.ATTRIBUTE_MISMATCH);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
installType = WALLPAPER;
|
||||
previewImage(type, bytes);
|
||||
} else {
|
||||
if (!hasDescriptor) {
|
||||
askSongOrRingtone(function(response) {
|
||||
installType = response;
|
||||
previewAudio(type, bytes);
|
||||
});
|
||||
} else {
|
||||
// We already asked how to install the audio before
|
||||
// downloading
|
||||
previewAudio(type, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function previewImage(type, bytes) {
|
||||
// Hide the confirmation screen and show the preview screen
|
||||
$('download-confirmation').hidden = true;
|
||||
$('preview-image-screen').hidden = false;
|
||||
|
||||
// Load the image into an off-screen image
|
||||
var blob = new Blob([bytes], { type: type });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var image = new Image();
|
||||
image.src = url;
|
||||
|
||||
// If we can't load the image, abort with an error
|
||||
image.onerror = function() {
|
||||
URL.revokeObjectURL(url);
|
||||
reportError(INSTALL_ERROR, ERR_BAD_IMAGE,
|
||||
OMADownloadStatus.NON_ACCEPTABLE_CONTENT);
|
||||
return;
|
||||
};
|
||||
|
||||
// If the image loads successfully, use it as the background image
|
||||
// for the preview. And enable the install button
|
||||
image.onload = function() {
|
||||
var preview = $('preview-image');
|
||||
preview.style.backgroundImage = 'url(' + url + ')';
|
||||
$('preview-image-install').disabled = false;
|
||||
};
|
||||
|
||||
// If the user clicks Install, move on to step 8
|
||||
$('preview-image-install').onclick = function() {
|
||||
URL.revokeObjectURL(url);
|
||||
installContent(bytes);
|
||||
};
|
||||
|
||||
// If the user clicks cancel, return to the browser without installing
|
||||
$('preview-image-cancel').onclick = function() {
|
||||
URL.revokeObjectURL(url);
|
||||
sendStatus(OMADownloadStatus.USER_CANCELLED, function() {
|
||||
activity.postError('cancelled');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function previewAudio(type, bytes) {
|
||||
// Hide the confirmation screen and show the preview screen
|
||||
$('download-confirmation').hidden = true;
|
||||
$('preview-audio-screen').hidden = false;
|
||||
|
||||
// Create a blob and a blob: url that we can play
|
||||
var blob = new Blob([bytes], { type: type });
|
||||
var url = URL.createObjectURL(blob);
|
||||
|
||||
var player = $('preview-audio');
|
||||
player.src = url;
|
||||
|
||||
// If we can't play the audio, abort with an error
|
||||
player.onerror = function() {
|
||||
cleanup();
|
||||
reportError(INSTALL_ERROR,
|
||||
ERR_BAD_AUDIO, OMADownloadStatus.NON_ACCEPTABLE_CONTENT);
|
||||
return;
|
||||
};
|
||||
|
||||
// If we can play the music, enable the Install button
|
||||
player.oncanplay = function() {
|
||||
$('preview-audio-install').disabled = false;
|
||||
};
|
||||
|
||||
// Don't allow the user to play more than 50% of the audio, or
|
||||
// 5 seconds, whichever is longer
|
||||
player.ontimeupdate = player.ondurationchange = function() {
|
||||
if (player.currentTime > Math.max(5, player.duration / 2)) {
|
||||
player.pause();
|
||||
player.currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// If the user clicks Install, move on to step 8
|
||||
$('preview-audio-install').onclick = function() {
|
||||
cleanup();
|
||||
installContent(bytes);
|
||||
};
|
||||
|
||||
// If the user clicks cancel, return to the browser without installing
|
||||
$('preview-audio-cancel').onclick = function() {
|
||||
cleanup();
|
||||
sendStatus(OMADownloadStatus.USER_CANCELLED, function() {
|
||||
activity.postError('cancelled');
|
||||
});
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
player.pause();
|
||||
player.removeAttribute('src');
|
||||
player.load();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8
|
||||
function installContent(bytes) {
|
||||
switch (installType) {
|
||||
case SONG:
|
||||
installSong(bytes);
|
||||
return;
|
||||
case RINGTONE:
|
||||
case WALLPAPER:
|
||||
install(installType, bytes, descriptor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function installSong(buffer) {
|
||||
debug('installSong');
|
||||
ForwardLock.getOrCreateKey(function(secret) {
|
||||
debug('got secret key');
|
||||
var blob = ForwardLock.lockBuffer(secret, buffer, mimeType, {
|
||||
vendor: descriptor.vendor,
|
||||
name: descriptor.name,
|
||||
description: descriptor.description
|
||||
});
|
||||
debug('encrypted song');
|
||||
|
||||
getStorageIfAvailable('music', blob.size,
|
||||
storageSuccess, storageError);
|
||||
|
||||
function storageError(err) {
|
||||
debug('storageError:', err);
|
||||
if (err === 'unavailable') {
|
||||
reportError(INSTALL_ERROR, ERR_NO_SDCARD,
|
||||
OMADownloadStatus.INSUFFICIENT_MEMORY);
|
||||
} else if (err === 'shared') {
|
||||
reportError(INSTALL_ERROR, ERR_SDCARD_IN_USE,
|
||||
OMADownloadStatus.INSUFFICIENT_MEMORY);
|
||||
} else {
|
||||
reportError(INSTALL_ERROR, ERR_NO_SPACE,
|
||||
OMADownloadStatus.INSUFFICIENT_MEMORY, err);
|
||||
}
|
||||
}
|
||||
|
||||
function storageSuccess(storage) {
|
||||
debug('got music storage');
|
||||
var filename = 'locked/';
|
||||
if (descriptor.vendor) {
|
||||
filename += fixPath(descriptor.vendor) + '/';
|
||||
}
|
||||
filename += fixPath(descriptor.name) + '.lcka';
|
||||
debug('trying filename', filename);
|
||||
|
||||
// Remove characters in s that might be significant in a filename
|
||||
function fixPath(s) { return s.replace(/[\/\\\.\,\?\*\:]/g, ''); }
|
||||
|
||||
getUnusedFilename(storage, filename, function(filename) {
|
||||
debug('using filename', filename);
|
||||
var addreq = storage.addNamed(blob, filename);
|
||||
|
||||
addreq.onerror = function() {
|
||||
// We don't expect this to fail, since we've already checked
|
||||
// available storage space.
|
||||
reportError(INSTALL_ERROR, ERR_DS_SAVE_FAILURE,
|
||||
OMADownloadStatus.LOADER_ERROR,
|
||||
addreq.error.name);
|
||||
};
|
||||
|
||||
addreq.onsuccess = function() {
|
||||
// The song is installed and ready to be played, so
|
||||
// send a success notification, tell the user that the
|
||||
// song was installed and return to the invoking application.
|
||||
reportSuccess(SUCCESS_SONG);
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function install(installType, buffer, descriptor) {
|
||||
// We don't need the secret key here, but we need to create one
|
||||
// if it doesn't already exist
|
||||
ForwardLock.getOrCreateKey();
|
||||
|
||||
var objectStoreName = installType === RINGTONE ? 'ringtones' : 'wallpapers';
|
||||
|
||||
var blob = new Blob([buffer], { type: mimeType });
|
||||
objectStore.readwrite(objectStoreName, function(store) {
|
||||
var req = store.add({ blob: blob, descriptor: descriptor });
|
||||
|
||||
req.onerror = function() {
|
||||
reportError(INSTALL_ERROR, ERR_DB_STORE_FAILURE,
|
||||
OMADownloadStatus.LOADER_ERROR,
|
||||
req.error.name);
|
||||
};
|
||||
|
||||
req.onsuccess = function() {
|
||||
// Report that we've successfully saved the media.
|
||||
sendStatus(OMADownloadStatus.SUCCESS);
|
||||
|
||||
// Install this blob as the default ringtone or wallpaper
|
||||
// We assume that if the user is downloading this media they
|
||||
// want to use it right away. Otherwise we could put a checkbox
|
||||
// on the confirmation screen.
|
||||
var lock = navigator.mozSettings.createLock();
|
||||
var settings = {};
|
||||
if (installType === RINGTONE) {
|
||||
settings[RINGTONE_KEY] = blob;
|
||||
settings[RINGTONE_NAME_KEY] = descriptor.name;
|
||||
settings[RINGTONE_ID_KEY] = req.result;
|
||||
}
|
||||
else {
|
||||
settings[WALLPAPER_KEY] = blob;
|
||||
}
|
||||
|
||||
lock.set(settings).onsuccess = function() {
|
||||
var msgid;
|
||||
if (installType === RINGTONE) {
|
||||
msgid = SUCCESS_RINGTONE;
|
||||
} else {
|
||||
msgid = SUCCESS_WALLPAPER;
|
||||
}
|
||||
|
||||
showDialog({
|
||||
message: _(msgid),
|
||||
okCallback: function() {
|
||||
// Return from the activity after the user has seen the message.
|
||||
activity.postResult({ success: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
193
gaia/apps/fl/js/pick.js
Executable file
193
gaia/apps/fl/js/pick.js
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
'use strict';
|
||||
/* global
|
||||
_,
|
||||
$,
|
||||
ForwardLock,
|
||||
objectStore,
|
||||
RINGTONE_NAME_KEY
|
||||
*/
|
||||
|
||||
// Wait until we're loaded, localized, and get an activity request
|
||||
window.addEventListener('load', function() {
|
||||
navigator.mozL10n.once(function() {
|
||||
navigator.mozSetMessageHandler('activity', function(activity) {
|
||||
var type = activity.source.data.type;
|
||||
if (type === 'ringtone' ||
|
||||
(Array.isArray(type) && type.indexOf('ringtone') !== -1)) {
|
||||
pickRingtone(activity);
|
||||
}
|
||||
else if (type === 'wallpaper' ||
|
||||
(Array.isArray(type) && type.indexOf('wallpaper') !== -1)) {
|
||||
pickWallpaper(activity);
|
||||
}
|
||||
else {
|
||||
console.error('unexpected activity request',
|
||||
activity.source.name,
|
||||
JSON.stringify(activity.source.data));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function pickRingtone(activity) {
|
||||
var selectedRingtone;
|
||||
var selectedRingtoneID;
|
||||
var player = new Audio(); // for previewing sounds
|
||||
var currentRingtoneName;
|
||||
var numRingtones = 0;
|
||||
var container = $('ringtones');
|
||||
var header = $('header');
|
||||
container.hidden = false;
|
||||
|
||||
// Display the right title
|
||||
$('done').hidden = false;
|
||||
$('title').textContent = _('pick-ringtone');
|
||||
|
||||
// Make the cancel button work
|
||||
header.addEventListener('action', function() {
|
||||
player.pause(); // Stop any currently playing sound.
|
||||
activity.postError('cancelled');
|
||||
});
|
||||
|
||||
// Make the done button work
|
||||
$('done').onclick = function() {
|
||||
player.pause(); // Stop any currently playing sound.
|
||||
|
||||
// Lock the ringtone and return it as the activity result
|
||||
ForwardLock.getKey(function(secret) {
|
||||
ForwardLock.lockBlob(secret, selectedRingtone.blob, {},
|
||||
function(lockedBlob) {
|
||||
activity.postResult({
|
||||
blob: lockedBlob,
|
||||
name: selectedRingtone.descriptor.name,
|
||||
id: selectedRingtoneID
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getCurrentRingtoneName(function(name) {
|
||||
currentRingtoneName = name;
|
||||
enumerateAndBuildUI();
|
||||
});
|
||||
|
||||
function getCurrentRingtoneName(callback) {
|
||||
navigator.mozSettings
|
||||
.createLock()
|
||||
.get(RINGTONE_NAME_KEY)
|
||||
.onsuccess = function(e) {
|
||||
callback(e.target.result[RINGTONE_NAME_KEY]);
|
||||
};
|
||||
}
|
||||
|
||||
function enumerateAndBuildUI() {
|
||||
// Enumerate the ringtones from the database and display them onscreen
|
||||
objectStore.readonly('ringtones', function(ringtoneStore) {
|
||||
var cursor = ringtoneStore.openCursor();
|
||||
cursor.onsuccess = function() {
|
||||
if (cursor.result) {
|
||||
addRingtone(cursor.result.value, 'forwardlock:' + cursor.result.key);
|
||||
cursor.result.continue();
|
||||
}
|
||||
else { // we reached the end of the enumeration
|
||||
if (numRingtones === 0) {
|
||||
displayError(activity, 'no-installed-ringtones');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addRingtone(ringtone, id) {
|
||||
numRingtones++;
|
||||
var name = ringtone.descriptor.name;
|
||||
var listItem = document.createElement('li');
|
||||
|
||||
var radio = document.createElement('gaia-radio');
|
||||
radio.name = 'ringtone';
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.textContent = name;
|
||||
radio.appendChild(label);
|
||||
|
||||
if (name === currentRingtoneName) {
|
||||
selectedRingtone = ringtone;
|
||||
selectedRingtoneID = id;
|
||||
radio.checked = true;
|
||||
}
|
||||
|
||||
radio.addEventListener('change', function() {
|
||||
if (radio.checked) {
|
||||
selectedRingtone = ringtone;
|
||||
selectedRingtoneID = id;
|
||||
play(ringtone);
|
||||
}
|
||||
});
|
||||
|
||||
listItem.appendChild(radio);
|
||||
container.appendChild(listItem);
|
||||
}
|
||||
|
||||
function play(ringtone) {
|
||||
// We create blob urls as needed. Since this app is always short-lived
|
||||
// we don't bother releasing them.
|
||||
if (!ringtone.url) {
|
||||
ringtone.url = URL.createObjectURL(ringtone.blob);
|
||||
}
|
||||
player.src = ringtone.url;
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
|
||||
function pickWallpaper(activity) {
|
||||
var numWallpapers = 0;
|
||||
var template = $('wallpaper-template');
|
||||
var container = $('wallpapers');
|
||||
container.hidden = false;
|
||||
|
||||
$('title').textContent = _('pick-wallpaper');
|
||||
$('header').addEventListener('action', function() {
|
||||
activity.postError('cancelled');
|
||||
});
|
||||
|
||||
// Enumerate the wallpapers from the database and display them onscreen
|
||||
objectStore.readonly('wallpapers', function(wallpaperStore) {
|
||||
var cursor = wallpaperStore.openCursor();
|
||||
cursor.onsuccess = function() {
|
||||
if (cursor.result) {
|
||||
addWallpaper(cursor.result.value);
|
||||
numWallpapers++;
|
||||
cursor.result.continue();
|
||||
}
|
||||
else { // we reached the end of the enumeration
|
||||
if (numWallpapers === 0) {
|
||||
displayError(activity, 'no-installed-wallpaper');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function addWallpaper(wallpaper) {
|
||||
var blob = wallpaper.blob;
|
||||
var url = URL.createObjectURL(blob);
|
||||
var wallpaperEl = template.content.cloneNode(true).firstElementChild;
|
||||
container.appendChild(wallpaperEl);
|
||||
wallpaperEl.style.backgroundImage = 'url(' + url + ')';
|
||||
wallpaperEl.onclick = function() {
|
||||
ForwardLock.getKey(function(secret) {
|
||||
ForwardLock.lockBlob(secret, blob, {}, function(lockedBlob) {
|
||||
activity.postResult({ blob: lockedBlob });
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function displayError(activity, id) {
|
||||
// using setTimeout here so we don't block with alert() while
|
||||
// a db transaction is still pending
|
||||
setTimeout(function() {
|
||||
alert(_(id));
|
||||
activity.postError(id);
|
||||
});
|
||||
}
|
||||
106
gaia/apps/fl/js/utils.js
Executable file
106
gaia/apps/fl/js/utils.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
'use strict';
|
||||
|
||||
/* global DEBUG */
|
||||
/* exported
|
||||
$,
|
||||
debug,
|
||||
showDialog */
|
||||
|
||||
function debug(...args) {
|
||||
if (!DEBUG) {
|
||||
return;
|
||||
}
|
||||
if (!debug.startTime) {
|
||||
debug.startTime = Date.now();
|
||||
}
|
||||
args.unshift('[Locked Content]', Date.now() - debug.startTime);
|
||||
console.log.apply(console, args);
|
||||
}
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function _(id, args) {
|
||||
return navigator.mozL10n.get(id, args);
|
||||
}
|
||||
|
||||
// Show a dialog with one or two buttons
|
||||
function showDialog(options) {
|
||||
|
||||
function close(e) {
|
||||
document.body.removeChild(dialog);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
var cancelButton;
|
||||
var okButton;
|
||||
|
||||
var dialog = document.createElement('form');
|
||||
dialog.setAttribute('role', 'dialog');
|
||||
dialog.dataset.type = 'confirm';
|
||||
var section = document.createElement('section');
|
||||
dialog.appendChild(section);
|
||||
|
||||
// Display an optional title
|
||||
if (options.title) {
|
||||
var title = document.createElement('h1');
|
||||
title.textContent = options.title;
|
||||
section.appendChild(title);
|
||||
}
|
||||
|
||||
// Display the dialog message
|
||||
var msg = document.createElement('p');
|
||||
msg.textContent = options.message;
|
||||
section.appendChild(msg);
|
||||
|
||||
// If there are details, display those in a separate, smaller paragraph
|
||||
if (options.details) {
|
||||
var details = document.createElement('p');
|
||||
var small = document.createElement('small');
|
||||
small.textContent = options.details;
|
||||
details.appendChild(small);
|
||||
section.appendChild(details);
|
||||
}
|
||||
|
||||
// Now the buttons
|
||||
var menu = document.createElement('menu');
|
||||
dialog.appendChild(menu);
|
||||
|
||||
// Add the cancel button if there is one
|
||||
if (options.cancelCallback) {
|
||||
cancelButton = document.createElement('button');
|
||||
menu.appendChild(cancelButton);
|
||||
cancelButton.textContent = options.cancelText || _('cancel');
|
||||
cancelButton.onclick = function(e) {
|
||||
close(e);
|
||||
options.cancelCallback();
|
||||
};
|
||||
}
|
||||
|
||||
if (options.okCallback) {
|
||||
okButton = document.createElement('button');
|
||||
menu.appendChild(okButton);
|
||||
okButton.textContent = options.okText || _('ok');
|
||||
if (options.danger) {
|
||||
okButton.classList.add('danger'); // scary red color
|
||||
}
|
||||
else if (!options.cancelCallback) {
|
||||
okButton.classList.add('recommend'); // default safe color
|
||||
}
|
||||
okButton.onclick = function(e) {
|
||||
close(e);
|
||||
options.okCallback();
|
||||
};
|
||||
}
|
||||
|
||||
// If there is only one button, make it full-size
|
||||
if (okButton && !cancelButton) {
|
||||
okButton.classList.add('full');
|
||||
}
|
||||
if (cancelButton && !okButton) {
|
||||
cancelButton.classList.add('full');
|
||||
}
|
||||
|
||||
// show the dialog
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
18
gaia/apps/fl/jsdoc.json
Executable file
18
gaia/apps/fl/jsdoc.json
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "fl",
|
||||
"tags": {
|
||||
"dictionaries": ["jsdoc"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["apps/fl/js/**/*.js"]
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown",
|
||||
"plugins/summarize"
|
||||
],
|
||||
"opts": {
|
||||
"private": true,
|
||||
"recurse": true,
|
||||
"destination": "docs/forwardlock"
|
||||
}
|
||||
}
|
||||
63
gaia/apps/fl/locales/download.en-US.properties
Executable file
63
gaia/apps/fl/locales/download.en-US.properties
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
ok = OK
|
||||
download-button = Download
|
||||
install-button = Install
|
||||
cancel-button = Cancel
|
||||
|
||||
download_error = Download Error
|
||||
install_error = Installation Error
|
||||
|
||||
err_descriptor_download_failed = The download failed because of a server error.
|
||||
err_bad_descriptor = The download failed because of a server error.
|
||||
err_too_big = File is too large to download.
|
||||
err_unsupported_type = This type of purchased media is not supported on your phone.
|
||||
err_bad_type = The download failed because of a server error.
|
||||
err_content_download_failed = The download failed because of a server error.
|
||||
err_bad_drm_message = The download failed because of a server error.
|
||||
err_bad_image = The downloaded image cannot be displayed on this device.
|
||||
err_bad_audio = The downloaded file cannot be played on this device.
|
||||
err_no_space = There is not enough storage space on your device to save this song.
|
||||
err_no_sdcard = Cannot save song because there is no memory card.
|
||||
err_sdcard_in_use = Cannot save song because the memory card is in use. Unplug your phone and try downloading again.
|
||||
err_ds_save_failure = Error saving media file.
|
||||
err_db_store_failure = Error saving media file.
|
||||
|
||||
success_song = Song downloaded and installed. Use the Music app to play it.
|
||||
success_ringtone = Ringtone downloaded and installed.
|
||||
success_wallpaper = Wallpaper downloaded and installed.
|
||||
|
||||
ringtone-or-song-question = Do you want to install this audio file as a song or as a ringtone?
|
||||
ringtone-response = Ringtone
|
||||
song-response = Song
|
||||
|
||||
download-confirmation-title-song = Download song?
|
||||
download-confirmation-title-ringtone = Download ringtone?
|
||||
download-confirmation-title-wallpaper = Download wallpaper?
|
||||
|
||||
downloading-song = Downloading song…
|
||||
downloading-ringtone = Downloading ringtone…
|
||||
downloading-wallpaper = Downloading wallpaper…
|
||||
|
||||
locked-content-advisory = {{vendor}} has locked this media file so that it cannot be shared with others or transferred to a new device.
|
||||
|
||||
# This version is like the above, but used when we don't know the name of
|
||||
# the vendor.
|
||||
locked-content-advisory-generic = The seller has locked this media file so that it cannot be shared with others or transferred to a new device.
|
||||
|
||||
download-confirmation-name = Name
|
||||
download-confirmation-type = Type
|
||||
download-confirmation-size = Size
|
||||
download-confirmation-vendor = Vendor
|
||||
download-confirmation-description = Info
|
||||
|
||||
download-progress-title = Downloading
|
||||
|
||||
preview-image-title = Preview image
|
||||
preview-audio-title = Preview audio
|
||||
|
||||
byteUnit-B = bytes
|
||||
byteUnit-KB = KB
|
||||
byteUnit-MB = MB
|
||||
byteUnit-GB = GB
|
||||
|
||||
# Localization note (fileSize): {{size}} is a floating number with 2 decimal places
|
||||
fileSize = {{size}} {{unit}}
|
||||
6
gaia/apps/fl/locales/pick.en-US.properties
Executable file
6
gaia/apps/fl/locales/pick.en-US.properties
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
done = Done
|
||||
|
||||
pick-ringtone = Select Ringtone
|
||||
pick-wallpaper = Select Wallpaper
|
||||
no-installed-ringtones = No Locked Ringtones Installed
|
||||
no-installed-wallpaper = No Locked Wallpaper Installed
|
||||
74
gaia/apps/fl/manifest.webapp
Executable file
74
gaia/apps/fl/manifest.webapp
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"name": "Purchased Media",
|
||||
"description": "Download and manage forward locked music, ringtones and wallpaper",
|
||||
"type": "certified",
|
||||
"role": "system",
|
||||
"developer": {
|
||||
"name": "The Gaia Team",
|
||||
"url": "https://github.com/mozilla-b2g/gaia"
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"name": "Purchased Media",
|
||||
"description": "Download and manage forward locked music, ringtones and wallpaper"
|
||||
},
|
||||
"en-US": {
|
||||
"name": "Purchased Media",
|
||||
"description": "Download and manage forward locked music, ringtones and wallpaper"
|
||||
},
|
||||
"fr": {
|
||||
"name": "Médias achetés",
|
||||
"description": "Télécharger et gérer les chansons, sonneries et fonds d’écran verrouillés"
|
||||
},
|
||||
"zh-TW": {
|
||||
"name": "Purchased Media",
|
||||
"description": "Download and manage forward locked music, ringtones and wallpaper"
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"activities": {
|
||||
"view": {
|
||||
"filters": {
|
||||
"type": {
|
||||
"value": [
|
||||
"application/vnd.oma.dd+xml",
|
||||
"application/vnd.oma.drm.message"
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"disposition": "inline",
|
||||
"href": "download.html",
|
||||
"returnValue": true
|
||||
},
|
||||
"pick": {
|
||||
"filters": {
|
||||
"type": [
|
||||
"wallpaper",
|
||||
"ringtone"
|
||||
],
|
||||
"includeLocked": {
|
||||
"required": true,
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"disposition": "inline",
|
||||
"href": "pick.html",
|
||||
"returnValue": true
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"themeable": {},
|
||||
"systemXHR": {},
|
||||
"settings": {
|
||||
"access": "readwrite"
|
||||
},
|
||||
"device-storage:music": {
|
||||
"access": "readwrite"
|
||||
},
|
||||
"moz-extremely-unstable-and-will-change-webcomponents": {}
|
||||
},
|
||||
"icons": {
|
||||
"60": "/style/icons/60/fl.png"
|
||||
}
|
||||
}
|
||||
10
gaia/apps/fl/package.json
Executable file
10
gaia/apps/fl/package.json
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"webide": {
|
||||
"prepackage": {
|
||||
"command": "make",
|
||||
"env": ["APP=fl"],
|
||||
"cwd": "../.."
|
||||
},
|
||||
"packageDir": "../../build_stage/fl/"
|
||||
}
|
||||
}
|
||||
46
gaia/apps/fl/pick.html
Executable file
46
gaia/apps/fl/pick.html
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8" />
|
||||
<title>Locked Content</title>
|
||||
|
||||
<!-- Web Components -->
|
||||
<script defer src="shared/elements/config.js"></script>
|
||||
<script defer src="/shared/js/component_utils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" />
|
||||
<link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css" />
|
||||
<script defer src="shared/elements/gaia-header/dist/gaia-header.js"></script>
|
||||
<script defer src="/shared/elements/gaia_radio/script.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="style/pick.css" type="text/css" />
|
||||
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="locales/pick.{locale}.properties">
|
||||
|
||||
<script type="text/javascript" src="shared/js/l10n.js"></script>
|
||||
<script defer src="shared/js/omadrm/fl.js"></script>
|
||||
|
||||
<script defer src="js/constants.js"></script>
|
||||
<script defer src="js/utils.js"></script>
|
||||
<script defer src="js/db.js"></script>
|
||||
<script defer src="js/pick.js"></script>
|
||||
</head>
|
||||
<body class="theme-media">
|
||||
<section role="region">
|
||||
<gaia-header ignore-dir id="header" action="close">
|
||||
<h1 id="title"></h1>
|
||||
<button id="done" data-l10n-id="done" hidden>Done</button>
|
||||
</gaia-header>
|
||||
</section>
|
||||
<ul id="ringtones" hidden>
|
||||
</ul>
|
||||
<div id="wallpapers" hidden>
|
||||
<template id="wallpaper-template">
|
||||
<!-- Set style.backgroundImage when instantiating -->
|
||||
<div class="wallpaper"></div>
|
||||
</template>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
49
gaia/apps/fl/style/download.css
Executable file
49
gaia/apps/fl/style/download.css
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
* { -moz-box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#download-confirmation {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#download-progress {
|
||||
width: calc(100% - 3rem);
|
||||
margin: .5rem 1.5rem .5rem 1.5rem;
|
||||
background-color: black;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#preview-image {
|
||||
width: 18rem;
|
||||
height: 27rem;
|
||||
margin: 0 auto 0 auto;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
border: solid white 2px;
|
||||
}
|
||||
|
||||
#preview-audio {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
BIN
gaia/apps/fl/style/icons/60/fl.png
Executable file
BIN
gaia/apps/fl/style/icons/60/fl.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
gaia/apps/fl/style/images/actionicon_lock.png
Executable file
BIN
gaia/apps/fl/style/images/actionicon_lock.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 325 B |
BIN
gaia/apps/fl/style/images/actionicon_lock@1.5x.png
Executable file
BIN
gaia/apps/fl/style/images/actionicon_lock@1.5x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 482 B |
BIN
gaia/apps/fl/style/images/actionicon_lock@2x.png
Executable file
BIN
gaia/apps/fl/style/images/actionicon_lock@2x.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 592 B |
125
gaia/apps/fl/style/pick.css
Executable file
125
gaia/apps/fl/style/pick.css
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 10px;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* for wallpaper */
|
||||
#wallpapers {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
top: 5rem;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--background, #26272c);
|
||||
}
|
||||
|
||||
.wallpaper {
|
||||
background-size:cover;
|
||||
background-repeat: no-repeat;
|
||||
border-bottom: 0.1rem solid #000000;
|
||||
border-right: 0.1rem solid #000000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 15.9rem;
|
||||
float: left;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
#wallpapers .wallpaper:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* for ringtones */
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul li {
|
||||
-moz-box-sizing: border-box;
|
||||
position: relative;
|
||||
min-height: 6rem;
|
||||
margin: 0 1.5rem;
|
||||
border-bottom: 0.1rem solid #e6e6e3;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
ul li label:not([for]) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
ul li > a,
|
||||
ul li > span {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
color: #000;
|
||||
-moz-box-sizing: border-box;
|
||||
height: 6rem;
|
||||
font-size: 1.9rem;
|
||||
line-height: 6rem;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
ul li.disabled > a {
|
||||
color: #797e80;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* text ellipsis */
|
||||
ul li > * {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* Boolean inputs
|
||||
*/
|
||||
|
||||
gaia-radio label {
|
||||
/* TODO: Currently this app uses the media theme, which expects a dark list and light labels.
|
||||
That would make the label invisible, so instead we need to use !important to override the scoped style of the component. */
|
||||
color: #222 !important;
|
||||
}
|
||||
|
||||
/* Setting panels: full-size <section role="region"> blocks */
|
||||
section[role="region"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: url(images/document_bg.png);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers should not scroll with the rest of the page
|
||||
*/
|
||||
section[role="region"] > header {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#ringtones {
|
||||
position: absolute;
|
||||
top: 5rem;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: calc(100% - 5rem);
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
27
gaia/apps/fl/test/build/integration/fl_test.js
Executable file
27
gaia/apps/fl/test/build/integration/fl_test.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
/* jshint node: true, mocha: true */
|
||||
/* global suiteSetup */
|
||||
|
||||
var helper = require('helper');
|
||||
|
||||
suite('FL tests', function() {
|
||||
suiteSetup(helper.cleanupWorkspace);
|
||||
teardown(helper.cleanupWorkspace);
|
||||
|
||||
test('APP=fl make', function(done) {
|
||||
helper.exec('APP=fl make', function(error, stdout, stderr) {
|
||||
helper.checkError(error, stdout, stderr);
|
||||
|
||||
var zipPath = process.cwd() +
|
||||
'/profile/webapps/fl.gaiamobile.org/application.zip';
|
||||
var checkList = ['manifest.webapp'];
|
||||
|
||||
checkList.forEach(function(filePath) {
|
||||
helper.checkFilePathInZip(zipPath, filePath);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
17
gaia/apps/fl/test/unit/.jshintrc
Executable file
17
gaia/apps/fl/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../../.jshintrc",
|
||||
"predef": [
|
||||
"assert",
|
||||
"suite",
|
||||
"test",
|
||||
"setup",
|
||||
"teardown",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"require",
|
||||
"requireApp",
|
||||
"sinon",
|
||||
"mocha",
|
||||
"loadBodyHTML"
|
||||
]
|
||||
}
|
||||
70
gaia/apps/fl/test/unit/binary_string_view_test.js
Executable file
70
gaia/apps/fl/test/unit/binary_string_view_test.js
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
/* global BinaryStringView */
|
||||
|
||||
requireApp('fl/js/binary_string_view.js');
|
||||
|
||||
suite('BinaryStringView tests', function() {
|
||||
var testStrings = [
|
||||
'abcabcabc',
|
||||
' abcabcabc',
|
||||
'abcabcabc ',
|
||||
' abcabcabc ',
|
||||
' \r\n\t abcabcabc \r\n\t ',
|
||||
''
|
||||
];
|
||||
|
||||
testStrings.forEach(function(s) {
|
||||
test('String "' + s + '"', function() {
|
||||
var bsv = new BinaryStringView(s);
|
||||
assert.equal(s.length, bsv.length, 'length');
|
||||
assert.equal(s, bsv.toString(), 'toString');
|
||||
assert.equal(s.trim(), bsv.trim().toString(), 'trim');
|
||||
|
||||
assert.equal(s.slice(0), bsv.slice(0).toString(), 'slice(0)');
|
||||
assert.equal(s.slice(1), bsv.slice(1).toString(), 'slice(1)');
|
||||
assert.equal(s.slice(-1), bsv.slice(-1).toString(), 'slice(-1)');
|
||||
assert.equal(s.slice(-2), bsv.slice(-2).toString(), 'slice(-2)');
|
||||
assert.equal(s.slice(1, 2), bsv.slice(1, 2).toString(), 'slice(1,2)');
|
||||
assert.equal(s.slice(1, -1), bsv.slice(1, -1).toString(), 'slice(1,-1)');
|
||||
|
||||
assert.equal(s.indexOf(''), bsv.indexOf(''), 'indexOf("")');
|
||||
assert.equal(s.indexOf('', 1), bsv.indexOf('', 1), 'indexOf("",1)');
|
||||
assert.equal(s.indexOf('a'), bsv.indexOf('a'), 'indexOf(a)');
|
||||
assert.equal(s.indexOf('a', 2), bsv.indexOf('a', 2), 'indexOf(a,2)');
|
||||
assert.equal(s.indexOf('bca'), bsv.indexOf('bca'), 'indexOf(bca)');
|
||||
assert.equal(s.indexOf('bca', 6), bsv.indexOf('bca', 6),
|
||||
'indexOf(bca,6)');
|
||||
assert.equal(s.indexOf('\r\n'), bsv.indexOf('\r\n'), 'indexOf(\\r\\n)');
|
||||
assert.equal(s.indexOf('\r\n', 8), bsv.indexOf('\r\n', 8),
|
||||
'indexOf(\\r\\n, 8)');
|
||||
assert.equal(s.indexOf('abcabcabc'), bsv.indexOf('abcabcabc'),
|
||||
'indexOf(abcabcabc)');
|
||||
assert.equal(s.indexOf(s), bsv.indexOf(s), 'indexOf(s)');
|
||||
assert.equal(s.indexOf(s, 3), bsv.indexOf(s, 3), 'indexOf(s, 3)');
|
||||
|
||||
assert.equal(s.lastIndexOf(''), bsv.lastIndexOf(''),
|
||||
'lastIndexOf("")');
|
||||
assert.equal(s.lastIndexOf('', 1), bsv.lastIndexOf('', 1),
|
||||
'lastIndexOf("",1)');
|
||||
assert.equal(s.lastIndexOf('a'), bsv.lastIndexOf('a'),
|
||||
'lastIndexOf(a)');
|
||||
assert.equal(s.lastIndexOf('a', 2), bsv.lastIndexOf('a', 2),
|
||||
'lastIndexOf(a,2)');
|
||||
assert.equal(s.lastIndexOf('bca'), bsv.lastIndexOf('bca'),
|
||||
'lastIndexOf(bca)');
|
||||
assert.equal(s.lastIndexOf('bca', 6), bsv.lastIndexOf('bca', 6),
|
||||
'lastIndexOf(bca,6)');
|
||||
assert.equal(s.lastIndexOf('\r\n'), bsv.lastIndexOf('\r\n'),
|
||||
'lastIndexOf(\\r\\n)');
|
||||
assert.equal(s.lastIndexOf('\r\n', 8), bsv.lastIndexOf('\r\n', 8),
|
||||
'lastIndexOf(\\r\\n, 8)');
|
||||
|
||||
assert.equal(s.lastIndexOf('abcabcabc'), bsv.lastIndexOf('abcabcabc'),
|
||||
'lastIndexOf(abcabcabc)');
|
||||
assert.equal(s.lastIndexOf(s), bsv.lastIndexOf(s),
|
||||
'lastIndexOf(s)');
|
||||
assert.equal(s.lastIndexOf(s, 3), bsv.lastIndexOf(s, 3),
|
||||
'lastIndexOf(s, 3)');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue