Полностью скрыть телефонный звонок в iOS (устройство для джейлбрейка)

Я хочу полностью скрыть телефонный звонок в ios. Мой приоритет - сделать это на ios 7 (последняя версия ios на данный момент!), но я хотел бы знать, как скрыть телефонный звонок на ios 6 и ниже, если это возможно. Я нашел некоторые функции для этого, такие как подключение к методу initWithAlertController класса SBUIFullscreenAlertAdapter. Благодаря creker в этой ссылке я нашел другой способ перехвата, который лучше сделать так. Проблема в том, что у него все еще есть панель вызова, когда телефон не заблокирован или когда телефон заблокирован, телефон показывает, что он находится в процессе общения. Вот скриншоты: ссылка на изображение

Я хочу знать, какие методы борьбы с этим, чтобы зацепить? Есть ли что-то еще, что я должен знать для достижения того, чего я хочу?

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


person iFred    schedule 29.03.2014    source источник


Ответы (1)


Я постараюсь опубликовать как можно больше кода, но с нуля это не сработает. Я использую свои собственные макросы для создания хуков, так что вам придется переписать их, чтобы они работали с вашим кодом. Я буду использовать псевдофункцию IsHiddenCall, чтобы определить, является ли данный вызов нашим скрытым вызовом (простая проверка номера телефона). Это здесь, чтобы упростить код. Вы, очевидно, должны реализовать это самостоятельно. Будут и другие псевдофункции, но их реализация очень проста и будет очевидна из их названий. Это не простая настройка, так что потерпите меня.

Кроме того, код не является ARC.

По сути, мы перехватываем все, что может сообщить iOS о телефонном звонке.

IOS 7

Начнем с iOS 7, так как это последняя версия iOS на данный момент, и реализация скрытых вызовов проще, чем в iOS 6 и ниже.

Почти все, что нам нужно, находится в частном TelephonyUtilities.framework. В iOS 7 Apple перенесла почти все, что связано с телефонными звонками, в этот фреймворк. Вот почему все стало проще — все остальные компоненты iOS используют этот фреймворк, так что нам нужно подключить его только один раз, без необходимости копаться в каждом демоне iOS, фреймворке, который может что-то делать с телефонными звонками.

Все методы цепляются за два процесса — SpringBoard и MobilePhone (телефонное приложение). Идентификаторы пакетов — com.apple.springboard и com.apple.mobilephone соответственно.

Вот список TelephonyUtilities.framework методов, которые я подключаю в обоих процессах.

//TUTelephonyCall -(id)initWithCall:(CTCallRef)call
//Here we return nil in case of a hidden call. That way iOS will ignore it
//as it checks for nil return value.
InsertHookA(id, TUTelephonyCall, initWithCall, CTCallRef call)
{
    if (IsHiddenCall(call) == YES)
    {
        return nil;
    }

    return CallOriginalA(TUTelephonyCall, initWithCall, call);
}

//TUCallCenter -(void)handleCallerIDChanged:(TUTelephonyCall*)call
//This is CoreTelephony notification handler. We ignore it in case of a hidden call.
//call==nil check is required because of our other hooks that might return
//nil object. Passing nil to original implementation might break something.
InsertHookA(void, TUCallCenter, handleCallerIDChanged, TUTelephonyCall* call)
{
    if (call == nil || IsHiddenCall([call destinationID]) == YES)
    {
        return;
    }

    CallOriginalA(TUCallCenter, handleCallerIDChanged, call);
}

//TUCallCenter +(id)callForCTCall:(CTCallRef)call;
//Just like TUTelephonyCall -(id)initWithCall:(CTCallRef)call
InsertHookA(id, TUCallCenter, callForCTCall, CTCallRef call)
{
    if (IsHiddenCall(call) == YES)
    {
        return nil;
    }

    return CallOriginalA(TUCallCenter, callForCTCall, call);
}

//TUCallCenter -(void)disconnectAllCalls
//Here we disconnect every call there is except our hidden call.
//This is required in case of a hidden conference call with hidden call.
//Our call will stay hidden but active while other call is active. This method is called
//when disconnect button is called - we don't wont it to cancel our hidden call
InsertHook(void, TUCallCenter, disconnectAllCalls)
{
    DisconnectAllExceptHiddenCall();
}

//TUCallCenter -(void)disconnectCurrentCallAndActivateHeld
//Just like TUCallCenter -(void)disconnectAllCalls 
InsertHook(void, TUCallCenter, disconnectCurrentCallAndActivateHeld)
{
    DisconnectAllExceptHiddenCall();
}

