Создание твика на примере приложения Телефон. Да будет плюс!

от автора

Здравствуйте, хабражители!

Предыстория

После выхода iOS 7 некоторые пользователи начали жаловаться на проблемы с приложением Телефон. Суть проблемы в том, что при наборе номера в международном формате +375 (код) xxx-xx-xx не удается набрать ‘+’. Если удерживать ‘0’, то вместо плюса получаем комбинацию из трех пальцев ‘0+’. Проблема скорее всего локальна, так как кроме пользователей из Беларуси больше никто свое недовольство не высказывал.

По разным причинам я долго не обновлялся до iOS 7. Но обновившись был неприятно удивлен. Проблема осталась, несмотря на выход нескольких минорных обновлений. Почитав форумы, нашел следующие варианты решения этой проблемы:

  1. использовать 8 при наборе номера
  2. использовать 00
  3. удерживая ‘0’, нажать кнопку удалить до появления плюса

Первый вариант вполне себе работоспособный. С помощью второго звонок сделать не получилось, скорее всего мой оператор не поддерживает такой формат. Третий работает, но для этого нужна сноровка и вторая рука, а на улице холодно 🙂

И тогда я решил исправить это маленькое недоразумение с помощью твика.

Поехали!

На хабре уже была статья про создание твика с помощью theos, поэтому создание проекта и настройку theos пропустим и перейдем сразу к интересному.

Итак, раз мы собрались что-то менять в приложении Телефон, хорошо бы узнать что оно из себя представляет внутри. У меня на устройстве приложение лежало в /var/stash/Applications.BTFCTa/MobilePhone.app. Воспользуемся утилитой class-dump и получим список прототипов классов.

➜  class-dump -H -o MobilePhone MobilePhone.app 

Как оказалось их не так уж и мало.

Список хедеров

