mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-26 09:37:40 +00:00
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled
367 lines
16 KiB
Objective-C
367 lines
16 KiB
Objective-C
/*
|
|
* Keyman is copyright (C) SIL International. MIT License.
|
|
*
|
|
* Created by Shawn Schantz on 2024-07-30.
|
|
*
|
|
* Singleton object which serves as an abstraction for the reading and writing of Keyman data.
|
|
* The 'data' currently consists of keyman keyboards installed by the user. All data is saved locally on disk using NSFileManager.
|
|
* This is in contrast with the lighter weight Settings which is stored using UserDefaults and handled by KMSettingsRepository.
|
|
*/
|
|
|
|
#import "KMDataRepository.h"
|
|
#import "KMLogs.h"
|
|
/**
|
|
* the name of the iOS-style app group shared by the Keyman input method and the Keyman configuration app
|
|
* (Mac apps are now using iOS-style app groups that begin with 'group')
|
|
* It is defined for Keyman on developer.apple.com and linked to the provisioning profile for both apps.
|
|
*/
|
|
NSString *const kKeymanGroupId = @"group.com.keyman";
|
|
|
|
@interface KMDataRepository ()
|
|
@property (readonly) NSURL *applicationSupportSubDirectory;
|
|
@property (readonly) NSURL *documentsSubDirectory;
|
|
@property (readonly) NSURL *keyman17KeyboardsDirectory;
|
|
@end
|
|
|
|
@implementation KMDataRepository
|
|
/**
|
|
* Three directory trees are represented by the following properties, one in active use
|
|
* and two that are obsolete.
|
|
* The actively used directories, introduced in Keyman 19, are shared via the app group `group.com.keyman`:
|
|
* '~/Library/Group Containers/group.com.keyman/Library/Application Support/Keyman-Packages/'
|
|
* The obsolete directories from Keyman 18 are:
|
|
* applicationSupportSubDirectory: '~/Library/Application Support'
|
|
* keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
|
|
* keyman18KeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
|
|
* The obsolete directories from Keyman 17 and earlier are:
|
|
* documentsSubDirectory: '~/Documents'
|
|
* keyman17KeyboardsDirectory: '~/Documents/Keyman-Keyboards'
|
|
*/
|
|
@synthesize applicationSupportSubDirectory = _applicationSupportSubDirectory;
|
|
@synthesize keyman18DataDirectory = _keyman18DataDirectory;
|
|
@synthesize keyman18KeyboardsDirectory = _keyman18KeyboardsDirectory;
|
|
@synthesize documentsSubDirectory = _documentsSubDirectory;
|
|
@synthesize keyman17KeyboardsDirectory = _keyman17KeyboardsDirectory;
|
|
|
|
@synthesize keyman19ContainerDirectory = _keyman19ContainerDirectory;
|
|
@synthesize keyman19KeyboardsDirectory = _keyman19KeyboardsDirectory;
|
|
|
|
NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards";
|
|
/**
|
|
* The name of the subdirectory within '~/Library/Application Support'.
|
|
* We follow the convention of using the bundle identifier rather than our subsystem id.
|
|
* (Also, using the subsystem id, "com.keyman.app", is a poor choice because the API
|
|
* createDirectoryAtPath sees the .app extension and creates an application file.)
|
|
*/
|
|
NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
|
|
|
|
NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/Keyman-Packages";
|
|
|
|
+ (KMDataRepository *)shared {
|
|
static KMDataRepository *shared = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
shared = [[KMDataRepository alloc] init];
|
|
});
|
|
return shared;
|
|
}
|
|
|
|
/**
|
|
* Returns true if URL is an existing directory that is not empty
|
|
*/
|
|
+ (BOOL)isExistingNonEmptyDirectory:(NSURL *)directoryUrl {
|
|
return [KMDataRepository isExistingDirectory: directoryUrl] && ![KMDataRepository isEmptyDirectory: directoryUrl];
|
|
}
|
|
|
|
/**
|
|
* Returns true if URL is an existing directory that is empty
|
|
*/
|
|
+ (BOOL)isExistingEmptyDirectory:(NSURL *)directoryUrl {
|
|
return [KMDataRepository isExistingDirectory: directoryUrl] && [KMDataRepository isEmptyDirectory: directoryUrl];
|
|
}
|
|
|
|
/**
|
|
* Returns true if the directory exists
|
|
*/
|
|
+ (BOOL)isExistingDirectory:(NSURL *)directoryUrl {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isDirectory;
|
|
BOOL urlExists = ([fileManager fileExistsAtPath:directoryUrl.path isDirectory:&isDirectory]);
|
|
return urlExists && isDirectory;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the directory is empty
|
|
*/
|
|
+ (BOOL)isEmptyDirectory:(NSURL *)directoryUrl {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isEmpty = true;
|
|
|
|
NSError *error = nil;
|
|
NSArray *contents = [fileManager contentsOfDirectoryAtPath:directoryUrl.path error:&error];
|
|
|
|
if (error) {
|
|
os_log_debug([KMLogs dataLog], "cannot read directory: %@", directoryUrl.path);
|
|
} else if (contents.count > 0) {
|
|
isEmpty = false;
|
|
} else {
|
|
isEmpty = true;
|
|
}
|
|
|
|
return isEmpty;
|
|
}
|
|
|
|
- (NSURL *)documentsSubDirectory {
|
|
if (_documentsSubDirectory == nil) {
|
|
NSError *directoryError = nil;
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSURL *documentsUrl = [fileManager URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&directoryError];
|
|
|
|
if (directoryError) {
|
|
os_log_error([KMLogs dataLog], "error getting Documents subdirectory: '%{public}@'", directoryError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Documents subdirectory: '%{public}@'", documentsUrl);
|
|
_documentsSubDirectory = documentsUrl;
|
|
}
|
|
}
|
|
return _documentsSubDirectory;
|
|
}
|
|
|
|
- (NSURL *)applicationSupportSubDirectory {
|
|
if (_applicationSupportSubDirectory == nil) {
|
|
NSError *directoryError = nil;
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSURL *applicationSupportUrl = [fileManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&directoryError];
|
|
|
|
if (directoryError) {
|
|
os_log_error([KMLogs dataLog], "error getting Application Support subdirectory: '%{public}@'", directoryError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Application Support subdirectory: '%{public}@'", applicationSupportUrl.path);
|
|
_applicationSupportSubDirectory = applicationSupportUrl;
|
|
}
|
|
}
|
|
return _applicationSupportSubDirectory;
|
|
}
|
|
|
|
- (NSURL *)keyman18DataDirectory {
|
|
if (_keyman18DataDirectory == nil) {
|
|
NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE];
|
|
_keyman18DataDirectory = keymanDataUrl;
|
|
}
|
|
return _keyman18DataDirectory;
|
|
}
|
|
|
|
- (NSURL *)keyman18KeyboardsDirectory {
|
|
if (_keyman18KeyboardsDirectory == nil) {
|
|
NSURL *keyboardsUrl = [self.keyman18DataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
|
|
_keyman18KeyboardsDirectory = keyboardsUrl;
|
|
}
|
|
return _keyman18KeyboardsDirectory;
|
|
}
|
|
|
|
- (NSURL *)keyman19ContainerDirectory {
|
|
if (_keyman19ContainerDirectory == nil) {
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
os_log_info([KMLogs dataLog], "about to fetch containerUrl");
|
|
NSURL *containerUrl = [fileManager containerURLForSecurityApplicationGroupIdentifier: kKeymanGroupId];
|
|
os_log_info([KMLogs dataLog], "containerUrl: '%{public}@'", containerUrl.path);
|
|
|
|
_keyman19ContainerDirectory = containerUrl;
|
|
}
|
|
return _keyman19ContainerDirectory;
|
|
}
|
|
|
|
- (NSURL *)keyman19KeyboardsDirectory {
|
|
if (_keyman19KeyboardsDirectory == nil) {
|
|
NSURL *keyboardsUrl = [self.keyman19ContainerDirectory URLByAppendingPathComponent:kContainerKeyboardsPartialPath isDirectory: TRUE];
|
|
_keyman19KeyboardsDirectory = keyboardsUrl;
|
|
}
|
|
return _keyman19KeyboardsDirectory;
|
|
}
|
|
|
|
/**
|
|
* Creates Keyman 19 data directory if it does not exist yet. This is under 'Group Containers'.
|
|
*/
|
|
- (void)createSharedDirectoriesIfNecessary {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isDir;
|
|
BOOL exists = [fileManager fileExistsAtPath:self.keyman19KeyboardsDirectory.path isDirectory:&isDir];
|
|
|
|
if (!exists) {
|
|
NSError *createError = nil;
|
|
os_log_info([KMLogs dataLog], "createKeyman19SharedDirectoriesIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keyman19KeyboardsDirectory.path);
|
|
[fileManager createDirectoryAtPath:self.keyman19KeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:&createError];
|
|
if (createError) {
|
|
os_log_error([KMLogs dataLog], "error creating Keyman data directory: '%{public}@'", createError.localizedDescription);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "created Keyman data directory: '%{public}@'", self.keyman19KeyboardsDirectory.path);
|
|
}
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keyman19KeyboardsDirectory.path);
|
|
}
|
|
}
|
|
|
|
- (NSURL *)keyman17KeyboardsDirectory {
|
|
if (_keyman17KeyboardsDirectory == nil) {
|
|
NSURL *keymanUrl = [self.documentsSubDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
|
|
_keyman17KeyboardsDirectory = keymanUrl;
|
|
}
|
|
return _keyman17KeyboardsDirectory;
|
|
}
|
|
|
|
/**
|
|
* Only called from migrateDataFromKeyman17.
|
|
* Causes user to be prompted for permission to access ~/Documents, but they should already have it.
|
|
* otherwise we would not be attempting to migrate.
|
|
*/
|
|
- (BOOL)keyboardsExistInDocumentsDirectory {
|
|
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman17KeyboardsDirectory];
|
|
}
|
|
|
|
/**
|
|
* Only called from migrateDataFromKeyman18.
|
|
* Checks to see if the keyboards directory exists in '~/Library/Application Support/keyman.inputmethod.Keyman' and is not empty
|
|
*/
|
|
- (BOOL)keyboardsExistInInputMethodDataDirectory {
|
|
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman18KeyboardsDirectory];
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the keyboards directory exists in '~/Library/Group Containers/group.com.keyman/Library/Application Support' and is not empty
|
|
*/
|
|
- (BOOL)keyboardsExistInGroupContainerDirectory {
|
|
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman19KeyboardsDirectory];
|
|
}
|
|
|
|
- (NSString*)buildFullPath:(NSString *)fromPartialPath {
|
|
NSString *fullPath = [self.keyman19KeyboardsDirectory.path stringByAppendingString:fromPartialPath];
|
|
os_log_debug([KMLogs dataLog], "buildFullPath: '%{public}@' fromPartialPath '%{public}@'",
|
|
fullPath, fromPartialPath);
|
|
return fullPath;
|
|
}
|
|
|
|
- (NSString *)trimToPartialPath:(NSString *)fromFullPath {
|
|
NSString *partialPath = fromFullPath;
|
|
if(fromFullPath != nil) {
|
|
NSRange range = [fromFullPath rangeOfString:kKeyboardsDirectoryName];
|
|
if (range.length > 0) {
|
|
partialPath = [fromFullPath substringFromIndex:range.location + range.length];
|
|
os_log_debug([KMLogs dataLog], "trimToPartialPath: fromFullPath: '%{public}@' to partialPath: '%{public}@'", fromFullPath, partialPath);
|
|
}
|
|
}
|
|
return partialPath;
|
|
}
|
|
|
|
- (NSString *)buildPartialPathFrom:(NSString *)keyboardSubdirectory keyboardFile:(NSString *)kmxFilename {
|
|
NSMutableArray *pathComponents = [[NSMutableArray alloc] initWithCapacity:0];
|
|
[pathComponents addObject:@"/"];
|
|
[pathComponents addObject:keyboardSubdirectory];
|
|
[pathComponents addObject:kmxFilename];
|
|
NSString *keyboardPartialPath = [NSString pathWithComponents:pathComponents];
|
|
os_log_debug([KMLogs keyboardLog], "buildPartialPathFrom, keyboardSubdirectory: %{public}@, kmxFileName: %{public}@, keyboardPartialPath : %{public}@",
|
|
keyboardSubdirectory, kmxFilename, keyboardPartialPath);
|
|
return keyboardPartialPath;
|
|
}
|
|
|
|
// MARK: Data migration
|
|
|
|
/**
|
|
* Move all the packages contained in the directory
|
|
*/
|
|
+ (void)movePackages: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSArray *keys = @[NSURLIsDirectoryKey];
|
|
|
|
// fetch the individual keyboard package directories within the keyboards directory
|
|
NSArray *packageDirectories = [fileManager contentsOfDirectoryAtURL:sourceDirectory includingPropertiesForKeys:keys options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
|
|
|
|
// Iterate and filter for directories
|
|
for (NSURL *packageUrl in packageDirectories) {
|
|
NSNumber *isDirectory;
|
|
[packageUrl getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
|
|
|
|
if ([isDirectory boolValue]) {
|
|
NSString *packageName = packageUrl.lastPathComponent;
|
|
NSURL *packageSourceUrl = [sourceDirectory URLByAppendingPathComponent:packageName isDirectory: true];
|
|
NSURL *packageDestinationUrl = [destinationDirectory URLByAppendingPathComponent:packageName isDirectory: true];
|
|
|
|
os_log_info([KMLogs dataLog], "moving package directory: '%{public}@'", packageName);
|
|
BOOL movedDirectory = [KMDataRepository moveDirectory: packageSourceUrl to: packageDestinationUrl];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move a single directory
|
|
*/
|
|
+ (BOOL)moveDirectory: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
NSError *moveError = nil;
|
|
BOOL didMoveData = [fileManager moveItemAtURL:sourceDirectory
|
|
toURL:destinationDirectory
|
|
error:&moveError];
|
|
if (moveError) {
|
|
os_log_error([KMLogs dataLog], "data migration failed with error: '%{public}@', source: '%{public}@', destination: '%{public}@'", moveError.localizedDescription, sourceDirectory.path, destinationDirectory.path);
|
|
} else {
|
|
os_log_info([KMLogs dataLog], "data migrated successfully, source: '%{public}@' destination: '%{public}@'", sourceDirectory.path, destinationDirectory.path);
|
|
}
|
|
return didMoveData;
|
|
}
|
|
|
|
/**
|
|
* Migrate the keyboards data from the old location in '~/Documents' to the shared location in
|
|
* '~/Library/Group Containers/group.com.keyman/Library/Application Support'
|
|
* This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location.
|
|
* If this is the case, then we expect the user to have already granted permission for Keyman to access the ~/Documents directory.
|
|
* If that permission has been removed for some reason, then calling this code will cause the user to be asked for permission again.
|
|
*/
|
|
- (void)migrateDataFromKeyman17 {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL dataExistsInOldLocation = [self keyboardsExistInDocumentsDirectory];
|
|
os_log_debug([KMLogs dataLog], "keyman 17 keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO");
|
|
|
|
// only move data if there is something to move
|
|
if (dataExistsInOldLocation) {
|
|
[KMDataRepository movePackages:[self keyman17KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]];
|
|
|
|
// delete the Keyman-Keyboards directory
|
|
NSError *error = nil;
|
|
[fileManager removeItemAtURL: self.keyman17KeyboardsDirectory error:&error];
|
|
if (error == nil) {
|
|
os_log_debug([KMLogs dataLog], "deleted obsolete keyboards directory: '%{public}@'", [self keyman17KeyboardsDirectory]);
|
|
} else {
|
|
os_log_error([KMLogs dataLog], "error attempting to delete obsolete keyboards directory: '%{public}@'", [error localizedDescription]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate the keyboards data from the input method specific location in '~/Application Support/keyman.inputmethod.Keyman/'
|
|
* to the shared location in '~/Library/Group Containers/group.com.keyman/Library/Application Support'
|
|
* This should only be called if the Keyman settings written to the UserDefaults indicates that we have data in the old location.
|
|
*/
|
|
- (void)migrateDataFromKeyman18 {
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL dataExistsInOldLocation = [self keyboardsExistInInputMethodDataDirectory];
|
|
os_log_debug([KMLogs dataLog], "keyman 18 keyboards directory exists: %{public}@", dataExistsInOldLocation?@"YES":@"NO");
|
|
|
|
// only move data if there is something to move
|
|
if (dataExistsInOldLocation) {
|
|
[KMDataRepository movePackages:[self keyman18KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]];
|
|
|
|
// delete the Keyman-Keyboards directory
|
|
NSError *error = nil;
|
|
[fileManager removeItemAtURL: self.keyman18KeyboardsDirectory error:&error];
|
|
|
|
if (error == nil) {
|
|
os_log_debug([KMLogs dataLog], "deleted obsolete keyboards directory: '%{public}@'", [self keyman18KeyboardsDirectory]);
|
|
} else {
|
|
os_log_error([KMLogs dataLog], "error attempting to delete obsolete keyboards directory: '%{public}@'", [error localizedDescription]);
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|