Commits

Jeff Laing committed fe2af2f

Moved common parts of screensnap service into separate class.
Added a little more error handling
Added method to retrieve screensnap directly into image file.

Comments (0)

Files changed (2)

MobileDeviceAccess.h

 /// on their implementing binaries, others don't.  For those, look in \p /System/Library/LaunchDaemons in the appropriate
 /// plist file (named for the service).
 ///
+/// Some are only present on "developer" systems - look in /Developer/Library/Lockdown/ServiceAgents/* - this may only
+/// be available if XCode is connected (?)
+///
 /// On a jailbroken 3.1.2 iPod Touch, the \p Services.plist lists the following as
 /// valid service names:
 /// - \p "com.apple.afc" - see AFCMediaDirectory
 
 @end
 
+/// This class represents a service on the device that uses the DeviceLink protocol.
+/// Mostly it exists to add a few helper methods to its subclasses
+@interface AMDLService : AMService {
+}
+@end
+
 /// This class represents an installed application on the device.  To retrieve
 /// the list of installed applications on a device use [AMDevice installedApplications]
 /// or one of the methods on AMInstallationProxy.
 
 /// This class communicates with the AMService to remotely request and download device screenshots.
 ///
-/// Under the covers, it is implemented as a service called \p "com.apple.screenshotr"
-@interface AMScreenshotService : AMService
+/// Under the covers, it is implemented as a service called \p "com.apple.screenshotr" - this seems to only
+/// be present on devices that are enabled for development, and is implemented by
+/// <PRE>
+///	/Developer/usr/bin/ScreenShotr
+/// </PRE>
+@interface AMScreenshotService : AMDLService
+
+/// Return a raw NSImage of the current screen
 - (NSImage *)getScreenshot;
+
+/// Create an image file of the current screen in an PNG format
+- (BOOL)getPNGScreenshot:(NSString*)path;
+
+/// Create an image file of the current screen in an JPEG format
+- (BOOL)getJPEGScreenshot:(NSString*)path;
+
+/// Create an image file of the current screen in an arbitrary file format
+- (BOOL)getScreenshot:(NSString*)path
+			   ofType:(NSBitmapImageFileType)fileType
+		   properties:(NSDictionary*)properties;
 @end
     
 /// This class communicates with the syslog_relay.
 /// with the device. For more information, see AMMobileSync.
 - (AMMobileSync*)newAMMobileSync;
 
-/// Create a mobile screenshot service
+/// Create a mobile screenshot service.  Allows screensnaps to be captured from the
+/// device.  This only works on development devices (ie, devices that have been
+/// marked as "Used for Development" in XCode.  Otherwise the required
+/// service is not present.
 - (AMScreenshotService*)newAMScreenshotService;
 
 /// Returns an informational value from the device's root domain.

MobileDeviceAccess.m

 	return(result);
 }
 