ABNewPersonViewControllerDelegate-Protocol.h
ABPeoplePickerNavigationControllerDelegate-Protocol.h
ABPeoplePickerNavigationControllerPrivateMemberCellDelegate-Protocol.h
ABUnknownPersonViewControllerDelegate-Protocol.h
AVCaptureFileOutputRecordingDelegate-Protocol.h
AudioDeviceController.h
CDStructures.h
CNFRegWizardControllerDelegate-Protocol.h
CommunicationDisplayViewController.h
ConferenceManagementTable.h
DialerController.h
DialerLCDFieldDelegate-Protocol.h
DialerLCDFieldProtocol-Protocol.h
DialerViewDelegate-Protocol.h
IDSIDQueryControllerDelegate-Protocol.h
InCallBottomButton.h
InCallController.h
InCallLCDField.h
InCallLCDView.h
MPDetailSliderDelegate-Protocol.h
MobilePhoneApplication.h
NSArray-MPRecentsExtensions.h
NSDate-DayComparison.h
NSDictionary-PHVoicemailAudioController.h
NSDictionary-VoicemailAudioRouting.h
NSError-VoicemailExtras.h
NSIndexSet-MPRecentsExtensions.h
NSObject-Protocol.h
PHAbstractDialerView.h
PHAddressBookController.h
PHAudioPlayer.h
PHAudioPlayerDataSource-Protocol.h
PHAudioPlayerDelegate-Protocol.h
PHAudioPlayerVoicemailDataSource.h
PHAudioRecorder.h
PHAudioRecorderDelegate-Protocol.h
PHConferenceParticipantCell.h
PHConferenceParticipantCellProtocol-Protocol.h
PHEmergencyDialerButton.h
PHEmergencyDialerViewController.h
PHEmergencyHandsetDialerLCDView.h
PHEmergencyHandsetDialerView.h
PHFavoritesCell.h
PHFavoritesContactPhotoView.h
PHFavoritesEntry.h
PHFavoritesManager.h
PHFavoritesViewController.h
PHHandsetDialerLCDView.h
PHHandsetDialerNameLabelView.h
PHHandsetDialerView.h
PHInCallNumberPadButton.h
PHInCallRingView.h
PHInfoButtonMaskView.h
PHRecentCall.h
PHRecentMultiCall.h
PHRecentsCell.h
PHRecentsManager.h
PHRecentsPersonFaceTimeHeaderSummaryView.h
PHRecentsPersonFaceTimeHeaderView.h
PHRecentsPersonHeaderSummaryView-Protocol.h
PHRecentsPersonHeaderView.h
PHRecentsPersonPhoneHeaderSummaryView.h
PHRecentsPersonPhoneHeaderView.h
PHRecentsToggleButton.h
PHRecentsViewController.h
PHStarkActionSheetTableViewCell.h
PHStarkActionSheetViewController.h
PHStarkDialerLCDView.h
PHStarkDialerView.h
PHStarkDialerViewController.h
PHStarkFavoritesTableViewCell.h
PHStarkFavoritesViewController.h
PHStarkGenericTableViewCell.h
PHStarkGenericTableViewController.h
PHStarkGenericViewController.h
PHStarkHardwareControlsBroadcaster.h
PHStarkHardwareMenuTableViewCell.h
PHStarkInCallDialerLCDView.h
PHStarkInCallDialerView.h
PHStarkInCallKeypadViewController.h
PHStarkInCallViewController.h
PHStarkLozengeLabel.h
PHStarkMainMenuContainerViewController.h
PHStarkManager.h
PHStarkNoContentBannerView.h
PHStarkPlayPauseButton.h
PHStarkRecentsTableViewCell.h
PHStarkRecentsViewController.h
PHStarkRootContainerViewController.h
PHStarkTelephonyStateMonitor.h
PHStarkTelephonyStateMonitorDelegate-Protocol.h
PHStarkVoicemailManager.h
PHStarkVoicemailPlayerViewController.h
PHStarkVoicemailTableViewCell.h
PHStarkVoicemailViewController.h
PHStaticDialerPad.h
PHTextCycleLabel.h
PHVoicemailAudioController.h
PHVoicemailBlockedListViewController.h
PHVoicemailCell.h
PHVoicemailCellConfigurationDelegate-Protocol.h
PHVoicemailCellDelegate-Protocol.h
PHVoicemailFolderCell.h
PHVoicemailGreetingCell.h
PHVoicemailGreetingViewController.h
PHVoicemailGreetingViewControllerDelegate-Protocol.h
PHVoicemailInboxListViewController.h
PHVoicemailListMaskView.h
PHVoicemailListMaskViewDelegate-Protocol.h
PHVoicemailListViewController.h
PHVoicemailListViewControllerConcrete-Protocol.h
PHVoicemailNavigationController.h
PHVoicemailNoContentViewController.h
PHVoicemailSetupViewController.h
PHVoicemailSlider.h
PHVoicemailTrashListViewController.h
PHVoicemailUnavailableCell.h
PhoneApplication.h
PhoneBadgeable-Protocol.h
PhoneBaseViewController-Protocol.h
PhoneContentView.h
PhoneDesktopView.h
PhoneNavigationController.h
PhoneRootView.h
PhoneRootViewController.h
PhoneTabBarController.h
PhoneTabViewController-Protocol.h
PhoneViewController.h
RadiosPreferencesDelegate-Protocol.h
SixSquareButton.h
SixSquareView.h
TPDialerKeypadDelegate-Protocol.h
TPSetPINViewControllerDelegate-Protocol.h
TPStarkInCallViewControllerDelegate-Protocol.h
TPSuperBottomBarDelegateProtocol-Protocol.h
UIActionSheetDelegate-Protocol.h
UIApplicationDelegate-Protocol.h
UIFont-MobilePhoneAdditions.h
UIFont-UIFont_InCallLCDView.h
UIGestureRecognizerDelegate-Protocol.h
UIImage-MobilePhoneAdditions.h
UINavigationControllerDelegate-Protocol.h
UIScrollViewDelegate-Protocol.h
UITabBarControllerDelegate-Protocol.h
UITableView-PHStarkExtensions.h
UITableViewCell-VoicemailCellAdditions.h
UITableViewDataSource-Protocol.h
UITableViewDelegate-Protocol.h
UIViewController-Testing.h
VMVoicemail-MobilePhone.h
VideoConferenceController.h

Дальнейшая логика моих действий была следующей. Раз у нас проблема при нажатии на кнопку, значит нужно искать что-то, что связано с ней.

➜  ls | grep -i "key" PHStarkInCallKeypadViewController.h TPDialerKeypadDelegate-Protocol.h 

