summaryrefslogtreecommitdiff
path: root/SmartDeviceLink/private/SDLPreloadPresentChoicesOperation.m
blob: 692013e084159ab9c401b6d1b304ec88b7c9c66f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
//
//  SDLPreloadChoicesOperation.m
//  SmartDeviceLink
//
//  Created by Joel Fischer on 5/23/18.
//  Copyright © 2018 smartdevicelink. All rights reserved.
//

#import "SDLPreloadPresentChoicesOperation.h"

#import "SDLCancelInteraction.h"
#import "SDLChoice.h"
#import "SDLChoiceCell.h"
#import "SDLChoiceSet.h"
#import "SDLChoiceSetDelegate.h"
#import "SDLConnectionManagerType.h"
#import "SDLCreateInteractionChoiceSet.h"
#import "SDLCreateInteractionChoiceSetResponse.h"
#import "SDLDisplayType.h"
#import "SDLError.h"
#import "SDLFileManager.h"
#import "SDLGlobals.h"
#import "SDLImage.h"
#import "SDLKeyboardProperties.h"
#import "SDLLogMacros.h"
#import "SDLOnKeyboardInput.h"
#import "SDLPreloadPresentChoicesOperationUtilities.h"
#import "SDLPerformInteraction.h"
#import "SDLPerformInteractionResponse.h"
#import "SDLRPCNotificationNotification.h"
#import "SDLSetGlobalProperties.h"
#import "SDLVersion.h"
#import "SDLWindowCapability.h"
#import "SDLWindowCapability+ScreenManagerExtensions.h"

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, SDLPreloadPresentChoicesOperationState) {
    SDLPreloadPresentChoicesOperationStateNotStarted,
    SDLPreloadPresentChoicesOperationStateUploadingImages,
    SDLPreloadPresentChoicesOperationStateUploadingChoices,
    SDLPreloadPresentChoicesOperationStateUpdatingKeyboardProperties,
    SDLPreloadPresentChoicesOperationStatePresentingChoices,
    SDLPreloadPresentChoicesOperationStateCancellingPresentChoices,
    SDLPreloadPresentChoicesOperationStateResettingKeyboardProperties,
    SDLPreloadPresentChoicesOperationStateFinishing
};

@interface SDLChoiceCell()

@property (assign, nonatomic) UInt16 choiceId;
@property (copy, nonatomic, readwrite, nullable) NSString *secondaryText;
@property (copy, nonatomic, readwrite, nullable) NSString *tertiaryText;
@property (copy, nonatomic, readwrite, nullable) NSArray<NSString *> *voiceCommands;
@property (strong, nonatomic, readwrite, nullable) SDLArtwork *artwork;
@property (strong, nonatomic, readwrite, nullable) SDLArtwork *secondaryArtwork;

@property (assign, nonatomic) NSUInteger uniqueTextId;

@end

@interface SDLChoiceSet()

@property (copy, nonatomic) SDLChoiceSetCanceledHandler canceledHandler;

@end

@interface SDLPreloadPresentChoicesOperation()

// Dependencies
@property (weak, nonatomic) id<SDLConnectionManagerType> connectionManager;
@property (weak, nonatomic) SDLFileManager *fileManager;
@property (strong, nonatomic) SDLWindowCapability *windowCapability;

// Preload Dependencies
@property (strong, nonatomic) NSMutableOrderedSet<SDLChoiceCell *> *cellsToUpload;
@property (strong, nonatomic) NSString *displayName;
@property (assign, nonatomic, getter=isVROptional) BOOL vrOptional;
@property (copy, nonatomic) SDLUploadChoicesCompletionHandler preloadCompletionHandler;

// Present Dependencies
@property (strong, nonatomic) SDLChoiceSet *choiceSet;
@property (strong, nonatomic, nullable) SDLInteractionMode presentationMode;
@property (strong, nonatomic, nullable) SDLKeyboardProperties *originalKeyboardProperties;
@property (strong, nonatomic, nullable) SDLKeyboardProperties *customKeyboardProperties;
@property (weak, nonatomic, nullable) id<SDLKeyboardDelegate> keyboardDelegate;
@property (assign, nonatomic) UInt16 cancelId;