-- (NSDictionary*)sendRequestAndWaitForSingleReply:(NSDictionary*)message
+- (id)sendRequestAndWaitForSingleReply:(id)message
 {
 	if ([self sendXMLRequest:message]) {
 		return [self readXMLReply];
 }
 
 // common method to send a request, then loop waiting for final reply to a lengthy operation
+// this is only appropriate for messages that return NSDictionary replies
 - (BOOL)sendRequestAndWaitUntilDone:(NSDictionary*)message
 {
 	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
 */
 @end
 
+@implementation AMDLService
+
+- (id)waitForDLResponse:(NSString*)response
+{
+	id result = nil;
+	NSArray *reply = [self readXMLReply];
+	if (reply) {
+		NSString *s = [reply firstObject];
+		if (s) {
+			if ([s isEqualToString:response]) {
+				result = reply;
+			} else if ([s isEqualToString:@"DLMessageDisconnect"]) {
+				[self setLastError:[reply objectAtIndex:1]];
+			} else {
+				[self setLastError:[NSString stringWithFormat:@"Expected '%@', got '%@'",response,s]];
+			}
+		} else {
+			[self setLastError:[NSString stringWithFormat:@"Response truncated"]];
+		}
+	}
+	return result;
+}
+
+- (BOOL)versionHandshake
+{
+	BOOL result = NO;
+	NSArray *request, *reply;
+
+	// step 1 - he sends us DLMessageVersionExchange, major, minor
+	reply = [self waitForDLResponse:@"DLMessageVersionExchange"];
+	if (reply) {
+		// we could check a bit harder here
+		NSInteger major = [[reply objectAtIndex:1] intValue];
+		NSInteger minor = [[reply objectAtIndex:2] intValue];
+		NSLog(@"Device requesting DeviceLinkProtocol %ld,%ld", (long)major, (long)minor);
+
+		// we say yes, we accept that version.  I suspect we might be able to tell him a lower
+		// version number here, but I don't know what the versions really mean at this point so
+		// lets just claim to understand him...
+		request = @[
+					@"DLMessageVersionExchange",
+					@"DLVersionsOk",
+					@(major)
+		];
+
+		if ([self sendXMLRequest:request]) {
+			// he then replies with DLMessageDeviceReady
+			if ([self waitForDLResponse:@"DLMessageDeviceReady"]) {
+				// and we are cool to go
+				result = YES;
+			}
+		}
+	}
+
+	return result;
+}
+
+- (id)sendDLProcessMessage:(NSDictionary*)message
+		   andWaitForReply:(NSString*)replyType
+{
+	static NSString *header = @"DLMessageProcessMessage";
+	
+	// messages are actually send as arrays, first element being DLMessageProcessMessage
+    NSArray *request = @[ header, message ];
+	
+	// transaction is still raw xml representations, but reply is an array whose first element
+	// should also be our header, and whose second element is a dictionary
+	NSDictionary *result = nil;
+	if ([self sendXMLRequest:request]) {
+		// and wait for a corresponding response
+		NSArray *reply = [self waitForDLResponse:header];
+		NSString *s;
+		if (reply) {
+			// if it gets here, we know that its the right kind of message, but we still need to
+			// make sure it isn't truncated
+			if ([reply count] < 2) {
+				[self setLastError:[NSString stringWithFormat:@"Reply is too short"]];
+				return nil;
+			}
+
+			// lift out the response dictionary
+			result = [reply objectAtIndex:1];
+			
+			// look for an error message
+			s = [result objectForKey:@"ErrorMessage"];
+			if (s) {
+				[self setLastError:s];
+				return nil;
+			}
+			
+			// no error message, is it the right message type ?
+			s = [result objectForKey:@"MessageType"];
+			if (![s isEqualToString:replyType]) {
+				[self setLastError:[NSString stringWithFormat:@"Expected '%@', got '%@'", replyType, s]];
+				return nil;
+			}
+			
+			// fall through to return
+		}
+	}
+	return result;
+}
+
+// DLServices need an additional handshake to ensure that we all agree on the version
+// of the protocol we are talking.
+- (id)initWithName:(NSString*)name onDevice:(AMDevice*)device
+{
+	if (self = [super initWithName:name onDevice:device]) {
+		// at this point, the service is started.  We need to do the version handshake.  If
+		// that fails, we need to nuke ourselves
+		if (![self versionHandshake]) {
+			NSLog(@"failed to handshake: %@",self.lasterror);
+			[self release];
+			self = nil;
+		}
+	}
+	return self;
+}
+
+@end
+
 @implementation AFCFileReference
 
 @synthesize lasterror = _lasterror;
 - (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.screenshotr" onDevice:device]) {
-		// wait for the version exchange
-		NSLog(@"waiting for the version exchange");
-		NSLog(@"%@", [self readXMLReply]);
-		NSLog(@"sending OK");
-		[self sendXMLRequest:[NSArray arrayWithObjects:@"DLMessageVersionExchange", @"DLVersionsOk", @300, nil]];
-		NSLog(@"%@", [self readXMLReply]);
+		// at this point, we are connected to the service, and the initial version handshake has
+		// completed successfully.
 	}
 	return self;
 }
 
-// note, this has not been tested at all - I doubt that it works
 - (NSImage *)getScreenshot
 {
-    NSArray *message = [NSArray arrayWithObjects:@"DLMessageProcessMessage", @{@"MessageType" : @"ScreenShotRequest"}, nil];
-    NSArray *arrayResponse;
-    
+    NSDictionary *request = @{
+		@"MessageType" : @"ScreenShotRequest"
+	};
+
     // Send and receive
-    if ([self sendXMLRequest:message]) {
-		arrayResponse = [self readXMLReply];
+    NSDictionary *dict = [self sendDLProcessMessage:request andWaitForReply:@"ScreenShotReply"];
+	if (dict) {
+		NSImage *image = nil;
+		image = [[NSImage alloc] initWithData:[dict objectForKey:@"ScreenShotData"]];
+		if (image) {
+			return [image autorelease];
+		}
+		[self setLastError:@"Bad image data in ScreenShotData"];
 	}
-    
-    if(arrayResponse)
-    {
-        NSDictionary *dict = [arrayResponse objectAtIndex:1];
-        if(dict)
-        {
-            if([[dict objectForKey:@"MessageType"] isEqualToString:@"ScreenShotReply"])
-            {
-                NSLog(@"return image");
-                return [[NSImage alloc] initWithData:[dict objectForKey:@"ScreenShotData"]];
-            }
-            else
-                [self setLastError:@"Invalid Image Data"];
-        }
-    }
-    
-    [self setLastError:@"Received no data"];
-    
-    return nil;
+	return nil;
 }
+
+- (BOOL)getScreenshot:(NSString*)path
+			   ofType:(NSBitmapImageFileType)fileType
+		   properties:(NSDictionary*)properties
+{
+	NSImage *image = [self getScreenshot];
+	if (image) {
+		// based on http://stackoverflow.com/questions/17507170/how-to-save-png-file-from-nsimage-retina-issues?rq=1
+		CGImageRef cgRef = [image CGImageForProposedRect:NULL
+												 context:nil
+												   hints:nil];
+		NSBitmapImageRep *newRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
+		[newRep setSize:[image size]];
+		NSData *imgData = [newRep representationUsingType:fileType properties:properties];
+		[imgData writeToFile:path atomically:YES];
+		[newRep release];
+		return YES;
+	}
+	return NO;
+}
+
+- (BOOL)getPNGScreenshot:(NSString*)path
+{
+	return [self getScreenshot:path ofType:NSPNGFileType properties:nil];
+}
+
+- (BOOL)getJPEGScreenshot:(NSString*)path
+{
+	return [self getScreenshot:path ofType:NSJPEGFileType properties:nil];
+}
+
 @end
 
 @implementation AMDevice