//TUCallCenter -(int)currentCallCount
//Here we return current calls count minus our hidden call
InsertHook(int, TUCallCenter, currentCallCount)
{
    return CallOriginal(TUCallCenter, currentCallCount) - GetHiddenCallsCount();
}

//TUCallCenter -(NSArray*)conferenceParticipantCalls
//Hide our call from conference participants
InsertHook(id, TUCallCenter, conferenceParticipantCalls)
{
    NSArray* calls = CallOriginal(TUCallCenter, conferenceParticipantCalls);

    BOOL isThereHiddenCall = NO;
    NSMutableArray* callsWithoutHiddenCall = [NSMutableArray array];
    for (id i in calls)
    {
        if (IsHiddenCall([i destinationID]) == NO)
        {
            [callsWithoutHiddenCall addObject:i];
        }
        else
        {
            isThereHiddenCall = YES;
        }
    }

    if (callsWithoutHiddenCall.count != calls.count)
    {
        //If there is only two calls - hidden call and normal - there shouldn't be any sign of a conference call
        if (callsWithoutHiddenCall.count == 1 && isThereHiddenCall == YES)
        {
            [callsWithoutHiddenCall removeAllObjects];
        }
        [self setConferenceParticipantCalls:callsWithoutHiddenCall];
        [self _postConferenceParticipantsChanged];
    }
    else
    {
        return calls;
    }
}

//TUTelephonyCall -(BOOL)isConferenced
//Hide conference call in case of two calls - our hidden and normal
InsertHook(BOOL, TUTelephonyCall, isConferenced)
{
    if (CTGetCurrentCallCount() > 1)
    {
        if (CTGetCurrentCallCount() > 2)
        {
            //There is at least two normal calls - let iOS do it's work
            return CallOriginal(TUTelephonyCall, isConferenced);
        }

        if (IsHiddenCallExists() == YES)
        {
            //There is hidden call and one normal call - conference call should be hidden
            return NO;
        }
    }

    return CallOriginal(TUTelephonyCall, isConferenced);
}

//TUCallCenter -(void)handleCallStatusChanged:(TUTelephonyCall*)call userInfo:(id)userInfo
//Call status changes handler. We ignore all events except those
//that we marked with special key in userInfo object. Here we answer hidden call, setup
//audio routing and doing other stuff. Our hidden call is indeed hidden,
//iOS doesn't know about it and don't even setup audio routes. "AVController" is a global variable.
InsertHookAA(void, TUCallCenter, handleCallStatusChanged, userInfo, TUTelephonyCall* call, id userInfo)
{
    //'call' is nil when this is a hidden call event that we should ignore
    if (call == nil)
    {
        return;
    }

    //Detecting special key that tells us that we should process this hidden call event
    if ([[userInfo objectForKey:@"HiddenCall"] boolValue] == YES)
    {
        if (CTCallGetStatus(call) == kCTCallStatusIncoming)
        {
            CTCallAnswer(call);
        }
        else if (CTCallGetStatus(call) == kCTCallStatusActive)
        {
            //Setting up audio routing
            [AVController release];
            AVController = [[objc_getClass("AVController") alloc] init];
            SetupAVController(AVController);
        }
        else if (CTCallGetStatus(call) == kCTCallStatusHanged)
        {
            NSArray *calls = CTCopyCurrentCalls(nil);
            for (CTCallRef call in calls)
            {
                CTCallResume(call);
            }
            [calls release];

            if (CTGetCurrentCallCount() == 0)
            {
                //No calls left - destroying audio controller
                [AVController release];
                AVController = nil;
            }
        }

        return;
    }
    else if (IsHiddenCall([call destinationID]) == YES)
    {
        return;
    }

    CallOriginalAA(TUCallCenter, handleCallStatusChanged, userInfo, call, userInfo);
}

Вот метод Foundation.framework, который я подключаю в обоих процессах.

//In iOS 7 telephony events are sent through local NSNotificationCenter. Here we suppress all hidden call notifications.
InsertHookAAA(void, NSNotificationCenter, postNotificationName, object, userInfo, NSString* name, id object, NSDictionary* userInfo)
{
    if ([name isEqualToString:@"TUCallCenterControlFailureNotification"] == YES || [name isEqualToString:@"TUCallCenterCauseCodeNotification"] == YES)
    {
        //'object' usually holds TUCall object. If 'object' is nil it indicates that these notifications are about hidden call and should be suppressed
        if (object == nil)
        {
            return;
        }
    }

    //Suppressing if something goes through
    if ([object isKindOfClass:objc_getClass("TUTelephonyCall")] == YES && IsHiddenCall([object destinationID]) == YES)
    {
        return;
    }

    CallOriginalAAA(NSNotificationCenter, postNotificationName, object, userInfo, name, object, userInfo);
}