// Internal operation properties
@property (assign, nonatomic) SDLPreloadPresentChoicesOperationState currentState;
@property (strong, nonatomic) NSUUID *operationId;
@property (copy, nonatomic, nullable) NSError *internalError;

// Mutable state
@property (strong, nonatomic) NSMutableSet<SDLChoiceCell *> *mutableLoadedCells;

// Present completion handler properties
@property (strong, nonatomic, nullable) SDLChoiceCell *selectedCell;
@property (strong, nonatomic, nullable) SDLTriggerSource selectedTriggerSource;
@property (assign, nonatomic) NSUInteger selectedCellRow;

@end

@implementation SDLPreloadPresentChoicesOperation

- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager fileManager:(SDLFileManager *)fileManager displayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)windowCapability isVROptional:(BOOL)isVROptional cellsToPreload:(NSArray<SDLChoiceCell *> *)cellsToPreload loadedCells:(NSSet<SDLChoiceCell *> *)loadedCells preloadCompletionHandler:(SDLUploadChoicesCompletionHandler)preloadCompletionHandler {
    self = [super init];
    if (!self) { return nil; }

    _currentState = SDLPreloadPresentChoicesOperationStateNotStarted;
    _operationId = [NSUUID UUID];

    _connectionManager = connectionManager;
    _fileManager = fileManager;
    _cancelId = UINT16_MAX;
    _displayName = displayName;
    _windowCapability = windowCapability;
    _vrOptional = isVROptional;

    _cellsToUpload = [NSMutableOrderedSet orderedSetWithArray:cellsToPreload];
    _mutableLoadedCells = [loadedCells mutableCopy];
    _preloadCompletionHandler = preloadCompletionHandler;

    return self;
}

- (instancetype)initWithConnectionManager:(id<SDLConnectionManagerType>)connectionManager fileManager:(SDLFileManager *)fileManager choiceSet:(SDLChoiceSet *)choiceSet mode:(SDLInteractionMode)mode keyboardProperties:(nullable SDLKeyboardProperties *)originalKeyboardProperties keyboardDelegate:(nullable id<SDLKeyboardDelegate>)keyboardDelegate cancelID:(UInt16)cancelID displayName:(NSString *)displayName windowCapability:(SDLWindowCapability *)windowCapability isVROptional:(BOOL)isVROptional loadedCells:(NSSet<SDLChoiceCell *> *)loadedCells preloadCompletionHandler:(SDLUploadChoicesCompletionHandler)preloadCompletionHandler {
    self = [super init];
    if (!self) { return nil; }

    _currentState = SDLPreloadPresentChoicesOperationStateNotStarted;
    _operationId = [NSUUID UUID];

    _connectionManager = connectionManager;
    _fileManager = fileManager;
    _choiceSet = choiceSet;
    _presentationMode = mode;

    __weak typeof(self) weakSelf = self;
    _choiceSet.canceledHandler = ^{
        [weakSelf sdl_cancelInteraction];
    };

    _originalKeyboardProperties = originalKeyboardProperties;
    _customKeyboardProperties = originalKeyboardProperties;
    _keyboardDelegate = keyboardDelegate;
    _cancelId = cancelID;

    _displayName = displayName;
    _windowCapability = windowCapability;
    _vrOptional = isVROptional;
    _mutableLoadedCells = [loadedCells mutableCopy];
    _cellsToUpload = [NSMutableOrderedSet orderedSetWithArray:choiceSet.choices];
    _preloadCompletionHandler = preloadCompletionHandler;

    _selectedCellRow = NSNotFound;

    return self;
}

