1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#import "TapLatencyController.h"
18
19#import "NSArray+Extensions.h"
20#import "UIAlertView+Extensions.h"
21#import "WALTAppDelegate.h"
22#import "WALTClient.h"
23#import "WALTLogger.h"
24#import "WALTTouch.h"
25
26@interface TapLatencyController ()
27- (void)updateCountDisplay;
28- (void)processEvent:(UIEvent *)event;
29- (void)appendToLogView:(NSString *)string;
30- (void)computeStatisticsForPhase:(UITouchPhase)phase;
31@end
32
33@implementation TapLatencyController {
34  WALTClient *_client;
35  WALTLogger *_logger;
36
37  // Statistics
38  unsigned int _downCount;
39  unsigned int _downCountRecorded;
40  unsigned int _upCount;
41  unsigned int _upCountRecorded;
42
43  NSMutableArray<WALTTouch *> *_touches;
44}
45
46- (void)viewDidLoad {
47  [super viewDidLoad];
48
49  self.logView.selectable = YES;
50  self.logView.text = [NSString string];
51  self.logView.selectable = NO;
52
53  _logger = [WALTLogger sessionLogger];
54  _client = ((WALTAppDelegate *)[UIApplication sharedApplication].delegate).client;
55}
56
57- (void)viewWillAppear:(BOOL)animated {
58  [super viewWillAppear:animated];
59
60  [_logger appendString:@"TAPLATENCY\n"];
61  [self reset:nil];
62}
63
64- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
65  [self processEvent:event];
66  [self updateCountDisplay];
67}
68
69- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
70  [self processEvent:event];
71  [self updateCountDisplay];
72}
73
74- (void)updateCountDisplay {
75  NSString *counts = [NSString stringWithFormat:@"N ↓%u (%u) ↑%u (%u)",
76                      _downCountRecorded, _downCount, _upCountRecorded, _upCount];
77  self.countLabel.text = counts;
78}
79
80- (void)processEvent:(UIEvent *)event {
81  // TODO(pquinn): Pick first/last coalesced touch?
82
83  NSTimeInterval kernelTime = event.timestamp;
84  NSTimeInterval callbackTime = _client.currentTime;
85
86  NSError *error = nil;
87  NSTimeInterval physicalTime = [_client lastShockTimeWithError:&error];
88  if (physicalTime == -1) {
89    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error];
90    [alert show];
91    return;
92  }
93
94  WALTTouch *touch = [[WALTTouch alloc] initWithEvent:event];
95  touch.callbackTime = callbackTime;
96  touch.physicalTime = physicalTime;
97
98  NSString *actionString = nil;
99  if (touch.phase == UITouchPhaseBegan) {
100    _downCount += 1;
101    actionString = @"ACTION_DOWN";
102  } else {
103    _upCount += 1;
104    actionString = @"ACTION_UP";
105  }
106
107  if (physicalTime == 0) {
108    [_logger appendFormat:@"%@\tX\tno shock\n", actionString];
109    [self appendToLogView:[NSString stringWithFormat:@"%@: No shock detected\n", actionString]];
110    return;
111  }
112
113  NSTimeInterval physicalToKernel = kernelTime - physicalTime;
114  NSTimeInterval kernelToCallback = callbackTime - kernelTime;
115
116  if (physicalToKernel < 0 || physicalToKernel > 0.2) {
117    [_logger appendFormat:@"%@\tX\tbogus kernelTime\t%f\n", actionString, physicalToKernel];
118    [self appendToLogView:
119        [NSString stringWithFormat:@"%@: Bogus P → K: %.3f s\n", actionString, physicalToKernel]];
120    return;
121  }
122
123  [_logger appendFormat:@"%@\tO\t%f\t%f\t%f\n",
124      actionString, _client.baseTime, physicalToKernel, kernelToCallback];
125
126  [self appendToLogView:
127      [NSString stringWithFormat:@"%@: P → K: %.3f s; K → C: %.3f s\n",
128        actionString, physicalToKernel, kernelToCallback]];
129
130  [_touches addObject:touch];
131  if (touch.phase == UITouchPhaseBegan) {
132    _downCountRecorded += 1;
133  } else {
134    _upCountRecorded += 1;
135  }
136}
137
138- (IBAction)reset:(id)sender {
139  _downCount = 0;
140  _downCountRecorded = 0;
141  _upCount = 0;
142  _upCountRecorded = 0;
143  [self updateCountDisplay];
144
145  _touches = [[NSMutableArray<WALTTouch *> alloc] init];
146
147  NSError *error = nil;
148  if (![_client syncClocksWithError:&error]) {
149    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"WALT Connection Error" error:error];
150    [alert show];
151  }
152
153  [_logger appendString:@"RESET\n"];
154  [self appendToLogView:@"===========================================\n"];
155}
156
157- (IBAction)computeStatistics:(id)sender {
158  [self appendToLogView:@"-------------------------------------------\n"];
159  [self appendToLogView:@"Medians:\n"];
160  [self computeStatisticsForPhase:UITouchPhaseBegan];
161  [self computeStatisticsForPhase:UITouchPhaseEnded];
162
163  [self reset:sender];
164}
165
166- (void)computeStatisticsForPhase:(UITouchPhase)phase {
167  NSMutableArray<NSNumber *> *p2k = [[NSMutableArray<NSNumber *> alloc] init];
168  NSMutableArray<NSNumber *> *k2c = [[NSMutableArray<NSNumber *> alloc] init];
169
170  for (WALTTouch *touch in _touches) {
171    if (touch.phase != phase) {
172      continue;
173    }
174
175    [p2k addObject:[NSNumber numberWithDouble:touch.kernelTime - touch.physicalTime]];
176    [k2c addObject:[NSNumber numberWithDouble:touch.callbackTime - touch.kernelTime]];
177  }
178
179  NSNumber *p2kMedian = [p2k medianValue];
180  NSNumber *k2cMedian = [k2c medianValue];
181
182  NSString *actionString = (phase == UITouchPhaseBegan ? @"ACTION_DOWN" : @"ACTION_UP");
183  [self appendToLogView:
184      [NSString stringWithFormat:@"%@: P → K: %.3f s; K → C: %.3f s\n",
185        actionString, p2kMedian.doubleValue, k2cMedian.doubleValue]];
186}
187
188- (void)appendToLogView:(NSString*)string {
189  self.logView.selectable = YES;
190  self.logView.text = [self.logView.text stringByAppendingString:string];
191  [self.logView scrollRangeToVisible:NSMakeRange(self.logView.text.length - 2, 1)];
192  self.logView.selectable = NO;
193}
194@end
195