Skip to content

Commit

Permalink
Merge pull request #12106 from keymanapp/change/mac/2542-store-keyboa…
Browse files Browse the repository at this point in the history
…rds-elsewhere

change(mac): store data in Library directory instead of Documents
  • Loading branch information
sgschantz authored Aug 30, 2024
2 parents 7dc3b06 + 8dd258d commit a283fd1
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 37 deletions.
16 changes: 16 additions & 0 deletions mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
29015ABD2C58D86F00CCBB94 /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; };
290BC680274B9DB1005CD1C3 /* KMPackageInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 290BC67F274B9DB1005CD1C3 /* KMPackageInfo.m */; };
290BC75E274F3FD7005CD1C3 /* KMKeyboardInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 290BC75D274F3FD7005CD1C3 /* KMKeyboardInfo.m */; };
2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B4A0D32BF7675A00682049 /* KMLogs.m */; };
Expand All @@ -30,6 +31,8 @@
29B42A602728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A622728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib */; };
29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B4A0D32BF7675A00682049 /* KMLogs.m */; };
29B6FB732BC39DD60074BF7F /* TextApiComplianceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */; };
29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; };
29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; };
37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; };
37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; };
37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; };
Expand Down Expand Up @@ -72,6 +75,7 @@
98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */; };
9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A3D6C5C221531B0008785A3 /* KMOSVersion.m */; };
B90818AF7ED302187DE0E026 /* Pods_Keyman.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B644E63217FFE54B91C71C94 /* Pods_Keyman.framework */; };
D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; };
E211769D20E182DD00F8065D /* NoContextTestClient.m in Sources */ = {isa = PBXBuildFile; fileRef = E211769C20E182DD00F8065D /* NoContextTestClient.m */; };
E21176A020E18C5200F8065D /* AppleCompliantTestClient.m in Sources */ = {isa = PBXBuildFile; fileRef = E211769F20E18C5200F8065D /* AppleCompliantTestClient.m */; };
E211CCF620B600A500505C36 /* KeymanEngine4Mac.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = E211CCF520B600A500505C36 /* KeymanEngine4Mac.framework.dSYM */; };
Expand Down Expand Up @@ -135,6 +139,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
29015ABB2C58D86F00CCBB94 /* KMDataRepository.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMDataRepository.h; sourceTree = "<group>"; };
29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMDataRepository.m; sourceTree = "<group>"; };
2901BA8A292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMAboutWindowController.strings"; sourceTree = "<group>"; };
2901BA8B292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/preferences.strings"; sourceTree = "<group>"; };
2901BA8C292332B3009903EC /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/KMInfoWindowController.strings"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -356,6 +362,8 @@
CEFFECDB2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
CEFFECDC2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = "<group>"; };
CEFFECDD2A4180FD00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
D861B03D2C5747F70003675E /* KMSettingsRepository.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSettingsRepository.h; sourceTree = "<group>"; };
D861B03E2C5747F70003675E /* KMSettingsRepository.m */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = KMSettingsRepository.m; sourceTree = "<group>"; tabWidth = 2; };
E211769B20E1826800F8065D /* NoContextTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NoContextTestClient.h; sourceTree = "<group>"; };
E211769C20E182DD00F8065D /* NoContextTestClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NoContextTestClient.m; sourceTree = "<group>"; };
E211769E20E18C0B00F8065D /* AppleCompliantTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppleCompliantTestClient.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -554,6 +562,10 @@
29B4A0D32BF7675A00682049 /* KMLogs.m */,
299ABD6F29ECE75B00AA5948 /* KeySender.m */,
299ABD7029ECE75B00AA5948 /* KeySender.h */,
D861B03D2C5747F70003675E /* KMSettingsRepository.h */,
D861B03E2C5747F70003675E /* KMSettingsRepository.m */,
29015ABB2C58D86F00CCBB94 /* KMDataRepository.h */,
29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */,
297A501128DF4D360074EB1B /* Privacy */,
98FE105B1B4DE86300525F54 /* Categories */,
98D6DA791A799EE700B09822 /* Frameworks */,
Expand Down Expand Up @@ -981,8 +993,10 @@
29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */,
98BF924F1BF02DC20002126A /* KMBarView.m in Sources */,
E240F599202DED740000067D /* KMPackage.m in Sources */,
D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */,
984B8F441AF1C3D900E096A8 /* OSKWindowController.m in Sources */,
9836B3711AE5F11D00780482 /* mztools.c in Sources */,
29015ABD2C58D86F00CCBB94 /* KMDataRepository.m in Sources */,
9836B3701AE5F11D00780482 /* ioapi.c in Sources */,
E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */,
98E6729F1B532F5E00DBDE2F /* KMDownloadKBWindowController.m in Sources */,
Expand Down Expand Up @@ -1012,6 +1026,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */,
29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */,
2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */,
2992F4202A28482800E08929 /* PrivacyWindowController.m in Sources */,
2992F41F2A2847C900E08929 /* TextApiCompliance.m in Sources */,
Expand Down
25 changes: 25 additions & 0 deletions mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Created by Shawn Schantz on 2024-07-30.
*
*/

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface KMDataRepository : NSObject
// keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keymanDataDirectory;

// keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
@property (readonly) NSURL *keymanKeyboardsDirectory;

+ (KMDataRepository *)shared;
- (void)createDataDirectoryIfNecessary;
- (void)createKeyboardsDirectoryIfNecessary;
- (BOOL)migrateData;
@end

NS_ASSUME_NONNULL_END
199 changes: 199 additions & 0 deletions mac/Keyman4MacIM/Keyman4MacIM/KMDataRepository.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* 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"