- (void)start {
    [super start];
    if (self.isCancelled) { return; }

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_keyboardInputNotification:) name:SDLDidReceiveKeyboardInputNotification object:nil];

    // If we have no loaded cells, reset choice ids to ensure reconnections restart numbering
    if (self.loadedCells.count == 0) {
        SDLPreloadPresentChoicesOperationUtilities.choiceId = 0;
        SDLPreloadPresentChoicesOperationUtilities.reachedMaxId = NO;
    }

    // Remove cells that are already loaded so that we don't try to re-upload them
    [self.cellsToUpload minusSet:self.loadedCells];

    // If loaded cells is full and we need to upload cells, just fail the operation since we can't successfully upload or present
    if ((self.loadedCells.count == UINT16_MAX) && (self.cellsToUpload.count > 0)) {
        return [self finishOperation:[NSError sdl_choiceSetManager_noIdsAvailable]];
    }

    // Assign Ids, then make cells to upload unique so that they upload properly (if necessary)
    [SDLPreloadPresentChoicesOperationUtilities assignIdsToCells:self.cellsToUpload loadedCells:self.loadedCells];
    [SDLPreloadPresentChoicesOperationUtilities makeCellsToUploadUnique:self.cellsToUpload basedOnLoadedCells:self.mutableLoadedCells windowCapability:self.windowCapability];

    // If we have a choice set, we need to replace the choices with the cells that we're uploading (with new ids and unique text) and the cells that are already on the head unit (with the correct cell ids and unique text)
    if (self.choiceSet != nil) {
        [SDLPreloadPresentChoicesOperationUtilities updateChoiceSet:self.choiceSet withLoadedCells:self.loadedCells cellsToUpload:self.cellsToUpload.set];
    }

    // Start uploading cell artworks, then cells themselves, then determine if we want to present, then update keyboard properties if necessary, then present the choice set, then revert keyboard properties if necessary
    [self sdl_uploadCellArtworksWithCompletionHandler:^(NSError * _Nullable uploadArtError) {
        // If some artworks failed to upload, we are still going to try to load the cells
        if (self.isCancelled || uploadArtError != nil) { return [self finishOperation:uploadArtError]; }

        [self sdl_uploadCellsWithCompletionHandler:^(NSError * _Nullable uploadCellsError) {
            // If this operation has been cancelled or if there was an error with loading the cells, we don't want to present, so we'll end the operation
            if (self.isCancelled || uploadCellsError != nil) { return [self finishOperation:uploadCellsError]; }

            // If necessary, present the choice set
            if (self.choiceSet == nil) { return [self finishOperation]; }
            [self sdl_updateKeyboardPropertiesWithCompletionHandler:^(NSError * _Nullable updateKeyboardPropertiesError) {
                if (self.isCancelled || updateKeyboardPropertiesError != nil) { return [self finishOperation]; }

                [self sdl_presentChoiceSetWithCompletionHandler:^(NSError * _Nullable presentError) {
                    [self sdl_resetKeyboardPropertiesWithCompletionHandler:^(NSError * _Nullable resetKeyboardPropertiesError) {
                        if (presentError != nil) { return [self finishOperation:presentError]; }
                        return [self finishOperation:resetKeyboardPropertiesError];
                    }];
                }];
            }];
        }];
    }];
}

#pragma mark - Getters / Setters

- (void)setLoadedCells:(NSSet<SDLChoiceCell *> *)loadedCells {
    _mutableLoadedCells = [loadedCells mutableCopy];
}

- (NSSet<SDLChoiceCell *> *)loadedCells {
    return [_mutableLoadedCells copy];
}

#pragma mark - Uploading Choice Data

- (void)sdl_uploadCellArtworksWithCompletionHandler:(void(^)(NSError *_Nullable error))completionHandler {
    self.currentState = SDLPreloadPresentChoicesOperationStateUploadingImages;

    NSArray<SDLArtwork *> *artworksToUpload = [self.class sdl_findAllArtworksToBeUploadedFromCells:self.cellsToUpload.array fileManager:self.fileManager windowCapability:self.windowCapability];
    if (artworksToUpload.count == 0) {
        SDLLogD(@"No choice artworks to be uploaded");
        return completionHandler(nil);
    }

    [self.fileManager uploadArtworks:[artworksToUpload copy] completionHandler:^(NSArray<NSString *> * _Nonnull artworkNames, NSError * _Nullable error) {
        if (error != nil) {
            SDLLogE(@"Error uploading choice artworks: %@", error);
        } else {
            SDLLogD(@"Finished uploading choice artworks");
            SDLLogV(@"%@", artworkNames);
        }

        completionHandler(error);
    }];
}

