I have developed a custom input method and now would like to develop a tweak that would register it as a keyboard in iOS.
There are many different keyboards in Cydia (mainly from Chinese developers) such as TouchPal and Baidu Input that appear in settings as a keyboard, so it is definitely possible.
I have tried looking into the following options (barely 4 days in IDA, Xcode with theos and console):
Text Input bundles located in /System/Library/TextInput — seems to have nothing to deal with the keyboards themselves? Some superclass headers are missing (i.e. TIZephyr... classes) so I couldn't quite figure it out. However a native integration would be awesome.
TextInput private framework — also seems to be just for dictionary and so on
UIKit's UIKB.. and UIKeyboard.. classes — UIKeyboardImpl seems to be something related with the keyboard functioning and UIKeyboardLayout is the thing you build upon.
I tried hooking UIKeyboardDictationLayout to just give a plain instance of a UIKeyboardLayout upon initialization — and when I tapped the mic button on the keyboard, the keyboard went blank! That kind of implementation would be nice too (even though killing dictation functionality is undesired). However, I can't find where do I send typing events as well.
So the points are:
What is responsible for registering a class as an input method?
What is responsible for receiving typing events?
I am asking this in hope that there are developers who had to do something similar already, because I couldn't find any articles nor anything that would give me a hint in the header files and bundles.
Thanks in advance.
I got it right this february even though didn't have the time to respond and it's not quite necessary now that iOS 8 has come.
Still, this is how you load your own keyboard:
%hook UIKeyboardInputMode
+ (id)keyboardInputModeWithIdentifier:(id)arg1 {
id o = %orig;
return o;
}
- (id)primaryLanguage {
if([TegakiLayout isTegaki:[self identifier]]) return #"Tegaki";
return %orig;
}
%end
%hook UIKeyboardImpl
/* This is where the magic is! */
+ (Class)layoutClassForInputMode:(NSString*)arg1 keyboardType:(int)arg2 {
Class sass = %orig;
if ([TegakiLayout isTegaki: arg1]) {
return [TegakiLayout class];
}
return sass;
}
%end
extern "C" NSArray*UIKeyboardGetSupportedInputModes();
extern "C" NSArray*UIKeyboardGetActiveInputModes();
static NSArray* (*orig_modes)();
NSArray* rep_modes() {
NSArray* res = [orig_modes() arrayByAddingObjectsFromArray:#[#"TEGAKI", #"TEGAKI_Graffiti"]];
return res;
}
static NSArray* (*orig_active_modes)();
NSArray* rep_active_modes() {
NSArray* res = orig_active_modes();
return res;
}
%ctor {
%init;
MSHookFunction(UIKeyboardGetSupportedInputModes, rep_modes, &orig_modes);
MSHookFunction(UIKeyboardGetActiveInputModes, rep_active_modes, &orig_active_modes);
}
where TegakiLayout is a subclass of UIKeyboardLayout.
You then implement - (BOOL)isAlphabeticPlane for returning whether it's a traditional keyboard thing and do the custom view creation in showKeyboardWithInputTraits:screenTraits:splitTraits:.
To type in you then use [[UIKeyboardImpl activeInstance]insertText:#"\n"];.
To create a 'globe' button you use this:
Class sw = NSClassFromString(#"UIInputSwitcherView");
[[sw sharedInstance]selectNextInputMode];
Don't forget to implement -keyboardName and -keyplaneName as well!
I'll post the whole project one day probably, but for now it's too large to describe here. This should be enough to get you up and running, though.
Related
I've created a (stripped-down version of my) tweak, which logs all URLs an app instantiates with one specific method. It works fine for several apps, but at the start of one app, the tweak is not loaded. I tried to use other filters, neither the bundle id, the class name nor the executable name worked.
Any idea?
Tweak.xm:
%hook NSURL
+ (instancetype)URLWithString:(NSString *)URLString {
%log;
return %orig;
}
%end
.plist:
{ Filter = { Bundles = ( "com.htsu.hsbcpersonalbanking" ); Executables = ("HSBC"); Classes = ("NSURL"); }; }
Three possibilities:
The binary has __RESTRICTED section and normal injection won't work
otool -l /PATH/TO/BINARY|grep sectname, this would be the case if you see __RESTRICTED in the result. optool will force the injection for you but then again you might need to bypass the app's anti-injection checks as well
Your tweak is not compiled correctly.
See syslog, MobileSubstrate will warn you in syslog if this is the case
That method is not called
try:
%ctor{
NSLog(#"I'm injected");
}
and see if that is logged to rule out possibilities of 1&2
I'm working on an iOS text to speech app and trying to add an option to use the Alex voice, which is new for iOS 9. I need to determine whether or not the user has downloaded the Alex voice in Settings -> Accessibility. I can't seem to find out how to do this.
if ([AVSpeechSynthesisVoice voiceWithIdentifier:AVSpeechSynthesisVoiceIdentifierAlex] == "Not Found" ) {
// Do something...
}
The reason is the other language voices that are standard, play back at a certain rate, different from the Alex voice. So I have a working app, but if the user hasn't downloaded the voice, iOS automatically defaults to a basic voice, but it plays back at the incorrect rate. If I can detect the voice hasn't been downloaded, I can compensate for the difference and / or advise the user.
OK, so I guess I was overthinking this and thought it was more complicated. The solution was simple.
if (![AVSpeechSynthesisVoice voiceWithIdentifier:AVSpeechSynthesisVoiceIdentifierAlex]) {
// Normalize the speech rate since the user hasn't downloaded the voice and/or trigger a notification that they need to go into settings and download the voice.
}
Thanks to everyone who looked at this and to #CeceXX for the edit. Hope this helps someone else.
Here's one way to do it. Let's stick with Alex as an example:
- (void)checkForAlex {
// is Alex installed?
BOOL alexInstalled = NO;
NSArray *voices = [AVSpeechSynthesisVoice speechVoices];
for (id voiceName in voices) {
if ([[voiceName valueForKey:#"name"] isEqualToString:#"Alex"]) {
alexInstalled = YES;
}
}
// react accordingly
if (alexInstalled) {
NSLog(#"Alex is installed on this device.");
} else {
NSLog(#"Alex is not installed on this device.");
}
}
This method loops through all installed voices and queries each voice's name. If Alex is among them, he's installed.
Other values you can query are "language" (returns a language code like en-US) and quality (1 = standard, 2 = enhanced).
I am facing an issue with converting NSString into executable statement of objective C.
For example,
NSString *strColorAssembly = #"[UIColor redColor]";
Now, I need to convert this string into an executable code and pass to .color property.
This is just an example, I am trying to build a project in which everything will be dynamic, so It would be much helpful if anyone can provide me right direction to go ahead.
I'm not sure if you can programmatically accomplish that goal. If it were me I would implement a method that accepted an NSString (or an NSInteger) and return a UIColor.
- (UIColor *)colorDecode:(NSString *)colorPassed
{
if ([colorPassed isEqualToString #"red"])
{
return [UIColor redColor];
}
else if
.
.
.
}
OR
- (UIColor *)colorDecodewithInt:(NSUInteger)colorIndex
{
switch (colorIndex)
{
case (0)
{
return [UIColor redColor];
break;
}
case (1)
.
.
.
.
default
{
return <some default color>;
break;
}
}
}
EDIT:
I suppose you could also use an NSDictionary that consists of a collection of UIColor objects as well. The bottom line is that you're going to be restricted to a pre-defined set of colors in your compiled code unless you start playing with CGColor at which point you can start dynamically pass the RGB values.
It sounds like what you are looking for is the setValue:ForKey: method. Anything that conforms to the NSKeyValueCoding Protocol will have that option available. However, this will only work for the key part (i.e. the name of the property). iOS explicitly prohibits any modification to the distributed code. I would recommend using the keys as strings and writing some kind of interpreter for the rest of your data.
You can't do that on iOS, period. iOS prevents you from allocating memory pages that can both be written to and also executed.
The best you could manage is an interpreter that takes strings in, parses them, and attempts to map them to Objective-C calls (using NSClassFromString and NSSelectorFromString).
I would recommend not doing what you're trying to do, and instead use one of the many bindings from Cocoa to a dynamic language. I think the most popular ones bind Cocoa to Lua, so then you can write your Lua code and everything will be dynamic.
I'm trying to figure out how to detect that the Escape (and other key combinations like Ctrl and alt) have been pressed on a bluetooth keyboard attached to an iOS device.
Some answers seem to suggest this isn't possible. However there are apps in the Appstore that do this (for example iSSH) so I assume it is possible using the public APIs somehow.
I've tried creating my own UITextInput however this receives nothing when the Escape key is pressed. The only part of the API I can see where the iPad might respond is when VoiceOver is enabled (Escape works as back in Safari), so I'm wondering if there's a way in via the accessibility API?
I've also tried to see if there's something I can observe from NSNotificationCenter that might help, but have yet to find anything.
Suggestions welcome, I've been hacking away at this for a day and I'm at a bit of a loss now.
You can do this now in iOS 7. For example, to implement the escape key, override UITextView and place the following methods in your class:
- (NSArray *) keyCommands {
UIKeyCommand *esc = [UIKeyCommand keyCommandWithInput: UIKeyInputEscape modifierFlags: 0 action: #selector(esc:)];
return [[NSArray alloc] initWithObjects: esc, nil];
}
- (void) esc: (UIKeyCommand *) keyCommand {
// Your custom code goes here.
}
You don't need to check to be sure you are on iOS 7, since earlier versions of the OS won't call the keyCommands method.
There are no public APIs for what you intend to accomplish, so this may lead to a rejection.
If you are willing to risk it, you can try this. Wich basically intercepts all events sent to your App by overwriting sendEvent: in your UIApplication.
AFAIK this is not possible using public API.
I've done a bit of searching and the esc key is not recognized.
The only thing that I didn't do is to try iSSH (it costs 9€ :-), but if you read the description on the AppStore it seems clear that ESC key on a hardware (bluetooth) keyboard doesn't work:
Exhaustive key configuration support. Has arrow keys (by pop-up or by toolbar). ctrl, alt, esc, tab, shift, Fn keys (1-10), ` key, PgUp, PgDown and for those keys not listed provides multiple means to add them.
Bluetooth keyboard support for arrow keys, function keys and a remapping of the ctrl key through option key mapping in either X11/VNC server or terminal. When enabled, an Option+key press maps to equivalent Ctrl+key press.
As you can see, in the second line the ESC key is not mentioned.
Moreover, I've found this (old) post.
EDIT:
As your last updates, I've found a way to "hide" the _gsEvent inside the binary. I don't know if Apple static analyser can find it, however.
The trick is simple...create the _gsEvent selector (and other private selectors) at runtime!
-(void)sendEvent:(UIEvent *)event
{
SEL aSelector = NSSelectorFromString([self theSelector]);
if ([event respondsToSelector:aSelector]) {
NSLog(#"Event: %#", event.description);
}
[super sendEvent:event];
}
-(NSString *)theSelector
{
// compose the keyword as you prefer
NSString *sel = [NSString stringWithFormat:#"%#g%#%#ent", #"_", #"s", #"Ev"];
return sel;
}
I've tried to search inside the binary and I don't find the _gsEvent keyword, obviously because it's created only at runtime.
Hope this helps.
I have seems some apps can change the language internally within the app without the need of restarting the app, I am wondering how they are implemented.
For example, for us using NSLocalizedString, I know it is possible to set the language at runtime at main.m when your AppDelegate is not initialized, but once it is initialized (particularly your view controller is created), change it has not effect until the next restart
[[NSUserDefaults standardUserDefaults]
setObject:[NSMutableArray arrayWithObjects:language, nil]
forKey:#"AppleLanguages"];
Anyone have idea how those dynamic language change can be done without restarting the app?
There's some discussion of other approaches here, in particular a notification based approach:
iOS: How to change app language programmatically WITHOUT restarting the app?
In my view there are really three tasks here:
(1) re-localization of resources automatically loaded from nibs. (for example if you dynamically instantiate another custom UIView from a nib, the "old" language strings and settings (images, text direction) will still be loaded)
(2) re-localization of strings currently displayed on the screen.
(3) re-localization of strings inserted by the developer (you) in program code.
Let's start with (3). If you look for the definition you will notice that NSLocalizedString is a macro. So if you don't want to change existing code too much, you can probably solve the problem of (3) by creating a new header file. In that header file, #undef and then re-#define NSLocalizedString to pick the localized string from the appropriate place--not the one that iOS defaults to, but one that you keep track of in some global variable (e.g., in an app delegate ivar). If you don't want to redefine NSLocalizedString but you still make your own alternative , you should probably still #undef NSLocalizedString if you don't want future developers to accidentally call it instead of the macro you replace it with. Not an ideal solution, but maybe the most practical.
As for (1), if you haven't done your localization in Interface Builder, but rather you do it dynamically in viewDidLoad, etc., no problem. You can use the same behavior just discussed (i.e., the modified NSLocalizedString, etc.). Otherwise you can either (a) implement a notification system as described in the link above (complicated), or (b) consider moving localization from IB to viewDidLoad, or (c) try overriding initWithNibName: and swap out the object loaded with the old language resources, with one loaded with the new language resources. This was an approach mentioned by Mohamed at the very bottom of this discussion: http://learning-ios.blogspot.ca/2011/04/advance-localization-in-ios-apps.html. He claims it causes problems (viewDidLoad isn't called). Even if it doesn't work, trying it out might point you towards something that does.
Finally, (2) is presumably the easiest task: just remove and re-add the current view (or in some cases, just redraw it).
the idea is to write a new macro like NSLocalizedString which should check if to take the translation from another specific bundle or not.
The method 2 in this article explain exactly how to do it.
In this particular case, the author doesn't use a new macro, but directly set a custom class for [NSBundle mainBundle].
I hope that #holex will understand the problem reading this.
I'm always using this way, it works perfectly, it might help you as well.
you should set all the texts with NSLocalizableString(...) for the UI for the current language in the -viewWillAppear: method of your every UIViewController.
using this way you (I mean, the users) don't need to restart the application after changing the language of iOS in the Settings.
of course, I'm using the Apple's standard localisation architecture.
UPDATE on (24 Oct 2013)
I've experienced the –viewWillAppear: method won't be performed for the actual view when the application enters to foreground; to solve that issue I also commit the procedure (see above) when I receive UIApplicationWillEnterForegroundNotification notification in the view.
My implementation uses a class to change the language and access the current language bundle. It's an example so if you were to use different languages than I am then change the methods to use your exact language codes.
This class will access the preferred languages from NSLocale and take the first object which is the language being used.
#implementation OSLocalization
+ (NSBundle *)currentLanguageBundle
{
// Default language incase an unsupported language is found
NSString *language = #"en";
if ([NSLocale preferredLanguages].count) {
// Check first object to be of type "en","es" etc
// Codes seen by my eyes: "en-US","en","es-US","es" etc
NSString *letterCode = [[NSLocale preferredLanguages] objectAtIndex:0];
if ([letterCode rangeOfString:#"en"].location != NSNotFound) {
// English
language = #"en";
} else if ([letterCode rangeOfString:#"es"].location != NSNotFound) {
// Spanish
language = #"es";
} else if ([letterCode rangeOfString:#"fr"].location != NSNotFound) {
// French
language = #"fr";
} // Add more if needed
}
return [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:#"lproj"]];
}
/// Check if preferred language is English
+ (BOOL)isCurrentLanguageEnglish
{
if (![NSLocale preferredLanguages].count) {
// Just incase check for no items in array
return YES;
}
if ([[[NSLocale preferredLanguages] objectAtIndex:0] rangeOfString:#"en"].location == NSNotFound) {
// No letter code for english found
return NO;
} else {
// Tis English
return YES;
}
}
/* Swap language between English & Spanish
* Could send a string argument to directly pass the new language
*/
+ (void)changeCurrentLanguage
{
if ([self isCurrentLanguageEnglish]) {
[[NSUserDefaults standardUserDefaults] setObject:#[#"es"] forKey:#"AppleLanguages"];
} else {
[[NSUserDefaults standardUserDefaults] setObject:#[#"en"] forKey:#"AppleLanguages"];
}
}
#end
Use the class above to reference a string file / image / video / etc:
// Access a localized image
[[OSLocalization currentLanguageBundle] pathForResource:#"my_image_name.png" ofType:nil]
// Access a localized string from Localizable.strings file
NSLocalizedStringFromTableInBundle(#"StringKey", nil, [OSLocalization currentLanguageBundle], #"comment")
Change language in-line like below or update the "changeCurrentLanguage" method in the class above to take a string parameter referencing the new language code.
// Change the preferred language to Spanish
[[NSUserDefaults standardUserDefaults] setObject:#[#"es"] forKey:#"AppleLanguages"];
I was stuck in same issue, my requirement was "User can select language from drop down & application have to work according selected language (English or arabic)" What i have done i create two XIB and fetch XIB and Text according selected language. In this way user can select language. I used NSBundle for the same. like
For XIB
self.homeScreen = [[HomeScreen alloc] initWithNibName:#"HomeScreen" bundle:[CommonData sharedCommonData].languageBundle];
For Text
_lblHeading.text = [self languageSelectedStringForKey:#"ViewHeadingInfo"];
/**
This method is responsible for selecting language bundle according to user's selection.
#param: the string which is to be converted in selected language.
#return: the converted string.
#throws:
*/
-(NSString*) languageSelectedStringForKey:(NSString*) key
{
NSString* str=[[CommonData sharedCommonData].languageBundle localizedStringForKey:key value:#"" table:nil];
return str;
}
You need to load another bundle like this(where #"en" could be locale you need):
NSString *path = [[NSBundle mainBundle] pathForResource:#"en" ofType:#"lproj"];
NSBundle *languageBundle = [NSBundle bundleWithPath:path];
and make macros/function like NSLocalizedString which use your loaded bundle or use methods on that bundle directly like this
[languageBundle localizedStringForKey:key value:value table:tableName];
[[NSBundle mainBundle] localizations] lists all app localizations(including "Base").
Also I wrote helper class which does this(note that it has ReactiveCocoa as a dependency). It allows language change without app restart and sends current locale each time it's changed.