iOS: Узнать и отслеживать состояние физического переключателя тихого режима

от автора

Инстаграм же так умеет, и мы тоже так хотим.

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/


Комментарии

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

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