Инстаграм же так умеет, и мы тоже так хотим.
TLDR: и даже никакого приватного апи
import notify var token = NOTIFY_TOKEN_INVALID notify_register_dispatch( "com.apple.springboard.ringerstate", &token, .main ) { token in var state: UInt64 = 0 notify_get_state(token, &state) print("Changed to", state == 1 ? "ON" : "OFF") } var state: UInt64 = 0 notify_get_state(token, &state) print("Initial", state == 1 ? "ON" : "OFF")
Гугление показало, что самый «рабочий» способ был описан здесь. Вкратце, если проиграть особый звук, и если событие окончания проигрывания приходит почти мгновенно – значит silent mode включён. По ссылке описано более подробно, и так же описано почему это ненадёжный способ.
Но что-то мне подсказывало, что всегда есть способ получше.
Изначально у меня было несколько идей о том, с чего хотя бы начать поиски.
Сперва я вспомнил о том, что многие события (например тачи и клавиатура) приходили в UIApplication как структуры GSEvent из фреймворка GraphicsServices, далее GSEvent превращались в UIEvent, и наконец UIEvent уже посылались в -[UIApplication sendEvent:]. Для обработки GSEventRef у UIApplication есть приватный метод -[UIApplication handleEvent:]. Я установил на него брейкпоинт и ожидал, что он вызовется, когда я переключу silent mode. Но чуда не случилось, брейкпоинт не сработал, и более того, нажатия на экран так же не вызывали этот код.
Я всё же надеялся, что кто-то да сообщает приложению о событии переключения режима, но было даже не за что зацепиться, и как будто бы некуда было ставить брейкпоинты. И тут я подумал «а поставлю-ка я брейкпоинт на objc_msgSend!». И посмотрю, вызовется ли хоть что-нибудь, а дальше будет видно. К сожалению, это тоже не помогло, переключение silent mode не порождало вообще никаких вызовов методов objc.
Далее оказалось, что первая идея с GSEvent была всё же хороша, т.к. я наткнулся на этот вопрос на SO: https://stackoverflow.com/questions/24145386/detect-ring-silent-switch-position-change. Автор приводит резюме всему, что он пробовал, и мой глаз зацепился за типы события:
kGSEventRingerOff = 1012, kGSEventRingerOn = 1013,
Значит, они всё же когда-то приходили…
Затем я догадался поискать по всем загруженным символ слово «Ringer». Что-то мне подсказывало, что в системных фреймворках должно бы быть что-то реализованное.
Я запустил своё тестовое приложение, запаузил его, и в отладчике выполнил
image lookup -r -s "[rR]inger"
Я тут же получил многообещающие результаты:
<...> Summary: AssistantServices`+[AFDeviceRingerSwitchObserver sharedObserver] Address: AssistantServices[0x000000019d801770] (AssistantServices.__TEXT.__text + 1036984) Summary: AssistantServices`__46+[AFDeviceRingerSwitchObserver sharedObserver]_block_invoke Address: AssistantServices[0x000000019d8017ac] (AssistantServices.__TEXT.__text + 1037044) Summary: AssistantServices`-[AFDeviceRingerSwitchObserver init] Address: AssistantServices[0x000000019d8018a8] (AssistantServices.__TEXT.__text + 1037296) Summary: AssistantServices`-[AFDeviceRingerSwitchObserver state] Address: AssistantServices[0x000000019d8018e0] (AssistantServices.__TEXT.__text + 1037352) Summary: AssistantServices`-[AFDeviceRingerSwitchObserver addListener:] Address: AssistantServices[0x000000019d801990] (AssistantServices.__TEXT.__text + 1037528) Summary: AssistantServices`__44-[AFDeviceRingerSwitchObserver addListener:]_block_invoke Address: AssistantServices[0x000000019d80199c] (AssistantServices.__TEXT.__text + 1037540) Summary: AssistantServices`-[AFDeviceRingerSwitchObserver removeListener:] <...>
Заодно я увидел и -[UIApplication ringerChanged:], но, как мы уже поняли из теста с objc_msgSend, он не вызывался.
Но AFDeviceRingerSwitchObserver – выглядит как то, что надо! Глянув на остальные методы, я сделал вывод, что на AFDeviceRingerSwitchObserver можно подписаться в addListener:, а observer оповещает своих подписчиков о новом состоянии через метод -(void)deviceRingerObserver:(id<Observer>)observer didChangeState:(long)state;
Манипуляции с Objective-C Runtime я предпочитаю делать прямо на Objective-C.
Чтобы не мучаться с objc_msgSend и приведением сигнатур функций, я объявил нужные мне селекторы в протоколах-хелперах. Они нужны только для того, чтобы можно было закастить объект к этому протоколу, и вызвать нужный метод по-человечески.
@protocol Observer; @protocol Listener <NSObject> -(void)deviceRingerObserver:(id<Observer>)observer didChangeState:(long)state; @end @protocol Observer<NSObject> + (id<Observer>)sharedObserver; - (void)addListener:(id<Listener>)listener; @end
Затем я добавил реализацию для слушателя: и подписал его на AFDeviceRingerSwitchObserver.sharedObserver:
@interface MyListener: NSObject<Listener> @end @implementation MyListener -(void)deviceRingerObserver:(id)observer didChangeState:(long)state { NSLog(@"state: %ld", state); } @end static void enableListener(void) { static id<Listener> listener = nil; listener = [MyListener new]; Class<Observer> cls = NSClassFromString(@"AFDeviceRingerSwitchObserver"); [[cls sharedObserver] addListener:_listener]; }
Переключение silent mode перехватывается!
2023-06-29 02:12:23.132505+0100 objc[2046:417227] state: 1 // выкл 2023-06-29 02:12:23.689309+0100 objc[2046:417171] state: 2 // вкл
Это была уже почти победа, но хотелось бы понять как работает сам AFDeviceRingerSwitchObserver, откуда берёт события.
Зайдя в [AFDeviceRingerSwitchObserver init], я увидел, что он вызывает-[AFNotifyObserver initWithName:options:queue:delegate:] с аргументом "com.apple.springboard.ringerstate". Который, в свою очередь, использует libnotify для коммуникации с системой.
Код там примерно такой:
#import <notify.h>
void print_state(int token) { uint64_t state; notify_get_state(token, &state); NSLog(@"%@", state == 0 ? @"OFF" : @"ON"); } int token = NOTIFY_TOKEN_INVALID; notify_register_dispatch( "com.apple.springboard.ringerstate", &token, dispatch_get_main_queue(), ^(int token) { print_state(token); } ); print_state(token);
Что интересно, мы можем не только подписаться на обновления, но и узнавать текущее значение! А самое приятное то, что libnotify это не приватное API.
Через приватное же апи можно узнать, а есть ли вообще наш переключатель на устройстве (на iPad их нет).
#import <dlfcn.h> void* h = dlopen(NULL, 0); BOOL(*AFHasRingerSwitch)(void) = dlsym(h, "AFHasRingerSwitch"); NSLog(@"%d", AFHasRingerSwitch()); // AFHasRingerSwitch делает dispatch_once { MGGetBoolAnswer("ringer-switch") } BOOL(*MGGetBoolAnswer)(CFStringRef) = dlsym(h, "MGGetBoolAnswer"); NSLog(@"%d", MGGetBoolAnswer(CFSTR("ringer-switch")));
Наконец, я сделал обёртку над libnotify, которая превращает события изменения состояния источника в Combine Publisher.
let listener = try Notify.Listener(name: "com.apple.springboard.ringerstate") // throws Notify.Status try listener.value() // reads current value listener.publisher.sink { ... } // Combine publisher
Код можно найти на гитхабе: https://gist.github.com/storoj/bc5c0d24dde6b5bb0b5f7fe2706c61e9. Но на всякий случай вставлю и под спойлер сюда.
Notify.swift
import notify import Combine enum Notify {} extension Notify { struct Status: Error { let rawValue: UInt32 init(_ rawValue: UInt32) { self.rawValue = rawValue } func ok() throws { guard rawValue == NOTIFY_STATUS_OK else { throw self } } } } extension Notify { struct Token { typealias State = UInt64 typealias RawValue = Int32 var rawValue: RawValue = NOTIFY_TOKEN_INVALID init(_ rawValue: RawValue) { self.rawValue = rawValue } init(dispatch name: String, queue: DispatchQueue = .main, handler: @escaping notify_handler_t) throws { try Status(notify_register_dispatch(name, &rawValue, queue, handler)).ok() } init(check name: String) throws { try Status(notify_register_check(name, &rawValue)).ok() } func state() throws -> State { var state: State = 0 try Status(notify_get_state(rawValue, &state)).ok() return state } func cancel() throws { try Status(notify_cancel(rawValue)).ok() } } } extension Notify { class Listener { private class Helper { let name: String var token: Token? let publisher = PassthroughSubject<UInt64, Status>() init(name: String) { self.name = name } func subscribe() { do { token = try Token(dispatch: name) { [publisher] token in do { publisher.send(try Token(token).state()) } catch { publisher.send(completion: .failure(error as! Status)) } } } catch { publisher.send(completion: .failure(error as! Status)) } } func cancel() { try? token?.cancel() } func value() throws -> UInt64 { try Token(check: name).state() } } private let helper: Helper init(name: String) { helper = Helper(name: name) } func value() throws -> UInt64 { try helper.value() } lazy var publisher: AnyPublisher<UInt64, Status> = { helper.publisher .handleEvents(receiveSubscription: { [helper] sub in helper.subscribe() }, receiveCancel: helper.cancel) .share() .eraseToAnyPublisher() }() } }
Почему код Notify.Listener такой навороченный? У паблишеров могут быть ноль, один, два и более подписчиков, и я долго пытался сделать так, чтобы notify_register_dispatch во-первых вызывался «лениво», т.е. в момент первой подписки. А во-вторых, чтобы notify_cancel вызывался после того, как все отписались.
Надеюсь, было интересно, задавайте вопросы. Но, честно говоря, хоть я и получил удовольствие от расследования, мне не кажется, что фича с переключением звука на видео в инстаграме имела право на существование. По-моему, это всё же нецелевое применение физического выключателя мелодии звонка, и использовать это «апи» в своих приложениях скорее не стоит.
ссылка на оригинал статьи https://habr.com/ru/articles/744652/
Добавить комментарий