- (void)sdl_uploadCellsWithCompletionHandler:(void(^)(NSError *_Nullable error))completionHandler {
    self.currentState = SDLPreloadPresentChoicesOperationStateUploadingChoices;
    if (self.cellsToUpload.count == 0) { return completionHandler(nil); }

    NSMutableArray<SDLCreateInteractionChoiceSet *> *choiceRPCs = [NSMutableArray arrayWithCapacity:self.cellsToUpload.count];
    for (SDLChoiceCell *cell in self.cellsToUpload) {
        SDLCreateInteractionChoiceSet *csCell =  [self.class sdl_choiceFromCell:cell windowCapability:self.windowCapability displayName:self.displayName isVROptional:self.isVROptional];
        if (csCell != nil) {
            [choiceRPCs addObject:csCell];
        }
    }
    if (choiceRPCs.count == 0) {
        SDLLogE(@"All choice cells to send are nil, so the choice set will not be shown");
        return completionHandler([NSError sdl_choiceSetManager_failedToCreateMenuItems]);
    }
    
    __weak typeof(self) weakSelf = self;
    __block NSMutableDictionary<SDLRPCRequest *, NSError *> *errors = [NSMutableDictionary dictionary];
    [self.connectionManager sendRequests:[choiceRPCs copy] progressHandler:^(__kindof SDLRPCRequest * _Nonnull request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error, float percentComplete) {
        SDLCreateInteractionChoiceSet *sentRequest = (SDLCreateInteractionChoiceSet *)request;
        if (error != nil) {
            errors[request] = error;
        } else {
            [weakSelf.mutableLoadedCells addObject:[self sdl_cellFromChoiceId:(UInt16)sentRequest.interactionChoiceSetID.unsignedIntValue]];
        }
    } completionHandler:^(BOOL success) {
        NSError *preloadError = nil;
        if (!success) {
            SDLLogE(@"Error preloading choice cells: %@", errors);
            preloadError = [NSError sdl_choiceSetManager_choiceUploadFailed:errors];
        }

        SDLLogD(@"Finished preloading choice cells");

        return completionHandler(preloadError);
    }];
}

#pragma mark - Artwork

/// Get an array of artwork that needs to be uploaded form a list of menu cells
/// @param cells The menu cells to get artwork from
/// @param fileManager The file manager to use for checking artwork availability
/// @param windowCapability The window capability to use to check if artwork fields are supported
/// @returns The array of artwork that needs to be uploaded
+ (NSArray<SDLArtwork *> *)sdl_findAllArtworksToBeUploadedFromCells:(NSArray<SDLChoiceCell *> *)cells fileManager:(SDLFileManager *)fileManager windowCapability:(SDLWindowCapability *)windowCapability {
    NSMutableSet<SDLArtwork *> *mutableArtworks = [NSMutableSet set];
    for (SDLChoiceCell *cell in cells) {
        if ([windowCapability hasImageFieldOfName:SDLImageFieldNameChoiceImage] && [fileManager fileNeedsUpload:cell.artwork]) {
            [mutableArtworks addObject:cell.artwork];
        }

        if ([windowCapability hasImageFieldOfName:SDLImageFieldNameChoiceSecondaryImage] && [fileManager fileNeedsUpload:cell.secondaryArtwork]) {
            [mutableArtworks addObject:cell.secondaryArtwork];
        }
    }

    return [mutableArtworks allObjects];
}

#pragma mark - Presenting Choice Set

#pragma mark Updating Keyboard Properties