TPDialerKeypadDelegate-Protocol.h оказался довольно интересным. В нем, как видно, описаны методы, отвечающие за нажатие кнопок.

#import "NSObject.h"  @protocol TPDialerKeypadDelegate <NSObject>  @optional - (void)phonePad:(id)arg1 dialerCharacterButtonWasHeld:(unsigned int)arg2; - (void)phonePad:(id)arg1 replaceLastDigitWithString:(id)arg2; - (void)phonePadDidEndSounds:(id)arg1; - (void)phonePadWillBeginSounds:(id)arg1; - (void)phonePad:(id)arg1 keyUp:(BOOL)arg2; - (void)phonePad:(id)arg1 keyDown:(BOOL)arg2; - (void)phonePadDeleteLastDigit:(id)arg1; - (void)phonePad:(id)arg1 appendString:(id)arg2; @end 

Что ж, раз это протокол, значит кто-то его реализует!

➜  grep -l -r "TPDialerKeypadDelegate" . ./DialerController.h ./InCallController.h ./PHEmergencyDialerViewController.h ./PHHandsetDialerView.h ./TPDialerKeypadDelegate-Protocol.h 

В дальнейшем методом научного тыка было выявлено, что DialerController — нужный нам класс, а phonePad:replaceLastDigitWithString: — нужный нам метод. В этом можно убедиться написав следующий код:

#import <substrate.h>  %hook DialerController  - (void)phonePad:(id)arg1 replaceLastDigitWithString:(id)arg2 {     %log;     %orig(arg1, arg2); }  %end 

В логе видно, что последний параметр это то, что нам надо:

<Warning>: -[<DialerController: 0x165e91e0> phonePad:<TPDialerNumberPad: 0x1677fc40; baseClass = UIControl; frame = (28 84; 264 296); opaque = NO; layer = <CALayer: 0x1677f1d0>> replaceLastDigitWithString:+] 

Хорошо бы теперь найти где хранится сама строка, к которой и добавляется наш плюс. Для этого снова заглянем в DialerController.

DialerController.h

