FBDeviceControl/Management/FBAMDeviceManager.m (266 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBAMDeviceManager.h"
#import "FBAMDevice+Private.h"
#import "FBAMRestorableDevice.h"
#import "FBDeviceControlError.h"
#import "FBDeviceControlFrameworkLoader.h"
static NSString *const MobileBackupDomain = @"com.apple.mobile.backup";
@interface FBAMDeviceManager ()
+ (BOOL)startConnectionToDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error;
+ (BOOL)startSessionByPairingWithDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error;
+ (BOOL)stopConnectionToDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error;
+ (BOOL)stopSessionWithDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error;
+ (NSDictionary<NSString *, id> *)obtainDeviceValues:(AMDeviceRef)device calls:(AMDCalls)calls;
@property (nonatomic, strong, readonly) dispatch_queue_t workQueue;
@property (nonatomic, strong, readonly) dispatch_queue_t asyncQueue;
@property (nonatomic, assign, readonly) AMDCalls calls;
@property (nonatomic, copy, nullable, readonly) NSString *ecidFilter;
@property (nonatomic, assign, readwrite) AMDNotificationSubscription subscription;
- (NSString *)identifierForDevice:(AMDeviceRef)device;
@end
static BOOL FB_AMDeviceConnected(AMDeviceRef device, FBAMDeviceManager *manager)
{
NSError *error = nil;
id<FBControlCoreLogger> logger = manager.logger;
AMDCalls calls = manager.calls;
// Start with a basic connection. This should always succeed, even if the device is not paired.
if (![FBAMDeviceManager startConnectionToDevice:device calls:calls logger:logger error:&error]) {
[logger.error logFormat:@"Cannot connect to device, ignoring device %@", error];
return NO;
}
NSString *uniqueChipID = [CFBridgingRelease(calls.CopyValue(device, NULL, (__bridge CFStringRef)(FBDeviceKeyUniqueChipID))) stringValue];
if (!uniqueChipID) {
[FBAMDeviceManager stopConnectionToDevice:device calls:calls logger:logger error:nil];
[logger.error logFormat:@"Ignoring device as cannot obtain ECID for it"];
return NO;
}
if (manager.ecidFilter && ![uniqueChipID isEqualToString:manager.ecidFilter]) {
[FBAMDeviceManager stopConnectionToDevice:device calls:calls logger:logger error:nil];
[logger.error logFormat:@"Ignoring device as ECID %@ does not match filter %@", uniqueChipID, manager.ecidFilter];
return NO;
}
NSError *pairingError = nil;
BOOL pairedWithSession = [FBAMDeviceManager startSessionByPairingWithDevice:device calls:calls logger:logger error:&pairingError];
if (!pairedWithSession) {
[logger logFormat:@"Device is not paired, degraded device information will be provied %@", pairingError];
}
// Now extract all of the values.
NSDictionary<NSString *, id> * info = [FBAMDeviceManager obtainDeviceValues:device calls:calls];
// Stop the session if one was created.
if (pairedWithSession) {
[FBAMDeviceManager stopSessionWithDevice:device calls:calls logger:logger error:nil];
}
// Always disconnect, regardless of whether there was a session or not.
[FBAMDeviceManager stopConnectionToDevice:device calls:calls logger:logger error:nil];
if (!info) {
[logger.error log:@"Ignoring device as no values were returned for it"];
return NO;
}
NSString *udid = info[FBDeviceKeyUniqueDeviceID];
if (!udid) {
[logger.error logFormat:@"Ignoring device as %@ is not present in %@", FBDeviceKeyUniqueDeviceID, info];
return NO;
}
[logger.debug logFormat:@"Obtained Device Values %@", info];
[manager deviceConnected:device identifier:uniqueChipID info:info];
return YES;
}
static void FB_AMDeviceListenerCallback(AMDeviceNotification *notification, FBAMDeviceManager *manager)
{
AMDeviceNotificationType notificationType = notification->status;
AMDeviceRef device = notification->amDevice;
id<FBControlCoreLogger> logger = manager.logger;
switch (notificationType) {
case AMDeviceNotificationTypeConnected:
case AMDeviceNotificationTypePaired:
FB_AMDeviceConnected(device, manager);
return;
case AMDeviceNotificationTypeDisconnected: {
NSString *identifier = [manager identifierForDevice:device];
if (!identifier) {
[logger logFormat:@"Cannot obtain identifier for device %@", device];
return;
}
[manager deviceDisconnected:device identifier:[manager identifierForDevice:device]];
return;
}
case AMDeviceNotificationTypeUnsubscribed:
[logger logFormat:@"Unsubscribed from AMDeviceNotificationSubscribe"];
return;
default:
[manager.logger logFormat:@"Got Unknown status %d from AMDeviceNotificationSubscribe", notificationType];
return;
}
}
@implementation FBAMDeviceManager
#pragma mark Initializers
- (instancetype)initWithCalls:(AMDCalls)calls workQueue:(dispatch_queue_t)workQueue asyncQueue:(dispatch_queue_t)asyncQueue ecidFilter:(NSString *)ecidFilter logger:(id<FBControlCoreLogger>)logger
{
self = [super initWithLogger:logger];
if (!self) {
return nil;
}
_calls = calls;
_workQueue = workQueue;
_asyncQueue = asyncQueue;
_ecidFilter = ecidFilter;
return self;
}
#pragma mark Public
+ (BOOL)startUsing:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
// Connect first
if (![self startConnectionToDevice:device calls:calls logger:logger error:error]) {
return NO;
}
// Confirm pairing and start a session
if (![self startSessionByPairingWithDevice:device calls:calls logger:logger error:error]) {
return NO;
}
[logger logFormat:@"%@ ready for use", device];
return YES;
}
+ (BOOL)stopUsing:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
// Stop the session first.
[self stopSessionWithDevice:device calls:calls logger:logger error:nil];
// Then the connection.
[self stopConnectionToDevice:device calls:calls logger:logger error:nil];
return YES;
}
#pragma mark FBDeviceManager Implementation
- (BOOL)startListeningWithError:(NSError **)error
{
if (self.subscription) {
return [[FBDeviceControlError
describe:@"An AMDeviceNotification Subscription already exists"]
failBool:error];
}
// Perform a bridging retain, so that the context of the callback can be strongly referenced.
// Tidied up when unsubscribing.
AMDNotificationSubscription subscription = nil;
int result = self.calls.NotificationSubscribe(
(AMDeviceNotificationCallback) FB_AMDeviceListenerCallback,
0,
0,
(void *) CFBridgingRetain(self),
&subscription
);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"AMDeviceNotificationSubscribe failed with %d", result]
failBool:error];
}
self.subscription = subscription;
return YES;
}
- (BOOL)stopListeningWithError:(NSError **)error
{
if (!self.subscription) {
return [[FBDeviceControlError
describe:@"An AMDeviceNotification Subscription does not exist"]
failBool:error];
}
int result = self.calls.NotificationUnsubscribe(self.subscription);
if (result != 0) {
return [[FBDeviceControlError
describeFormat:@"AMDeviceNotificationUnsubscribe failed with %d", result]
failBool:error];
}
// Cleanup after the subscription.
CFRelease((__bridge CFTypeRef)(self));
self.subscription = NULL;
return YES;
}
static const NSTimeInterval ServiceReuseTimeout = 6.0;
- (id)constructPublic:(AMDeviceRef)privateDevice identifier:(NSString *)identifier info:(NSDictionary<NSString *,id> *)info
{
return [[FBAMDevice alloc] initWithAllValues:info calls:self.calls connectionReuseTimeout:nil serviceReuseTimeout:@(ServiceReuseTimeout) workQueue:self.workQueue asyncQueue:self.asyncQueue logger:self.logger];
}
+ (void)updatePublicReference:(FBAMDevice *)publicDevice privateDevice:(AMDeviceRef)privateDevice identifier:(NSString *)identifier info:(NSDictionary<NSString *,id> *)info
{
publicDevice.amDeviceRef = privateDevice;
publicDevice.allValues = info;
}
+ (AMDeviceRef)extractPrivateReference:(FBAMDevice *)publicDevice
{
return publicDevice.amDeviceRef;
}
#pragma mark Private
- (NSString *)identifierForDevice:(AMDeviceRef)amDevice
{
if (amDevice == NULL) {
return nil;
}
for (FBAMDevice *device in self.storage.referenced.allValues) {
if (device.amDeviceRef != amDevice) {
continue;
}
return device.uniqueIdentifier;
}
return nil;
}
+ (BOOL)startConnectionToDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
if (device == NULL) {
return [[FBDeviceControlError
describe:@"Cannot utilize a non existent AMDeviceRef"]
failBool:error];
}
[logger logFormat:@"Connecting to %@", device];
int status = calls.Connect(device);
if (status != 0) {
NSString *errorDescription = CFBridgingRelease(calls.CopyErrorText(status));
return [[FBDeviceControlError
describeFormat:@"Failed to connect to %@. (%@)", device, errorDescription]
failBool:error];
}
return YES;
}
+ (BOOL)startSessionByPairingWithDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
// Then confirm the pairing.
[logger logFormat:@"Checking whether %@ is paired", device];
if (!calls.IsPaired(device)) {
[logger logFormat:@"%@ is not paired, attempting to pair", device];
int status = calls.Pair(device);
if (status != 0) {
NSString *errorDescription = CFBridgingRelease(calls.CopyErrorText(status));
return [[FBDeviceControlError
describeFormat:@"%@ is not paired with this host %@", device, errorDescription]
failBool:error];
}
[logger logFormat:@"%@ succeeded pairing request", device];
}
[logger logFormat:@"Validating Pairing to %@", device];
int status = calls.ValidatePairing(device);
if (status != 0) {
NSString *errorDescription = CFBridgingRelease(calls.CopyErrorText(status));
return [[FBDeviceControlError
describeFormat:@"Failed to validate pairing for %@. (%@)", device, errorDescription]
failBool:error];
}
// A session may also be required.
[logger logFormat:@"Starting Session on %@", device];
status = calls.StartSession(device);
if (status != 0) {
calls.Disconnect(device);
NSString *errorDescription = CFBridgingRelease(calls.CopyErrorText(status));
return [[FBDeviceControlError
describeFormat:@"Failed to start session with device. (%@)", errorDescription]
failBool:error];
}
return YES;
}
+ (BOOL)stopSessionWithDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
[logger logFormat:@"Stopping Session on %@", device];
calls.StopSession(device);
return YES;
}
+ (BOOL)stopConnectionToDevice:(AMDeviceRef)device calls:(AMDCalls)calls logger:(id<FBControlCoreLogger>)logger error:(NSError **)error
{
[logger logFormat:@"Disconnecting from %@", device];
calls.Disconnect(device);
[logger logFormat:@"Disconnected from %@", device];
return YES;
}
+ (NSDictionary<NSString *, id> *)obtainDeviceValues:(AMDeviceRef)device calls:(AMDCalls)calls
{
// Get the values from the default domain, this will obtain information regardless of whether pairing was successful or not.
NSMutableDictionary<NSString *, id> *info = [CFBridgingRelease(calls.CopyValue(device, NULL, NULL)) mutableCopy];
if (!info) {
return nil;
}
// Synthetic Values.
BOOL isPaired = calls.IsPaired(device) != 0;
info[FBDeviceKeyIsPaired] = @(isPaired);
// Get values from mobile backup, this will only return meaningful information if paired.
NSDictionary<NSString *, id> * backupInfo = [CFBridgingRelease(calls.CopyValue(device, (__bridge CFStringRef)(MobileBackupDomain), NULL)) copy] ?: @{};
// Insert the values from subdomains.
info[MobileBackupDomain] = backupInfo;
return info;
}
@end