From 16b11d479b5a5b6a09a415a7c477fa0a32dbbbb9 Mon Sep 17 00:00:00 2001 From: Theodore Dubois Date: Sun, 20 Oct 2019 20:48:29 -0700 Subject: [PATCH] Add location tracking device Supports tracking your location in the background, which has the nice side effect of keeping everything in app running in the background. #249 --- app/AppDelegate.m | 8 +- app/Info.plist | 12 +- app/LocationDevice.h | 8 ++ app/LocationDevice.m | 174 +++++++++++++++++++++++ app/PasteboardDevice.h | 2 + app/PasteboardDevice.m | 252 ++++++++++++++++++++++++++++++++++ fs/devices.h | 2 + fs/fd.h | 3 + iSH.xcodeproj/project.pbxproj | 30 +++- 9 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 app/LocationDevice.h create mode 100644 app/LocationDevice.m create mode 100644 app/PasteboardDevice.h create mode 100644 app/PasteboardDevice.m diff --git a/app/AppDelegate.m b/app/AppDelegate.m index 0432f847..0953c70c 100644 --- a/app/AppDelegate.m +++ b/app/AppDelegate.m @@ -9,7 +9,8 @@ #include #include #import "AppDelegate.h" -#import "Pasteboard.h" +#import "PasteboardDevice.h" +#import "LocationDevice.h" #import "TerminalViewController.h" #import "UserPreferences.h" #include "kernel/init.h" @@ -116,6 +117,11 @@ static void ios_handle_die(const char *msg) { return err; } generic_mknod("/dev/clipboard", S_IFCHR|0666, dev_make(DYN_DEV_MAJOR, DEV_CLIPBOARD_MINOR)); + + err = dyn_dev_register(&location_dev, DEV_CHAR, DYN_DEV_MAJOR, DEV_LOCATION_MINOR); + if (err != 0) + return err; + generic_mknod("/dev/location", S_IFCHR|0666, dev_make(DYN_DEV_MAJOR, DEV_LOCATION_MINOR)); do_mount(&procfs, "proc", "/proc", 0); do_mount(&devptsfs, "devpts", "/dev/pts", 0); diff --git a/app/Info.plist b/app/Info.plist index a020ff44..178e99a0 100644 --- a/app/Info.plist +++ b/app/Info.plist @@ -20,12 +20,16 @@ 60 LSRequiresIPhoneOS + UIBackgroundModes + + location + + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UIFileSharingEnabled - UIRequiredDeviceCapabilities armv7 @@ -46,5 +50,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSLocationAlwaysAndWhenInUseUsageDescription + A program running in iSH wants to track your location in the background. + NSLocationWhenInUseUsageDescription + A program running in iSH wants to track your location. diff --git a/app/LocationDevice.h b/app/LocationDevice.h new file mode 100644 index 00000000..32f5572c --- /dev/null +++ b/app/LocationDevice.h @@ -0,0 +1,8 @@ +// +// LocationDevice.h +// iSH +// +// Created by Theodore Dubois on 10/20/19. +// + +extern struct dev_ops location_dev; diff --git a/app/LocationDevice.m b/app/LocationDevice.m new file mode 100644 index 00000000..12a26d94 --- /dev/null +++ b/app/LocationDevice.m @@ -0,0 +1,174 @@ +// +// LocationDevice.m +// iSH +// +// Created by Theodore Dubois on 10/20/19. +// + +#import +#import +#include "kernel/fs.h" +#include "fs/dev.h" +#include "util/sync.h" + +@interface LocationTracker : NSObject + ++ (LocationTracker *)instance; + +@property CLLocationManager *locationManager; +@property (nonatomic) CLLocation *latest; +@property lock_t lock; +@property cond_t updateCond; + +- (int)waitForUpdate; + +@end + +BOOL CLIsAuthorized(CLAuthorizationStatus status) { + return status == kCLAuthorizationStatusAuthorizedWhenInUse || status == kCLAuthorizationStatusAuthorizedAlways; +} + +@implementation LocationTracker + ++ (LocationTracker *)instance { + static __weak LocationTracker *tracker; + if (tracker == nil) { + __block LocationTracker *newTracker; + dispatch_sync(dispatch_get_main_queue(), ^{ + if (tracker == nil) { + newTracker = [LocationTracker new]; + tracker = newTracker; + } + }); + return newTracker; + } + return tracker; +} + +- (instancetype)init { + if (self = [super init]) { + self.locationManager = [CLLocationManager new]; + self.locationManager.delegate = self; + self.locationManager.allowsBackgroundLocationUpdates = YES; + if (CLIsAuthorized([CLLocationManager authorizationStatus])) { + [self.locationManager startUpdatingLocation]; + [self.locationManager requestLocation]; + } else { + [self.locationManager requestAlwaysAuthorization]; + } + + lock_init(&_lock); + cond_init(&_updateCond); + } + return self; +} + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + lock(&_lock); + self.latest = locations.lastObject; + notify(&_updateCond); + unlock(&_lock); +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { + NSLog(@"location failed %@", error); +} + +- (void)dealloc { + [self.locationManager stopUpdatingLocation]; + cond_destroy(&_updateCond); +} + +- (int)waitForUpdate { + lock(&_lock); + CLLocation *oldLatest = self.latest; + int err = 0; + while (self.latest == oldLatest) { + err = wait_for(&_updateCond, &_lock, NULL); + if (err < 0) + break; + } + unlock(&_lock); + return err; +} + +- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + if (status == kCLAuthorizationStatusAuthorizedAlways || status == kCLAuthorizationStatusAuthorizedWhenInUse) { + NSLog(@"got auth, starting updates"); + [manager startUpdatingLocation]; + } +} + +@end + +@interface LocationFile : NSObject { + NSData *buffer; + size_t bufferOffset; +} + +@property LocationTracker *tracker; + +- (ssize_t)readIntoBuffer:(void *)buf size:(size_t)size; + +@end + +@implementation LocationFile + +- (instancetype)init { + if (self = [super init]) { + self.tracker = [LocationTracker instance]; + } + return self; +} + +- (int)waitForUpdate { + if (buffer != nil) + return 0; + int err = [self.tracker waitForUpdate]; + if (err < 0) + return err; + CLLocation *location = self.tracker.latest; + NSString *output = [NSString stringWithFormat:@"%+f,%+f\n", location.coordinate.latitude, location.coordinate.longitude]; + buffer = [output dataUsingEncoding:NSUTF8StringEncoding]; + bufferOffset = 0; + return 0; +} + +- (ssize_t)readIntoBuffer:(void *)buf size:(size_t)size { + @synchronized (self) { + int err = [self waitForUpdate]; + if (err < 0) + return err; + size_t remaining = buffer.length - bufferOffset; + if (size > remaining) + size = remaining; + [buffer getBytes:buf range:NSMakeRange(bufferOffset, size)]; + bufferOffset += size; + if (bufferOffset == buffer.length) + buffer = nil; + return size; + } +} + +@end + +static int location_open(int major, int minor, struct fd *fd) { + fd->data = (void *) CFBridgingRetain([LocationFile new]); + return 0; +} + +static int location_close(struct fd *fd) { + CFBridgingRelease(fd->data); + return 0; +} + +static ssize_t location_read(struct fd *fd, void *buf, size_t size) { + LocationFile *file = (__bridge LocationFile *) fd->data; + return [file readIntoBuffer:buf size:size]; +} + +const struct dev_ops location_dev = { + .open = location_open, + .fd.close = location_close, + .fd.read = location_read, +}; diff --git a/app/PasteboardDevice.h b/app/PasteboardDevice.h new file mode 100644 index 00000000..36151c5c --- /dev/null +++ b/app/PasteboardDevice.h @@ -0,0 +1,2 @@ +// Pasteboard is implementation of /dev/clipboard device +extern struct dev_ops clipboard_dev; diff --git a/app/PasteboardDevice.m b/app/PasteboardDevice.m new file mode 100644 index 00000000..d6bfb6c1 --- /dev/null +++ b/app/PasteboardDevice.m @@ -0,0 +1,252 @@ +#include +#import +#include "fs/poll.h" +#include "fs/dyndev.h" +#include "kernel/errno.h" +#include "debug.h" + +/** + * buffer is dynamically sized buffer of size buffer_cap + * All writes go to it, and buffer_len is length of data held in buffer + */ + +// Prepare for fd separation +#define fd_priv(fd) fd->clipboard +typedef struct fd clip_fd; + +#define INITIAL_BUFFER_CAP 1024 +// 8MB: https://stackoverflow.com/a/3523175 +#define MAXIMAL_BUFFER_CAP 8*1024*1024 + +// If pasteboard contents were changed since file was opened, +// all read operations on in return error +static int check_read_generation(clip_fd *fd) { + UIPasteboard *pb = UIPasteboard.generalPasteboard; + + uint64_t pb_gen = (uint64_t) pb.changeCount; + uint64_t fd_gen = fd_priv(fd).generation; + + if (fd_gen == 0 || fd->offset == 0) { + fd_priv(fd).generation = pb_gen; + } else if (fd_gen != pb_gen) { + return -1; + } + + return 0; +} + +static const char *get_data(clip_fd *fd, size_t *len) { + if (fd_priv(fd).buffer != NULL) { + *len = fd_priv(fd).buffer_len; + return fd_priv(fd).buffer; + } + + if (check_read_generation(fd) != 0) { + return NULL; + } + + NSString __autoreleasing *contents = UIPasteboard.generalPasteboard.string; + *len = contents.length; + return contents.UTF8String; +} + +static int realloc_to_fit(clip_fd* fd, size_t fit_len) { + // (Re)allocate buffer if there's not enough space to fit fit_len + if (fit_len <= fd_priv(fd).buffer_cap) { + return 0; + } + if (fit_len > MAXIMAL_BUFFER_CAP) { + return 1; + } + + size_t size = fd_priv(fd).buffer_cap * 2; + if (size == 0) { + size = INITIAL_BUFFER_CAP; + } + while (size < fit_len) size *= 2; + + void *new_buf = realloc(fd_priv(fd).buffer, size); + if (new_buf == NULL) { + return 1; + } + + fd_priv(fd).buffer = new_buf; + fd_priv(fd).buffer_cap = size; + + return 0; +} + +// buffer => UIPasteboard +static int clipboard_write_sync(clip_fd *fd) { + if (fd_priv(fd).buffer == NULL) { + return 0; + } + + void *data = fd_priv(fd).buffer; + size_t len = fd_priv(fd).buffer_len; + + // FIXME(stek29): This logs "Returning local object of class NSString" + // and I have no idea why (or how to fix it) + UIPasteboard.generalPasteboard.string = [[NSString alloc] + initWithBytes:data + length:len + encoding:NSUTF8StringEncoding]; + + // Reset generation since we've just updated UIPasteboard + // note: offset doesn't change + fd_priv(fd).generation = 0; + + return 0; +} + +// UIPasteboard => buffer, return len +static ssize_t clipboard_read_sync(clip_fd *fd) { + if (fd_priv(fd).buffer != NULL) { + free(fd_priv(fd).buffer); + fd_priv(fd).buffer = NULL; + fd_priv(fd).buffer_cap = 0; + fd_priv(fd).buffer_len = 0; + } + + @autoreleasepool { + size_t len; + const void *data = get_data(fd, &len); + + // Make sure size is still INITIAL_BUFFER_CAP based + if (realloc_to_fit(fd, len)) { + return _ENOMEM; + } + + memcpy(fd_priv(fd).buffer, data, len); + fd_priv(fd).buffer_len = len; + + return len; + } +} + +static int clipboard_poll(clip_fd *fd) { + return POLL_READ | POLL_WRITE; +} + +static ssize_t clipboard_read(clip_fd *fd, void *buf, size_t bufsize) { + @autoreleasepool { + size_t length = 0; + const char *data = get_data(fd, &length); + + if (data == NULL) { + return _EIO; + } + + size_t remaining = length - fd->offset; + if ((size_t) fd->offset > length) + remaining = 0; + + size_t n = bufsize; + if (n > remaining) + n = remaining; + + if (n != 0) { + memcpy(buf, data + fd->offset, n); + fd->offset += n; + } + + return n; + } +} + +static ssize_t clipboard_write(clip_fd *fd, const void *buf, size_t bufsize) { + size_t new_len = fd->offset + bufsize; + size_t old_len = fd_priv(fd).buffer_len; + + if (old_len > new_len) { + new_len = old_len; + } + + if (realloc_to_fit(fd, new_len)) { + return _ENOMEM; + } + + // fill the hole between new offset and old len + if (old_len < fd->offset) { + memset(fd_priv(fd).buffer + old_len, '\0', fd->offset - old_len); + } + + memcpy(fd_priv(fd).buffer + fd->offset, buf, bufsize); + fd->offset += bufsize; + fd_priv(fd).buffer_len = new_len; + + return bufsize; +} + +static off_t_ clipboard_lseek(clip_fd *fd, off_t_ off, int whence) { + off_t_ old_off = fd->offset; + size_t length = 0; + + if (whence != LSEEK_SET || off != 0) { + @autoreleasepool { + if (get_data(fd, &length) == NULL) { + return _EIO; + } + } + } + + switch (whence) { + case LSEEK_SET: + fd->offset = off; + break; + + case LSEEK_CUR: + fd->offset += off; + break; + + case LSEEK_END: + fd->offset = length + off; + break; + + default: + return _EINVAL; + } + + if (fd->offset < 0) { + fd->offset = old_off; + return _EINVAL; + } + + return fd->offset; +} + +static int clipboard_close(clip_fd *fd) { + clipboard_write_sync(fd); + if (fd_priv(fd).buffer != NULL) { + free(fd_priv(fd).buffer); + } + return 0; +} + +static int clipboard_open(int major, int minor, clip_fd *fd) { + // Zero fd_priv data + memset(&fd_priv(fd), 0, sizeof(fd_priv(fd))); + + // If O_TRUNC is not set, initialize buffer with current pasteboard contents + if (!(fd->flags & O_TRUNC_)) { + ssize_t len = clipboard_read_sync(fd); + if (len < 0) { + return (int) len; + } + if (fd->flags & O_APPEND_) { + fd->offset = (size_t) len; + } + } + + return 0; +} + +struct dev_ops clipboard_dev = { + .open = clipboard_open, + .fd.read = clipboard_read, + .fd.write = clipboard_write, + .fd.lseek = clipboard_lseek, + .fd.poll = clipboard_poll, + .fd.close = clipboard_close, + .fd.fsync = clipboard_write_sync, +}; diff --git a/fs/devices.h b/fs/devices.h index f9fd8d79..63150a98 100644 --- a/fs/devices.h +++ b/fs/devices.h @@ -38,5 +38,7 @@ // /dev/clipboard #define DEV_CLIPBOARD_MINOR 0 +// /dev/gps +#define DEV_LOCATION_MINOR 1 #endif diff --git a/fs/fd.h b/fs/fd.h index 9b503a0b..e903b3fc 100644 --- a/fs/fd.h +++ b/fs/fd.h @@ -72,6 +72,9 @@ struct fd { // length of actual data stored in the buffer size_t buffer_len; } clipboard; + + // can fit anything in here + void *data; }; // fs data union { diff --git a/iSH.xcodeproj/project.pbxproj b/iSH.xcodeproj/project.pbxproj index 01519208..5cd9ab2d 100644 --- a/iSH.xcodeproj/project.pbxproj +++ b/iSH.xcodeproj/project.pbxproj @@ -7,12 +7,14 @@ objects = { /* Begin PBXBuildFile section */ - 650B337422EA235C00B4C03E /* Pasteboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 650B337322EA235C00B4C03E /* Pasteboard.m */; }; + 650B337422EA235C00B4C03E /* PasteboardDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 650B337322EA235C00B4C03E /* PasteboardDevice.m */; }; 8632A7BF219A59FB00F02325 /* UserPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 8632A7BE219A59FB00F02325 /* UserPreferences.m */; }; 9A28E4EA219A8B670073D200 /* AboutAppearanceViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A28E4E9219A8B670073D200 /* AboutAppearanceViewController.m */; }; BB0FC5921F980A6C00803272 /* Terminal.m in Sources */ = {isa = PBXBuildFile; fileRef = BB0FC5911F980A6B00803272 /* Terminal.m */; }; BB13F7EA200ADCED003D1C4D /* libish.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BB13F7DC200AD81D003D1C4D /* libish.a */; }; BB1D9D93234A8FE100F364E8 /* AboutNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = BB1D9D92234A8FE100F364E8 /* AboutNavigationController.m */; }; + BB235534235D488500139E00 /* LocationDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = BB235533235D488400139E00 /* LocationDevice.m */; }; + BB235537235D49B300139E00 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB235536235D49B300139E00 /* CoreLocation.framework */; }; BB23F58D231E1D1400585522 /* ScrollbarView.m in Sources */ = {isa = PBXBuildFile; fileRef = BB23F58C231E1D1400585522 /* ScrollbarView.m */; }; BB2B4DAC231D94C300CB578B /* hterm_all.js in Resources */ = {isa = PBXBuildFile; fileRef = BB2B4DAB231D94C300CB578B /* hterm_all.js */; }; BB2B4DAD231D998300CB578B /* term.js in Resources */ = {isa = PBXBuildFile; fileRef = BB4A539C1FAA490C00A72ACE /* term.js */; }; @@ -100,8 +102,8 @@ 650B336822E9F0B500B4C03E /* root.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = root.c; sourceTree = ""; }; 650B336C22EA052400B4C03E /* dyndev.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = dyndev.c; sourceTree = ""; }; 650B336D22EA052400B4C03E /* dyndev.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dyndev.h; sourceTree = ""; }; - 650B337222EA235C00B4C03E /* Pasteboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Pasteboard.h; sourceTree = ""; }; - 650B337322EA235C00B4C03E /* Pasteboard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Pasteboard.m; sourceTree = ""; }; + 650B337222EA235C00B4C03E /* PasteboardDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PasteboardDevice.h; sourceTree = ""; }; + 650B337322EA235C00B4C03E /* PasteboardDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PasteboardDevice.m; sourceTree = ""; }; 650B337522EA728B00B4C03E /* devices.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = devices.h; sourceTree = ""; }; 8632A7BD219A59FB00F02325 /* UserPreferences.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserPreferences.h; sourceTree = ""; }; 8632A7BE219A59FB00F02325 /* UserPreferences.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserPreferences.m; sourceTree = ""; }; @@ -115,6 +117,9 @@ BB13F7DC200AD81D003D1C4D /* libish.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libish.a; sourceTree = BUILT_PRODUCTS_DIR; }; BB1D9D91234A8FE100F364E8 /* AboutNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AboutNavigationController.h; sourceTree = ""; }; BB1D9D92234A8FE100F364E8 /* AboutNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AboutNavigationController.m; sourceTree = ""; }; + BB235533235D488400139E00 /* LocationDevice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocationDevice.m; sourceTree = ""; }; + BB235535235D489400139E00 /* LocationDevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocationDevice.h; sourceTree = ""; }; + BB235536235D49B300139E00 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; BB23F58B231E1D1400585522 /* ScrollbarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScrollbarView.h; sourceTree = ""; }; BB23F58C231E1D1400585522 /* ScrollbarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScrollbarView.m; sourceTree = ""; }; BB2B4DAB231D94C300CB578B /* hterm_all.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = hterm_all.js; path = libapps/hterm/dist/js/hterm_all.js; sourceTree = ""; }; @@ -294,6 +299,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BB235537235D49B300139E00 /* CoreLocation.framework in Frameworks */, BB6DB261216435340047A611 /* libiconv.tbd in Frameworks */, BBFB55662158644C00DFE6DE /* libresolv.tbd in Frameworks */, BB13F7EA200ADCED003D1C4D /* libish.a in Frameworks */, @@ -329,6 +335,17 @@ name = Scripts; sourceTree = ""; }; + BB235532235D472F00139E00 /* Devices */ = { + isa = PBXGroup; + children = ( + 650B337222EA235C00B4C03E /* PasteboardDevice.h */, + 650B337322EA235C00B4C03E /* PasteboardDevice.m */, + BB235535235D489400139E00 /* LocationDevice.h */, + BB235533235D488400139E00 /* LocationDevice.m */, + ); + name = Devices; + sourceTree = ""; + }; BB2D71072354244700A10D1E /* platform */ = { isa = PBXGroup; children = ( @@ -407,8 +424,7 @@ BB23F58B231E1D1400585522 /* ScrollbarView.h */, BB23F58C231E1D1400585522 /* ScrollbarView.m */, BB4A53991FAA40FD00A72ACE /* terminal */, - 650B337222EA235C00B4C03E /* Pasteboard.h */, - 650B337322EA235C00B4C03E /* Pasteboard.m */, + BB235532235D472F00139E00 /* Devices */, BB792B591F96D90D00FFB7A4 /* Main.storyboard */, BBFB557321586C7600DFE6DE /* About */, BBFB557221586C6600DFE6DE /* Utilities */, @@ -425,6 +441,7 @@ BB792B7D1F96E32B00FFB7A4 /* Frameworks */ = { isa = PBXGroup; children = ( + BB235536235D49B300139E00 /* CoreLocation.framework */, BB6DB260216435330047A611 /* libiconv.tbd */, BBFB55652158644C00DFE6DE /* libresolv.tbd */, ); @@ -949,6 +966,7 @@ files = ( 9A28E4EA219A8B670073D200 /* AboutAppearanceViewController.m in Sources */, BB1D9D93234A8FE100F364E8 /* AboutNavigationController.m in Sources */, + BB235534235D488500139E00 /* LocationDevice.m in Sources */, BB792B581F96D90D00FFB7A4 /* TerminalViewController.m in Sources */, BB78AB2B1FAD22440013E782 /* TerminalView.m in Sources */, BB23F58D231E1D1400585522 /* ScrollbarView.m in Sources */, @@ -961,7 +979,7 @@ BB60F55221573FCA003A4E52 /* BarButton.m in Sources */, BBFB557C215878C600DFE6DE /* UIApplication+OpenURL.m in Sources */, BBFB5579215876CD00DFE6DE /* AboutViewController.m in Sources */, - 650B337422EA235C00B4C03E /* Pasteboard.m in Sources */, + 650B337422EA235C00B4C03E /* PasteboardDevice.m in Sources */, BBFB557121586C4800DFE6DE /* UIViewController+Back.m in Sources */, BBFB558021587B6800DFE6DE /* ArrowBarButton.m in Sources */, BB792B551F96D90D00FFB7A4 /* AppDelegate.m in Sources */,