#import "PhoneViewController.h"  #import "ABNewPersonViewControllerDelegate.h" #import "ABPeoplePickerNavigationControllerDelegate.h" #import "DialerViewDelegate.h" #import "TPDialerKeypadDelegate.h" #import "UIActionSheetDelegate.h"  @class NSString, NSTimer, PHAbstractDialerView, UINavigationController;  @interface DialerController : PhoneViewController <ABNewPersonViewControllerDelegate, ABPeoplePickerNavigationControllerDelegate, DialerViewDelegate, UIActionSheetDelegate, TPDialerKeypadDelegate> {     PHAbstractDialerView *_dialerView;     UINavigationController *_newContactNavigationController;     NSTimer *_deleteTimer;     NSTimer *_lookupTimer;     NSString *_lastDialedNumberCache;     NSString *_myPrefix;     int _shouldUseMyPrefixAsHint;     unsigned int _calledNumber:1;     unsigned int _didDeleteRepeat:1;     unsigned int _dtmfPlaying;     int _dialerType; }  + (id)defaultPNGName; + (id)tabBarIconName; + (id)tabBarIconImageSelected; + (id)tabBarIconImage; + (id)tabBarIconImageName; + (int)tabViewType; + (BOOL)launchFieldTestIfNeeded:(id)arg1; + (BOOL)shouldStringAutoDial:(id)arg1 givenLastChar:(BOOL)arg2; @property int dialerType; // @synthesize dialerType=_dialerType; @property(readonly) PHAbstractDialerView *dialerView; // @synthesize dialerView=_dialerView; - (void)_statusBarHeightChanged:(id)arg1; - (void)_handleSIMInsertionOrRemoval; - (void)performDeleteAction; - (void)performCallAction; - (void)_deleteButtonDown:(id)arg1; - (void)_deleteButtonClicked:(id)arg1; - (void)_stopDeleteTimer; - (void)_startDeleteTimer; - (void)_deleteRepeat; - (void)peoplePickerNavigationController:(id)arg1 insertEditorDidConfirm:(BOOL)arg2 forPerson:(void *)arg3; - (BOOL)peoplePickerNavigationController:(id)arg1 shouldShowInsertEditorForPerson:(void *)arg2 insertProperty:(int *)arg3 copyInsertValue:(id *)arg4 copyInsertLabel:(id *)arg5; - (BOOL)peoplePickerNavigationController:(id)arg1 shouldContinueAfterSelectingPerson:(void *)arg2 property:(int)arg3 identifier:(int)arg4; - (BOOL)peoplePickerNavigationController:(id)arg1 shouldContinueAfterSelectingPerson:(void *)arg2; - (void)peoplePickerNavigationControllerDidCancel:(id)arg1; - (void)newPersonViewController:(id)arg1 didCompleteWithNewPerson:(void *)arg2; - (void)actionSheet:(id)arg1 clickedButtonAtIndex:(int)arg2; - (void)_dismissNewContactView:(BOOL)arg1; - (void)actionSheet:(id)arg1 didDismissWithButtonIndex:(int)arg2; - (void)_addButtonClicked:(id)arg1; - (void)_addToExistingContact; - (void)_addToNewContact; - (id)_qualifyNumberIfNecessary:(id)arg1; - (void *)_newPersonWithValue:(id)arg1 forMultiValueProperty:(int)arg2; - (void)_hideNewContactView; - (void)_showNewContactView; - (void)_dialVoicemail; - (void)phonePad:(id)arg1 keyUp:(BOOL)arg2; - (void)phonePad:(id)arg1 keyDown:(BOOL)arg2; - (void)phonePadDidEndSounds:(id)arg1; - (id)_myPrefix; - (BOOL)_shouldUseMyPrefixAsHint; - (void)phonePadDeleteLastDigit:(id)arg1; - (void)phonePad:(id)arg1 replaceLastDigitWithString:(id)arg2; - (void)_phonePad:(id)arg1 appendString:(id)arg2 suppressClearingDialedNumber:(BOOL)arg3; - (void)phonePad:(id)arg1 appendString:(id)arg2; - (void)phonePad:(id)arg1 dialerCharacterButtonWasHeld:(unsigned int)arg2; - (void)starkInCallViewControllerAppearedNotification:(id)arg1; - (void)_callButtonPressed:(id)arg1; - (void)_callButtonLongPress; - (void)_updateCallButtonEnabledState:(id)arg1; - (void)_updateLCDNameLabelWithOriginallyPastedString:(id)arg1; - (void)_updateLCDNameLabelWithAMatchingName:(BOOL)arg1; - (void)_updateCallButtonEnabledState:(id)arg1 updateNameNow:(BOOL)arg2; - (void)dialerView:(id)arg1 stringWasPasted:(id)arg2; - (void)dialerViewTextDidChange:(id)arg1; @property(retain) NSString *lastDialedNumber; - (void)_getPersonName:(id *)arg1 personLabel:(id *)arg2 personUID:(int *)arg3 forPhoneNumberString:(id)arg4; - (void)_updateName; - (void)_stopLookupTimer; - (BOOL)shouldSnapshot; - (void)prepareForSnapshot; - (void)_clearDisplayIfNecessary; - (void)dealloc; - (id)initWithDialerType:(int)arg1; - (void)applicationDidResume; - (void)viewDidDisappear:(BOOL)arg1; - (void)viewWillDisappear:(BOOL)arg1; - (void)viewDidAppear:(BOOL)arg1; - (void)viewWillAppear:(BOOL)arg1; - (BOOL)_isFirstLaunchFromDefaultPNGToDialer; - (BOOL)isShowingDoubleHeightStatusBar; - (void)unloadView; - (void)didReceiveMemoryWarning; - (void)dialerViewPhoneNumberWasTapped:(id)arg1; - (void)loadView;  @end 

Печаль, явного места, где бы хранился текст, не видно. Но посмотрим на переменную _dialerView и класс PHAbstractDialerView.

PHAbstractDialerView.h

