Skip to content

Commit 30dd1f2

Browse files
authored
Merge pull request #382 from smartdevicelink/feature/0180-choice-uniqueness
Feature/0180 choice uniqueness
2 parents e060b96 + 20083b3 commit 30dd1f2

6 files changed

Lines changed: 165 additions & 23 deletions

File tree

lib/js/src/manager/screen/choiceset/ChoiceCell.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
3030
* POSSIBILITY OF SUCH DAMAGE.
3131
*/
32+
import { SdlArtwork } from '../../file/filetypes/SdlArtwork.js';
3233

3334
class ChoiceCell {
3435
/**
@@ -40,6 +41,7 @@ class ChoiceCell {
4041
this._text = null;
4142
this._secondaryText = null;
4243
this._tertiaryText = null;
44+
this._uniqueText = null;
4345
this._voiceCommands = null;
4446
this._artwork = null;
4547
this._secondaryArtwork = null;
@@ -51,6 +53,7 @@ class ChoiceCell {
5153
}
5254

5355
this._text = text;
56+
this._uniqueText = text;
5457
}
5558

5659
/**
@@ -112,6 +115,29 @@ class ChoiceCell {
112115
return this;
113116
}
114117

118+
/**
119+
* Get the state unique text. USED INTERNALLY
120+
* @private
121+
* @returns {String} - The uniqueText to be used in place of primaryText when core does not support identical names for ChoiceSets
122+
*/
123+
_getUniqueText () {
124+
return this._uniqueText;
125+
}
126+
127+
/**
128+
* Primary text of the cell to be displayed on the module. Used to distinguish cells with the
129+
* same `text` but other fields are different. This is autogenerated by the screen manager.
130+
* Attempting to use cells that are exactly the same (all text and artwork fields are the same)
131+
* will not cause this to be used. This will not be used when connected to modules supporting RPC 7.1+.
132+
* @private
133+
* @param {String} uniqueText - The uniqueText to be used in place of primaryText when core does not support identical names for ChoiceSets
134+
* @returns {ChoiceCell} - A reference to this instance to support method chaining
135+
*/
136+
_setUniqueText (uniqueText) {
137+
this._uniqueText = uniqueText;
138+
return this;
139+
}
140+
115141
/**
116142
* Get the state voiceCommands
117143
* @returns {String[]|null} - The list of voice command strings
@@ -251,6 +277,27 @@ class ChoiceCell {
251277
}
252278
return true;
253279
}
280+
281+
/**
282+
* Creates a deep copy of the object
283+
* @returns {ChoiceCell} - A deep copy of the object
284+
*/
285+
clone () {
286+
const clonedParams = Object.assign({}, this); // shallow copy. copy all objects afterwards
287+
288+
if (clonedParams._artwork !== null) {
289+
clonedParams._artwork = Object.assign(new SdlArtwork(), clonedParams._artwork);
290+
}
291+
if (clonedParams._secondaryArtwork !== null) {
292+
clonedParams._secondaryArtwork = Object.assign(new SdlArtwork(), clonedParams._secondaryArtwork);
293+
}
294+
295+
if (Array.isArray(this.getVoiceCommands())) {
296+
clonedParams._voiceCommands = this.getVoiceCommands().map(vc => vc);
297+
}
298+
299+
return Object.assign(new ChoiceCell(this.getText()), clonedParams);
300+
}
254301
}
255302

256303
// MAX ID for cells - Reasoning is from Java library: cannot use Integer.MAX_INT as the value is too high.

