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

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

Предыстория


После выхода 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 *_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.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 21

    +3
    Костыль же. Надо решать проблему в корне.
      0
      А почему у меня при долгом нажатии на ноль отображается плюс без ноля и всяких твиков? Версия 7.0.4 (11В554а)

      изм. Прошу прощения, пропустил, что проблема, возможно, локальная (только для Беларуси)
        0
        Именно так, проблема актуальна только для Беларуси
          0
          Удивительно. Еще один вопрос — это актуально для всех белорусских iPhone, как джейлбрейкнутых, так и белых?
            0
              0
              Мистика, нечистая сила, не иначе.
                0
                У меня на четверка с 7.1b3 (Россия) нет такой проблемы (и раньше не было).
                У коллеги на 5s c 7.1b4 — тоже всё хорошо.
                  0
                  Меркушев Егор
                  Не участвует в рейтинге хабралюдей
                  Заметка:
                  Написать заметку
                  Дата рождения:
                  28 февраля 1987
                  Откуда:
                  Россия, Карелия, Петрозаводск
                  Не читает комментарии
                    0
                    Смешно.
                    Я прочитал все комментарии и долго выбирал, какой из них прокомментировать.

                    Прокомментировал этот. Почему?
                    Потому что я увидел диалог:
                    — Удивительно. Еще один вопрос — это актуально для всех белорусских iPhone, как джейлбрейкнутых, так и белых?
                    — Да,для всех

                    Прошел по ссылке. Не увидел ни слова на форуме про Белорусь. Решил, что обстоятельства изменились и теперь проблема обнаружена не только в этой стране.
                    А вы тут со своим юмором.

                    Сначала сами научитесь правила читать на форумах, прежде чем язвить и обзываться «криворукими ушлёпками».
                      –1
                      Вероятно, ни слова про Беларусь вы не увидели потому, что там идет переписка на английском. Кстати, это совсем не удивительно, ведь вы не можете правильно написать название страны даже на русском языке.

                      Попробуйте поискать на страничке слово «Belarus».
        +2
        Странная проблема. Вообще, как «радикальное решение» можно было бы добавить свою кнопку на клавиатуру с одним плюсом. Не люблю жать на ноль. =)
          0
          Запостил ссылку на гитхаб в тред о проблеме на форуме Apple, сообщение удалили.
          Криворукие ушлепки. Мало того, что не решили проблему в 7.1 beta 4, так еще и отказываются от инфы, которая может пригодится.
            0
            А попробуйте вернуть айфон из-за этой проблемы назад. И закажите себе из России? Что скажут в этом случае? Требование возврата так просто не удалишь
            0
            Попробовал в своем айфоне включить регион Беларусь. Ничего не изменилось. Я так понимаю, установка Россия в Белорусских айфонах тоже не спасает?
              0
              Не спасает. Вот только что приехал человек из России, никаких настроек не менял, даже симку не доставал, а + уже не набирается.
                0
                — В какой стране вы живете?
                — 0+
                  0
                  еще сводки с фронта, после въезда на Украину + набирается как и должен
                    0
                    ну а толку, по возвращению в Беларусь опять перестает работать
                    0
                    Набор плюса зависит от геолокации? Хмммм… Apple умеет удивлять.
                      0
                      А может от оператора что-то зависит?
                      0
                      Сколько раз был в Беларуси, ни разу не пришлось набирать руками номер :) В следующий раз обязательно проверю.
                      Но выглядит весьма удивительно. Хочется отследить момент, когда это происходит.

                      На следующей неделе буду у родителей, на границе с Белоруссией, там ловятся белорусские операторы. Посмотрю, как отреагирует айфон.

                  Only users with full accounts can post comments. Log in, please.