- (void)sdl_updateKeyboardPropertiesWithCompletionHandler:(void(^)(NSError *_Nullable))completionHandler {
    self.currentState = SDLPreloadPresentChoicesOperationStateUpdatingKeyboardProperties;
    if (self.keyboardDelegate == nil) { return completionHandler(nil); }

    // Check if we're using a keyboard (searchable) choice set and setup keyboard properties if we need to
    if (self.keyboardDelegate != nil && [self.keyboardDelegate respondsToSelector:@selector(customKeyboardConfiguration)]) {
        SDLKeyboardProperties *customProperties = self.keyboardDelegate.customKeyboardConfiguration;
        if (customProperties != nil) {
            self.customKeyboardProperties = customProperties;
        }
    }

    // Create the keyboard configuration based on the window capability's keyboard capabilities
    SDLKeyboardProperties *modifiedKeyboardConfig = [self.windowCapability createValidKeyboardConfigurationBasedOnKeyboardCapabilitiesFromConfiguration:self.customKeyboardProperties];
    if (modifiedKeyboardConfig == nil) { return completionHandler(nil); }

    SDLSetGlobalProperties *setProperties = [[SDLSetGlobalProperties alloc] init];
    setProperties.keyboardProperties = modifiedKeyboardConfig;

    [self.connectionManager sendConnectionRequest:setProperties withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) {
        if (error != nil) {
            SDLLogE(@"Error setting keyboard properties to new value: %@, with error: %@", request, error);
        }

        return completionHandler(error);
    }];
}

- (void)sdl_resetKeyboardPropertiesWithCompletionHandler:(void(^)(NSError *_Nullable))completionHandler {
    self.currentState = SDLPreloadPresentChoicesOperationStateResettingKeyboardProperties;
    if (self.keyboardDelegate == nil || self.originalKeyboardProperties == nil) { return completionHandler(nil); }

    SDLSetGlobalProperties *setProperties = [[SDLSetGlobalProperties alloc] init];
    setProperties.keyboardProperties = self.originalKeyboardProperties;

    [self.connectionManager sendConnectionRequest:setProperties withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) {
        if (error != nil) {
            SDLLogE(@"Error resetting keyboard properties to values: %@, with error: %@", request, error);
        }

        completionHandler(error);
    }];
}

#pragma mark Present

- (void)sdl_presentChoiceSetWithCompletionHandler:(void(^)(NSError *_Nullable error))completionHandler {
    self.currentState = SDLPreloadPresentChoicesOperationStatePresentingChoices;

    __weak typeof(self) weakself = self;
    [self.connectionManager sendConnectionRequest:[self sdl_performInteractionFromChoiceSet] withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) {
        if (error != nil) {
            SDLLogE(@"Presenting choice set request: %@, failed with response: %@, error: %@", request, response, error);
            return completionHandler(error);
        }

        SDLPerformInteractionResponse *performResponse = response;
        if (![performResponse.triggerSource isEqualToEnum:SDLTriggerSourceKeyboard]) {
            [weakself sdl_setSelectedCellWithId:performResponse.choiceID];
            weakself.selectedTriggerSource = performResponse.triggerSource;
        }

        return completionHandler(error);
    }];
}

#pragma mark Cancel

/**
 * Cancels the choice set. If the choice set has not yet been sent to Core, it will not be sent. If the choice set is already presented on Core, the choice set will be immediately dismissed. Canceling an already presented choice set will only work if connected to Core versions 6.0+. On older versions of Core, the choice set will not be dismissed.
 */
- (void)sdl_cancelInteraction {
    if (self.isFinished) {
        SDLLogW(@"This operation has already finished so it can not be canceled.");
        return;
    } else if (self.isCancelled) {
        SDLLogW(@"This operation has already been canceled. It will be finished at some point during the operation.");
        return;
    } else if (self.isExecuting) {
        if (self.currentState != SDLPreloadPresentChoicesOperationStatePresentingChoices) {
            SDLLogD(@"Canceling the operation before a present.");
            return [self cancel];
        } else if ([SDLGlobals.sharedGlobals.rpcVersion isLessThanVersion:[[SDLVersion alloc] initWithMajor:6 minor:0 patch:0]]) {
            SDLLogW(@"Canceling a currently displaying choice set is not supported on this head unit. Trying to cancel the operation.");
            return [self cancel];
        }

        self.currentState = SDLPreloadPresentChoicesOperationStateCancellingPresentChoices;
        SDLLogD(@"Canceling the presented choice set interaction");

        __weak typeof(self) weakSelf = self;
        SDLCancelInteraction *cancelInteraction = [[SDLCancelInteraction alloc] initWithPerformInteractionCancelID:self.cancelId];
        [self.connectionManager sendConnectionRequest:cancelInteraction withResponseHandler:^(__kindof SDLRPCRequest * _Nullable request, __kindof SDLRPCResponse * _Nullable response, NSError * _Nullable error) {
            if (error != nil) {
                weakSelf.internalError = error;
                SDLLogE(@"Error canceling the presented choice set: %@, with error: %@", request, error);
                return;
            }
            SDLLogD(@"The presented choice set was canceled successfully");
        }];
    } else {
        SDLLogD(@"Canceling a choice set that has not yet been sent to Core");
        [self cancel];
    }
}