lib/js/src/manager/screen/choiceset/ChoiceSet.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ import { VrHelpItem } from '../../../rpc/structs/VrHelpItem.js';
3636
class ChoiceSet {
3737
/**
3838
* Create a new instance of ChoiceSet
39+
* Initialize with a title, choices, and listener. It will use the default timeout and layout, all other properties (such as prompts) will be null.
40+
* WARNING: If you display multiple cells with the same title with the only uniquing property between cells being different `vrCommands` or a feature
41+
* that is not displayed on the head unit (e.g. if the head unit doesn't display `secondaryArtwork` and that's the only uniquing property between two cells)
42+
* then the cells may appear to be the same to the user in `Manual` mode. This only applies to RPC connections >= 7.1.0.
43+
* WARNING: On < 7.1.0 connections, the title cell will be automatically modified among cells that have the same title when they are preloaded, so they will
44+
* always appear differently on-screen when they are displayed. Unique text will be created by appending " (2)", " (3)", etc.
3945
* @class
4046
* @param {String} title - The choice set's title
4147
* @param {ChoiceCell[]} choices - The choices to be displayed to the user for interaction

lib/js/src/manager/screen/choiceset/_ChoiceSetManagerBase.js

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,14 @@ class _ChoiceSetManagerBase extends _SubManagerBase {
130130
* @param {ChoiceCell[]} choices - A list of ChoiceCell objects that will be part of a choice set later
131131
* @returns {Promise} - A promise that resolves to a Boolean of whether the operation is a success
132132
*/
133-
async preloadChoices (choices) {
133+
async preloadChoices (choices = null) {
134134
if (this._getState() === _SubManagerBase.ERROR) {
135135
console.warn('ChoiceSetManager: Choice Manager In Error State');
136136
return false;
137137
}
138-
const choicesToUpload = choices.map(choice => choice); // shallow copy
138+
139+
const choicesToUpload = this._getChoicesToBeUploadedWithArray(choices);
140+
139141
this._removeChoicesFromChoices(this._preloadedChoices, choicesToUpload);
140142
this._removeChoicesFromChoices(this._pendingPreloadChoices, choicesToUpload);
141143

@@ -167,6 +169,25 @@ class _ChoiceSetManagerBase extends _SubManagerBase {
167169
});
168170
}
169171

172+
/**
173+
* Checks if 2 or more cells have the same text/title. In case this condition is true, this function will handle the presented issue by adding "(count)".
174+
* E.g. Choices param contains 2 cells with text/title "Address" will be handled by updating the uniqueText/uniqueTitle of the second cell to "Address (2)".
175+
* @param {ChoiceCell[]} choices - A list of ChoiceCell objects to be uploaded
176+
*/
177+
_addUniqueNamesToCells (choices) {
178+
const dictCounter = {}; // create a string to number hash for counting similar primary texts
179+
180+
choices.forEach(choice => {
181+
const choiceName = choice.getText();
182+
if (dictCounter[choiceName] === undefined) {
183+
dictCounter[choiceName] = 1; // unique text
184+
} else { // found a duplicate
185+
dictCounter[choiceName] += 1;
186+
choice._setUniqueText(`${choiceName} (${dictCounter[choiceName]})`);
187+
}
188+
});
189+
}
190+
170191
/**
171192
* Deletes choices that were sent previously
172193
* @param {ChoiceCell[]} choices - A list of ChoiceCell objects
@@ -276,15 +297,16 @@ class _ChoiceSetManagerBase extends _SubManagerBase {
276297
}
277298
}
278299

279-
const uniqueChoiceTexts = {};
300+
const uniqueChoiceCells = [];
280301
const uniqueVoiceCommands = {};
281302
let choiceCellWithVoiceCommandCount = 0;
282303
let allVoiceCommandsCount = 0;
283304

284305
for (let index = 0; index < choices.length; index++) {
285-
const choiceText = choices[index].getText();
286306
const choiceVoiceCommands = choices[index].getVoiceCommands();
287-
uniqueChoiceTexts[choiceText] = true;
307+
if (uniqueChoiceCells.findIndex(choice => choice.equals(choices[index])) === -1) {
308+
uniqueChoiceCells.push(choices[index]);
309+
}
288310

289311
if (choiceVoiceCommands !== null) {
290312
choiceCellWithVoiceCommandCount++;
@@ -297,9 +319,8 @@ class _ChoiceSetManagerBase extends _SubManagerBase {
297319
}
298320
}
299321

300-
// Cell text MUST be unique
301-
if (Object.keys(uniqueChoiceTexts).length < choices.length) {
302-
console.error('ChoiceSetManager: Attempted to create a choice set with duplicate cell text. Cell text must be unique. The choice set will not be set.');
322+
if (uniqueChoiceCells.length !== choices.length) {
323+
console.error('Attempted to create a choice set with a duplicate cell. Cell must have a unique value other than its primary text. The choice set will not be set.');
303324
return false;
304325
}
305326

@@ -579,6 +600,22 @@ class _ChoiceSetManagerBase extends _SubManagerBase {
579600
.setKeypressMode(KeypressMode.RESEND_CURRENT_ENTRY);
580601
}
581602

603+
/**
604+
* Modifies the choices names depending on SDL version
605+
* @param {ChoiceCell[]} choices - The first list of choices
606+
* @returns {ChoiceCell[]} - A deep copy of the name modified choices
607+
*/
608+
_getChoicesToBeUploadedWithArray (choices) {
609+
// If we're running on a connection < RPC 7.1, we need to de-duplicate cells because presenting them will fail if we have the same cell primary text.
610+
if (choices !== null && this._lifecycleManager.getSdlMsgVersion() !== null
611+
&& (this._lifecycleManager.getSdlMsgVersion().getMajorVersion() < 7
612+
|| (this._lifecycleManager.getSdlMsgVersion().getMajorVersion() === 7 && this._lifecycleManager.getSdlMsgVersion().getMinorVersion() === 0))) {
613+
// version if 7.0.0 or lower
614+
this._addUniqueNamesToCells(choices);
615+
}
616+
return choices.map(choice => choice.clone()); // deep copy
617+
}
618+
582619
/**
583620
* Listen for DISPLAYS capability updates
584621
* @private

lib/js/src/manager/screen/choiceset/_PreloadChoicesOperation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class _PreloadChoicesOperation extends _Task {
166166
vrCommands = this._isVrOptional ? null : [`${cell._getChoiceId()}`]; // stringified choice id
167167
}
168168

169-
const menuName = this._shouldSendChoiceText() ? cell.getText() : null;
169+
const menuName = this._shouldSendChoiceText() ? cell._getUniqueText() : null;
170170
if (menuName === null) {
171171
console.error('PreloadChoicesOperation: Could not convert Choice Cell to CreateInteractionChoiceSet. It will not be shown');
172172
return null;
@@ -247,4 +247,4 @@ class _PreloadChoicesOperation extends _Task {
247247
}
248248
}
249249

250-
export { _PreloadChoicesOperation };
250+
export { _PreloadChoicesOperation };

tests/managers/screen/choiceset/ChoiceCellTests.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = function (appClient) {
1717
Validator.assertNull(choiceCell.getTertiaryText());
1818
Validator.assertNull(choiceCell.getArtwork());
1919
Validator.assertNull(choiceCell.getSecondaryArtwork());
20+
Validator.assertNull(choiceCell._getUniqueText());
2021
});
2122

2223
it('testSettersAndGetters', function () {
@@ -37,6 +38,11 @@ module.exports = function (appClient) {
3738
Validator.assertEquals(choiceCell.getSecondaryArtwork(), artwork);
3839
Validator.assertEquals(choiceCell._getChoiceId(), MAX_ID);
3940

41+
Validator.assertEquals(choiceCell._getUniqueText(), choiceCell.getText());
42+
43+
choiceCell._setUniqueText('hi');
44+
Validator.assertEquals(choiceCell._getUniqueText(), 'hi');
45+
4046
choiceCell.setText('hello');
4147
Validator.assertEquals(choiceCell.getText(), 'hello');
4248
});
@@ -62,8 +68,13 @@ module.exports = function (appClient) {
6268
.setSecondaryText(Test.GENERAL_STRING)
6369
.setTertiaryText(Test.GENERAL_STRING);
6470

71+
// UniqueText should not be taken into consideration when checking equality
72+
choiceCell._setUniqueText('1');
73+
choiceCell2._setUniqueText('2');
74+
choiceCell3._setUniqueText('3');
75+
6576
Validator.assertTrue(choiceCell.equals(choiceCell2));
6677
Validator.assertTrue(!choiceCell.equals(choiceCell3));
6778
});
6879
});
69-
};
80+
};

tests/managers/screen/choiceset/ChoiceSetManagerTests.js

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ module.exports = function (appClient) {
4949
});
5050

5151
it('testSetupChoiceSet', function () {
52+
const stub = sinon.stub(sdlManager._lifecycleManager, 'getSdlMsgVersion')
53+
.returns(new SDL.rpc.structs.SdlMsgVersion()
54+
.setMajorVersion(7)
55+
.setMinorVersion(0)
56+
.setPatchVersion(0));
57+
5258
const choiceSetSelectionListener = new SDL.manager.screen.choiceset.ChoiceSetSelectionListener()
5359
.setOnChoiceSelected(() => {})
5460
.setOnError(() => {});
@@ -57,34 +63,45 @@ module.exports = function (appClient) {
5763
const choiceSet1 = new SDL.manager.screen.choiceset.ChoiceSet('test', [], choiceSetSelectionListener);
5864
Validator.assertTrue(!csm._setUpChoiceSet(choiceSet1));
5965

60-
// cells cant have duplicate text
66+
// Identical cells will not be allowed
6167
const cell1 = new SDL.manager.screen.choiceset.ChoiceCell('test');
6268
const cell2 = new SDL.manager.screen.choiceset.ChoiceCell('test');
6369
const choiceSet2 = new SDL.manager.screen.choiceset.ChoiceSet('test', [cell1, cell2], choiceSetSelectionListener);
6470
Validator.assertTrue(!csm._setUpChoiceSet(choiceSet2));
6571

66-
// cells cannot mix and match VR / non-VR
72+
// cells that have duplicate text will be allowed if there is another property to make them unique
73+
// because a unique name will be assigned and used
6774
const cell3 = new SDL.manager.screen.choiceset.ChoiceCell('test')
68-
.setVoiceCommands(['Test']);
69-
const cell4 = new SDL.manager.screen.choiceset.ChoiceCell('test2');
75+
.setSecondaryText('text 1');
76+
const cell4 = new SDL.manager.screen.choiceset.ChoiceCell('test')
77+
.setSecondaryText('text 2');
7078
const choiceSet3 = new SDL.manager.screen.choiceset.ChoiceSet('test', [cell3, cell4], choiceSetSelectionListener);
71-
Validator.assertTrue(!csm._setUpChoiceSet(choiceSet3));
79+
Validator.assertTrue(csm._setUpChoiceSet(choiceSet3));
7280

73-
// VR Commands must be unique
81+
// cells cannot mix and match VR / non-VR
7482
const cell5 = new SDL.manager.screen.choiceset.ChoiceCell('test')
7583
.setVoiceCommands(['Test']);
76-
const cell6 = new SDL.manager.screen.choiceset.ChoiceCell('test2')
77-
.setVoiceCommands(['Test']);
84+
const cell6 = new SDL.manager.screen.choiceset.ChoiceCell('test2');
7885
const choiceSet4 = new SDL.manager.screen.choiceset.ChoiceSet('test', [cell5, cell6], choiceSetSelectionListener);
7986
Validator.assertTrue(!csm._setUpChoiceSet(choiceSet4));
8087

81-
// Passing Case
88+
// VR Commands must be unique
8289
const cell7 = new SDL.manager.screen.choiceset.ChoiceCell('test')
8390
.setVoiceCommands(['Test']);
8491
const cell8 = new SDL.manager.screen.choiceset.ChoiceCell('test2')
85-
.setVoiceCommands(['Test2']);
92+
.setVoiceCommands(['Test']);
8693
const choiceSet5 = new SDL.manager.screen.choiceset.ChoiceSet('test', [cell7, cell8], choiceSetSelectionListener);
87-
Validator.assertTrue(csm._setUpChoiceSet(choiceSet5));
94+
Validator.assertTrue(!csm._setUpChoiceSet(choiceSet5));
95+
96+
// Passing Case
97+
const cell9 = new SDL.manager.screen.choiceset.ChoiceCell('test')
98+
.setVoiceCommands(['Test']);
99+
const cell10 = new SDL.manager.screen.choiceset.ChoiceCell('test2')
100+
.setVoiceCommands(['Test2']);
101+
const choiceSet6 = new SDL.manager.screen.choiceset.ChoiceSet('test', [cell9, cell10], choiceSetSelectionListener);
102+
Validator.assertTrue(csm._setUpChoiceSet(choiceSet6));
103+
104+
stub.restore();
88105
});
89106

90107
it('testUpdateIdsOnChoices', function () {
@@ -273,5 +290,29 @@ module.exports = function (appClient) {
273290
// restore state
274291
csm._defaultMainWindowCapability = originCapability;
275292
});
293+
294+
it('testAddUniqueNamesToCells', function () {
295+
const cell1 = new SDL.manager.screen.choiceset.ChoiceCell('McDonalds')
296+
.setSecondaryText('1 mile away');
297+
const cell2 = new SDL.manager.screen.choiceset.ChoiceCell('McDonalds')
298+
.setSecondaryText('2 miles away');
299+
const cell3 = new SDL.manager.screen.choiceset.ChoiceCell('Starbucks')
300+
.setSecondaryText('3 miles away');
301+
const cell4 = new SDL.manager.screen.choiceset.ChoiceCell('McDonalds')
302+
.setSecondaryText('4 miles away');
303+
const cell5 = new SDL.manager.screen.choiceset.ChoiceCell('Starbucks')
304+
.setSecondaryText('5 miles away');
305+
const cell6 = new SDL.manager.screen.choiceset.ChoiceCell('Meijer')
306+
.setSecondaryText('6 miles away');
307+
308+
csm._addUniqueNamesToCells([cell1, cell2, cell3, cell4, cell5, cell6]);
309+
310+
Validator.assertEquals(cell1._getUniqueText(), 'McDonalds');
311+
Validator.assertEquals(cell2._getUniqueText(), 'McDonalds (2)');
312+
Validator.assertEquals(cell3._getUniqueText(), 'Starbucks');
313+
Validator.assertEquals(cell4._getUniqueText(), 'McDonalds (3)');
314+
Validator.assertEquals(cell5._getUniqueText(), 'Starbucks (2)');
315+
Validator.assertEquals(cell6._getUniqueText(), 'Meijer');
316+
});
276317
});
277-
};
318+
};

0 commit comments

Comments
 (0)