Вот последний метод, который я перехватываю в обоих процессах из CoreTelephony.framwork

//CTCall +(id)callForCTCallRef:(CTCallRef)call
//Return nil in case of hidden call
InsertHookA(id, CTCall, callForCTCallRef, CTCallRef call)
{
    if (IsHiddenCall(call) == YES)
    {
        return nil;
    }

    return CallOriginalA(CTCall, callForCTCallRef, call);
}

Вот функция SetupAVController, которую я использовал ранее. Скрытый вызов действительно скрыт - iOS ничего о нем не знает, поэтому, когда мы отвечаем на него, маршрутизация звука не выполняется, и мы ничего не услышим на другом конце. SetupAVController делает это — настраивает маршрутизацию звука, как это делает iOS, когда есть активный телефонный звонок. Я использую частные API от частного Celestial.framework

extern id AVController_PickableRoutesAttribute;
extern id AVController_AudioCategoryAttribute;
extern id AVController_PickedRouteAttribute;
extern id AVController_AllowGaplessTransitionsAttribute;
extern id AVController_ClientPriorityAttribute;
extern id AVController_ClientNameAttribute;
extern id AVController_WantsVolumeChangesWhenPaused;

void SetupAVController(id controller)
{
    [controller setAttribute:[NSNumber numberWithInt:10] forKey:AVController_ClientPriorityAttribute error:NULL];
    [controller setAttribute:@"Phone" forKey:AVController_ClientNameAttribute error:NULL];
    [controller setAttribute:[NSNumber numberWithBool:YES] forKey:AVController_WantsVolumeChangesWhenPaused error:NULL];
    [controller setAttribute:[NSNumber numberWithBool:YES] forKey:AVController_AllowGaplessTransitionsAttribute error:NULL];
    [controller setAttribute:@"PhoneCall" forKey:AVController_AudioCategoryAttribute error:NULL];
}

Вот метод, который я подключаю только в процессе MobilePhone

/*
PHRecentCall -(id)initWithCTCall:(CTCallRef)call
Here we hide hidden call from call history. Doing it in MobilePhone
will hide our call even if we were in MobilePhone application when hidden call
was disconnected. We not only delete it from the database but also prevent UI from       
showing it.
*/
InsertHookA(id, PHRecentCall, initWithCTCall, CTCallRef call)
{
    if (call == NULL)
    {
        return CallOriginalA(PHRecentCall, initWithCTCall, call);
    }

    if (IsHiddenCall(call) == YES)
    {
        //Delete call from call history
        CTCallDeleteFromCallHistory(call);

        //Update MobilePhone app UI
        id PHRecentsViewController = [[[[[UIApplication sharedApplication] delegate] rootViewController] tabBarViewController] recentsViewController];
        if ([PHRecentsViewController isViewLoaded])
        {
            [PHRecentsViewController resetCachedIndexes];
            [PHRecentsViewController _reloadTableViewAndNavigationBar];
        }
    }

    return CallOriginalA(PHRecentCall, initWithCTCall, call);
}

Методы, которые я подключаю в процессе SpringBoard.

//SpringBoard -(void)_updateRejectedInputSettingsForInCallState:(char)state isOutgoing:(char)outgoing triggeredbyRouteWillChangeToReceiverNotification:(char)triggered
//Here we disable proximity sensor 
InsertHookAAA(void, SpringBoard, _updateRejectedInputSettingsForInCallState, isOutgoing, triggeredbyRouteWillChangeToReceiverNotification, char state, char outgoing, char triggered)
{
    CallOriginalAAA(SpringBoard, _updateRejectedInputSettingsForInCallState, isOutgoing, triggeredbyRouteWillChangeToReceiverNotification, state, outgoing, triggered);

    if (IsHiddenCallExists() == YES && CTGetCurrentCallCount() == 1)
    {
        BKSHIDServicesRequestProximityDetectionMode = (void (*)(int))dlsym(RTLD_SELF, "BKSHIDServicesRequestProximityDetectionMode");
        BKSHIDServicesRequestProximityDetectionMode(0);
    }
}