#pragma mark Present Helpers

- (void)sdl_setSelectedCellWithId:(NSNumber<SDLInt> *)cellId {
    for (NSUInteger i = 0; i < self.choiceSet.choices.count; i++) {
        SDLChoiceCell *thisCell = self.choiceSet.choices[i];
        if (thisCell.choiceId == cellId.unsignedIntValue) {
            self.selectedCell = thisCell;
            self.selectedCellRow = i;
            break;
        }
    }
}

- (SDLPerformInteraction *)sdl_performInteractionFromChoiceSet {
    NSParameterAssert(self.choiceSet != nil);

    SDLLayoutMode layoutMode = nil;
    switch (self.choiceSet.layout) {
        case SDLChoiceSetLayoutList:
            layoutMode = self.keyboardDelegate ? SDLLayoutModeListWithSearch : SDLLayoutModeListOnly;
            break;
        case SDLChoiceSetLayoutTiles:
            layoutMode = self.keyboardDelegate ? SDLLayoutModeIconWithSearch : SDLLayoutModeIconOnly;
            break;
    }

    NSMutableArray<NSNumber<SDLInt> *> *choiceIds = [NSMutableArray arrayWithCapacity:self.choiceSet.choices.count];
    for (SDLChoiceCell *cell in self.choiceSet.choices) {
        [choiceIds addObject:@(cell.choiceId)];
    }

    SDLPerformInteraction *performInteraction = [[SDLPerformInteraction alloc] init];
    performInteraction.interactionMode = self.presentationMode;
    performInteraction.initialText = self.choiceSet.title;
    performInteraction.initialPrompt = self.choiceSet.initialPrompt;
    performInteraction.helpPrompt = self.choiceSet.helpPrompt;
    performInteraction.timeoutPrompt = self.choiceSet.timeoutPrompt;
    performInteraction.vrHelp = self.choiceSet.helpList;
    performInteraction.timeout = @((NSUInteger)(self.choiceSet.timeout * 1000));
    performInteraction.interactionLayout = layoutMode;
    performInteraction.interactionChoiceSetIDList = [choiceIds copy];
    performInteraction.cancelID = @(self.cancelId);

    return performInteraction;
}

#pragma mark Finding Cells

- (nullable SDLChoiceCell *)sdl_cellFromChoiceId:(UInt16)choiceId {
    for (SDLChoiceCell *cell in self.cellsToUpload) {
        if (cell.choiceId == choiceId) { return cell; }
    }

    return nil;
}

#pragma mark - Assembling Choice RPCs

+ (nullable SDLCreateInteractionChoiceSet *)sdl_choiceFromCell:(SDLChoiceCell *)cell windowCapability:(SDLWindowCapability *)windowCapability displayName:(NSString *)displayName isVROptional:(BOOL)isVROptional {
    NSArray<NSString *> *vrCommands = nil;
    if (cell.voiceCommands == nil) {
        vrCommands = isVROptional ? nil : @[[NSString stringWithFormat:@"%hu", cell.choiceId]];
    } else {
        vrCommands = cell.voiceCommands;
    }

    NSString *menuName = nil;
    if ([self sdl_shouldSendChoiceTextBasedOnWindowCapability:windowCapability displayName:displayName]) {
        menuName = cell.uniqueText;
    }

    if (menuName == nil) {
        SDLLogE(@"Could not convert SDLChoiceCell to SDLCreateInteractionChoiceSet because there was no menu name. This could be because the head unit does not support text field 'menuName', which means it does not support Choice Sets. It will not be shown. Cell: %@", cell);
        return nil;
    }
    
    NSString *secondaryText = [self sdl_shouldSendChoiceSecondaryTextBasedOnWindowCapability:windowCapability] ? cell.secondaryText : nil;
    NSString *tertiaryText = [self sdl_shouldSendChoiceTertiaryTextBasedOnWindowCapability:windowCapability] ? cell.tertiaryText : nil;

    SDLImage *image = [self sdl_shouldSendChoicePrimaryImageBasedOnWindowCapability:windowCapability] ? cell.artwork.imageRPC : nil;
    SDLImage *secondaryImage = [self sdl_shouldSendChoiceSecondaryImageBasedOnWindowCapability:windowCapability] ? cell.secondaryArtwork.imageRPC : nil;

    SDLChoice *choice = [[SDLChoice alloc] initWithId:cell.choiceId menuName:menuName vrCommands:vrCommands image:image secondaryText:secondaryText secondaryImage:secondaryImage tertiaryText:tertiaryText];

    return [[SDLCreateInteractionChoiceSet alloc] initWithId:(UInt32)choice.choiceID.unsignedIntValue choiceSet:@[choice]];
}

/// Determine if we should send primary text. If textFields is nil, we don't know the capabilities and we will send everything.
+ (BOOL)sdl_shouldSendChoiceTextBasedOnWindowCapability:(SDLWindowCapability *)windowCapability displayName:(NSString *)displayName {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    if ([displayName isEqualToString:SDLDisplayTypeGen38Inch]) {
        return YES;
    }
#pragma clang diagnostic pop

    return [windowCapability hasTextFieldOfName:SDLTextFieldNameMenuName];
}

/// Determine if we should send secondary text. If textFields is nil, we don't know the capabilities and we will send everything.
+ (BOOL)sdl_shouldSendChoiceSecondaryTextBasedOnWindowCapability:(SDLWindowCapability *)windowCapability {
    return [windowCapability hasTextFieldOfName:SDLTextFieldNameSecondaryText];
}

/// Determine if we should send tertiary text. If textFields is nil, we don't know the capabilities and we will send everything.
+ (BOOL)sdl_shouldSendChoiceTertiaryTextBasedOnWindowCapability:(SDLWindowCapability *)windowCapability {
    return [windowCapability hasTextFieldOfName:SDLTextFieldNameTertiaryText];
}

/// Determine if we should send the primary image. If imageFields is nil, we don't know the capabilities and we will send everything.
+ (BOOL)sdl_shouldSendChoicePrimaryImageBasedOnWindowCapability:(SDLWindowCapability *)windowCapability {
    return [windowCapability hasImageFieldOfName:SDLImageFieldNameChoiceImage];
}

/// Determine if we should send the secondary image. If imageFields is nil, we don't know the capabilities and we will send everything.
+ (BOOL)sdl_shouldSendChoiceSecondaryImageBasedOnWindowCapability:(SDLWindowCapability *)windowCapability {
    return [windowCapability hasImageFieldOfName:SDLImageFieldNameChoiceSecondaryImage];
}

#pragma mark - SDL Notifications

