1. Introduction
While the preceding article discussed Chrome extensions, this article focuses on developing Chrome apps. The two topics have a lot in common, and the difference between them may seem subtle at first. It helps to clarify three terms:
- Software application — Software intended to help a user perform an activity, such as processing text, editing an image, or playing a game. Usually has a dedicated user interface.
- Web app — A software application that runs inside a browser (an application inside an application)
- Browser extension — Software that customizes the browsing experience or augments a browser's capabilities. Rarely has a user interface.
A Chrome application (usually just app) is a web app intended to run in the Chrome browser. To a user, it looks and feels like a regular web app, but it can access a wealth of features that regular apps cannot. The drawback is that Chrome apps can only run in Chrome.
From a developer's perspective, the difference between Chrome apps and extensions is clear. In an extension, content scripts provide access to web documents and popups provide a basic user interface. Apps don't have content scripts or popups, but the user interface is much more powerful—an app can run in a window separate from the browser.
In addition to the user interface, another advantage of apps over extensions involves low-level access to the user's system. Chrome apps can access hardware capabilities including Bluetooth, TCP/UDP sockets, and the universal serial bus (USB). They can also read and write to the user's filesystem. Personally, my favorite feature is tts (text-to-speech), which allows an app to generate speech in a number of different languages.
This article explains how to code apps that create windows, access the user's filesystem, and generate speech from text. But before delving into the code, it's important to understand the structure of a Chrome app and the manner in which it can be launched in the browser. These topics are discussed in the next section.
2. Fundamentals of App Development
The file structure of a Chrome app is nearly identical to that of an extension. There are three fundamental characteristics to be aware of:
- An app's features and capabilities must be defined in a file called manifest.json.
- An app may have background scripts, but no content scripts or popups.
- Like extensions, apps can be managed through the Chrome Developer Dashboard at the URL chrome://extensions.
This section starts by discussing the format of manifest.json. Then it walks through the process of installing and launching a simple app.
2.1 manifest.json
An app's manifest accepts many of the same properties as an extension's. These include manifest_version
, name
, description
, version
, icons
, and permissions
. The primary difference is that an app's manifest must have a property named app
. This contains a background
property that associates a scripts
field with an array containing one or more background scripts.
If you download the example archive for this article, you'll find a folder called basic_app. The manifest.json for this app is easy to understand:
{
"manifest_version": 2,
"name": "Trivial app",
"description": "This app doesn't do anything",
"version": "1.0",
"icons": { "16": "images/cog_sm.png",
"48": "images/cog_med.png",
"128": "images/cog_lg.png" },
"app": {
"background": {
"scripts": ["scripts/background.js"]
}
}
}
This should look familiar, as most of the properties have the same names and values as the extensions presented in the preceding article. Remember that the scripts
array must name at least one background script. The rest of this article discusses many features that an app's background script can access.
2.2 Loading and Launching Apps
If you open Chrome and visit chrome://apps, you'll see all of the apps that can be executed. But the page won't let you install new apps. To manage apps, you need to visit the Chrome Developer Dashboard discussed in the preceding article. The URL is chrome://extensions, and if the Developer mode box is checked, the top of the page will look as shown in Figure 1.
Figure 1: The Chrome Developer Dashboard in Developer Mode
On the left, the Load unpacked extension... button makes it possible to select a folder and load an app into Chrome. The best way to understand this process is to walk through an example.
The example_code.zip archive attached to this article contains a folder called basic_app. To load this app into Chrome, follow these four steps:
- Download example_code.zip and decompress the archive.
- In Chrome, open the Chrome Developer Dashboard by navigating to chrome://extensions.
- Press the Load unpacked extension... button and select the basic_app folder.
- Press OK to load the app.
When the extension is loaded, an entry will be added to the dashboard's list of extensions. Figure 2 shows what it looks like.
Figure 2: New Entry in the Chrome Developer Dashboard
It's important to notice the Launch link. Unlike extensions, apps don't start executing when they're installed. An app must be specifically launched.
The Reload link reloads the app into Chrome. At the bottom, the background page link opens the app's background page. This is helpful when you want to view the console associated with a background script.
3. chrome.app.runtime and chrome.app.window
A major difference between apps and extensions is that apps can execute in their own windows. To create these windows, an app must call functions from two APIs: chrome.app.runtime
and chrome.app.window
. The first API makes it possible to respond to events in the app's lifecycle. The second API provides functions that create and configure windows.
3.1 chrome.app.runtime
The chrome.app.runtime
API (not to be confused with the chrome.runtime
API) defines three events. Table 1 lists each of them.
Table 1: Events of chrome.app.runtime
Event | Trigger |
onLaunched | Fires when the app is executed |
onRestarted | Fires when the app is restarted |
onEmbedRequested | Fires when another app seeks to embed the app |
Each of these has a function called addListener
. This accepts a callback function to be invoked when the event is triggered. For example, the following code defines an anonymous function to be invoked when the app is launched:
chrome.app.runtime.onLaunched.addListener(function(...) {
...
});
The arguments received by an event's callback function depend on the type of event.
3.2 chrome.app.window
To define its user interface, an app can create one or more windows. In code, an app's window is represented by an AppWindow
object. The functions of the chrome.app.window
API make it possible to create and manage AppWindow
s, and they're listed in Table 2.
Table 2: Functions of chrome.app.window
Function | Description |
create(string url,
CreateWindowOptions opts,
function callback) | Creates a new AppWindow |
current() | Returns the current AppWindow |
get(string id) | Returns the AppWindow with the given name |
getAll() | Returns an array containing all AppWindow s |
canSetVisibleOnAllWorkspace() | Identifies whether the platform supports displaying
windows on all workspaces |
create
is the main function to know. Its only required argument is the first, which identifies the HTML file that defines the window's structure and appearance. The third argument is a callback function that receives the newly-created AppWindow
.
The second argument of create
is a CreateWindowOptions
object. This has five optional properties:
id
— a string that uniquely identifies the window (no relation to the app's ID) outerBounds
— A BoundsSpecification
that sets the window's position and size innerBounds
— A BoundsSpecification
that sets the position and size of the window's content frame
— Frame type: none
or chrome
(the default is chrome
) state
— The window's initial state: normal
, fullscreen
, maximized
, or minimized
.
The second and third properties are BoundsSpecification
s. A BoundsSpecification
has eight properties that define position and size: left
, top
, width
, height
, minWidth
, minHeight
, maxWidth
, and maxHeight
. For example, the following code creates a 200x200 AppWindow
that can't be resized less than 100x100 pixels or greater than 300x300 pixels:
chrome.app.window.create("window.html",
{ "outerBounds": {"width": 200, "height": 200,
"minWidth": 100, "minHeight": 100,
"maxWidth": 300, "maxHeight": 300}},
function(win) {
...
}
);
As shown in this code, the third argument of create
is a callback function that receives the AppWindow
after it's created. Table 3 lists the properties of an AppWindow
object.
Table 3: AppWindow Properties
Property | Description |
id | The window's ID, if available |
outerBounds | The window's bounds |
innerBounds | The bounds of the window's inner content |
contentWindow | The corresponding JavaScript window object |
fullscreen() | Resizes the window to fill the screen |
isFullscreen() | Identifies whether the window occupies the screen |
minimize() | Minimizes the window size |
isMinimized() | Identifies whether the window is minimized |
maximize() | Maximizes the window size |
restore() | Return the window to its original size/position |
drawAttention() | Draw attention to the window |
clearAttention() | Clear attention from the window |
close() | Close the window |
show() | Show the window |
hide() | Hide the window |
setAlwaysOnTop
(boolean onTop) | Configures whether the window should always
be displayed on top of others |
isAlwaysOnTop() | Identifies whether the window is always on top |
setInterceptAllKeys
(boolean intercept) | Configures whether the window should receive
all key events |
setVisibleOnAllWorkspaces
(boolean visible) | Configures whether the window should be
displayed on all workspaces |
These properties are straightforward to use and understand. The contentWindow
property is interesting because it provides access to the underlying JavaScript
Window
. This allows the background script to access common JavaScript Window
properties like document
, setTimeout()
, and alert()
.
In addition to providing the functions in Table 3, AppWindow
provides events that enable scripts to respond to the window's events. Table 4 lists these events and the occurrences that trigger them.
Table 4: AppWindow Events
Event | Trigger |
onBoundsChanged | Fires when the window's bounds change |
onClosed | Fires when the window is closed |
onFullscreened | Fires when the window is set to occupy the screen |
onMaximized | Fires when the window is maximized |
onMinimized | Fires when the window is minimized |
onRestored | Fires when the window returns to its original bounds |
Each window event has a property named addListener
that accepts a callback function to be invoked when the event occurs. In all cases, this callback doesn't receive any arguments. To demonstrate, the following code prints a message to the console when the window is moved or resized:
chrome.app.window.create("window.html",
{...},
function(win) {
win.onBoundsChanged.addListener(function() {
console.log("Bounds changed");
});
}
);
In this example, the window's content is determined by window.html. This HTML document may include one or more JavaScript files called window scripts, A window script can access the same capabilities as a background script, and the next section presents a window script that accesses the user's filesystem.
4. Interfacing the Filesystem
Most web apps can't interact with the user's files or system hardware. But a Chrome app can access the client's filesystem through the chrome.fileSystem
API. This capability is powerful but complex. Therefore, I've split the discussion into four parts:
- Obtaining permission
- The
chooseEntry
function - The File API
- The
chrome.fileSystem
API
The last part of this section presents an example app that doubles a file's content. That is, it reads text from a file and writes it to the end of the file,
4.1 Obtaining Permission
To access a user's filesystem, an app must request permission by adding one or more values to the manifest's permissions
array:
permissions: ["fileSystem"]
— requests read-only permission permissions: [{"fileSystem": ["write"]}]
— requests read/write permission permissions: [{"fileSystem": ["write", "retainEntries", "directory"]}]
— requests permission to read, write, retain files, and access directories in addition to files
If permission is granted, a script can access the user's files. If not, an attempt to access the user's filesystem will result in an error.
4.2 The chooseEntry Function
In the chrome.fileSystem
API, the chooseEntry
function opens a dialog that lets the user choose an existing file/directory or identify a new file/directory. Its signature is given as follows:
chooseEntry(Object opts, Function callback)
The first argument is an object that configures the dialog's appearance and behavior. This has five optional properties:
type
— Type of file dialog (openFile
, openWritableFile
, saveFile
, or openDirectory
) suggestedName
— Suggested name for the file to be opened accepts
— An array of objects that filter which files can be selected. Each has three optional properties: description
, mimeTypes
, and extensions
acceptsAllTypes
— Whether the dialog accepts files of all types acceptsMultiple
— Whether the dialog accepts multiple selections
The second argument of chooseEntry
is a callback function that receives the result of the user's selection. The selection result is provided as an Entry
or as an array of FileEntry
objects.
An example will clarify how chooseEntry
works. The following code, executed in a window script, displays a file selection dialog when the user clicks on the button whose ID is browse
.
document.getElementById("browse").onclick = function() {
chrome.fileSystem.chooseEntry( {
type: "openFile",
accepts: [ { description: "Data files (*.dat)",
extensions: ["dat"]} ]
}, function(entry) {
console.log("Entry name: " + entry.name);
});
}
The dialog's appearance and the initial search directory depends on the user's operating system. Figure 3 shows what the resulting dialog looks like on my Windows 7 machine:
Figure 3: File Selection Dialog Created by chooseEntry
If acceptsAllTypes
had been set to true
, the dialog would allow the user to select any file, regardless of its extension. If acceptsMultiple
had been set to true
, the dialog would allow the user to select multiple files. But neither property was set, so the user can open one, and only one, *.dat file. The required suffix was configured by setting the extensions
field to ["dat"]
.
When the user selects a file, the example callback function receives a corresponding Entry
and obtains the file's name using the name
property. To use these Entry
objects, you need to be familiar with Google's File API.
4.3 The File API
When HTML5 was being developed, Google proposed a File API that enables web applications to access a user's files. You can read the specification here. The File API wasn't included in HTML5, but the chrome.fileSystem
API relies on it to interact with the client's filesystem. This discussion looks at five objects of the File API: Entry
, FileEntry
, File
, FileReader
, and FileWriter
.
According to the File API, files are represented by FileEntry
objects and directories are represented by DirectoryEntry
objects. Both inherit from the Entry
type, whose properties are listed in Table 5.
Table 5: Entry Properties
Function | Description |
name | The entry's name |
fullPath | The entry's full path |
filesystem | The FileSystem containing the entry |
isFile | Identifies if the entry is a file |
isDirectory | Identifies if the entry is a directory |
toURL(String mimeType) | Returns the entry's URL |
getMetadata
(Function successCallback,
Function errorCallback) | Provides the entry's last modification date |
getParent
(Function successCallback,
Function errorCallback) | Provides the entry's parent |
remove
(Function successCallback,
Function errorCallback) | Removes the entry from the filesystem |
moveTo(DirectoryEntry parent,
String newName,
Function successCallback,
Function errorCallback) | Moves the entry to the given directory |
copyTo(DirectoryEntry parent,
String newName,
Function successCallback,
Function errorCallback) | Copies the entry to the given directory |
Many of these functions accept two callback functions: one to be invoked if the operation completes successfully and one to be invoked if an error occurs. The following code demonstrates how this works. When the user selects the entry in the dialog, the remove
function deletes it from the filesystem and prints a message to the log. If an error occurs, the app logs an error message.
chrome.fileSystem.chooseEntry( {
type: "openFile",
accepts: [ { description: "Text files (*.text)",
extensions: ["txt"]} ]
}, function(entry) {
entry.remove(
function() {console.log("Deletion successful")},
function() {console.log("Error occurred")}
);
});
A FileEntry
represents a file in the user's filesystem, and it has two functions beyond those listed in Table 5:
file(Function successCallback, Function errorCallback)
— Provides the entry's underlying File
object createWriter(Function successCallback, Function errorCallback)
— Provides a FileWriter
capable of writing to the file
By accessing the FileEntry
's File
, an app can read a file's content using a FileReader
. Similarly, the FileWriter
makes it possible to write data to a file. The following subsections discuss both capabilities.
4.3.1 Reading Data from a File
Google's File API defines a FileReader
object capable of reading data inside a File
. In general, using this object requires four steps:
- Create a new
FileReader
with its constructor: var reader = new FileReader();
- Invoke one of the
FileReader
's three read functions. - Assign a function to respond when the
FileReader
has finished reading data. - Inside the function, access the data through the
FileReader
's result
property.
For the second step, a FileReader
object has three functions for reading data:
readAsArrayBuffer(File f)
— Returns the file's content in an ArrayBuffer
(used for binary data) readAsText(File f, String encoding)
— Returns the file's content as a string readAsDataURL(File f)
— Returns the file's content as a Base64-encoded string
A FileReader
has a number of properties related to events, and the onload
property identifies a function to be called when the reader has finished loading data from the file. The following code shows how this can be used:
var reader = new FileReader();
reader.onload = function() {
console.log("Result: " + reader.result);
}
entry.file(function(f) {
reader.readAsText(f);
}
To be precise, the FileReader
's read functions operate on Blob
s, and a File
is a type of Blob
. The following discussion explains Blob
s in greater detail.
4.3.2 Writing Data to a File
The createWriter
function of the FileEntry
object provides a FileWriter
capable of writing data to the underlying file. This is easier to work with than the FileReader
, and Table 6 lists its properties.
Table 6: FileWriter Properties
Function | Description |
length | The length of the file in bytes |
position | The current write position in the file |
readyState | The writer's status (INIT , WRITING , or DONE ) |
error | An error condition |
write(Blob data) | Writes data to the file |
seek(long long offset) | Move the write position to the given location |
truncate(unsigned long long size) | Changes the file length to the given size |
abort() | Terminate the write operation |
The write
function accepts a Blob
. According to the documentation, a Blob
is a "file-like object of immutable, raw data". The important thing to know about Blob
s is that the Blob
constructor accepts an array of strings, Blob
s, ArrayBuffer
s, ArrayBufferView
s, or any combination of these objects. For example, the following code writes a string to the file associated with the FileWriter
:
writer.write(new Blob(["Example text"]);
Like a FileReader
, a FileWriter
provides events that can be assigned to functions. If a function is set equal to the writer's onwriteend
event, that function will be called when the write operation has completed. If a function is set equal to the writer's onerror
event, the function will be called if an error occurs. If entry
is a FileEntry
, the following code demonstrates how to create a FileWriter
and write a Blob
to the corresponding file:
entry.createWriter(function(writer) {
writer.onwriteend = function() {
console.log("Write finished.");
};
writer.onerror = function(error) {
console.log("Write error: " + error.toString());
};
writer.write(new Blob(["Example text"]));
});
By default, the text will be written to the start of the file. To change where text is written, the app should call the FileWriter
's seek
function with the desired offset. The next example app demonstrates how this is done.
4.4 The chrome.fileSystem API
The chrome.fileSystem
API provides a number of other functions in addition to the chooseEntry
function discussed earlier. Table 7 lists chooseEntry
and six other functions.
Table 7: Functions of chrome.fileSystem (Abridged)
Function | Description |
chooseEntry(Options opts,
Function callback) | Creates a file selection dialog |
getDisplayPath(Entry entry,
Function callback) | Provides the full path for the given entry |
getWritableEntry(Entry entry,
Function callback) | Provides a writeable entry for the given entry |
isWritableEntry(Entry entry,
Function callback) | Identifies whether the app has permission to
write to the entry |
retainEntry(Entry entry) | Returns a string identifier for the entry |
restoreEntry(String id,
Function callback) | Accesses an entry according to its identifier |
isRestorable(String id,
Function callback) | Identifies whether the app has permission to
restore the entry according to its identifier |
In most of these functions, the second argument is a callback function to be invoked when the operation is complete. For example, the second argument of getWritableEntry
is a callback function that provides the Entry
if the app has permission to write to it.
If an app needs to operate on multiple Entry
objects, it can assign an identifier to each. retainEntry
returns an identifier for an Entry
, and then restoreEntry
can be used to access Entry
objects according to their identifiers.
4.5 Example application
Within the example archive, the code in the file_demo app shows how a file can be read and written to. This contains a background script, background.js, which creates a 200x200 window to serve as the app's user interface. Its code is given as follows:
chrome.app.runtime.onLaunched.addListener(function() {
chrome.app.window.create(
"window.html",
{ "outerBounds": {"width": 200, "height": 200}
});
});
The window's appearance is defined by the window.html file, whose markup is given as:
<html>
<body>
<button id="browse">Select File</button>
<script src="scripts/window.js"></script>
</body>
</html>
This creates a button and injects the JavaScript code in the window.js script. When the button is clicked, this code invokes chooseEntry
to allow the user to select a text file. The script's code is as follows:
document.getElementById("browse").onclick = function() {
chrome.fileSystem.chooseEntry( {
type: "openFile",
accepts: [ { description: "Text files (*.txt)",
extensions: ["txt"]} ]
}, function(entry) {
var reader = new FileReader();
reader.onload = function() {
entry.createWriter(function(writer) {
writer.seek(writer.length);
writer.onwriteend = function() {
console.log("Write successful");
};
writer.onerror = function(error) {
console.log("Write error: " + error.toString());
};
writer.write(new Blob([reader.result]));
});
}
entry.file(function(f) {
reader.readAsText(f);
});
});
}
After the user selects a file, the app creates a FileReader
and associates its onload
property with an anonymous function. When the file's text is read, this function creates a FileWriter
and calls its seek
function to skip to the end of the file. The script writes the file's text to the end of the file, and if the operation completes successfully, it prints a message to the console.
5. Text to Speech
Since Version 14, the Chrome browser has contained an engine for converting text to speech. If the permissions
array in an app's manifest contains "tts"
, the app can access the speech engine using the chrome.tts
API. Table 8 lists the API's functions and provides a description of each.
Table 8: Functions of chrome.tts
Function | Description |
speak(String utterance,
Object options,
Function callback) | Generates and emits speech for the given text |
getVoices(Function callback) | Returns the set of available voices |
stop( ) | Halts speech in progress |
pause( ) | Pause speech in progress |
resume() | Resumes speaking after a pause |
isSpeaking(Function callback) | Identifies whether the engine is currently speaking |
As its name implies, the speak
function generates speech from text and emits the speech. The only required argument is the first, which identifies the text to be uttered. The following code utters the browser's name:
chrome.tts.speak("Chrome");
The third argument identifies an function to be invoked before the speech finishes. The second argument of speak
is an object that configures how the speech is generated. All of its properties are optional:
voiceName
— Name of the voice to be used lang
— Language to be used gender
— Desired gender (male
or female
) rate
— Speaking rate relative to the default rate (2 - twice as fast, 0.5 - half as fast) pitch
— Speaking pitch relative to the default pitch (2 - higher pitch, 0 - lower pitch) volume
— Speaking volume (between 0 and 1) enqueue
— Enqueues the speech after current speech if true, interrupts the current speech if false (default) extensionId
— ID of the extension containing the desired speech engine requiredEventTypes
— Event types that must be supported desiredEventTypes
— Event types of interest onEvent
— A function to be called on desired events
As an example, the following code speaks in a British male voice at high speed:
chrome.tts.speak("It's just a flesh wound", {voiceName: "Google UK English Male", rate: 1.5});
getVoices
provides an array containing all of the voices that can be generated. Each voice is represented by a TtsVoice
object, and the following subsection discusses this in detail.
5.1 The TtsVoice
A Chrome app can generate speech using a number of different voices. Each is represented by a TtsVoice
, and Table 9 lists its different properties.
Table 9: TtsVoice Properties
Function | Description |
voiceName | The name of the voice |
lang | The voice's language |
gender | The voice's gender: male or female |
remote | Whether the voice is provided over a network |
extensionId | The ID of the extension providing this voice |
eventTypes | Events that can be triggered by the voice |
For example, the following code uses getVoices
to access Chrome's pre-installed voices and display their properties:
chrome.tts.getVoices(function(voices) {
for (var i = 0; i < voices.length; i++) {
console.log("Name: " + voices[i].voiceName);
console.log("Language: " + voices[i].lang);
console.log("Gender: " + voices[i].gender);
console.log("Extension: " + voices[i].extensionId);
console.log("Events: " + voices[i].eventTypes);
}
});
When this executes in Chrome 51, the array contains twenty voices. Table 10 lists twelve of them along with their gender, language, and supported events.
Table 10: Built-in Voices
Voice Name | Gender/Language | Events |
Google US English | female /en-US | start , end , interrupted , cancelled , error |
Google UK English Male | male /en-GB | start , end , interrupted , cancelled , error |
Google UK English Female | female /en-GB | start , end , interrupted , cancelled , error |
Google español | female /es-ES | start , end , interrupted , cancelled , error |
Google español de Estados Unidos | female /en-US | start , end , interrupted , cancelled , error |
Google français | female /fr-FR | start , end , interrupted , cancelled , error |
Google Deutsch | female /de-DE | start , end , interrupted , cancelled , error |
Google italiano | female /it-IT | start , end , interrupted , cancelled , error |
Google Bahasa Indonesia | female /id-ID | start , end , interrupted , cancelled , error |
Google Nederlands | female /nl-NL | start , end , interrupted , cancelled , error |
Google polski | female /pl-PL | start , end , interrupted , cancelled , error |
Google português do Brasil | female /pt-BR | start , end , interrupted , cancelled , error |
When an app selects a voice for text-to-speech generation, that voice determines which speech events the app can respond to. Event handling is configured by setting the desiredEventTypes
property in the second argument of speak
and by assigning a function to the onEvent
property.
5.2 Example Code
In this article's example code, the tts_demo shows how a Chrome app can generate speech from text. The window provides controls for setting the utterance and selecting the voice. Figure 4 shows what this window looks like.
Figure 4: The tts_demo App Window
When the Speak button is pressed, the speak
function is invoked with the text in the text box. The name of the voice is defined by the combo box selection. The code in the project's scripts/window.js shows how this is accomplished:
var voiceselect = document.getElementById("voiceselect");
chrome.tts.getVoices(function(voices) {
for (var i = 0; i < voices.length; i++) {
voiceselect.options[i] = new Option(voices[i].voiceName, i);
}
voiceselect.selectedIndex = 2;
});
document.getElementById("stop").onclick = function() {
chrome.tts.isSpeaking(function(speaking) {
if (speaking) {
chrome.tts.stop();
}
});
}
document.getElementById("speak").onclick = function() {
var text = document.getElementById("utterance").value;
var voice = voiceselect.options[voiceselect.selectedIndex].text;
chrome.tts.speak(text, {voiceName: voice});
}
This demonstrates how the isSpeaking
and stop
functions are used. If the user clicks on the stop image, the app calls isSpeaking
to make sure the engine is currently generating speech. If the value provided by the callback function is true, the app calls the stop
function. This not only halts the current speech, but also flushes the queue of any pending utterances.
History
6/17/2016 - Initial article submission