//BBServer -(void)publishBulletin:(id)bulletin destinations:(unsigned int)destinations alwaysToLockScreen:(char)toLockScreen
//Suppress hidden call bulletins
InsertHookAAA(void, BBServer, publishBulletin, destinations, alwaysToLockScreen, id bulletin, unsigned int destinations, char toLockScreen)
{
    if ([[bulletin section] isEqualToString:@"com.apple.mobilephone"] == YES)
    {
        NSArray *recordTypeComponents = [[bulletin recordID] componentsSeparatedByString:@" "];
        NSString *recordType = recordTypeComponents[0];
        NSString *recordCode = recordTypeComponents[1];

        //Missed call bulletin
        if ([recordType isEqualToString:@"missedcall"] == YES)
        {
            NSArray *recordCodeComponents = [recordCode componentsSeparatedByString:@"-"];
            NSString *phoneNumber = recordCodeComponents[0];

            if (IsHiddenCall(phoneNumber) == YES)
            {
                return;
            }
        }
    }

    CallOriginalAAA(BBServer, publishBulletin, destinations, alwaysToLockScreen, bulletin, destinations, toLockScreen);
}

//TUCallCenter -(id)init
//CoreTelephony notifications handler setup
InsertHook(id, TUCallCenter, init)
{
    CTTelephonyCenterAddObserver(CTTelephonyCenterGetDefault(), self, CallStatusNotificationCallback, kCTCallStatusChangeNotification, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);

    return CallOriginal(TUCallCenter, init);
}

//Call status changes handler. Here we redirect status changes into hooked TUCallCenter method and doing some other stuff.
void CallStatusNotificationCallback(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo)
{
    if (object == NULL)
    {
        return;
    }

    if (IsHiddenCall((CTCallRef)object) == YES)
    {
        [observer handleCallStatusChanged:object userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:@"HiddenCall"]];
    }
    else
    {
        if (CTCallGetStatus((CTCallRef)object) == kCTCallStatusHanged)
        {
            if (IsHiddenCallExists() == YES)
            {
                //Setting up all the audio routing again. When normal call is hanged iOS may break audio routing as it doesn't know there is another active call exists (hidden call)
                SetupAVController(AVController);
            }
            else if (CTGetCurrentCallCount() == 0)
            {
                [AVController release];
                AVController = nil;
            }
        }
    }

    if (CTGetCurrentCallCount() > 1 && IsHiddenCallExists() == YES)
    {
        //Here we setup hidden conference call
        NSArray *calls = CTCopyCurrentCalls(nil);
        for (CTCallRef call in calls)
        {
            CTCallJoinConference(call);
        }
        [calls release];
    }
}

iOS 5-6

iOS 5-6 сложнее. Код телефонии разбросан по многим компонентам iOS и API. Я могу опубликовать код позже, так как сейчас у меня нет времени. Ответ уже очень длинный.