@interface KMDataRepository ()
@property (readonly) NSURL *applicationSupportSubDirectory;
@property (readonly) NSURL *documentsSubDirectory;
@property (readonly) NSURL *obsoleteKeymanKeyboardsDirectory;
@end

@implementation KMDataRepository
/**
* Two directory trees are represented by the following properties, one in active use
* and one that is obsolete.
* The actively used directories, begin with the parent
* applicationSupportSubDirectory: '~/Library/Application Support'
* keymanDataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
* keymanKeyboardsDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman/Keyman-Keyboards'
* The obsolete directories, begin with the parent
* documentsSubDirectory: '~/Documents'
* obsoleteKeymanKeyboardsDirectory: '~/Documents/Keyman-Keyboards'
*/
@synthesize applicationSupportSubDirectory = _applicationSupportSubDirectory;
@synthesize keymanDataDirectory = _keymanDataDirectory;
@synthesize keymanKeyboardsDirectory = _keymanKeyboardsDirectory;
@synthesize documentsSubDirectory = _documentsSubDirectory;
@synthesize obsoleteKeymanKeyboardsDirectory = _obsoleteKeymanKeyboardsDirectory;

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";

+ (KMDataRepository *)shared {
static KMDataRepository *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[KMDataRepository alloc] init];
});
return shared;
}

- (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 *)keymanDataDirectory {
if (_keymanDataDirectory == nil) {
NSURL *keymanDataUrl = [self.applicationSupportSubDirectory URLByAppendingPathComponent:kKeymanSubdirectoryName isDirectory: TRUE];
_keymanDataDirectory = keymanDataUrl;
}
return _keymanDataDirectory;
}

- (NSURL *)keymanKeyboardsDirectory {
if (_keymanKeyboardsDirectory == nil) {
NSURL *keyboardsUrl = [self.keymanDataDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
_keymanKeyboardsDirectory = keyboardsUrl;
}
return _keymanKeyboardsDirectory;
}

/**
* Creates Keyman data directory if it do not exist yet. This is the main data subdirectory: keyman.inputmethod.Keyman
*/
- (void)createDataDirectoryIfNecessary {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = [fileManager fileExistsAtPath:self.keymanDataDirectory.path isDirectory:&isDir];

if (!exists) {
NSError *createError = nil;
os_log_info([KMLogs dataLog], "createDataDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanDataDirectory.path);
[fileManager createDirectoryAtPath:self.keymanDataDirectory.path withIntermediateDirectories:YES attributes:nil error:nil];
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.keymanDataDirectory.path);
}
} else {
os_log_info([KMLogs dataLog], "Keyman data directory already exists: '%{public}@'", self.keymanDataDirectory.path);
}
}

/**
* Creates Keyman keyboard directory if it does not exist yet. This is the 'Keyman-Keyboards' directory.
* It should not be created until after migrating because its existence would block migrating data from the old location.
*/
- (void)createKeyboardsDirectoryIfNecessary {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = [fileManager fileExistsAtPath:self.keymanKeyboardsDirectory.path isDirectory:&isDir];

if (!exists) {
NSError *createError = nil;
os_log_info([KMLogs dataLog], "createKeyboardsDirectoryIfNecessary, about to attempt createDirectoryAtPath for: '%{public}@'", self.keymanKeyboardsDirectory.path);
[fileManager createDirectoryAtPath:self.keymanKeyboardsDirectory.path withIntermediateDirectories:YES attributes:nil error:nil];
if (createError) {
os_log_error([KMLogs dataLog], "error creating Keyman-Keyboards directory: '%{public}@'", createError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "created Keyman-Keyboards subdirectory: '%{public}@'", self.keymanKeyboardsDirectory.path);
}
} else {
os_log_info([KMLogs dataLog], "Keyman-Keyboards already exists: '%{public}@'", self.keymanKeyboardsDirectory.path);
}
}

- (NSURL *)obsoleteKeymanKeyboardsDirectory {
if (_obsoleteKeymanKeyboardsDirectory == nil) {
NSURL *keymanUrl = [self.documentsSubDirectory URLByAppendingPathComponent:kKeyboardsDirectoryName isDirectory: TRUE];
_obsoleteKeymanKeyboardsDirectory = keymanUrl;
}
return _obsoleteKeymanKeyboardsDirectory;
}

/**
* Only called from migrateData.
* 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)keyboardsExistInObsoleteDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = ([fileManager fileExistsAtPath:self.obsoleteKeymanKeyboardsDirectory.path isDirectory:&isDir]);
return exists;
}

/**
* Migrate the keyboards data from the old location in '~/Documents' to the new location '~/Application Support/keyman.inputmethod.Keyman/'
* 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.
*/
- (BOOL)migrateData {
BOOL didMoveData = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL dataExistsInOldLocation = [self keyboardsExistInObsoleteDirectory];
os_log([KMLogs dataLog], "obsolete keyman keyboards directory exists: %@", dataExistsInOldLocation?@"YES":@"NO");

// only move data if there is something to move
if (dataExistsInOldLocation) {
NSError *moveError = nil;
didMoveData = [fileManager moveItemAtURL:self.obsoleteKeymanKeyboardsDirectory
toURL:self.keymanKeyboardsDirectory
error:&moveError];
if (moveError) {
os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", self.keymanKeyboardsDirectory.path);
}
}

return didMoveData;
}

@end
1 change: 0 additions & 1 deletion mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0;
- (NSString *)oskWindowTitle;
- (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent;
- (KeymanVersionInfo)versionInfo;
- (NSString *)keymanDataPath;
- (void)registerConfigurationWindow:(NSWindowController *)window;
@end

Expand Down
Loading

0 comments on commit a283fd1

Please sign in to comment.