- (void)sdl_keyboardInputNotification:(SDLRPCNotificationNotification *)notification {
    if (self.isCancelled) { return [self finishOperation]; }

    if (self.keyboardDelegate == nil) { return; }
    SDLOnKeyboardInput *onKeyboard = notification.notification;

    if ([self.keyboardDelegate respondsToSelector:@selector(keyboardDidSendEvent:text:)]) {
        [self.keyboardDelegate keyboardDidSendEvent:onKeyboard.event text:onKeyboard.data];
    }

    __weak typeof(self) weakself = self;
    if ([onKeyboard.event isEqualToEnum:SDLKeyboardEventVoice] || [onKeyboard.event isEqualToEnum:SDLKeyboardEventSubmitted]) {
        // Submit voice or text
        [self.keyboardDelegate userDidSubmitInput:onKeyboard.data withEvent:onKeyboard.event];
    } else if ([onKeyboard.event isEqualToEnum:SDLKeyboardEventKeypress]) {
        // Notify of keypress
        if ([self.keyboardDelegate respondsToSelector:@selector(updateAutocompleteWithInput:autoCompleteResultsHandler:)]) {
            [self.keyboardDelegate updateAutocompleteWithInput:onKeyboard.data autoCompleteResultsHandler:^(NSArray<NSString *> * _Nullable updatedAutoCompleteList) {
                __strong typeof(self) strongself = weakself;
                NSArray<NSString *> *newList = nil;
                if (updatedAutoCompleteList.count > 100) {
                    newList = [updatedAutoCompleteList subarrayWithRange:NSMakeRange(0, 100)];
                } else {
                    newList = updatedAutoCompleteList;
                }

                strongself.customKeyboardProperties.autoCompleteList = (newList.count > 0) ? newList : @[];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
                strongself.customKeyboardProperties.autoCompleteText = (newList.count > 0) ? newList.firstObject : nil;
#pragma clang diagnostic pop
                [strongself sdl_updateKeyboardPropertiesWithCompletionHandler:^(NSError * _Nullable error) {}];
            }];
        }

        if ([self.keyboardDelegate respondsToSelector:@selector(updateCharacterSetWithInput:completionHandler:)]) {
            [self.keyboardDelegate updateCharacterSetWithInput:onKeyboard.data completionHandler:^(NSArray<NSString *> *updatedCharacterSet) {
                __strong typeof(self) strongself = weakself;
                strongself.customKeyboardProperties.limitedCharacterList = updatedCharacterSet;
                [strongself sdl_updateKeyboardPropertiesWithCompletionHandler:^(NSError * _Nullable error) {}];
            }];
        }
    } else if ([onKeyboard.event isEqualToEnum:SDLKeyboardEventAborted] || [onKeyboard.event isEqualToEnum:SDLKeyboardEventCancelled]) {
        // Notify of abort / cancellation
        [self.keyboardDelegate keyboardDidAbortWithReason:onKeyboard.event];
    } else if ([onKeyboard.event isEqualToEnum:SDLKeyboardEventInputKeyMaskEnabled] || [onKeyboard.event isEqualToEnum:SDLKeyboardEventInputKeyMaskDisabled]) {
        // Notify of key mask change
        if ([self.keyboardDelegate respondsToSelector:@selector(keyboardDidUpdateInputMask:)]) {
            BOOL isEnabled = [onKeyboard.event isEqualToEnum:SDLKeyboardEventInputKeyMaskEnabled];
            [self.keyboardDelegate keyboardDidUpdateInputMask:isEnabled];
        }
    }
}

#pragma mark - Property Overrides

- (void)finishOperation {
    [self finishOperation:nil];
}

- (void)finishOperation:(nullable NSError *)error {
    self.currentState = SDLPreloadPresentChoicesOperationStateFinishing;

    self.internalError = error;
    self.preloadCompletionHandler(self.loadedCells, self.internalError);

    if (self.choiceSet.delegate == nil) {
        SDLLogD(@"Preload finished, no choice set delegate was set, so no present will occur.");
    } else if (error != nil) {
        SDLLogW(@"Choice set did error: %@", self.internalError);
        [self.choiceSet.delegate choiceSet:self.choiceSet didReceiveError:self.internalError];
    } else if (self.selectedCell != nil) {
        SDLLogD(@"Choice set did present successfully: %@, selected choice: %@, trigger source: %@, row index: %ld", self.choiceSet, self.selectedCell, self.selectedTriggerSource, self.selectedCellRow);
        [self.choiceSet.delegate choiceSet:self.choiceSet didSelectChoice:self.selectedCell withSource:self.selectedTriggerSource atRowIndex:self.selectedCellRow];
    } else {
        SDLLogE(@"Present finished, but an unhandled state occurred and callback failed");
    }

    [super finishOperation];
}

- (nullable NSString *)name {
    return [NSString stringWithFormat:@"%@ - %@", self.class, self.operationId];
}

- (NSOperationQueuePriority)queuePriority {
    return NSOperationQueuePriorityNormal;
}

- (nullable NSError *)error {
    return self.internalError;
}

@end

NS_ASSUME_NONNULL_END