person creker    schedule 06.04.2014
comment
Большое спасибо. Ваш ответ мне очень помог. Я проверил это, и у меня есть некоторые проблемы. 1- Здесь ошибка: extern id AVController_PickableRoutesAttribute; и подобные строки, пишет что не может найти. в чем проблема? 2- DisconnectAllExceptGoodCall() что он должен делать? Я имею в виду, какой звонок является хорошим звонком? - person iFred; 09.04.2014
comment
Для других функций, таких как DisconnectAllExceptHiddenCall(), я использовал CTCopyCurrentCalls(nil) для доступа ко всем вызовам, а затем использовал CTCallDisconnect(call) для каждого его объекта. Я делаю это правильно? - person iFred; 09.04.2014
comment
Когда вы говорите о подключении в процессе трамплина, вы имеете в виду настройку фильтра пакета на com.apple.springboard, а с мобильным телефоном вы имеете в виду com.apple.mobilephone? Могу ли я построить все это в одном твике или их должно быть три? (один для процесса MobilePhone, один для процесса SpringBoard и один для обоих?) - person iFred; 09.04.2014
comment
1. DisconnectAllExceptGoodCall должно быть DisconnectAllExceptHiddenCall, я его переименовал. 2. AVController_PickableRoutesAttribute - ты связал фреймворк? 3. DisconnectAllExceptHiddenCall должен отключить все звонки, кроме нашего скрытого звонка. 4. Можно было написать один твик на все. Собственно, я так и делаю. - person creker; 09.04.2014
comment
2- Я скопировал дамп класса Celestial framework, чтобы включить папку iOSopendev и импортировал Celestial.h. Должен ли я сделать что-нибудь еще? - person iFred; 09.04.2014
comment
Возможно, вам придется связать его с вашим проектом (перетащите его в папку framework в вашем проекте). Я не думаю, что Xcode может найти частную структуру самостоятельно. - person creker; 09.04.2014
comment
В методе handleCallStatusChanged класса TUCallCenter есть строка CTCallAnswer(call). Тип callTUTelephonyCall*, но аргумент CTCallAnswer должен быть CTCallRef. Как это возможно? В некоторых других местах у вас есть IsHiddenCall(TUTelephonyCall*) и IsHiddenCall([TUTelephonyCall* destinationID]), как мне с ними справиться? Я пытался [вызов TUTelephony*], но это было неправильно. Спасибо - person iFred; 10.04.2014
comment
См. CallStatusNotificationCallback. Там я вызываю handleCallStatusChanged, передавая CTCallRef аргумент и userInfo аргумент с ключом HiddenCall. Так что типы правильные. Я просто повторно использую существующие функции, чтобы выполнять всю работу в одном месте. Что касается IsHiddenCall, то IsHiddenCall(TUTelephonyCall*) я не нашел. Есть два случая - IsHiddenCall(CTCallRef) и IsHiddenCall(NSString*). - person creker; 10.04.2014
comment
В прототипе handleCallStatusChanged call это TUTelephonyCall*. Итак, почему вы передаете CTCallRef? - person iFred; 10.04.2014
comment
Это имеет значение? Аргумент call является указателем, я могу передать все, что захочу — метод зацеплен, вызов его вызовет мою собственную реализацию. Я хотел перенести обработку статуса вызова в одно место для обоих процессов. Могут быть и другие причины, но я их не помню - это был скорее процесс проб и ошибок. Вам не нужно делать именно то, что сделал я, но мой код работает, он проверен много раз в разных сценариях. - person creker; 10.04.2014
comment
Спасибо, крекер! Скрытый вызов теперь работает хорошо, за исключением приложения для мобильного телефона, в котором есть вызов в истории вызовов, и я попытаюсь понять это сам. Но теперь есть проблема с обычными звонками. В handleCallStatusChanged происходит сбой. Я не могу найти, где это происходит, но я думаю, что это в последнем операторе if и исходной функции. Если звонок обычный, тип call будет TUTelephonyCall* или все еще CTCallRef? - person iFred; 10.04.2014
comment
Я предполагаю, что это будет TUTelephonyCall*, потому что вы используете [call destinationID]. Какой тип destinationID? я думаю CTCallRef - person iFred; 10.04.2014
comment
destinationID возвращает NSString* - person creker; 10.04.2014
comment
Для обычных вызовов аргумент handleCallStatusChanged равен TUTelephonyCall*, я написал правильные прототипы методов выше всех хуков. Если я передам CTCallRef, то исходная реализация НЕ вызывается - для этого есть оператор return. - person creker; 10.04.2014
comment
Ok! проблема была в том, что я думал, что destinationID вернул CTCallRef!!! Теперь лучше. У меня есть вопрос, а именно я не могу подключить PHRecentCall. Я имею в виду, когда я вхожу в initWithCall, ничего не регистрируется. Есть идеи? (Я использую пакетные фильтры com.apple.mobilephone и com.apple.springboard плюс фильтр Mode = "Any".) - person iFred; 10.04.2014
comment
Селектор метода проверки, фильтр идентификатора пакета. Я мало чем могу помочь. Вот дамп класса github.com/limneos/classdump-dyld/blob/master/iphoneheaders/ Возможно, это поможет. - person creker; 11.04.2014
comment
Привет еще раз! Немного поздно, но есть вопрос. Как мы могли включить только микрофон и выключить динамик? Потому что, если телефон воспроизводит какой-либо звук, он будет поставлен на паузу, а телефонный звонок будет звучать в динамике. - person iFred; 10.05.2014
comment
Я не знаю. Попробуйте, может быть, это сработает и поможет кому-то еще. - person creker; 15.05.2014
comment
Я думаю, что мне следует изменить атрибут, установленный для AVController_AudioCategoryAttribute в функции SetupAVController. То, что установлено, это @"PhoneCall", которое, я думаю, следует изменить. Проблема в том, что я не знаю, на что мне его заменить!;) Можете помочь? - person iFred; 15.05.2014
comment
Я тоже не знаю. Я получил эту строку @"PhoneCall" из разборки SpringBoard - именно так она настраивает маршрутизацию звука при активном телефонном звонке. - person creker; 15.05.2014
comment
потрясающая работа @creker! насчет iOS 6, у вас есть названия методов, чтобы зацепить хотя бы? или фреймворки, которые нужно прицепить? - person Alexandre Blin; 17.05.2014
comment
@creker Хотя это было давно, но не могли бы вы немного помочь мне с разборкой. Я пытался сделать некоторые вещи, но я не могу получить то, что я хочу. Я просмотрел разборку небесного каркаса, но в методе setAttribute:forKey нет проверенных строк. Итак, как мне найти правильный атрибут для моей проблемы (то есть настроить только микрофон, а не динамик) - person iFred; 01.07.2014