#import "UIView.h"  #import "DialerLCDFieldDelegate.h"  @class UIControl, UIView<DialerLCDFieldProtocol>, UIView<TPDialerKeypadProtocol>;  @interface PHAbstractDialerView : UIView <DialerLCDFieldDelegate> {     BOOL _inCallMode;     UIView<DialerLCDFieldProtocol> *_lcdView;     UIView<TPDialerKeypadProtocol> *_phonePadView;     id <DialerViewDelegate> _delegate;     UIControl *_addContactButton;     UIControl *_callButton;     UIControl *_deleteButton; }  @property(retain, nonatomic) UIControl *deleteButton; // @synthesize deleteButton=_deleteButton; @property(retain, nonatomic) UIControl *callButton; // @synthesize callButton=_callButton; @property(retain, nonatomic) UIControl *addContactButton; // @synthesize addContactButton=_addContactButton; @property(nonatomic) id <DialerViewDelegate> delegate; // @synthesize delegate=_delegate; @property(retain, nonatomic) UIView<TPDialerKeypadProtocol> *phonePadView; // @synthesize phonePadView=_phonePadView; @property(retain, nonatomic) UIView<DialerLCDFieldProtocol> *lcdView; // @synthesize lcdView=_lcdView; @property(nonatomic) BOOL inCallMode; // @synthesize inCallMode=_inCallMode; - (void)dialerField:(id)arg1 stringWasPasted:(id)arg2; - (void)dialerLCDFieldTextDidChange:(id)arg1; - (void)dealloc;  @end 

В нем есть вьюшка UIView<DialerLCDFieldProtocol> *_lcdView, которая поддерживает протокол DialerLCDFieldProtocol.

DialerLCDFieldProtocol-Protocol.h

#import "NSObject.h"  @protocol DialerLCDFieldProtocol <NSObject> - (void)setDelegate:(id)arg1; - (void)setHighlighted:(BOOL)arg1; - (BOOL)highlighted; - (void)setInCallMode:(BOOL)arg1; - (BOOL)inCallMode; - (void)deleteCharacter; - (void)setText:(id)arg1 needsFormat:(BOOL)arg2; - (id)text;  @optional - (void)setText:(id)arg1 needsFormat:(BOOL)arg2 name:(id)arg3 label:(id)arg4; - (void)setName:(id)arg1 numberLabel:(id)arg2; @end 

Методы setText:needsFormat: и text выглядят многообещающими. Самое время проверить нашу догадку!

#import <substrate.h>  %hook DialerController  - (void)phonePad:(id)arg1 replaceLastDigitWithString:(id)arg2 {     id dialerView = MSHookIvar<id>(self, "_dialerView");          id lcdView = MSHookIvar<id>(dialerView, "_lcdView");          NSString *currentText;     currentText = objc_msgSend(lcdView, @selector(text));     NSLog(@"text: %@", currentText);          objc_msgSend(lcdView, @selector(setText:needsFormat:), @"+375123456789", YES); }  %end 

Жмем ‘0’ на клавиатуре и через некоторое время в логе видим текст с экрана, а еще через мгновение на экране видим следующий результат:

<Warning>: text: (0  ) 

Картинка

Ну что же, дело осталось за малым. Пишем финальную версию твика.

#import <substrate.h>  %hook DialerController  - (void)phonePad:(id)arg1 replaceLastDigitWithString:(id)arg2 {     id dialerView = MSHookIvar<id>(self, "_dialerView");          id lcdView = MSHookIvar<id>(dialerView, "_lcdView");          NSString *currentText;     currentText = objc_msgSend(lcdView, @selector(text));      currentText = [currentText stringByReplacingOccurrencesOfString:@"(" withString:@""];     currentText = [currentText stringByReplacingOccurrencesOfString:@")" withString:@""];     currentText = [currentText stringByReplacingOccurrencesOfString:@"-" withString:@""];     currentText = [currentText stringByReplacingOccurrencesOfString:@" " withString:@""];      if ([arg2 isEqualToString:@"+"] && currentText.length && [currentText characterAtIndex:currentText.length - 1] == '0') {         currentText = [currentText stringByReplacingCharactersInRange:NSMakeRange(currentText.length - 1, 1) withString:@"+"];         objc_msgSend(lcdView, @selector(setText:needsFormat:), currentText, YES);     }     else {         %orig(arg1, arg2);     } }  %end 

Надеюсь код в пояснении не нуждается, замечу лишь, что значение с экрана приходит отформатированным, поэтому из строки пришлось убрать символы ()- .

Заключение

Код можно найти на github.

Твик можно установить через сидию, предварительно добавив репозиторий http://gennick.ru/cydia/. Название твика Plus4Belarus.

Работоспособность протестирована на iPhone 4 и iPhone 5 c версией прошивки 7.0.4.

ссылка на оригинал статьи http://habrahabr.ru/post/209990/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *