Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 37 additions & 61 deletions lib/js/src/manager/screen/_VoiceCommandManagerBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@

import { _SubManagerBase } from '../_SubManagerBase.js';
import { _Task } from '../_Task.js';
import { _VoiceCommandUpdateOperation } from './utils/_VoiceCommandUpdateOperation.js';
import { FunctionID } from '../../rpc/enums/FunctionID.js';
import { DeleteCommand } from '../../rpc/messages/DeleteCommand.js';
import { AddCommand } from '../../rpc/messages/AddCommand.js';

class _VoiceCommandManagerBase extends _SubManagerBase {
/**
Expand All @@ -46,9 +45,11 @@ class _VoiceCommandManagerBase extends _SubManagerBase {
constructor (lifecycleManager) {
super(lifecycleManager);
this._voiceCommands = [];
this._currentVoiceCommands = [];
this._voiceCommandIdMin = 1900000000;
this._lastVoiceCommandId = this._voiceCommandIdMin;
this._commandListener = null;
this._updateOperation = null;
this._handleTaskQueue();
this._addListeners();
}
Expand Down Expand Up @@ -81,38 +82,42 @@ class _VoiceCommandManagerBase extends _SubManagerBase {
* @returns {Promise} - A promise which resolves after old commands are deleted and new ones are added
*/
async setVoiceCommands (voiceCommands) {
// we actually need voice commands to set.
if (!Array.isArray(voiceCommands)) {
// we actually need voice commands to set. checks if the array of voice commands passed in contains the same content as the current voice commands
if (!Array.isArray(voiceCommands) || !voiceCommands.map((vc, index) => vc.equals(this._voiceCommands[index])).includes(false)) {
console.log('Voice commands list non-existent or matches the current voice commands');
return;
}

this._updateIdsOnVoiceCommands(voiceCommands);
this._voiceCommands = voiceCommands;

// add the commands to a queue to be processed later
// remove all other tasks except this one
// clear all tasks
this._cancelAllTasks();
const task = new _Task();
task.onExecute = this._update(voiceCommands);
this._addTask(task);

this._updateOperation = new _VoiceCommandUpdateOperation(this._lifecycleManager, this._currentVoiceCommands, voiceCommands, (newVoiceCommands, errorArray) => {
if (errorArray.length !== 0) {
console.log('Failed updated voice commands for the following:');
console.log(JSON.stringify(errorArray, null, 4)); // print like this so that the inner _parameters object shows
}
this._currentVoiceCommands = newVoiceCommands;
// any additional operations that may have come in after the completion of this one need the updated voice commands now
this._updatePendingOperations(newVoiceCommands);
this._updateOperation = null;
});
this._addTask(this._updateOperation);
}

/**
* Processes incoming voice commands
* Processes incoming voice commands to set command IDs
* @private
* @returns {function} - An async function that returns after old commands are deleted and new ones are added
* @param {VoiceCommand[]} voiceCommands - An array of VoiceCommand instances.
*/
_update (voiceCommands) {
return async (task) => {
this._lastVoiceCommandId = this._voiceCommandIdMin;
for (const voiceCommand of voiceCommands) {
this._lastVoiceCommandId++;
voiceCommand._setCommandId(this._lastVoiceCommandId);
}

await this._sendDeleteVoiceCommands(this._voiceCommands);
// old voice commands are now deleted
// now add the new ones
await this._sendAddVoiceCommands(voiceCommands);
this._voiceCommands = voiceCommands;
};
_updateIdsOnVoiceCommands (voiceCommands) {
for (const voiceCommand of voiceCommands) {
this._lastVoiceCommandId++;
voiceCommand._setCommandId(this._lastVoiceCommandId);
}
}

/**
Expand All @@ -124,46 +129,17 @@ class _VoiceCommandManagerBase extends _SubManagerBase {
}

/**
* Sends requests to delete all voice commands passed in
* @private
* @param {VoiceCommand[]} voiceCommands - An array of VoiceCommand instances.
* @returns {Promise} - A promise.
*/
async _sendDeleteVoiceCommands (voiceCommands) {
if (!Array.isArray(voiceCommands) || voiceCommands.length === 0) {
return;
}

// make a DeleteCommand request for every voice command
const deleteCommandPromises = voiceCommands.map(voiceCommand => {
const deleteCommand = new DeleteCommand()
.setCmdID(voiceCommand._getCommandId());
return this._lifecycleManager.sendRpcResolve(deleteCommand);
});

return Promise.all(deleteCommandPromises);
}

/**
* Sends requests to add all voice commands passed in
* Updates all non-running operations with this operation's updated voice commands
* @private
* @param {VoiceCommand[]} voiceCommands - An array of VoiceCommand instances.
* @returns {Promise} - A promise.
* @param {VoiceCommand[]} voiceCommands - An array of VoiceCommands.
*/
async _sendAddVoiceCommands (voiceCommands) {
if (!Array.isArray(voiceCommands) || voiceCommands.length === 0) {
return;
}

// make an AddCommand request for every voice command
const addCommandPromises = voiceCommands.map(voiceCommand => {
const addCommand = new AddCommand()
.setCmdID(voiceCommand._getCommandId())
.setVrCommands(voiceCommand.getVoiceCommands());
return this._lifecycleManager.sendRpcResolve(addCommand);
_updatePendingOperations (voiceCommands) {
this._taskQueue.forEach(task => {
if (task.getState() === _Task.IN_PROGRESS) {
return;
}
task._oldVoiceCommands = voiceCommands;
});

return Promise.all(addCommandPromises);
}

/**
Expand Down
32 changes: 32 additions & 0 deletions lib/js/src/manager/screen/utils/VoiceCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,38 @@ class VoiceCommand {
_getCommandId () {
return this._commandId;
}

/**
* Checks whether two VoiceCommands can be considered equivalent
* @param {VoiceCommand} other - The object to compare
* @returns {Boolean} - Whether the objects are the same or not
*/
equals (other) {
if (other === null || other === undefined) {
return false;
}
if (this === other) {
return true;
}
if (!(other instanceof VoiceCommand)) {
return false;
}
// main comparison check
if (this._getCommandId() !== other._getCommandId()) {
return false;
}
const voiceCommands = this.getVoiceCommands();
const otherVoiceCommands = other.getVoiceCommands();
if (voiceCommands.length !== otherVoiceCommands.length) {
return false;
}
for (let i = 0; i < voiceCommands.length; i++) {
if (voiceCommands[i] !== otherVoiceCommands[i]) {
return false;
}
}
return true;
}
}

export { VoiceCommand };
193 changes: 193 additions & 0 deletions lib/js/src/manager/screen/utils/_VoiceCommandUpdateOperation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2020, Livio, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following
* disclaimer in the documentation and/or other materials provided with the
* distribution.
*
* Neither the name of the Livio Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

import { _Task } from '../../_Task';
import { DeleteCommand } from '../../../rpc/messages/DeleteCommand.js';
import { AddCommand } from '../../../rpc/messages/AddCommand.js';

class _VoiceCommandUpdateOperation extends _Task {
/**
* Initializes an instance of _VoiceCommandUpdateOperation
* @class
* @private
* @param {_LifecycleManager} lifecycleManager - A _LifecycleManager instance
* @param {VoiceCommand[]} oldVoiceCommands - A list of voice command objects
* @param {VoiceCommand[]} pendingVoiceCommands - A list of voice command objects
* @param {function} voiceCommandListener - A callback function for operation updates. Receives updated voice commands and errors
*/
constructor (lifecycleManager, oldVoiceCommands = null, pendingVoiceCommands = null, voiceCommandListener = null) {
super('VoiceCommandReplaceOperation');
this._lifecycleManager = lifecycleManager;
this._oldVoiceCommands = oldVoiceCommands;
this._pendingVoiceCommands = pendingVoiceCommands;
this._currentVoiceCommands = [];
if (this._oldVoiceCommands !== null) {
this._currentVoiceCommands = Array.from(this._oldVoiceCommands); // shallow copy
}
this._voiceCommandListener = voiceCommandListener;
this._errorArray = []; // an array of objects containing RPC Requests and their response String errors. JS doesn't support objects as keys
// the format is { request: <RPCRequest>, error: <String> }
}

/**
* The method that causes the task to run.
* @param {_Task} task - The task instance
* @returns {Promise}
*/
async onExecute (task) {
if (this.getState() === _Task.CANCELED) {
this.onFinished();
return;
}
await this._sendDeleteCurrentVoiceCommands();
if (this.getState() === _Task.CANCELED) {
this.onFinished();
return;
}
// ignore the result from deletions. send new add commands
await this._sendCurrentVoiceCommands();
// all errors from both operations will be stored in this._errorArray
if (typeof this._voiceCommandListener === 'function') {
this._voiceCommandListener(this._currentVoiceCommands, this._errorArray);
}
this.onFinished();
}

/**
* Sends requests to delete all old/current voice commands passed in
* @private
* @returns {Promise} - A promise which returns a Boolean of whether the operation is a success
*/
async _sendDeleteCurrentVoiceCommands () {
if (!Array.isArray(this._oldVoiceCommands) || this._oldVoiceCommands.length === 0) {
return true; // nothing to delete
}

// make a DeleteCommand request for every voice command
const deleteCommands = this._oldVoiceCommands.map(voiceCommand => {
return new DeleteCommand().setCmdID(voiceCommand._getCommandId());
});

const deleteCommandPromises = deleteCommands.map(deleteCommand => {
return this._lifecycleManager.sendRpcResolve(deleteCommand);
});

const responses = await Promise.all(deleteCommandPromises);
// go through all responses and inspect their statuses
responses.forEach((response, index) => {
const deleteRequest = deleteCommands[index]; // order is preserved between arrays of requests and their responses

if (!response.getSuccess()) {
this._errorArray.push({
request: deleteRequest,
error: response.getInfo(),
});
} else { // deletion is a success. remove from _currentVoiceCommands
this._removeCurrentVoiceCommandForCorrelatingDeleteCommand(deleteRequest);
}
});
// finished with reading responses. check for errors and return whether there are any
return this._errorArray.length === 0;
}

/**
* Removes this delete command from the current voice commands array
* @private
* @param {DeleteCommand} deleteCommand - The command to remove
*/
_removeCurrentVoiceCommandForCorrelatingDeleteCommand (deleteCommand) {
for (let index = 0; index < this._currentVoiceCommands.length; index++) {
const voiceCommand = this._currentVoiceCommands[index];
if (deleteCommand.getCmdID() === voiceCommand._getCommandId()) {
this._currentVoiceCommands.splice(index, 1); // remove this element and modify the index to account for the array manipulation
index--;
}
}
}

/**
* Sends requests to add all pending voice commands passed in
* @private
* @returns {Promise} - A promise which returns a Boolean of whether the operation is a success
*/
async _sendCurrentVoiceCommands () {
if (!Array.isArray(this._pendingVoiceCommands) || this._pendingVoiceCommands.length === 0) {
return true; // nothing to delete
}

// make an AddCommand request for every voice command
const addCommands = this._pendingVoiceCommands.map(voiceCommand => {
return new AddCommand()
.setCmdID(voiceCommand._getCommandId())
.setVrCommands(voiceCommand.getVoiceCommands());
});

const addCommandPromises = addCommands.map(addCommand => {
return this._lifecycleManager.sendRpcResolve(addCommand);
});


const responses = await Promise.all(addCommandPromises);
// go through all responses and inspect their statuses
responses.forEach((response, index) => {
const addRequest = addCommands[index]; // order is preserved between arrays of requests and their responses

if (!response.getSuccess()) {
this._errorArray.push({
request: addRequest,
error: response.getResultCode(), // getInfo() returns null for some of these errors
});
} else { // addition is a success. add to _currentVoiceCommands
this._pendingVoiceCommandForCorrelatingAddCommand(addRequest);
}
});
// finished with reading responses. check for errors and return whether there are any
return this._errorArray.length === 0;
}

/**
* Adds the AddCommand to the current voice commands array
* @private
* @param {AddCommand} addCommand - The command to add
*/
_pendingVoiceCommandForCorrelatingAddCommand (addCommand) {
for (let index = 0; index < this._pendingVoiceCommands.length; index++) {
const voiceCommand = this._pendingVoiceCommands[index];
if (addCommand.getCmdID() === voiceCommand._getCommandId()) {
this._currentVoiceCommands.push(voiceCommand);
return;
}
}
}
}

export { _VoiceCommandUpdateOperation };
Loading