FBDeviceControl/Management/FBSpringboardServicesClient.m (250 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 "FBSpringboardServicesClient.h" #import "FBAMDServiceConnection.h" NSString *const FBSpringboardServiceName = @"com.apple.springboardservices"; FBWallpaperName const FBWallpaperNameHomescreen = @"homescreen"; FBWallpaperName const FBWallpaperNameLockscreen = @"lockscreen"; @interface FBSpringboardServicesClient () @property (nonatomic, strong, readonly) FBAMDServiceConnection *connection; @property (nonatomic, strong, readonly) dispatch_queue_t queue; @property (nonatomic, strong, readonly) id<FBControlCoreLogger> logger; @end typedef NSArray<NSArray<NSString *> *> *IconLayoutJSONType; @interface FBSpringboardServicesIconContainer : NSObject <FBFileContainer> @property (nonatomic, strong, readonly) FBSpringboardServicesClient *client; @property (nonatomic, copy, readonly) NSArray<NSString *> *validFilenames; @end @implementation FBSpringboardServicesIconContainer - (instancetype)initWithClient:(FBSpringboardServicesClient *)client { self = [super init]; if (!self) { return nil; } _client = client; _validFilenames = @[IconPlistFile, IconJSONFile]; return self; } #pragma mark FBFileContainer Implementation static NSString *const IconPlistFile = @"icons.plist"; static NSString *const IconJSONFile = @"icons.json"; - (FBFuture<NSArray<NSString *> *> *)contentsOfDirectory:(NSString *)path { return [FBFuture futureWithResult:self.validFilenames]; } - (FBFuture<NSString *> *)copyFromContainer:(NSString *)sourcePath toHost:(NSString *)destinationPath { NSString *filename = sourcePath.lastPathComponent; return [[FBFuture onQueue:self.client.queue resolve:^ FBFuture<IconLayoutType> * { if (![self.validFilenames containsObject:filename]) { return [[FBControlCoreError describeFormat:@"%@ is not one of %@", filename, [FBCollectionInformation oneLineDescriptionFromArray:self.validFilenames]] failFuture]; } return [self.client getIconLayout]; }] onQueue:self.client.queue fmap:^ FBFuture<NSString *> * (IconLayoutType layout) { if ([filename isEqualToString:IconJSONFile]) { IconLayoutJSONType jsonLayout = [FBSpringboardServicesIconContainer flattenBaseFormat:layout]; NSError *error = nil; NSData *data = [NSJSONSerialization dataWithJSONObject:jsonLayout options:NSJSONWritingPrettyPrinted error:&error]; if (!data) { return [FBFuture futureWithError:error]; } if (![data writeToFile:destinationPath options:NSDataWritingAtomic error:&error]) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:destinationPath]; } else { NSError *error = nil; NSData *data = [NSPropertyListSerialization dataWithPropertyList:layout format:NSPropertyListXMLFormat_v1_0 options:0 error:&error]; if (!data) { return [FBFuture futureWithError:error]; } if (![data writeToFile:destinationPath options:NSDataWritingAtomic error:&error]) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:destinationPath]; } }]; } - (FBFuture<NSNull *> *)copyFromHost:(NSString *)sourcePath toContainer:(NSString *)destinationPath { return [[self iconLayoutFromSourcePath:sourcePath toDestinationFile:destinationPath.lastPathComponent] onQueue:self.client.queue fmap:^ FBFuture<NSNull *> * (IconLayoutType layout) { return [self.client setIconLayout:layout]; }]; } - (FBFuture<FBFuture<NSNull *> *> *)tail:(NSString *)path toConsumer:(id<FBDataConsumer>)consumer { return [[FBControlCoreError describeFormat:@"-[%@ %@] is not implemented", NSStringFromClass(self.class), NSStringFromSelector(_cmd)] failFuture]; } - (FBFuture<NSNull *> *)createDirectory:(NSString *)directoryPath { return [[FBControlCoreError describeFormat:@"%@ does not make sense for Springboard File Containers", NSStringFromSelector(_cmd)] failFuture]; } - (FBFuture<NSNull *> *)moveFrom:(NSString *)sourcePath to:(NSString *)destinationPath { return [[FBControlCoreError describeFormat:@"%@ does not make sense for Springboard File Containers", NSStringFromSelector(_cmd)] failFuture]; } - (FBFuture<NSNull *> *)remove:(NSString *)path { return [[FBControlCoreError describeFormat:@"%@ does not make sense for Springboard File Containers", NSStringFromSelector(_cmd)] failFuture]; } - (FBFuture<IconLayoutType> *)iconLayoutFromSourcePath:(NSString *)sourcePath toDestinationFile:(NSString *)filename { return [FBFuture onQueue:self.client.queue resolve:^ FBFuture<IconLayoutType> * { if ([filename isEqualToString:IconJSONFile]) { NSError *error = nil; NSData *data = [NSData dataWithContentsOfFile:sourcePath options:0 error:&error]; if (!data) { return [FBFuture futureWithError:error]; } IconLayoutJSONType layout = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (!layout) { return [FBFuture futureWithError:error]; } return [self convertJSONFormatToWireFormat:layout]; } if ([filename isEqualToString:IconPlistFile]) { NSError *error = nil; NSData *data = [NSData dataWithContentsOfFile:sourcePath options:0 error:&error]; if (!data) { return [FBFuture futureWithError:error]; } IconLayoutType layout = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:&error]; if (!layout) { return [FBFuture futureWithError:error]; } return [FBFuture futureWithResult:layout]; } return [[FBControlCoreError describeFormat:@"%@ is not one of %@", filename, [FBCollectionInformation oneLineDescriptionFromArray:self.validFilenames]] failFuture]; }]; } - (FBFuture<IconLayoutType> *)convertJSONFormatToWireFormat:(IconLayoutJSONType)jsonFormat { return [[self.client getIconLayout] onQueue:self.client.queue fmap:^ FBFuture<IconLayoutType> * (IconLayoutType currentApps) { NSDictionary<NSString *, NSDictionary<NSString *, id> *> *iconsByBundleID = [FBSpringboardServicesIconContainer keyIconsByBundleID:currentApps]; NSMutableArray<NSArray<NSDictionary<NSString *, id> *> *> *format = NSMutableArray.array; for (NSArray<NSString *> *jsonPage in jsonFormat) { NSMutableArray<NSDictionary<NSString *, id> *> *fullPage = NSMutableArray.array; for (NSString *bundleID in jsonPage) { NSDictionary<NSString *, id> *icon = iconsByBundleID[bundleID]; if (!bundleID) { return [[FBControlCoreError describeFormat:@"Cannot use layout %@ is not any of %@", bundleID, [FBCollectionInformation oneLineDescriptionFromArray:iconsByBundleID.allKeys]] failFuture]; } [fullPage addObject:icon]; } [format addObject:fullPage]; } return [FBFuture futureWithResult:format]; }]; } + (IconLayoutJSONType)flattenBaseFormat:(IconLayoutType)baseFormat { NSMutableArray<NSArray<NSString *> *> *flatFormat = NSMutableArray.array; for (NSArray<NSDictionary<NSString *, id> *> *basePage in baseFormat) { NSMutableArray<NSString *> *flatPage = NSMutableArray.array; for (NSDictionary<NSString *, id> *icon in basePage) { NSString *bundleIdentifier = icon[@"bundleIdentifier"]; [flatPage addObject:bundleIdentifier]; } [flatFormat addObject:flatPage]; } return flatFormat; } + (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)keyIconsByBundleID:(IconLayoutType)layout { NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *iconsByBundleID = NSMutableDictionary.dictionary; for (NSArray<NSDictionary<NSString *, id> *> *page in layout) { for (NSDictionary<NSString *, id> *icon in page) { NSString *bundleIdentifier = icon[@"bundleIdentifier"]; iconsByBundleID[bundleIdentifier] = icon; } } return iconsByBundleID; } @end @implementation FBSpringboardServicesClient #pragma mark Initializers + (instancetype)springboardServicesClientWithConnection:(FBAMDServiceConnection *)connection logger:(id<FBControlCoreLogger>)logger { dispatch_queue_t queue = dispatch_queue_create("com.facebook.FBDeviceControl.springboard_services", DISPATCH_QUEUE_SERIAL); return [[self alloc] initWithConnection:connection queue:queue logger:logger]; } - (instancetype)initWithConnection:(FBAMDServiceConnection *)connection queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger { self = [super init]; if (!self) { return nil; } _connection = connection; _queue = queue; _logger = logger; return self; } #pragma mark Public Methods - (FBFuture<IconLayoutType> *)getIconLayout { return [FBFuture onQueue:self.queue resolveValue:^ IconLayoutType (NSError **error) { IconLayoutType result = [self.connection sendAndReceiveMessage:@{@"command": @"getIconState", @"formatVersion": @"2"} error:error]; if (!result) { return nil; } return result; }]; } static size_t IconLayoutSize = 4; - (FBFuture<NSNull *> *)setIconLayout:(IconLayoutType)iconLayout { return [FBFuture onQueue:self.queue resolveValue:^ NSNull * (NSError **error) { // A message is not returned upon the connection, so we just have to send the data itself and check it was acked. if (![self.connection sendMessage:@{@"command": @"setIconState", @"iconState": iconLayout} error:error]) { return nil; } // Recieve some data to know that it reached the other side, in the event of a failure we will recive no bytes. NSData *data = [self.connection receive:IconLayoutSize error:error]; if (!data) { return nil; } return NSNull.null; }]; } - (FBFuture<NSData *> *)wallpaperImageDataForKind:(FBWallpaperName)name { return [FBFuture onQueue:self.queue resolveValue:^ NSData * (NSError **error) { NSDictionary<NSString *, id> *response = [self.connection sendAndReceiveMessage:@{@"command": @"getWallpaperPreviewImage", @"wallpaperName": name} error:error]; if (!response) { return nil; } NSData *data = response[@"pngData"]; if (![data isKindOfClass:NSData.class]) { return [[FBControlCoreError describeFormat:@"No pngData in response %@", response] fail:error]; } return data; }]; } - (id<FBFileContainer>)iconContainer { return [[FBSpringboardServicesIconContainer alloc] initWithClient:self]; } @end