forked from aws-amplify/aws-sdk-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAWSLexVoiceButton.m
549 lines (454 loc) · 21.3 KB
/
AWSLexVoiceButton.m
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
//
// Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//
#import "AWSLexVoiceButton.h"
#import <AVFoundation/AVFoundation.h>
#import <QuartzCore/QuartzCore.h>
#define LINE_WIDTH 3
#define RADIUS 25
#define SIZE 55
NSString *const AWSLexVoiceButtonErrorDomain = @"com.amazonaws.AWSLexVoiceButtonErrorDomain";
NSString *const AWSLexVoiceButtonKey = @"AWSLexVoiceButton";
static NSString *ProgressAnimationKey = @"progressanimation.rotation";
static NSString *MicrophoneImageKey = @"Microphone";
static NSString *LexSpeakImageKey = @"LexSpeak";
static NSString *VoiceButtonUserAgent = @"LexVoiceButton";
static NSString *ImageButtonTintColorUserInfoKey = @"imageButton.imageView.tintColor";
static NSString *BackgroundLayerStrokeColorUserInfoKey = @"backgroundLayer.strokeColor";
static NSString *RESOURCES_BUNDLE = @"AWSLex.bundle";
@implementation UIColor (AWSLexVoiceButton)
+ (UIColor *)colorWithHexValue:(NSInteger)hexValue {
float red = ((hexValue & 0xFF0000) >> 16) / 255.0f;
float green = ((hexValue & 0xFF00) >> 8) / 255.0f;
float blue = (hexValue & 0xFF) / 255.0f;
return [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
}
@end
@implementation UIView (AWSLexVoiceButton)
/**
Simple Push transition from bottom to top.
*/
- (void)pushTransition:(CFTimeInterval)duration {
CATransition *animation = [CATransition new];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.type = kCATransitionPush;
animation.subtype = kCATransitionFromTop;
animation.duration = duration;
[self.layer addAnimation:animation forKey:kCATransitionPush];
}
@end
@interface AWSLexVoiceButtonResponse()
@property (nonatomic, strong, readwrite) NSString * _Nullable intent;
@property (nonatomic, strong, readwrite) NSString * _Nullable outputText;
@property (nonatomic, strong, readwrite) NSDictionary * _Nullable slots;
@property (nonatomic, strong, readwrite) NSString * _Nullable elicitSlot;
@property (nonatomic, assign, readwrite) AWSLexDialogState dialogState;
@property (nonatomic, strong, readwrite) NSDictionary * _Nullable sessionAttributes;
@property (nonatomic, strong, readwrite) NSData * _Nullable audioStream;
@property (nonatomic, strong, readwrite) NSString * _Nullable audioContentType;
@property (nonatomic, strong, readwrite) NSString * _Nullable inputTranscript;
- (instancetype) initWithOutputText:(NSString *)outputText
intent:(NSString * _Nullable)intent
sessionAttributes:(NSDictionary * _Nullable)sessionAttributes
slotToElicit:(NSString * _Nullable)elicitSlot
slots:(NSDictionary * _Nullable)slots
dialogState:(AWSLexDialogState)dialogState
audioStream:(NSData * _Nullable)audioStream
audioContentType:(NSString * _Nullable)audioContentType
inputTranscript:(NSString * _Nullable)inputTranscript;
@end
@implementation AWSLexVoiceButtonResponse
- (instancetype) initWithOutputText:(NSString *)outputText
intent:(NSString * _Nullable)intent
sessionAttributes:(NSDictionary * _Nullable)sessionAttributes
slotToElicit:(NSString * _Nullable)elicitSlot
slots:(NSDictionary * _Nullable)slots
dialogState:(AWSLexDialogState)dialogState
audioStream:(NSData * _Nullable)audioStream
audioContentType:(NSString * _Nullable)audioContentType
inputTranscript:(NSString * _Nullable)inputTranscript{
self = [super init];
if(self) {
_intent = intent;
_slots = slots;
_sessionAttributes = sessionAttributes;
_outputText = outputText;
_elicitSlot = elicitSlot;
_dialogState = dialogState;
_audioStream = [audioStream copy];
_audioContentType = audioContentType;
_inputTranscript = inputTranscript;
}
return self;
}
@end
@interface AWSLexInteractionKit()
@property (nonatomic, readonly) AWSServiceConfiguration *configuration;
@end
@interface AWSLexVoiceButton()<AWSLexInteractionDelegate, AWSLexMicrophoneDelegate, AWSLexAudioPlayerDelegate>
@property (nonatomic, assign) double voiceLevel;
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, strong) CADisplayLink *progressLink;
@property (nonatomic, strong) CAShapeLayer *rightShapeLayer;
@property (nonatomic, strong) CAShapeLayer *leftShapeLayer;
@property (nonatomic, strong) UIImage *microphoneImage;
@property (nonatomic, strong) UIImage *listenImage;
@property (nonatomic, strong) CAShapeLayer *backgroundLayer;
@property (nonatomic, strong) CAShapeLayer *progressLayer;
@property (nonatomic, strong) AWSLexInteractionKit *interactionKit;
@property (nonatomic, strong) UIColor *defaultMicImageColor;
@property (nonatomic, strong) UIColor *defaultLexImageColor;
@end
@implementation AWSLexVoiceButton {
BOOL isListening;
BOOL canListen;
BOOL isAnimating;
UITapGestureRecognizer *onTouch;
UIButton *imageButton;
CAMediaTimingFunction *timingFunction;
CGPoint center;
UIColor *lightGrey;
BOOL errorFired;
}
@synthesize microphoneImageColor=_microphoneImageColor;
# pragma mark - Properties
- (UIColor *)defaultMicImageColor {
if (!_defaultMicImageColor) {
_defaultMicImageColor = [UIColor colorWithHexValue:0x329ad6];
}
return _defaultMicImageColor;
}
- (UIColor *)defaultLexImageColor {
if (!_defaultLexImageColor) {
_defaultLexImageColor = [UIColor colorWithHexValue:0x4383c4];
}
return _defaultLexImageColor;
}
- (void)setMicrophoneImageColor:(UIColor *)microphoneImageColor {
_microphoneImageColor = microphoneImageColor;
if (imageButton.imageView.image == self.microphoneImage) {
imageButton.imageView.tintColor = _microphoneImageColor;
}
}
- (UIColor *)microphoneImageColor {
if (_microphoneImageColor == nil) {
return self.defaultMicImageColor;
}
return _microphoneImageColor;
}
- (UIColor *)lexImageColor {
if (_lexImageColor == nil) {
return self.defaultLexImageColor;
}
return _lexImageColor;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
if(self = [super initWithCoder:aDecoder]) {
imageButton = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, SIZE, SIZE)];
// Use microphone image when the user speaks.
UIImage *temp = [self getImageFromBundle:MicrophoneImageKey];
self.microphoneImage = [temp imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[self setButtonImage:self.microphoneImage imageTintColor:self.microphoneImageColor animated:NO];
[imageButton addTarget:self action:@selector(startMonitoring:) forControlEvents:UIControlEventTouchDown];
imageButton.imageView.tintColor = self.microphoneImageColor;
// Use listen image when Lex speaks.
temp = [self getImageFromBundle:LexSpeakImageKey];
self.listenImage = [temp imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
lightGrey = [UIColor colorWithWhite:0 alpha:0.2];
[self addShapeLayer];
[self addSubview:imageButton];
self.layer.cornerRadius = RADIUS + 2;
self.clipsToBounds = YES;
timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
self.errorColor = [UIColor redColor];
}
return self;
}
- (UIImage *)getImageFromBundle:(NSString *)imageName {
NSBundle *currentBundle = [NSBundle bundleForClass:[self class]];
UIImage *imageFromBundle = [UIImage imageNamed:imageName inBundle:currentBundle compatibleWithTraitCollection:nil];
if (imageFromBundle) {
return imageFromBundle;
}
NSURL *url = [[currentBundle resourceURL] URLByAppendingPathComponent:RESOURCES_BUNDLE];
NSBundle *assetsBundle = [NSBundle bundleWithURL:url];
return [UIImage imageNamed:imageName inBundle:assetsBundle compatibleWithTraitCollection:nil];
}
//if the view is removed (view will disappear),
//then we cancel the request and reset everything
- (void)willMoveToSuperview:(UIView *)newSuperview{
if(!newSuperview) {
isListening = NO;
[self stopProgress];
[self stopDisplay];
[self removeDelegates];
[self.interactionKit cancel];
} else {
[self setDelegates];
}
}
- (void)setDelegates{
self.interactionKit.audioPlayerDelegate = self;
self.interactionKit.microphoneDelegate = self;
self.interactionKit.interactionDelegate = self;
}
- (void) removeDelegates {
self.interactionKit.audioPlayerDelegate = nil;
self.interactionKit.microphoneDelegate = nil;
self.interactionKit.interactionDelegate = nil;
}
- (CGSize)intrinsicContentSize{
return CGSizeMake(SIZE, SIZE);
}
- (void)layoutSubviews{
self.bounds = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, SIZE, SIZE);
[super layoutSubviews];
}
- (void)startMonitoring:(id)sender{
if(!self.interactionKit){
self.interactionKit = [AWSLexInteractionKit interactionKitForKey:AWSLexVoiceButtonKey];
if(!self.interactionKit){
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"Cannot find interactionKit with key %@", AWSLexVoiceButtonKey ]
userInfo:nil];
}
[self.interactionKit.configuration addUserAgentProductToken:VoiceButtonUserAgent];
[self setDelegates];
}
NSError *audioSessionError;
AWSLexAudioSession *session = [AWSLexAudioSession sharedInstance];
[session setPlayAndRecordCategory:&audioSessionError];
if(audioSessionError){
[self handleError:audioSessionError];
return;
}
[session requestRecordPermission:^(BOOL granted) {
self->canListen = granted;
if(granted) {
[self startListening];
} else {
NSError *permissionDeniedError = [[NSError alloc]initWithDomain:AWSLexVoiceButtonErrorDomain code:AWSLexVoiceButtonErrorCodeAudioRecordingPermisionDenied userInfo:nil];
[self handleError:permissionDeniedError];
}
}];
}
- (void)handleError:(NSError *)error{
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate voiceButton:self onError:error];
});
}
- (void)startListening{
if(!isListening && canListen){
[self.interactionKit audioInAudioOut];
}
}
- (void)startDisplay{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
-(void)stopDisplay{
[self.leftShapeLayer setStrokeEnd:0];
[self.rightShapeLayer setStrokeEnd:0];
[self.displayLink invalidate];
self.displayLink = nil;
}
/**
Set image and tintClor for imageButton.
*/
- (void)setButtonImage:(UIImage *)image imageTintColor:(UIColor *)color animated:(BOOL)animated {
if (self.animateOnImageSwitching && animated) {
// Use 0.25 seconds for animation to provide a clear visual indication in order to help the user
// to avoid talking too early.
[imageButton pushTransition:0.25];
}
[imageButton setImage:image forState:UIControlStateNormal];
imageButton.imageView.tintColor = color;
}
- (void)addShapeLayer{
self.backgroundLayer = [CAShapeLayer layer];
[self.backgroundLayer setStrokeColor:[lightGrey CGColor]];
[self.backgroundLayer setFillColor:nil];
[self.backgroundLayer setLineWidth:LINE_WIDTH];
UIBezierPath *backgroundPath = [UIBezierPath bezierPath];
[backgroundPath addArcWithCenter:imageButton.center radius:RADIUS startAngle:0 endAngle:2*M_PI clockwise:YES];
[self.backgroundLayer setPath:[backgroundPath CGPath]];
[self.backgroundLayer setStrokeEnd:1];
[self.layer addSublayer:self.backgroundLayer];
center = imageButton.center;
self.rightShapeLayer = [CAShapeLayer layer];
self.leftShapeLayer = [CAShapeLayer layer];
CGFloat startAngle = M_PI_2;
CGFloat endAngle = -M_PI_2;
for (CAShapeLayer *shapeLayer in @[self.rightShapeLayer, self.leftShapeLayer]) {
[shapeLayer setStrokeColor:[imageButton.imageView.tintColor CGColor]];
[shapeLayer setFillColor:nil];
[shapeLayer setLineWidth:LINE_WIDTH];
[self.layer addSublayer:shapeLayer];
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
[bezierPath addArcWithCenter:imageButton.center radius:RADIUS
startAngle:startAngle
endAngle:endAngle
clockwise:shapeLayer == self.leftShapeLayer];
[shapeLayer setPath:[bezierPath CGPath]];
[shapeLayer setStrokeEnd:0];
}
}
- (void)handleDisplayLink{
[self.leftShapeLayer setStrokeEnd:self.voiceLevel];
[self.rightShapeLayer setStrokeEnd:self.voiceLevel];
}
- (CAShapeLayer *)progressLayer {
if (!_progressLayer) {
_progressLayer = [CAShapeLayer layer];
_progressLayer.strokeColor = self.microphoneImageColor.CGColor;
_progressLayer.fillColor = nil;
_progressLayer.lineWidth = LINE_WIDTH;
_progressLayer.hidden = YES;
UIBezierPath *circlePath = [UIBezierPath bezierPath];
[circlePath addArcWithCenter:imageButton.center radius:RADIUS startAngle:-M_PI_4 endAngle:3 * M_PI_2 clockwise:YES];
_progressLayer.path = circlePath.CGPath;
_progressLayer.frame = imageButton.frame;
[self.layer addSublayer:_progressLayer];
}
return _progressLayer;
}
- (void)startProgress{
if(!isAnimating){
isAnimating = YES;
CABasicAnimation *rotationAnimation = [CABasicAnimation
animationWithKeyPath:@"transform.rotation.z"];
[rotationAnimation setFromValue:0];
[rotationAnimation setToValue:@(2*M_PI)];
[rotationAnimation setDuration:1.0f];
[rotationAnimation setRepeatCount:INFINITY];
[self.progressLayer addAnimation:rotationAnimation forKey:ProgressAnimationKey];
self.progressLayer.hidden = NO;
self.backgroundLayer.hidden = YES;
self.leftShapeLayer.hidden = YES;
self.rightShapeLayer.hidden = YES;
}
}
- (void)stopProgress{
if (isAnimating) {
[_progressLayer removeAnimationForKey:ProgressAnimationKey];
_progressLayer.hidden = YES;
isAnimating = NO;
self.backgroundLayer.hidden = NO;
self.leftShapeLayer.hidden = NO;
self.rightShapeLayer.hidden = NO;
self.voiceLevel = 0;
[_progressLayer removeFromSuperlayer];
_progressLayer = nil;
}
}
#pragma mark - AWSLexMicrophoneDelegate
- (void)interactionKit:(AWSLexInteractionKit *)interactionKit onSoundLevelChanged:(double)soundLevel{
self.voiceLevel = soundLevel;
}
- (void)interactionKitOnRecordingStart:(AWSLexInteractionKit *)interactionKit {
// Voice recording is about to start.
self.progressLayer.strokeColor = [self.microphoneImageColor CGColor];
[self.rightShapeLayer setStrokeColor:[imageButton.imageView.tintColor CGColor]];
[self.leftShapeLayer setStrokeColor:[imageButton.imageView.tintColor CGColor]];
isListening = YES;
[self startDisplay];
}
#pragma mark -
#pragma mark - AWSLexInteractionKitDelegate
- (void)interactionKitOnRecordingEnd:(AWSLexInteractionKit *)interactionKit audioStream:(nonnull NSData *)audioStream contentType:(nonnull NSString *)contentType{
isListening = NO;
[self stopDisplay];
[self startProgress];
}
- (void)interactionKit:(AWSLexInteractionKit *)interactionKit onError:(NSError *)error{
dispatch_async(dispatch_get_main_queue(), ^{
self->isAnimating = YES;//fake animation so that next step succeeds
self->isListening = NO;
[self stopProgress];
NSDictionary *userInfo;
// If AWSLexInteractionKitErrorCodeDialogFailed is encountered, audio would be playing.
// for the rest of errors, we would want to use microphone color.
if ([error.domain isEqualToString:AWSLexInteractionKitErrorDomain]
&& error.code == AWSLexInteractionKitErrorCodeDialogFailed) {
userInfo = @{
ImageButtonTintColorUserInfoKey: self->imageButton.imageView.tintColor,
BackgroundLayerStrokeColorUserInfoKey: [UIColor colorWithCGColor:self.backgroundLayer.strokeColor]
};
} else {
userInfo = @{
ImageButtonTintColorUserInfoKey: self.microphoneImageColor,
BackgroundLayerStrokeColorUserInfoKey: self->lightGrey
};
}
self.backgroundLayer.strokeColor = [self.errorColor CGColor];
self->imageButton.imageView.tintColor = self.errorColor;
//start a timer for a few secs to display error code to reset the error mode.
[NSTimer scheduledTimerWithTimeInterval:1.5f
target:self
selector:@selector(resetError:)
userInfo:userInfo
repeats:NO];
});
if(self.delegate && [self.delegate respondsToSelector:@selector(voiceButton:onError:)]){
[self.delegate voiceButton:self onError:error];
}
}
- (void)resetError:(NSTimer *)timer {
NSDictionary *userInfo = timer.userInfo;
self.backgroundLayer.strokeColor = ((UIColor *)userInfo[BackgroundLayerStrokeColorUserInfoKey]).CGColor;
imageButton.imageView.tintColor = (UIColor *)userInfo[ImageButtonTintColorUserInfoKey];
}
- (void)interactionKit:(AWSLexInteractionKit *)interactionKit
switchModeInput:(AWSLexSwitchModeInput *)switchModeInput
completionSource:(AWSTaskCompletionSource<AWSLexSwitchModeResponse *> *)completionSource{
dispatch_async(dispatch_get_main_queue(), ^{
[self stopProgress];
if(self.delegate && [self.delegate respondsToSelector:@selector(voiceButton:onResponse:)]){
AWSLexVoiceButtonResponse *response = [[AWSLexVoiceButtonResponse alloc] initWithOutputText:switchModeInput.outputText
intent:switchModeInput.intent
sessionAttributes:switchModeInput.sessionAttributes
slotToElicit:switchModeInput.elicitSlot
slots:switchModeInput.slots
dialogState:switchModeInput.dialogState
audioStream:switchModeInput.audioStream
audioContentType:switchModeInput.audioContentType
inputTranscript:switchModeInput.inputTranscript];
[self.delegate voiceButton:self onResponse:response];
}
});
AWSLexSwitchModeResponse *switchModeResponse = [AWSLexSwitchModeResponse new];
[switchModeResponse setInteractionMode:AWSLexInteractionModeSpeech];
[switchModeResponse setSessionAttributes:switchModeResponse.sessionAttributes];
[completionSource setResult:switchModeResponse];
}
- (void)interactionKit:(AWSLexInteractionKit *)interactionKit onDialogReadyForFulfillmentForIntent:(nonnull NSString *)intentName slots:(nonnull NSDictionary *)slots{
if(self.delegate && [self.delegate respondsToSelector:@selector(voiceButtononReadyToFullFill:withSlots:)]){
[self.delegate voiceButtononReadyToFullFill:self withSlots:slots];
}
}
#pragma mark -
#pragma mark - AWSLexAudioPlaybackDelegate
- (void)interactionKitOnAudioPlaybackStarted:(AWSLexInteractionKit *)interactionKit {
// Lex is about to talk. Switch listen image in order to provide clear visual indication that you need to listen.
[self setButtonImage:self.listenImage imageTintColor:self.lexImageColor animated:YES];
// When Lex speaks, backgroundLayer is used so we need to change its color instead of progressLayer.
self.backgroundLayer.strokeColor = [imageButton.imageView.tintColor CGColor];
}
- (void)interactionKitOnAudioPlaybackFinished:(AWSLexInteractionKit *)interactionKit {
// Lex finished talking. Switch to microphone image.
[self setButtonImage:self.microphoneImage imageTintColor:self.microphoneImageColor animated:YES];
self.backgroundLayer.strokeColor = [lightGrey CGColor];
}
#pragma mark -
@end