/*---------------------------------------------------------------------------*/
/* PhoneIosDelegate.m                                                        */
/* copyright (c) innovaphone 2015                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#import <Foundation/Foundation.h>
#import <CoreTelephony/CTCallCenter.h>
#import <CoreTelephony/CTCall.h>
//#import <MediaPlayer/MediaPlayer.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
//#define PHONE_IOS_USE_CRASHREPORTER
#ifdef PHONE_IOS_USE_CRASHREPORTER
#import "client/ios/CrashReporter.framework/Headers/CrashReporter.h"
#endif
#import "TargetConditionals.h"

#import "platform/platform.h"
#import "platform/IosNamespace.h"
#import "client/ios/ios_async/ios_async.h"
#import "client/ios/ios_async/ios_event.h"
#import "../ios_forms/FormsConst.h"
#import "client/linux/client_main/appclient_config.h"
#import "client/ios/ios_lib/IosPhone.h"
#import "PhoneIosDelegate.h"
#import "ToastView.h"
#import "Reachability.h"
#import "ActiveCamera.h"
#import "TranscodingSession.h"
#import "WebViewNavigationController.h"
#import "WebViewController.h"
#import "Contact.h"
#import <asl.h>
#import <stdio.h>
#import <time.h>
#import <sys/time.h>

@import Darwin.POSIX.net;
@import Darwin.POSIX.ioctl;
#import <fcntl.h>
#import <errno.h>
#import <signal.h>
#import <ctype.h>
#import <netdb.h>
#import <ifaddrs.h>
#import <arpa/inet.h>
#import <sys/socket.h>
#import <sys/poll.h>
#import <netinet/in.h>
#import <netinet/tcp.h> // for TCP_NODELAY
#import <sys/sockio.h>
#import <sys/socket.h>  // for SOL_SOCKET

#define ACCEPT_ACTION           "accept"
#define REJECT_ACTION           "reject"
#define DISCONNECT_ACTION       "disconnect"
#define CALLBACK_ACTION         "callback"
#define CALL_CATEGORY           "call"
#define CONN_CATEGORY           "conn"
#define MISSED_CATEGORY         "missed"
#define YES_ACTION              "yes"
#define NO_ACTION               "no"
#define REPLY_ACTION            "reply"
#define DECISION_CATEGORY       "decision"
#define REPLY_CATEGORY          "reply"
#define CRASH_FILE_NAME         "crash"
#define CRASH_FILE_EXT          ".txt"
#define MAX_CRASH_COUNT         10
#define SAVE_LOG_FILE_NAME      "trace"
#define SAVE_LOG_FILE_EXT       ".txt"
#define SAVE_LOG_FILE_LIMIT     1000000
#define SAVE_LOG_FILE_COUNT     4
#define CONFIG_FILE_NAME        "config.json"

#define kABPersonPhoneHomeLabel kABHomeLabel
#define kABPersonPhoneWorkLabel kABWorkLabel

char phone_ios_files_dir_container[PATH_MAX];
CTCallCenter * phone_ios_callCenter;
IosPhone * phone_ios_ios_phone;
bool phone_ios_ring_in_headset;
bool phone_ios_log_browser = false;

extern const char *default_tunes;
extern const char *default_tones;
extern bool fresh_install;
extern void start_sys(void);
extern t_ios_async_handler ios_async_handler_table[];
extern t_ios_event_handler ios_event_handler_table[];
extern void dialNormalizedNumber(const char *numString);
extern void do_shutdown();
extern void connectivity_changed(bool on, bool in_wlan);
extern void restart_listen_sockets();
extern void close_tcp_sockets();
extern void set_manually_started(bool started);
extern dword getIPv4Address();
extern void phone_configure_audio_session();
extern void phone_audio_session_will_deactivate();
extern void phone_audio_session_activated(bool activated);

static ABAddressBookRef staticAddressBook = nil;
void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void *context);
static bool save_log_on = false;
static bool save_log_is_on = false;
static unsigned clear_logs_req = 0;
static unsigned clear_logs_ack = 0;
void open_log();
void poll_log();
void close_log();
void clear_logs();

@class BadgeCount;
@interface BadgeCount : NSObject

@property (nonatomic) NSString *ref;
@property (nonatomic) int count;
@property (nonatomic) NSString *text;

+ (id) badgeCountWithRef:(NSString *)ref count:(int)count text:(NSString *)text;
- (id) initWithRef:(NSString *)ref count:(int)count text:(NSString *)text;

@end
@implementation BadgeCount

- (id) initWithRef:(NSString *)ref count:(int)count text:(NSString *)text
{
    self = [super init];
    if (self) {
        _ref = ref;
        _count = count;
        _text = text;
    }
    return self;
}

+ (id) badgeCountWithRef:(NSString *)ref count:(int)count text:(NSString *)text
{
    return [[self alloc] initWithRef:ref count:count text:text];
}

@end

@class ContactsSearchData;
@interface ContactsSearchData : NSObject

@property (nonatomic) int resultEvent;
@property (nonatomic) int state;
@property (nonatomic) NSString *ref;
@property (nonatomic) NSString *pattern;
@property (nonatomic) NSString *number;
@property (nonatomic) NSString *prefixIntl;
@property (nonatomic) NSString *prefixNtl;
@property (nonatomic) NSString *prefixSubs;
@property (nonatomic) NSString *areaCode;
@property (nonatomic) NSString *countryCode;

+ (id) contactsSearchDataWithRef:(NSString *)ref resultEvent:(int)resultEvent state:(int)state;
- (id) initWithRef:(NSString *)ref resultEvent:(int)resultEvent state:(int)state;

@end
@implementation ContactsSearchData

- (id) initWithRef:(NSString *)ref resultEvent:(int)resultEvent state:(int)state
{
    self = [super init];
    if (self) {
        _ref = ref;
        _resultEvent = resultEvent;
        _state = state;
    }
    return self;
}

+ (id) contactsSearchDataWithRef:(NSString *)ref resultEvent:(int)resultEvent state:(int)state
{
    return [[self alloc] initWithRef:ref  resultEvent:resultEvent state:state];
}

@end

@class WebserverGetData;
@interface WebserverGetData : NSObject

@property (nonatomic) NSString *context;
@property (nonatomic) NSString *resourceName;

+ (id) webserverGetDataWithContext:(NSString *)context resourceName:(NSString *)resourceName;
- (id) initWithContext:(NSString *)context resourceName:(NSString *)resourceName;

@end
@implementation WebserverGetData

- (id) initWithContext:(NSString *)context resourceName:(NSString *)resourceName
{
    self = [super init];
    if (self) {
        _context = context;
        _resourceName = resourceName;
    }
    return self;
}

+ (id) webserverGetDataWithContext:(NSString *)context resourceName:(NSString *)resourceName
{
    return [[self alloc] initWithContext:context resourceName:resourceName];
}

@end

@class CxCall;
@interface CxCall : NSObject

@property (nonatomic) NSUUID *uuid;
@property (nonatomic) NSUUID *conf_id;
@property (nonatomic) NSString *number;
@property (nonatomic) bool incoming;
@property (nonatomic) NSString *name;
@property (nonatomic) bool video;
@property (nonatomic) bool claimed;
@property (nonatomic) bool check_incoming_call;
@property (nonatomic) bool accept;
@property (nonatomic) bool reject;
@property (nonatomic) bool held;
@property (nonatomic) bool muted;
@property (nonatomic) bool connected;
@property (nonatomic) NSDate *create_time;
@property (nonatomic) bool released;
@property (nonatomic) NSDate *release_time;

+ (id) cxCallWithUUID:(NSUUID *)uuid conf_id:(NSUUID *)conf_id number:(NSString *)number claimed:(bool)claimed incoming:(bool)incoming name:(NSString *)name video:(bool)video;
- (id) initWithUUID:(NSUUID *)uuid conf_id:(NSUUID *)conf_id number:(NSString *)number claimed:(bool)claimed incoming:(bool)incoming name:(NSString *)name video:(bool)video;
- (void) released_now;
- (const byte *) getUUIDBytes:(byte *)uuid;
- (const byte *) getConfIdBytes:(byte *)conf_id;

@end
@implementation CxCall

- (id) initWithUUID:(NSUUID *)uuid conf_id:(NSUUID *)conf_id number:(NSString *)number claimed:(bool)claimed incoming:(bool)incoming name:(NSString *)name video:(bool)video
{
    self = [super init];
    if (self) {
        _uuid = uuid;
        _conf_id = conf_id;
        _number = number;
        _incoming = incoming;
        _name = name;
        _video = video;
        _claimed = claimed;
        _check_incoming_call = false;
        _accept = false;
        _reject = false;
        _held = false;
        _muted = false;
        _connected = false;
        _create_time = [NSDate date];
        _released = false;
    }
    return self;
}

+ (id) cxCallWithUUID:(NSUUID *)uuid conf_id:(NSUUID *)conf_id number:(NSString *)number claimed:(bool)claimed incoming:(bool)incoming name:(NSString *)name video:(bool)video
{
    return [[self alloc] initWithUUID:uuid conf_id:conf_id number:number claimed:claimed incoming:incoming name:name video:video];
}

- (void) released_now
{
    if (!self.released) {
        self.released = true;
        self.release_time = [NSDate date];
    }
}

- (const byte *) getUUIDBytes:(byte *)uuid
{
    [self.uuid getUUIDBytes:uuid];
    return uuid;
}

- (const byte *) getConfIdBytes:(byte *)conf_id
{
    [self.conf_id getUUIDBytes:conf_id];
    return conf_id;
}

@end

@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@end
@implementation SceneDelegate

- (void) scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"willConnectToSession");
    phoneIosDelegate.scene = scene;
    [phoneIosDelegate try_start_ui:nil];
    if ((connectionOptions != nil) && (connectionOptions.userActivities != nil)) {
        NSUserActivity *userActivity;
        for (userActivity in connectionOptions.userActivities) {
            [phoneIosDelegate application:[UIApplication sharedApplication] continueUserActivity:userActivity restorationHandler:nil];
        }
    }
}

- (void) sceneDidDisconnect:(UIScene *)scene
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"sceneDidDisconnect");
    phoneIosDelegate.scene = nil;
}

- (void) sceneWillEnterForeground:(UIScene *)scene
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"sceneWillEnterForeground");
    [phoneIosDelegate applicationWillEnterForeground:[UIApplication sharedApplication]];
}

- (void) sceneDidBecomeActive:(UIScene *)scene
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"sceneDidBecomeActive");
    [phoneIosDelegate applicationDidBecomeActive:[UIApplication sharedApplication]];
}

- (void) sceneWillResignActive:(UIScene *)scene
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"sceneWillResignActive");
    [phoneIosDelegate applicationWillResignActive:[UIApplication sharedApplication]];
}

- (void) sceneDidEnterBackground:(UIScene *)scene
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"sceneDidEnterBackground");
    [phoneIosDelegate applicationDidEnterBackground:[UIApplication sharedApplication]];
}

- (void) scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"openURLContexts");
    UIOpenURLContext *urlContext;
    for (urlContext in URLContexts) {
        [phoneIosDelegate application:[UIApplication sharedApplication] openURL:urlContext.URL sourceApplication:urlContext.options.sourceApplication annotation:urlContext.options.annotation];
    }
}

/*
- (void) scene:(UIScene *)scene willContinueUserActivityWithType:(NSString *)userActivityType
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"willContinueUserActivityWithType");
    [phoneIosDelegate application:[UIApplication sharedApplication] willContinueUserActivityWithType:userActivityType];
}
*/

- (void) scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"continueUserActivity");
    [phoneIosDelegate application:[UIApplication sharedApplication] continueUserActivity:userActivity restorationHandler:nil];
}

- (void) scene:(UIScene *)scene didFailToContinueUserActivityWithType:(NSString *)userActivityType error:(NSError *)error
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    NSLog(@"didFailToContinueUserActivityWithType");
    [phoneIosDelegate application:[UIApplication sharedApplication] didFailToContinueUserActivityWithType:userActivityType error:error];
}

/*
- (NSUserActivity *) stateRestorationActivityForScene:(UIScene *)scene
{
    NSLog(@"stateRestorationActivityForScene");
}

- (void) scene:(UIScene *)scene restoreInteractionStateWithUserActivity:(NSUserActivity *)stateRestorationActivity
{
    NSLog(@"restoreInteractionStateWithUserActivity");
}

- (void) scene:(UIScene *)scene didUpdateUserActivity:(NSUserActivity *)userActivity
{
    NSLog(@"didUpdateUserActivity");
}
*/

@end

@interface CPSceneDelegate : UIResponder <CPTemplateApplicationSceneDelegate>
@end
@implementation CPSceneDelegate

- (void) templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didConnectInterfaceController:(CPInterfaceController *)interfaceController toWindow:(CPWindow *)window
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    [phoneIosDelegate application:[UIApplication sharedApplication] didConnectCarInterfaceController:interfaceController toWindow:window];
}

- (void) templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didConnectInterfaceController:(CPInterfaceController *)interfaceController
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    [phoneIosDelegate application:[UIApplication sharedApplication] didConnectCarInterfaceController:interfaceController toWindow:nil];
}

- (void) templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didDisconnectInterfaceController:(CPInterfaceController *)interfaceController fromWindow:(CPWindow *)window
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    [phoneIosDelegate application:[UIApplication sharedApplication] didDisconnectCarInterfaceController:interfaceController fromWindow:window];
}

- (void) templateApplicationScene:(CPTemplateApplicationScene *)templateApplicationScene didDisconnectInterfaceController:(CPInterfaceController *)interfaceController
{
    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    [phoneIosDelegate application:[UIApplication sharedApplication] didDisconnectCarInterfaceController:interfaceController fromWindow:nil];
}

@end

@interface PhoneIosDelegate()

@property (nonatomic) IosAsync *ios_async;
@property (nonatomic) IosEvent *ios_event;
@property (nonatomic) NSDictionary *launchOptions;
@property (nonatomic) UIAlertView *popupDialog;
@property (nonatomic) NSString *toastText;
@property (nonatomic) int display_on_if_charging;
@property (nonatomic) NSString *add_contact_name;
@property (nonatomic) NSString *add_contact_h323;
@property (nonatomic) NSString *add_contact_e164;
@property (nonatomic) bool addressBookAccessRequested;
@property (nonatomic) bool addressBookInaccessibleWarned;
@property (nonatomic) int dialer_claim;
@property (nonatomic) int gsm_use;
@property (nonatomic) int gsm_call_method;
@property (nonatomic) NSString *mobility_device_id;
@property (nonatomic) NSString *mobility_phone_number;
@property (nonatomic) bool docking_switches_hook;
@property (nonatomic) int content_size_category;
@property (nonatomic) int registrationStateValue;
@property (nonatomic) NSString *dial_location;
@property (nonatomic) NSString *dial_location_country_code;
@property (nonatomic) NSString *dial_location_area_code;
@property (nonatomic) NSString *dial_location_national_prefix;
@property (nonatomic) NSString *dial_location_international_prefix;
@property (nonatomic) NSString *dial_location_subscriber_numbers;
@property (nonatomic) int dial_location_internal_length;
@property (nonatomic) CTCallCenter *callCenter;
@property (nonatomic) bool dialingGSM;
@property (nonatomic) bool transferToNormalizedNumber;
@property (nonatomic) bool registrationStateUp;
@property (nonatomic) NSMutableArray *contacts;
@property (nonatomic) int addressBookChangeCounter;
@property (nonatomic) int addressBookChangeAck;
@property (nonatomic) bool callPresentation;
@property (nonatomic) bool lockedPresentation;
@property (nonatomic) bool restrictedUse;
@property (nonatomic) bool videoUp;
@property (nonatomic) bool recordPermissionRequested;
@property (nonatomic) bool recordPermissionGranted;
@property (nonatomic) bool didEnterBackground;
@property (nonatomic) bool didBecomeActive;
@property (nonatomic) bool proximitySensorOn;
@property (nonatomic) bool remoteControlReceiverRegistered;
@property (nonatomic) bool batteryMonitoringOn;
@property (nonatomic) bool orientationMonitoringOn;
@property (nonatomic) bool powerPlugged;
@property (nonatomic) Reachability *internetReachability;
@property (nonatomic) bool connectivityUp;
@property (nonatomic) bool signallingConnected;
@property (nonatomic) UIBackgroundTaskIdentifier reachability_background_task;
@property (nonatomic) UIBackgroundTaskIdentifier two_factor_background_task;
@property (nonatomic) dword reachability_ip_address;
#ifdef PHONE_IOS_USE_CRASHREPORTER
@property (nonatomic) PLCrashReporter *crashReporter;
#endif
@property (nonatomic) bool uiStarted;
@property (nonatomic) bool manuallyStarted;
@property (nonatomic) NSData *defaultTunesData;
@property (nonatomic) NSData *defaultTonesData;
@property (nonatomic) NSMutableArray *remoteLocalNotifications;
@property (nonatomic) NSMutableArray *pendingLocalNotifications;
@property (nonatomic) NSMutableArray *presentedLocalNotifications;
@property (nonatomic) NSMutableArray *pendingBadgeCounts;
@property (nonatomic) bool notify_active_calls;
@property (nonatomic) int notify_missed_calls;
@property (nonatomic) int notify_missed_msgs;
@property (nonatomic) int notify_voice_msgs;
@property (nonatomic) bool audio_focus_loss;
@property (nonatomic) bool isShutdown;
@property (nonatomic) NSString *commands;
@property (nonatomic) int registration_check_request;
@property (nonatomic) int registration_check_confirm;
@property (nonatomic) bool webserver_started;
@property (nonatomic) BOOL statusBarHidden;
@property (nonatomic) UIStatusBarStyle statusBarStyle;
@property (nonatomic) UIColor *statusBarColor;
@property (nonatomic) UIColor *statusBarTextColor;
@property (nonatomic,weak) UIVisualEffectView *statusBarBlurViewLight;
@property (nonatomic,weak) UIVisualEffectView *statusBarBlurViewDark;
@property (nonatomic) NSData *apnToken;
@property (nonatomic) NSData *apnTokenVoIP;
@property (nonatomic) NSString *user_agent;
@property (nonatomic) NSString *webserver_url;
@property (nonatomic) NSString *fix_cert_url;
@property (nonatomic) NSString *start_fix_cert_url;
@property (nonatomic) bool got_certificate;
@property (nonatomic) bool fix_cert_running;
@property (nonatomic) SFSafariViewController *fixCertViewController;
@property (nonatomic) bool videoPermissionGranted;
@property (nonatomic) dispatch_queue_t videoDispatchQueue;
@property (nonatomic) NSMutableArray *activeCameras;
@property (nonatomic) NSMutableArray *transcodingSessions;
@property (nonatomic) int previewCoder;
@property (nonatomic) int webcamCoder;
@property (nonatomic) bool camerasBlanked;
@property (nonatomic) bool proximityNearby;
@property (nonatomic) int contactsSearchResults;
@property (nonatomic) NSArray *contactsSearchResult;
@property (nonatomic) int contactsSearchPos;
@property (nonatomic) NSMutableArray *contactsSearchQueue;
@property (nonatomic) NSData *webserverGetData;
@property (nonatomic) int webserverGetPos;
@property (nonatomic) NSMutableArray *webserverGetQueue;
@property (nonatomic) NSMutableArray *cxCalls;
@property (nonatomic) bool cxHideFromNativeCallList;
@property (nonatomic) CXProvider *cxProvider;
@property (nonatomic) CXCallController *cxCallController;
//@property (nonatomic) WKProcessPool *wkWebViewProcessPool;
@property (nonatomic) bool evaluatingJavaScript;
@property (nonatomic) bool audioSessionActive;
@property (nonatomic) bool audioSessionActivated;
@property (nonatomic) bool screen_capture_started;
@property (nonatomic) bool screen_capture_running;
@property (nonatomic) bool screen_capture_stopped;
@property (nonatomic) WKSnapshotConfiguration *snapshotConfiguration;
@property (nonatomic) FormsCP *formsCP;
@property (nonatomic) CPSessionConfiguration *cpSessionConfiguration;

@property (copy) void (^startProximitySensor)();
@property (copy) void (^stopProximitySensor)();
@property (copy) void (^registerRemoteControl)();
@property (copy) void (^unregisterRemoteControl)();
@property (copy) void (^startBatteryMonitoring)();
@property (copy) void (^stopBatteryMonitoring)();
@property (copy) void (^startOrientationMonitoring)();
@property (copy) void (^stopOrientationMonitoring)();
@property (copy) void (^on_display_on_if_charging)();
@property (copy) void (^forms_event_external_call)(CTCall *call);
@property (copy) void (^forms_event_webcam_added)(NSString *id, bool facing_front, int orientation, NSString *context, NSString *description);
@property (copy) void (^forms_event_webcam_query_result)(NSString *context);
@property (copy) void (^forms_event_webcam_failed)(NSString *id, NSString *context);
@property (copy) void (^forms_event_call_error)(NSString *uuid, int error);

@end
@implementation PhoneIosDelegate

@synthesize window = _window;

- (void) ios_async_signal_handler:(IosAsync *)ios_async
{
    while (ios_async.dequeue()) {
    }
}

- (id) ios_async_get_id_obj:(int)id
{
    return ((id >= 0) ? self.forms.get_id_obj(id) : self.forms.id_obj(id));
}

+ (UIColor *) colorRGB:(int)rgb
{
    return [UIColor colorWithRed:((rgb >> 16) & 0xff) / 255.0 green:((rgb >> 8) & 0xff) / 255.0 blue:(rgb & 0xff) / 255.0 alpha:1.0];
}

+ (void) openURL:(NSURL *)url
{
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(openURL:options:completionHandler:)]) {
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success){}];
    }
    else {
        [[UIApplication sharedApplication] openURL:url];
    }
}

- (bool) number_is_h323:(NSString *)number
{
    int i;

    i = 0;
    while ((i < number.length)
        && ((([number characterAtIndex:i] >= '0') && ([number characterAtIndex:i] <= '9'))
         || ([number characterAtIndex:i] == '+')
         || ([number characterAtIndex:i] == '-')
         || ([number characterAtIndex:i] == '*')
         || ([number characterAtIndex:i] == '/')
         || ([number characterAtIndex:i] == '(')
         || ([number characterAtIndex:i] == ')')
         || ([number characterAtIndex:i] == '#')
         || ([number characterAtIndex:i] == ' ')
         || ([number characterAtIndex:i] == ','))) {
        i++;
    }
    return (i < number.length);
}

- (void) checkNotifyObsolete
{
    UIApplication *application = [UIApplication sharedApplication];
    unsigned count;
    int i;

    count = 0;
    for (i = 0; i < self.pendingBadgeCounts.count; i++) {
        count += ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).count;
    }
    if ((application.applicationState == UIApplicationStateBackground)
     && !self.hookstateOffhook && !self.callPresentation
     && (self.pendingLocalNotifications.count == 0)
     && (count == 0)
     && (self.presentedLocalNotifications.count != 0)) {
        [self.presentedLocalNotifications removeAllObjects];
        [self cancelAllLocalNotifications];
    }
}

- (void) checkNotifyCall
{
/*
    UIApplication *application = [UIApplication sharedApplication];

    [self checkNotifyObsolete];
    if ((application.applicationState == UIApplicationStateBackground)
     && (self.hookstateOffhook || self.callPresentation)) {
        FormsCall *call = (FormsCall *)(forms_page.controls.get(position));
        NSString *callId = [NSString stringWithFormat:@"%d", call.call_id];
        i = 0;
        while ((i < self.presentedLocalNotifications.count)
         && (((presentedCallId = [((UILocalNotification *)[self.presentedLocalNotifications objectAtIndex:i]).userInfo valueForKey:@"callRef"]) == nil)
          || ![presentedCallId isEqualToString:callId])) {
            i++;
        }
        if ((i < self.presentedLocalNotifications.count)
         && [((UILocalNotification *)[self.presentedLocalNotifications objectAtIndex:i]) respondsToSelector:@selector(category)]
         && [((UILocalNotification *)[self.presentedLocalNotifications objectAtIndex:i]).category isEqualToString:@CALL_CATEGORY]
         && (((FormsCall *)(forms_page.controls.get(position))).call_state != CALL_STATE_RINGING)) {
            [self.presentedLocalNotifications removeObjectAtIndex:i];
            i = self.presentedLocalNotifications.count;
        }
        if (i >= self.presentedLocalNotifications.count) {
            UILocalNotification *localNotif = [[UILocalNotification alloc] init];

            localNotif.fireDate = [NSDate date];
            // Set the category to the category that contains our action
            if ([localNotif respondsToSelector:@selector(category)]) {
                localNotif.category = (((FormsCall *)(forms_page.controls.get(position))).call_state == CALL_STATE_RINGING) ? @CALL_CATEGORY : @CONN_CATEGORY;
            }
            NSString *calling_info;
            if((call.userb_name == nil) && (call.userb_h323 == nil) && (call.userb_e164 != nil)) {
                call.userb_name = self.nameForContactWithPhoneNumber(call.userb_e164_contact);
            }
            if(call.userb_name != nil)      calling_info = call.userb_name;
            else if(call.userb_h323 != nil) calling_info = call.userb_h323;
            else if(call.userb_e164 != nil) calling_info = call.userb_e164;
            else                            calling_info = @"";
            localNotif.alertBody = [[NSString stringWithFormat:@"%@ %@", self.forms.localized_texts[FORMS_TXT_CALL], calling_info] stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
            localNotif.userInfo = [NSDictionary dictionaryWithObject:callId forKey:@"callRef"];
            [self.presentedLocalNotifications addObject:localNotif];

            [application presentLocalNotificationNow:localNotif];

            if (((FormsCall *)(forms_page.controls.get(position))).call_state != CALL_STATE_RINGING) {
                self.notify_active_calls = true;
            }
            self.notify_missed_calls = 0;
            self.notify_missed_msgs = 0;
            self.notify_voice_msgs = 0;
        }
    }
*/
}

- (void) checkNotifyMessage
{
    UIApplication *application = [UIApplication sharedApplication];

    [self checkNotifyObsolete];
    if ((application.applicationState == UIApplicationStateBackground)
     && (self.hookstateOffhook || self.callPresentation)) {
/*
        NSString *from = [((FormsInput *)(formsPage.controls.get(FORMS_PHONE_MESSAGE_FROM))).value stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
        NSString *text = ((FormsInput *)(formsPage.controls.get(FORMS_PHONE_MESSAGE_TEXT))).value;
        NSString *msgId = [NSString stringWithFormat:@"%@\n%@", from, text];
        i = 0;
        while ((i < self.presentedLocalNotifications.count)
         && (((presentedMsgId = [((UILocalNotification *)[self.presentedLocalNotifications objectAtIndex:i]).userInfo valueForKey:@"msgRef"]) == nil)
          || ![presentedMsgId isEqualToString:msgId])) {
            i++;
        }
        if (i >= self.presentedLocalNotifications.count) {
            UILocalNotification *localNotif = [[UILocalNotification alloc] init];

            localNotif.fireDate = [NSDate date];
            // Set the category to the category that contains our action
            if ([localNotif respondsToSelector:@selector(category)]) {
                localNotif.category = @CALL_CATEGORY;
            }
            localNotif.alertBody = [[NSString stringWithFormat:@"%@ %@\n%@", self.forms.localized_texts.get(FORMS_TXT_MESSAGE), from, text] stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
            localNotif.userInfo = [NSDictionary dictionaryWithObject:msgId forKey:@"msgRef"];
            [self.presentedLocalNotifications addObject:localNotif];

            [application presentLocalNotificationNow:localNotif];

            self.notify_missed_calls = 0;
            self.notify_missed_msgs = 0;
            self.notify_voice_msgs = 0;
        }
*/
    }
}

- (void) removeLocalNotification:(NSString *)ref
{
    UIApplication *application = [UIApplication sharedApplication];
    NSString *r;
    unsigned i;

    if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        i = 0;
        while (i < self.pendingLocalNotifications.count) {
            if (((r = [((UNNotificationRequest *)[self.pendingLocalNotifications objectAtIndex:i]).content.userInfo valueForKey:@"callRef"]) != nil) && [r hasPrefix:ref]) {
                [self.pendingLocalNotifications removeObjectAtIndex:i];
            }
            else {
                i++;
            }
        }
        i = 0;
        while (i < self.presentedLocalNotifications.count) {
            if (((r = [((UNNotificationRequest *)[self.presentedLocalNotifications objectAtIndex:i]).content.userInfo valueForKey:@"callRef"]) != nil) && [r hasPrefix:ref]) {
                [center removePendingNotificationRequestsWithIdentifiers:@[((UNNotificationRequest *)[self.presentedLocalNotifications objectAtIndex:i]).identifier]];
                [center removeDeliveredNotificationsWithIdentifiers:@[((UNNotificationRequest *)[self.presentedLocalNotifications objectAtIndex:i]).identifier]];
                [self.presentedLocalNotifications removeObjectAtIndex:i];
            }
            else {
                i++;
            }
        }
    }
    else {
        i = 0;
        while (i < self.pendingLocalNotifications.count) {
            if (((r = [((UILocalNotification *)[self.pendingLocalNotifications objectAtIndex:i]).userInfo valueForKey:@"callRef"]) != nil) && [r hasPrefix:ref]) {
                [self.pendingLocalNotifications removeObjectAtIndex:i];
            }
            else {
                i++;
            }
        }
        i = 0;
        while (i < self.presentedLocalNotifications.count) {
            if (((r = [((UILocalNotification *)[self.presentedLocalNotifications objectAtIndex:i]).userInfo valueForKey:@"callRef"]) != nil) && [r hasPrefix:ref]) {
                [application cancelLocalNotification:((UILocalNotification *)[self.presentedLocalNotifications objectAtIndex:i])];
                [self.presentedLocalNotifications removeObjectAtIndex:i];
            }
            else {
                i++;
            }
        }
    }
}

- (void) cancelAllLocalNotifications
{
    UIApplication *application = [UIApplication sharedApplication];

    [application cancelAllLocalNotifications];
    if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
        NSObject *center = [NSClassFromString(@"UNUserNotificationCenter") performSelector:@selector(currentNotificationCenter)];
        [center performSelector:@selector(removeAllDeliveredNotifications)];
        [center performSelector:@selector(removeAllPendingNotificationRequests)];
    }
}

- (void) badgeUpdate
{
    UIApplication *application = [UIApplication sharedApplication];
    unsigned count;
    int i;

    if (self.pendingLocalNotifications.count == 0) {
        count = 0;
        for (i = 0; i < self.pendingBadgeCounts.count; i++) {
            count += ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).count;
        }
        i = 0;
        while ((i < self.pendingBadgeCounts.count)
         && ((((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text == nil)
          || (((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text.length == 0))) {
            i++;
        }

        application.applicationIconBadgeNumber = count;
        [self checkNotifyObsolete];
        if ((application.applicationState == UIApplicationStateBackground)
         && !self.hookstateOffhook && !self.callPresentation
         && (count != 0)) {
            [self cancelAllLocalNotifications];

            if (i < self.pendingBadgeCounts.count) {
                UILocalNotification *localNotif = [[UILocalNotification alloc] init];

                NSMutableString *notificationText = [[NSMutableString alloc] init];
                [notificationText appendString:((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text];
                i++;
                while (i < self.pendingBadgeCounts.count) {
                    if ((((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text != nil)
                     && (((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text.length != 0)) {
                        [notificationText appendString:@" ,"];
                        [notificationText appendString:((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text];
                    }
                    i++;
                }
                localNotif.alertBody = [notificationText stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
                [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];
            }
        }
    }
}

/*
- (id) wkWebViewProcessPool
{
    if (!_wkWebViewProcessPool) {
        _wkWebViewProcessPool = [[WKProcessPool alloc] init];
    }
    return _wkWebViewProcessPool;
}
*/

static void trigger_local_network_privacy_alert()
{
    byte b[] = { 0 };
    int i;
    struct ifaddrs * ifap, * ifa;
    struct sockaddr_in sin;
    struct sockaddr_in6 sin6;
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    int sock6 = socket(AF_INET6, SOCK_DGRAM, 0);
    if (getifaddrs(&ifap) != 0) {
        ifap = NULL;
    }
    if (ifap) {
        for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
            if ((ifa->ifa_flags & IFF_BROADCAST) && ifa->ifa_addr) {
                switch (ifa->ifa_addr->sa_family) {
                case AF_INET:
                    memcpy(&sin, (struct sockaddr_in *)(ifa->ifa_addr), sizeof(struct sockaddr_in));
                    sin.sin_port = htons(9);
                    if (sock != -1) sendto(sock, b, sizeof(b), 0, (struct sockaddr *)(&sin), sizeof(struct sockaddr_in));
                    break;
                case AF_INET6:
                    memcpy(&sin6, (struct sockaddr_in6 *)(ifa->ifa_addr), sizeof(struct sockaddr_in6));
                    sin6.sin6_port = htons(9);
                    if (sock6 != -1) sendto(sock6, b, sizeof(b), 0, (struct sockaddr *)(&sin6), sizeof(struct sockaddr_in6));
                    break;
                default:
                    break;
                }
            }
        }
        if (sock6 != -1) {
            for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
                if ((ifa->ifa_flags & IFF_BROADCAST) && ifa->ifa_addr && (ifa->ifa_addr->sa_family == AF_INET6) && (((struct sockaddr_in6 *)(ifa->ifa_addr))->sin6_addr.s6_addr[0] == 0xfe) && ((((struct sockaddr_in6 *)(ifa->ifa_addr))->sin6_addr.s6_addr[1] & 0xc0) == 0x80)) {
                    memcpy(&sin6, (struct sockaddr_in6 *)(ifa->ifa_addr), sizeof(struct sockaddr_in6));
                    sin6.sin6_port = htons(9);
                    for (i = 8; i < 16; i++) {
                        sin6.sin6_addr.s6_addr[i] ^= 0x55;
                    }
                    connect(sock6, (struct sockaddr *)(&sin6), sizeof(struct sockaddr_in6));
                    for (i = 8; i < 16; i++) {
                        sin6.sin6_addr.s6_addr[i] ^= 0xff;
                    }
                    connect(sock6, (struct sockaddr *)(&sin6), sizeof(struct sockaddr_in6));
                }
            }
        }
        freeifaddrs(ifap);
    }
    if (sock != -1) close(sock);
    if (sock6 != -1) close(sock6);
}

- (void) try_start_ui:(id)context
{
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(supportsMultipleScenes)] && (self.scene == nil)) {
        return;
    }
    if (!self.recordPermissionRequested) {
        if ([[AVAudioSession sharedInstance] respondsToSelector:@selector(requestRecordPermission:)]) { // we're on iOS 7
            [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
                if (granted) {
                    dispatch_async(dispatch_get_main_queue(), ^
                        {
                            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                            phoneIosDelegate.recordPermissionRequested = true;
                            phoneIosDelegate.recordPermissionGranted = true;
                            [phoneIosDelegate try_start_ui:nil];
                        });
                }
                else {
                    dispatch_async(dispatch_get_main_queue(), ^
                        {
                            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                            phoneIosDelegate.recordPermissionRequested = true;
                            [phoneIosDelegate try_start_ui:nil];
                        });
                }
            }];
            return;
        }
        else { // we're on iOS 6 or older
            self.recordPermissionGranted = true;
        }
    }
    if (staticAddressBook == nil) {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
        staticAddressBook = ABAddressBookCreate();
#else
        CFErrorRef *error = nil;
        staticAddressBook = ABAddressBookCreateWithOptions(NULL, error);
#endif
        if (!self.addressBookAccessRequested) {
            ABAddressBookRequestAccessWithCompletion(staticAddressBook, ^(bool granted, CFErrorRef error) {
                if (granted) {
                    dispatch_async(dispatch_get_main_queue(), ^
                        {
                            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                            phoneIosDelegate.addressBookAccessRequested = true;
                            [phoneIosDelegate try_start_ui:nil];
                        });
                }
                else {
                    dispatch_async(dispatch_get_main_queue(), ^
                        {
                            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                            phoneIosDelegate.addressBookAccessRequested = true;
                            phoneIosDelegate.addressBookInaccessibleWarned = true;
                            phoneIosDelegate.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/ADDRESS-BOOK-INACCESSIBLE-WARNED", @"1");
                            [phoneIosDelegate try_start_ui:nil];
                        });
                }
            });
            return;
        }
        if (!self.addressBookInaccessibleWarned) {
            if (staticAddressBook == nil) {
                self.popupDialog = [[UIAlertView alloc] initWithTitle:nil message:@"Address book inaccessible."
                    delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
                [self.popupDialog show];
                return;
            }
        }
    }
    if (staticAddressBook != nil) {
        ABAddressBookRegisterExternalChangeCallback(staticAddressBook, addressBookChanged, (__bridge void *)self);
        [self addressBookReload];
    }
    /*
        Check video authorization status. Video access is required and audio
        access is optional. If audio access is denied, audio is not recorded
        during movie recording.
    */
    if ([NSClassFromString(@"AVCaptureDevice") respondsToSelector:@selector(authorizationStatusForMediaType:)]) {
        switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:
            // The user has previously granted access to the camera.
            self.videoPermissionGranted = true;
            [self ReportWebcams:nil];
            break;
        case AVAuthorizationStatusNotDetermined:
            /*
                The user has not yet been presented with the option to grant
                video access. We suspend the session queue to delay session
                setup until the access request has completed.
                Note that audio access will be implicitly requested when we
                create an AVCaptureDeviceInput for audio during session setup.
            */
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    dispatch_async(dispatch_get_main_queue(), ^
                        {
                            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                            phoneIosDelegate.videoPermissionGranted = true;
                            [phoneIosDelegate try_start_ui:nil];
                        });
                }
                else {
                    dispatch_async(dispatch_get_main_queue(), ^
                        {
                            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                            [phoneIosDelegate try_start_ui:nil];
                        });
                }
            }];
            return;
        default:
            // The user has previously denied access.
            break;
        }
    }
    else { // we're on older
        self.videoPermissionGranted = true;
    }
    trigger_local_network_privacy_alert();
    [INPreferences requestSiriAuthorization:^(INSiriAuthorizationStatus status) {
            NSLog(@"Siri authorization status %@", (status == INSiriAuthorizationStatusAuthorized) ? @"authorized" :
                ((status == INSiriAuthorizationStatusDenied) ? @"denied" :
                ((status == INSiriAuthorizationStatusRestricted) ? @"restricted" : @"not determined")));
        }];
    NSLog(@"Refused permissions:%@%@%@",
        self.recordPermissionGranted ? @"" : @" microphone",
        (staticAddressBook != nil) ? @"" : @" contacts",
        self.videoPermissionGranted ? @"" : @" video");
 
    self.user_agent = [NSString stringWithFormat:@"%s/%s %s", PHONE_IOS_LOG_APP_NAME, _BUILD_STRING_,
#if TARGET_IPHONE_SIMULATOR
        "(iOS Simulator)"
#else
        "(iOS)"
#endif
        ];
    if (self.user_agent != nil) {
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
#ifdef CLIENT_IOS_USE_WKWEBVIEW
        WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
        configuration.preferences = [[WKPreferences alloc] init];
        configuration.preferences.javaScriptEnabled = true;
        WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration];
        NSString *userAgent = [webView valueForKey:@"userAgent"];
        if (userAgent != nil) {
            NSLog(@"User agent %@ %@", userAgent, self.user_agent);
            NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithFormat:@"%@ %@", userAgent, self.user_agent], @"UserAgent", nil];
            [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }
        else {
            [webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id __nullable userAgent, NSError * __nullable error) {
                NSLog(@"User agent %@ %@", userAgent, self.user_agent);
                NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithFormat:@"%@ %@", userAgent, self.user_agent], @"UserAgent", nil];
                [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
                [[NSUserDefaults standardUserDefaults] synchronize];
            }];
        }
#else
        UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
        NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
        NSLog(@"User agent %@ %@", userAgent, self.user_agent);
        NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithFormat:@"%@ %@", userAgent, self.user_agent], @"UserAgent", nil];
        [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
        [[NSUserDefaults standardUserDefaults] synchronize];
#endif
#endif
    }

#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
#ifdef CLIENT_IOS_USE_WKWEBVIEW
    UIViewController *mainViewController = [[WebViewController alloc] init];
#else
//    UIViewController *mainViewController = [[WebViewController alloc] init];
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    UIViewController *mainViewController = [storyboard instantiateInitialViewController];
#endif
    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    blurEffectView.translatesAutoresizingMaskIntoConstraints = false;
    [mainViewController.view addSubview:blurEffectView];
    [blurEffectView.leadingAnchor constraintEqualToAnchor:mainViewController.view.leadingAnchor].active = true;
    [blurEffectView.widthAnchor constraintEqualToAnchor:mainViewController.view.widthAnchor].active = true;
    [blurEffectView.topAnchor constraintEqualToAnchor:mainViewController.view.topAnchor].active = true;
    [blurEffectView.bottomAnchor constraintEqualToAnchor:mainViewController.view.safeAreaLayoutGuide.topAnchor].active = true;
    blurEffectView.hidden = self.statusBarHidden || (self.statusBarStyle == UIStatusBarStyleLightContent);
    self.statusBarBlurViewLight = blurEffectView;

    blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
    blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    blurEffectView.translatesAutoresizingMaskIntoConstraints = false;
    [mainViewController.view addSubview:blurEffectView];
    [blurEffectView.leadingAnchor constraintEqualToAnchor:mainViewController.view.leadingAnchor].active = true;
    [blurEffectView.widthAnchor constraintEqualToAnchor:mainViewController.view.widthAnchor].active = true;
    [blurEffectView.topAnchor constraintEqualToAnchor:mainViewController.view.topAnchor].active = true;
    [blurEffectView.bottomAnchor constraintEqualToAnchor:mainViewController.view.safeAreaLayoutGuide.topAnchor].active = true;
    blurEffectView.hidden = self.statusBarHidden || (self.statusBarStyle != UIStatusBarStyleLightContent);
    self.statusBarBlurViewDark = blurEffectView;

    if (self.scene != nil) {
        self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)(self.scene)];
    }
    else {
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    }
    self.window.rootViewController = mainViewController;
    ((WebViewController *)(self.window.rootViewController)).statusBarHidden = self.statusBarHidden;
    ((WebViewController *)(self.window.rootViewController)).statusBarStyle = self.statusBarStyle;
    ((WebViewController *)(self.window.rootViewController)).statusBarColor = self.statusBarColor;
    ((WebViewController *)(self.window.rootViewController)).statusBarTextColor = self.statusBarTextColor;
    [self.window makeKeyAndVisible];
#endif
    self.uiStarted = true;

    /*
     Observe the kNetworkReachabilityChangedNotification. When that notification is posted, the method reachabilityChanged will be called.
     */
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil];

    if (!self.manuallyStarted && ([UIApplication sharedApplication].applicationState == UIApplicationStateActive)) {
        self.manuallyStarted = true;
        set_manually_started(true);
    }
    if (self.internetReachability == nil) {
        self.internetReachability = [Reachability reachabilityForInternetConnection];
        [self.internetReachability startNotifier];
    }
    [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
    self.startBatteryMonitoring();
    [self batteryStateChange:nil];
    self.startOrientationMonitoring();
    if (self.webserver_started) {
        [self on_webserver_started];
    }
    if (self.launchOptions) {
        NSDictionary *userInfo = [self.launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
        if (userInfo) {
            userInfo = [self decryptNotificationPayload:userInfo pushPassword:[self push_password]];
        }
        if (!userInfo) {
            UILocalNotification *notification = [self.launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
            if (notification) {
                userInfo = notification.userInfo;
            }
        }
        if (userInfo) {
            NSString *link = [userInfo valueForKey:@"msgLink"];
            if (link != nil) {
                self.forms_event_open_permalink(link);
            }
        }
    }
    if (self.commands != nil) {
        self.forms_event_open_permalink(self.commands);
        self.commands = nil;
    }
}

- (void) alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if (!self.addressBookInaccessibleWarned) {
        self.addressBookInaccessibleWarned = true;
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/ADDRESS-BOOK-INACCESSIBLE-WARNED", @"1");
        [self performSelectorOnMainThread:@selector(try_start_ui:) withObject:nil waitUntilDone:NO];
    }
}

- (void) formsToastGone:(id)object
{
    self.toastText = nil;
}

- (void) formsShowToast:(Forms *)forms text:(NSString *)text
{
    [ToastView showToastInParentView:self.window.rootViewController.view withText:text withDuaration:3.0 withFont:self.fontForCategory(FONT_CATEGORY_FOOTNOTE)];
    self.toastText = text;
    [self performSelector:@selector(formsToastGone:) withObject:nil afterDelay:3];
}

- (NSString *) formsActiveToastText
{
    return self.toastText;
}

- (void) showToast_:(NSString *)text
{
    [self formsShowToast:self.forms text:text];
}

- (void) email_logs
{
    int i;
//    [MFMailComposeViewController canSendMail]
    // Email Subject
    NSString *emailTitle = @"";
    // Email Content
    NSString *messageBody= [NSString stringWithFormat:@"Traces for build %s. Details of incident: ", _BUILD_STRING_];
    // To address
    NSArray *toRecipients = [NSArray arrayWithObject:@""];
    
    MFMailComposeViewController *mc = [[MFMailComposeViewController alloc] init];
    if (mc == nil) {
        [self formsShowToast:self.forms text:@"Operation failed"/*self.forms.localized_texts[FORMS_TXT_OPERATION_FAILED]*/];
        return;
    }
    mc.mailComposeDelegate = self;
    [mc setSubject:emailTitle];
    [mc setMessageBody:[NSString stringWithFormat:@"%@%@%@", @"<p><b>", messageBody, @"</b></p>"] isHTML:true];
    [mc setToRecipients:toRecipients];

    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) objectAtIndex:0];
    NSString *config = [documentDir stringByAppendingPathComponent:@"config.json"];
    NSData *configData = [NSData dataWithContentsOfFile:config];
    if (configData != nil) {
        [mc addAttachmentData:configData mimeType:@"text/csv" fileName:@"config.json"];
    }
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSArray* dirs = [fileManager contentsOfDirectoryAtPath:documentDir error:NULL];
    for (i = 0; i < dirs.count; i++) {
        NSString *filename = (NSString *)[dirs objectAtIndex:i];
        if ([filename hasPrefix:@CRASH_FILE_NAME] || [filename hasPrefix:@SAVE_LOG_FILE_NAME]) {
            NSData *data = [NSData dataWithContentsOfFile:[documentDir stringByAppendingPathComponent:filename]];
            if (data != nil) {
                [mc addAttachmentData:data mimeType:@"text/csv" fileName:filename];
            }
        }
    }

    // Present mail view controller on screen
    [self.window.rootViewController presentViewController:mc animated:YES completion:nil];
}

- (void) email_cert
{
//    [MFMailComposeViewController canSendMail]
    // Email Subject
    NSString *emailTitle = [NSString stringWithFormat:@"%@ Self-signed certificate", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];
    // Email Content
    NSString *messageBody= [NSString stringWithFormat:@"%@ Self-signed certificate", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];
    // To address
    NSArray *toRecipients = [NSArray arrayWithObject:@""];
    
    MFMailComposeViewController *mc = [[MFMailComposeViewController alloc] init];
    if (mc == nil) {
        [self formsShowToast:self.forms text:@"Operation failed"/*self.forms.localized_texts[FORMS_TXT_OPERATION_FAILED]*/];
        return;
    }
    mc.mailComposeDelegate = self;
    [mc setSubject:emailTitle];
    [mc setMessageBody:[NSString stringWithFormat:@"%@%@%@", @"<p><b>", messageBody, @"</b></p>"] isHTML:true];
    [mc setToRecipients:toRecipients];

    NSData * cert_data = [WebViewController decodeBase64WithString:self.cert_string];
    if (cert_data != nil) {
        [mc addAttachmentData:cert_data mimeType:@"application/x-pem-file" fileName:@"cert.pem"];
    }

    // Present mail view controller on screen
    [self.window.rootViewController presentViewController:mc animated:YES completion:nil];
}

- (void) mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
    // Close the Mail Interface
    [controller dismissViewControllerAnimated:YES completion:nil];

    switch (result)
    {
    case MFMailComposeResultCancelled:
        [self formsShowToast:self.forms text:@"Mail cancelled"];
        break;
    case MFMailComposeResultSaved:
        [self formsShowToast:self.forms text:@"Mail saved"];
        break;
    case MFMailComposeResultSent:
        [self formsShowToast:self.forms text:@"Mail sent"];
        break;
    case MFMailComposeResultFailed:
        [self formsShowToast:self.forms text:[NSString stringWithFormat:@"Mail sending failed: %@", error.localizedDescription]];
        break;
    default:
        break;
    }
}

- (void) save_log_task:(id)sender
{
    if (clear_logs_req != clear_logs_ack) {
        clear_logs_ack = clear_logs_req;
        clear_logs();
    }
    if (save_log_on && !save_log_is_on) {
        save_log_is_on = true;
        open_log();
    }
    poll_log();
    if (save_log_on && save_log_is_on) {
        [NSThread sleepForTimeInterval:1.0];
        [self performSelectorInBackground:@selector(save_log_task:) withObject:nil];
    }
    else {
        save_log_is_on = false;
        close_log();
    }
}

- (void) save_log_runnable:(id)sender
{
    if (!save_log_is_on) {
        [self performSelectorInBackground:@selector(save_log_task:) withObject:nil];
    }
}

- (void) save_log:(bool)on
{
    save_log_on = on;
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(save_log_runnable:) object:nil];
    if (on) {
        [self performSelector:@selector(save_log_runnable:) withObject:nil afterDelay:0.1];
    }
}

- (void) clearLogs
{
    clear_logs_req++;
}

- (void) add_contact:(NSString *)name h323:(NSString *)h323 e164:(NSString *)e164
{
    ABRecordRef newPerson = ABPersonCreate();

    if ((name != nil) || (h323 != nil)) {
        NSString *fullName = (name != nil) ? name : h323;
        int i = 0;
        while ((i < fullName.length) && ([fullName characterAtIndex:i] != ' ')) {
            i++;
        }
        NSString *firstName = [fullName substringToIndex:i];
        NSString *lastName = (i < fullName.length) ? [fullName substringFromIndex:i+1] : @"";
        ABRecordSetValue(newPerson, kABPersonFirstNameProperty, (__bridge CFTypeRef)(firstName), nil);
        ABRecordSetValue(newPerson, kABPersonLastNameProperty, (__bridge CFTypeRef)(lastName), nil);
    }
    if (e164 != nil) {
        ABMutableMultiValueRef multiPhone = ABMultiValueCreateMutable(kABMultiStringPropertyType);
        ABMultiValueAddValueAndLabel(multiPhone, (__bridge CFTypeRef)e164, kABWorkLabel, NULL); 
        ABRecordSetValue(newPerson, kABPersonPhoneProperty, multiPhone, nil);
        CFRelease(multiPhone);
    }

    ABUnknownPersonViewController *unknownPersonViewController = [[ABUnknownPersonViewController alloc] init];
    unknownPersonViewController.displayedPerson = newPerson;
    CFRelease(newPerson);
    unknownPersonViewController.allowsAddingToAddressBook = true;
    unknownPersonViewController.unknownPersonViewDelegate = self;
    [self.window.rootViewController presentViewController:unknownPersonViewController animated:YES completion:nil];
}

- (void) unknownPersonViewController:(ABUnknownPersonViewController *)unknownPersonViewController didResolveToPerson:(ABRecordRef)person
{
    [unknownPersonViewController dismissViewControllerAnimated:YES completion:nil];
}

- (void) registrationState:(int)state
{
    switch(state) {
    case 1: // REGISTRATION_DONE
        self.registrationStateUp = true;
        break;
    case 0: // REGISTRATION_NONE
        self.registrationStateUp = false;
        break;
    case 2: // REGISTRATION_REJECTED
    case 3: // REGISTRATION_TIMEOUT
    case 4: // REGISTRATION_UNAUTHORIZED
    default:
        self.registrationStateUp = false;
        break;
    }
    [self updateNoMediaCall];
}

- (bool) shouldOpenURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication
{
    int i;

    if (url == nil) {
        return true;
    }
    if ((sourceApplication == nil) || ![sourceApplication isEqualToString:[[NSBundle mainBundle] bundleIdentifier]]) {
        NSString *decodedResource = [url.resourceSpecifier stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        if ([url.scheme caseInsensitiveCompare:@"com.innovaphone.clientiosstart"] == NSOrderedSame) {
            if (self.fixCertViewController != nil) {
                [self safariViewControllerDidFinish:self.fixCertViewController];
            }
            return true;
        }
        else if ([url.scheme caseInsensitiveCompare:@"com.innovaphone.myapps"] == NSOrderedSame) {
            return true;
        }
        else if ((([url.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
         && [url.resourceSpecifier hasPrefix:@"//config.innovaphone.com/com.innovaphone.myapps/"]) {
            return true;
        }
        else if (([url.scheme caseInsensitiveCompare:@"tel"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"com.innovaphone.tel"] == NSOrderedSame)) {
            NSString *phoneNumber = decodedResource;

            if((phoneNumber != nil)
             &&([self isEmergencyNumber:phoneNumber]
/*              ||((phoneNumber.length() == 3)
               &&(PhoneNumberUtils.getFormatTypeForLocale(Locale.getDefault()) == PhoneNumberUtils.FORMAT_NANP)
               &&(phoneNumber.charAt(1) == '1')
               &&(phoneNumber.charAt(2) == '1'))*/)) {
            }
            else if(!self.dialingGSM
             &&((phoneNumber == nil) || ![self isEmergencyNumber:phoneNumber])) {
                if (phoneNumber != nil) {
                    return true;
                }
            }
        }
        else if (([url.scheme caseInsensitiveCompare:@"sip"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"com.innovaphone.sip"] == NSOrderedSame)) {
            return true;
        }
        else if (([url.scheme caseInsensitiveCompare:@"im"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"com.innovaphone.im"] == NSOrderedSame)) {
            i = 0;
            while ((i < url.resourceSpecifier.length) && ([url.resourceSpecifier characterAtIndex:i] != '&')) {
                i++;
            }
            NSString *remoteHandle = [[url.resourceSpecifier substringToIndex:i] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            return ([self number_is_h323:remoteHandle] || [self callIsInternal:[self stripSeparators:remoteHandle]]);
        }
/*
        else {
            return true;
        }
*/
    }
    return false;
}

- (BOOL) application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    int i;

    if ([self shouldOpenURL:url sourceApplication:sourceApplication]) {
        if (url != nil) {
            NSString *decodedResource = [url.resourceSpecifier stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            if ([url.scheme caseInsensitiveCompare:@"com.innovaphone.clientiosstart"] == NSOrderedSame) {
            }
            else if ([url.scheme caseInsensitiveCompare:@"com.innovaphone.myapps"] == NSOrderedSame) {
                self.commands = [[url.resourceSpecifier stringByReplacingOccurrencesOfString:@"+" withString:@" "] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
                if (self.commands == nil) {
                    self.commands = @"";
                }
                if (self.forms != nil) {
                    self.forms_event_open_permalink(self.commands);
                    self.commands = nil;
                }
            }
            else if ((([url.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
             && [url.resourceSpecifier hasPrefix:@"//config.innovaphone.com/com.innovaphone.myapps/"]) {
                self.commands = [url.resourceSpecifier substringFromIndex:sizeof("//config.innovaphone.com/com.innovaphone.myapps/") - 1];
                if (self.commands == nil) {
                    self.commands = @"";
                }
                if (self.forms != nil) {
                    self.forms_event_open_permalink(self.commands);
                    self.commands = nil;
                }
            }
            else if (([url.scheme caseInsensitiveCompare:@"tel"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"com.innovaphone.tel"] == NSOrderedSame)) {
                self.dialContact(decodedResource);
            }
            else if (([url.scheme caseInsensitiveCompare:@"sip"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"com.innovaphone.sip"] == NSOrderedSame)) {
                self.forms.event_dial_sip(decodedResource, nil, FORMS_DIAL_NUMBER_VIDEO_MODE_DEFAULT);
            }
            else if (([url.scheme caseInsensitiveCompare:@"im"] == NSOrderedSame) || ([url.scheme caseInsensitiveCompare:@"com.innovaphone.im"] == NSOrderedSame)) {
                i = 0;
                while ((i < url.resourceSpecifier.length) && ([url.resourceSpecifier characterAtIndex:i] != '&')) {
                    i++;
                }
                NSString *remoteHandle = [[url.resourceSpecifier substringToIndex:i] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
                if ([self number_is_h323:remoteHandle] || [self callIsInternal:[self stripSeparators:remoteHandle]]) {
                    self.forms.event_start_chat(remoteHandle);
                }
                else {
                    NSString *smsURL = [NSString stringWithFormat:@"sms:%@", url.resourceSpecifier];
                    [PhoneIosDelegate openURL:[NSURL URLWithString:smsURL]];
                }
            }
/*
            else {
                if (url.fileURL) {
                    self.forms.event_handle_file(url.fileSystemRepresentation);
                }
                else {
                    self.forms.event_handle_url(decodedResource);
                }
            }
*/
        }
        return YES;
    }
    return NO;
}

- (bool) no_media_call_mode_
{
    if (self.internetReachability == nil) {
        self.internetReachability = [Reachability reachabilityForInternetConnection];
        [self.internetReachability startNotifier];
    }
    NetworkStatus netStatus = [self.internetReachability currentReachabilityStatus];

    if((self.gsm_use == FORMS_GSM_USE_ANY_CALLS)
     ||((self.gsm_use == FORMS_GSM_USE_OUTSIDE_WLAN)
      &&(netStatus != ReachableViaWiFi))) {
        return true;
    }
    else {
        return false;
    }
}

- (void) updateNoMediaCall
{
    if (self.registrationStateUp) {
        NSMutableString *str = [NSMutableString stringWithCapacity:1024];
        XmlIo *xml_io = [XmlIo xmlIo];

        int b = xml_io.add_tag(XML_ROOT_TAG, @"no-media-call");
        xml_io.add_attrib_bool(b, @"on", self.no_media_call_mode());
        xml_io.encode_to_string(0, str);
        self.forms.event_pbx_innovaphone_data_mobility(str);
    }
}

- (void) dialGSMTimeout:(id)sender
{
    self.dialingGSM = false;
}

- (void) dialDirectGSM:(NSString *)numberString
{
    self.dialingGSM = true;
    NSString *numberURL = [NSString stringWithFormat:@"telephony:%@", numberString];
    if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:numberURL]]) {
        [PhoneIosDelegate openURL:[NSURL URLWithString:numberURL]];
    }
    else {
        numberURL = [NSString stringWithFormat:@"tel:%@", numberString];
        if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:numberURL]]) {
            [PhoneIosDelegate openURL:[NSURL URLWithString:numberURL]];
        }
    }
    [self performSelector:@selector(dialGSMTimeout:) withObject:nil afterDelay:10];
}

- (void) dialGSM:(NSString *)numberString numberStringPBX:(NSString *)numberStringPBX
{
    if ((numberString.length != 0) && !self.dialingGSM) {
        if (self.gsm_call_method == FORMS_GSM_CALL_METHOD_CALLBACK) {
            if (self.registrationStateUp) {
                NSMutableString *str = [NSMutableString stringWithCapacity:1024];
                XmlIo *xml_io = [XmlIo xmlIo];

                int b = xml_io.add_tag(XML_ROOT_TAG, @"callback");
                xml_io.add_attrib(b, @"num", numberStringPBX);
                if ((self.mobility_device_id != nil) && (self.mobility_device_id.length != 0)) {
                    xml_io.add_attrib(b, @"hw", self.mobility_device_id);
                }
                xml_io.encode_to_string(0, str);
                self.forms.event_pbx_innovaphone_data_mobility(str);
                [self formsShowToast:self.forms text:[NSString stringWithFormat:@"%@...", @"Callback"/*self.forms.localized_texts[FORMS_TXT_CALLBACK]*/]];
            }
            else {
                [self dialDirectGSM:[NSString stringWithFormat:@"%@,%@", self.mobility_phone_number, numberStringPBX]];
            }
        }
        else {
            if ((self.gsm_call_method == FORMS_GSM_CALL_METHOD_CALLTHROUGH)
             && (self.mobility_phone_number != nil) && (self.mobility_phone_number.length != 0)) {
                if (self.registrationStateUp) {
                    NSMutableString *str = [NSMutableString stringWithCapacity:1024];
                    XmlIo *xml_io = [XmlIo xmlIo];

                    int b = xml_io.add_tag(XML_ROOT_TAG, @"callthru");
                    xml_io.add_attrib(b, @"num", numberStringPBX);
                    if ((self.mobility_device_id != nil) && (self.mobility_device_id.length != 0)) {
                        xml_io.add_attrib(b, @"hw",self. mobility_device_id);
                    }
                    xml_io.encode_to_string(0, str);
                    self.forms.event_pbx_innovaphone_data_mobility(str);
                    [self dialDirectGSM:self.mobility_phone_number];
                }
                else {
                    [self dialDirectGSM:[NSString stringWithFormat:@"%@,%@", self.mobility_phone_number, numberStringPBX]];
                }
            }
            else {
                [self dialDirectGSM:numberString];
            }
        }
    }
}

- (NSString *) stripSeparators:(NSString *)phoneNumber
{
    NSMutableString *number = [NSMutableString stringWithCapacity:phoneNumber.length];
    int i;

    for (i = 0; i < phoneNumber.length; i++) {
        switch ([phoneNumber characterAtIndex:i]) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '#':
        case '*':
        case '+':
        case ',':
            [number appendString:[phoneNumber substringWithRange:NSMakeRange(i,1)]];
            break;
        }
    }
    return number;
}

- (NSString *) contactNumber:(NSString *)phoneNumber
{
    NSString *number = [self stripSeparators:phoneNumber];
    if(![number hasPrefix:@"+"]
     &&(self.dial_location_country_code != nil)
     &&(self.dial_location_country_code.length != 0)
     &&(self.dial_location_national_prefix != nil)
     &&(self.dial_location_international_prefix != nil)) {
        if((self.dial_location_international_prefix.length != 0) && [number hasPrefix:self.dial_location_international_prefix]) {
            return [NSString stringWithFormat:@"+%@", [number substringFromIndex:self.dial_location_international_prefix.length]];
        }
        else if([number hasPrefix:self.dial_location_national_prefix]) {
            // in italy they always dial the full area code, independent of current lcoation.
            if([self.dial_location_country_code isEqualToString:@"39"]) {
                return [NSString stringWithFormat:@"+%@%@", self.dial_location_country_code, number];
            }
            else {
                return [NSString stringWithFormat:@"+%@%@", self.dial_location_country_code, [number substringFromIndex:self.dial_location_national_prefix.length]];
            }
        }
    }
    return(number);
}

- (bool) callIsInternal:(NSString *)number
{
    if((self.dial_location_country_code != nil)
     &&(self.dial_location_country_code.length != 0)
     &&(self.dial_location_national_prefix != nil)
     &&(self.dial_location_international_prefix != nil)
     &&(self.dial_location_subscriber_numbers != nil)
     &&(self.dial_location_subscriber_numbers.length != 0)) {
        NSArray *msns = [self.dial_location_subscriber_numbers componentsSeparatedByString:@"\\,"];
        int i;
        for(i = 0; i < msns.count; i++) {
            NSString *msn = (NSString *)[msns objectAtIndex:i];
            if((self.dial_location_international_prefix.length != 0) && [msn hasPrefix:self.dial_location_international_prefix]) {
                if([number hasPrefix:[NSString stringWithFormat:@"+%@", [msn substringFromIndex:self.dial_location_international_prefix.length]]]) {
                    return true;
                }
            }
            else if((self.dial_location_national_prefix.length != 0) && [msn hasPrefix:self.dial_location_national_prefix]) {
                if([number hasPrefix:[NSString stringWithFormat:@"+%@%@", self.dial_location_country_code, msn]]
                 ||[number hasPrefix:[NSString stringWithFormat:@"+%@%@", self.dial_location_country_code, [msn substringFromIndex:self.dial_location_national_prefix.length]]]) {
                    return true;
                }
            }
            else if(![msn hasPrefix:@"+"] && ((self.dial_location_country_code.length != 0) || (self.dial_location_area_code.length != 0))) {
                if([number hasPrefix:[NSString stringWithFormat:@"+%@%@%@", self.dial_location_country_code, ((self.dial_location_area_code != nil) ? self.dial_location_area_code : @""), msn]]
                 ||[number hasPrefix:[NSString stringWithFormat:@"+%@%@%@%@", self.dial_location_country_code, self.dial_location_national_prefix, ((self.dial_location_area_code != nil) ? self.dial_location_area_code : @""), msn]]) {
                    return true;
                }
            }
            else {
                if([number hasPrefix:msn]) {
                    return true;
                }
            }
        }
    }
    return false;
}

- (bool) isEmergencyNumber:(NSString *)number
{
    return ([number isEqualToString:@"112"] ||
        [number isEqualToString:@"911"] ||
        [number isEqualToString:@"999"]);
}

- (void) dialContact_:(NSString *)phoneNumber
{
    if (self.internetReachability == nil) {
        self.internetReachability = [Reachability reachabilityForInternetConnection];
        [self.internetReachability startNotifier];
    }
    NetworkStatus netStatus = [self.internetReachability currentReachabilityStatus];

    if([self isEmergencyNumber:phoneNumber]
/*     ||((phoneNumber.length == 3)
      &&(PhoneNumberUtils.getFormatTypeForLocale(Locale.getDefault()) == PhoneNumberUtils.FORMAT_NANP)
      &&(phoneNumber.charAt(1) == '1')
      &&(phoneNumber.charAt(2) == '1'))*/) {
        [self dialDirectGSM:phoneNumber];
        return;
    }
    NSString *number = [self contactNumber:phoneNumber];

    if(((self.gsm_call_method == FORMS_GSM_CALL_METHOD_DIRECT) || !self.registrationStateUp)
     &&((self.gsm_use == FORMS_GSM_USE_ANY_CALLS)
      ||((self.gsm_use == FORMS_GSM_USE_EXTERNAL_CALLS) && !((phoneNumber.length <= self.dial_location_internal_length) || [self callIsInternal:number]))
      ||((self.gsm_use == FORMS_GSM_USE_OUTSIDE_WLAN)
       &&(netStatus != ReachableViaWiFi))
      ||((self.gsm_use != FORMS_GSM_USE_NONE) && !self.registrationStateUp))) {
        [self dialDirectGSM:phoneNumber];
        return;
    }

    if([number hasPrefix:@"+"]) {
        self.transferToNormalizedNumber = false;
        dialNormalizedNumber([number cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    else {
        self.forms.event_dial_number(number, nil, FORMS_DIAL_NUMBER_VIDEO_MODE_DEFAULT);
    }
}

- (void) transferToContact_:(NSString *)phoneNumber
{
    NSString *number = [self contactNumber:phoneNumber];
    if([number hasPrefix:@"+"]) {
        self.transferToNormalizedNumber = true;
        dialNormalizedNumber([number cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    else {
        self.forms.event_transfer_to_number(number);
    }
}

- (void) dialNumber_:(NSString *)number
{
    if(self.transferToNormalizedNumber) {
        self.forms.event_transfer_to_number(number);
    }
    else {
        self.forms.event_dial_number(number, nil, FORMS_DIAL_NUMBER_VIDEO_MODE_DEFAULT);
    }
}

- (void) onCallPresentationStart_:(id)context
{
    if(self.hookstateOffhook) {
        self.hookstateOffhook = false;
        self.speakerphoneEnabled = false;
        self.microphoneMuted = false;
        self.trc(@"onCallPresentationStart: ONHOOK");
//        self.forms.event_keypress(FORMS_KEY_ONHOOK);
        self.dtmfActive = false;
    }
    self.speakerphoneSwitched = false;
    self.headsetSwitched = false;
    if(!self.callPresentation) {
        self.callPresentation = true;
//        self.registerRemoteControl();
        self.dtmfActive = false;
//        setInfrontofKeyguard();
//        update_phone_screen();
        self.stopProximitySensor();
        [self checkNotifyCall];
        [self checkNotifyMessage];
        [self startReachabilityTask];
    }
}

- (void) onCallPresentationEnd_:(id)context
{
    self.lockedPresentation = false;
    if(self.callPresentation) {
        self.callPresentation = false;
        if(self.hookstateOffhook) {
            self.headsetSwitched = false;
            self.speakerphoneSwitched = false;
            self.hookstateOffhook = false;
            self.speakerphoneEnabled = false;
            self.microphoneMuted = false;
            self.trc(@"onCallPresentationEnd: ONHOOK");
//            self.forms.event_keypress(FORMS_KEY_ONHOOK);
            self.dtmfActive = false;
        }
        if(!(self.calls_to_display() || self.messages_to_display())) {
            self.restrictedUse = false;
//            mHandler.removeCallbacks(check_restricted_use_runnable);
//            setBehindKeyguard();
        }
//        update_phone_screen();
        self.updateProximitySensor();
    }
}

- (void) onCommunicationStart_:(id)context
{
}

- (void) onCommunicationEnd_:(id)context
{
}

- (void) onHandsetStart_:(id)context
{
    self.callPresentation = false;
    self.lockedPresentation = false;
    if(!self.hookstateOffhook) {
        self.hookstateOffhook = true;
//        presentationWakeLock.acquire(10000);
    }
/*
    if(!self.microphoneDisabled && !self.microphoneMuted) {
        self.forms.event_keypress(FORMS_KEY_MIC);
    }
    self.microphoneDisabled = true;
*/
//    setInfrontofKeyguard();
//    update_phone_screen();
    self.updateProximitySensor();
}

- (void) onHandsetEnd_:(id)context
{
/*
    if(self.hookstateOffhook && self.microphoneDisabled && !self.microphoneMuted) {
        self.forms.event_keypress(FORMS_KEY_MIC);
    }
    self.microphoneDisabled = true;
*/
    if(self.hookstateOffhook && !self.headsetSwitched && !self.speakerphoneSwitched) {
        self.hookstateOffhook = false;
        self.speakerphoneEnabled = false;
        self.microphoneMuted = false;
        self.trc(@"onHandsetEnd: ONHOOK");
//        self.forms.event_keypress(FORMS_KEY_ONHOOK);
        self.dtmfActive = false;
    }
    if(!(self.calls_to_display() || self.messages_to_display())) {
        self.restrictedUse = false;
//        mHandler.removeCallbacks(check_restricted_use_runnable);
//        setBehindKeyguard();
    }
//    update_phone_screen();
    self.updateProximitySensor();
}

- (void) onSpeakerphoneStart_:(id)context
{
    self.callPresentation = false;
    self.lockedPresentation = false;
    self.headsetSwitched = false;
    if(!self.speakerphoneSwitched) {
        self.hookstateOffhook = true;
        self.speakerphoneSwitched = true;
//        presentationWakeLock.acquire(10000);
//        setInfrontofKeyguard();
//        update_phone_screen();
        self.updateProximitySensor();
    }
}

- (void) onSpeakerphoneEnd_:(id)context
{
    if(self.speakerphoneSwitched) {
        if(self.hookstateOffhook) {
            self.hookstateOffhook = false;
            self.speakerphoneEnabled = false;
            self.microphoneMuted = false;
            self.speakerphoneSwitched = false;
            self.trc(@"onSpeakerphoneEnd: ONHOOK");
//            self.forms.event_keypress(FORMS_KEY_ONHOOK);
            self.dtmfActive = false;
        }
    }
    if(!(self.calls_to_display() || self.messages_to_display())) {
        self.restrictedUse = false;
//        mHandler.removeCallbacks(check_restricted_use_runnable);
//        setBehindKeyguard();
    }
//    update_phone_screen();
    self.updateProximitySensor();
}

- (void) onHeadsetStart_:(id)context
{
    self.callPresentation = false;
    self.lockedPresentation = false;
    self.speakerphoneSwitched = false;
    if(!self.headsetSwitched) {
        self.hookstateOffhook = true;
        self.headsetSwitched = true;
//        presentationWakeLock.acquire(10000);
//        setInfrontofKeyguard();
//        update_phone_screen();
        self.updateProximitySensor();
    }
}

- (void) onHeadsetEnd_:(id)context
{
    if(self.headsetSwitched) {
        if(self.hookstateOffhook) {
            self.hookstateOffhook = false;
            self.speakerphoneEnabled = false;
            self.microphoneMuted = false;
            self.headsetSwitched = false;
            self.trc(@"onHeadsetEnd: ONHOOK");
//            self.forms.event_keypress(FORMS_KEY_ONHOOK);
            self.dtmfActive = false;
            if(!(self.calls_to_display() || self.messages_to_display())) {
                self.restrictedUse = false;
//                mHandler.removeCallbacks(check_restricted_use_runnable);
//                setBehindKeyguard();
            }
//            update_phone_screen();
            self.updateProximitySensor();
        }
    }
}

- (void) onVideoStart_:(id)context
{
    self.videoUp = true;
    [[UIApplication sharedApplication] setIdleTimerDisabled:true];
}

- (void) onVideoEnd_:(id)context
{
    self.videoUp = false;
    [[UIApplication sharedApplication] setIdleTimerDisabled:false];
}

#define START_HTTP_PORT 10008
#define END_HTTP_PORT 32767

static int http_redirect_listen_fd = -1;
static int http_redirect_accept_fd = -1;
static char http_redirect_error[256];

static const char *start_serve_http_redirect(word *port)
{
    http_redirect_listen_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(http_redirect_listen_fd < 0) {
        sprintf(http_redirect_error, "Unable to create system socket: %s", strerror(errno));
        http_redirect_listen_fd = -1;
        return http_redirect_error;
    }

    unsigned int nonBlocking;
    long flags;
    nonBlocking = 1;
    ioctl(http_redirect_listen_fd, FIONBIO, &nonBlocking);
    // listening sockets must be asynchronous, since otherwise indicated accept(2)s may disappear
    // between poll and accept and then our accept would block
    flags = fcntl(http_redirect_listen_fd, F_GETFL);
    if(flags == -1) {
        sprintf(http_redirect_error, "Cannot fcntl(F_GETFL): %s", strerror(errno));
        flags = 0;
    }
    // do not block if there is no incoming connection
    flags |= O_NONBLOCK;
    fcntl(http_redirect_listen_fd, F_SETFL, flags);
    if(fcntl(http_redirect_listen_fd, F_SETFL, flags) == -1) {
        sprintf(http_redirect_error, "Unable to set async mode: %s", strerror(errno));
        close(http_redirect_listen_fd);
        http_redirect_listen_fd = -1;
        return http_redirect_error;
    }
    // block SIGPIPE
    {
        sigset_t set;
        sigemptyset(&set);
        sigaddset(&set, SIGPIPE);
        if(sigprocmask(SIG_BLOCK, &set, 0) < 0) {
            sprintf(http_redirect_error, "Failed to block SIGPIPE in open(): %s", strerror(errno));
        }
    }
    // set TCP NODELAY for tcp sockets
    int off = 1;
    if(setsockopt(http_redirect_listen_fd, IPPROTO_TCP, TCP_NODELAY, &off, (socklen_t)(sizeof(off))) < 0) {
        sprintf(http_redirect_error, "Cannot set TCP_NODELAY: %s", strerror(errno));
        // ignored error
    }
    int b = 1;
    if(setsockopt(http_redirect_listen_fd, SOL_SOCKET, SO_NOSIGPIPE, &b, (socklen_t)(sizeof(b))) < 0) {
        sprintf(http_redirect_error, "Cannot set SO_NOSIGPIPE: %s", strerror(errno));
    }

    int err;
    struct sockaddr sock_addr;
    sock_addr.sa_family = AF_INET;
    ((byte *)(&(sock_addr.sa_data[2])))[0] = 0;
    ((byte *)(&(sock_addr.sa_data[2])))[1] = 0;
    ((byte *)(&(sock_addr.sa_data[2])))[2] = 0;
    ((byte *)(&(sock_addr.sa_data[2])))[3] = 0;
    memset(&(sock_addr.sa_data[6]), 0, sizeof(sock_addr.sa_data) - 6);
    // Bind the listening socket using the
    // information in the sockaddr structure
    (*port)--;
    do {
        (*port)++;
        *((word *)(&(sock_addr.sa_data[0]))) = htons((*port));
        if(bind(http_redirect_listen_fd, &sock_addr, sizeof(sock_addr)) != 0) {
            if(errno != EADDRINUSE) {
                sprintf(http_redirect_error, "Socket bind failed %s", strerror(errno));
                close(http_redirect_listen_fd);
                http_redirect_listen_fd = -1;
                return http_redirect_error;
            }
            err = errno;
        }
        else {
            err = 0;
        }
    }
    while ((err != 0) && (*port != END_HTTP_PORT));

# define SYSLISTENBACKLOG 128
    if(listen(http_redirect_listen_fd, SYSLISTENBACKLOG) < 0) {
        sprintf(http_redirect_error, "Failed listen: %s", strerror(errno));
        close(http_redirect_listen_fd);
        http_redirect_listen_fd = -1;
        return http_redirect_error;
    }
    
    return 0;
}

static const char *serve_http_redirect(const char *http_redirect, int http_redirect_len)
{
    struct pollfd pollfd_table[1];

    pollfd_table[0].fd = http_redirect_listen_fd;
    pollfd_table[0].events = POLLIN | POLLPRI;
    pollfd_table[0].revents = 0;
    poll(pollfd_table, 1, 1000);

    struct sockaddr_in6 addr;
    int addr_len;
    http_redirect_accept_fd = accept(http_redirect_listen_fd, (struct sockaddr * __restrict)&addr, (socklen_t * __restrict)&addr_len);
    if (http_redirect_accept_fd < 0) {
        sprintf(http_redirect_error, "Failed accept: %s", strerror(errno));
        http_redirect_accept_fd = -1;
        close(http_redirect_listen_fd);
        http_redirect_listen_fd = -1;
        return http_redirect_error;
    }

    pollfd_table[0].fd = http_redirect_accept_fd;
    pollfd_table[0].events = POLLIN | POLLPRI;
    pollfd_table[0].revents = 0;
    poll(pollfd_table, 1, 1000);

    if(send(http_redirect_accept_fd, http_redirect, http_redirect_len, 0) < 0) {
        sprintf(http_redirect_error, "Sending %i bytes failed: %s", http_redirect_len, strerror(errno));
        http_redirect_accept_fd = -1;
        close(http_redirect_listen_fd);
        http_redirect_listen_fd = -1;
        return http_redirect_error;
    }

    close(http_redirect_accept_fd);
    http_redirect_accept_fd = -1;
    close(http_redirect_listen_fd);
    http_redirect_listen_fd = -1;

    return 0;
}

- (void) restartViaRedirect
{
    static const char http_redirect_header[] =
        "HTTP/1.0 200 OK\r\n"
        "Refresh: 0;com.innovaphone.clientiosstart:\r\n"
/*
        "HTTP/1.0 301 Moved Permanently\r\n"
        "Location: com.innovaphone.clientiosstart:\r\n"
        "HTTP/1.0 302 Moved Temporarily\r\n"
        "Location: com.innovaphone.clientiosstart:\r\n"
*/
        "Content-Type: text/html\r\n"
        "Content-Length: %d\r\n\r\n%s%s%s";
    static const char http_redirect_body0[] =
        "<html>\r\n"
        "<head>\r\n"
        "<meta charset=\"utf-8\"/>\r\n"
        "<title>" PHONE_IOS_LOG_APP_NAME " reset</title>\r\n"
        "</head>\r\n"
        "<body>\r\n"
        "<h1>" PHONE_IOS_LOG_APP_NAME " reset<br/>";
    static const char http_redirect_body1[] =
        "</h1>\r\n"
        "<p></p>\r\n"
        "</body>\r\n"
        "</html>";
    char buf[512];
    const char *err, *str;
    word port;
    int n;

    port = START_HTTP_PORT;
    err = start_serve_http_redirect(&port);
    if (err) {
        NSLog(@"start_serve_http_redirect failed %s", err);
        return;
    }
    [PhoneIosDelegate openURL:[NSURL URLWithString:
        [NSString stringWithFormat:@"http://localhost:%d", port]]];
    str = /*((self.forms != nil) && self.uiStarted) ? [self.forms.localized_texts[FORMS_TXT_CONFIRM_OPENING] cStringUsingEncoding:NSUTF8StringEncoding] :*/ "Please confirm opening to restart it.";
    n = sprintf(buf, http_redirect_header,
        (int)(sizeof(http_redirect_body0) - 1 + strlen(str) + sizeof(http_redirect_body1) - 1),
        http_redirect_body0, str, http_redirect_body1);
    err = serve_http_redirect(buf, n);
    if (err) {
        NSLog(@"serve_http_redirect failed %s", err);
        return;
    }
}

- (void) onResetNotify_:(id)context
{
    bool restart_via_redirect = false;
    if (!self.isShutdown) {
        if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"com.innovaphone.clientiosrestart:"]]) {
            [PhoneIosDelegate openURL:[NSURL URLWithString:@"com.innovaphone.clientiosrestart:"]];
        }
        else {
            restart_via_redirect = true;
        }
    }
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(shutdown_no_response_runnable:) object:nil];
    do_shutdown();
    [self cancelAllLocalNotifications];
    poll_log();
    close_log();
    if (restart_via_redirect) {
        [self restartViaRedirect];
    }
    exit(0);
}

- (void) audioFocusLoss_:(id)context
{
    if (!self.audio_focus_loss) {
        self.audio_focus_loss = true;
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/AUDIO-FOCUS-LOSS", @"1");
    }
}

- (void) audioFocusGain_:(id)context
{
    if (self.audio_focus_loss) {
        self.audio_focus_loss = false;
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/AUDIO-FOCUS-LOSS", @"0");
    }
}

-(void)orientationChangeNotification:(NSNotification *)notification
{
    self.updateProximitySensor();
    [self configureVideoSessions];
}

- (void) startOrientationMonitoring_
{
    if (!self.orientationMonitoringOn) {
        self.orientationMonitoringOn = true;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChangeNotification:)
            name:UIDeviceOrientationDidChangeNotification object:nil];
    }
}

- (void) stopOrientationMonitoring_
{
    if (self.orientationMonitoringOn) {
        self.orientationMonitoringOn = false;
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
    }
}

- (bool) cameraFacingFront:(NSString *)uniqueID
{
    AVCaptureDevice *webcam = [AVCaptureDevice deviceWithUniqueID:uniqueID];
    return (webcam.position == AVCaptureDevicePositionFront);
}

// Call this on the session queue.
- (void) configureVideoSessions
{
    int cameraIndex;
    ActiveCamera *activeCamera;

    self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/STOP_FEED_CONTAINERS", ((self.proximitySensorOn && self.proximityNearby) || !self.didBecomeActive) ? @"1" : @"0");
    if (self.videoPermissionGranted) {
        for (cameraIndex = 0; cameraIndex < self.activeCameras.count; cameraIndex++) {
            activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:cameraIndex];
            if (activeCamera.startFlag && self.didBecomeActive && (!self.camerasBlanked || ![self cameraFacingFront:activeCamera.uniqueID])) {
                if ([activeCamera updateRotation]) {
                    dispatch_async(self.videoDispatchQueue, ^
                        { [activeCamera stop]; });
                }
                dispatch_async(self.videoDispatchQueue, ^
                    { [activeCamera start]; });
            }
            else {
                dispatch_async(self.videoDispatchQueue, ^
                    { [activeCamera stop]; });
            }
        }
    }
}

- (void) deleteTranscodingSession:(TranscodingSession *)transcodingSession
{
    [transcodingSession stop];
    [self.transcodingSessions removeObject:transcodingSession];
}

- (void) formsVideoTranscode:(int)sessionID timestamp:(int)timestamp duration:(int)duration sequenceDiff:(short)sequenceDiff frame:(byte *)frame length:(int)length width:(int)width height:(int)height stride:(int)stride flags:(int)flags from:(int)from to:(int)to
{
    TranscodingSession *transcodingSession;
    size_t i;

    i = 0;
    while ((i < self.transcodingSessions.count) && (((TranscodingSession *)[self.transcodingSessions objectAtIndex:i]).sessionID != sessionID)) {
        i++;
    }
    if ((frame == NULL) || !self.didBecomeActive) {
        if (i < self.transcodingSessions.count) {
            transcodingSession = (TranscodingSession *)[self.transcodingSessions objectAtIndex:i];
            [self deleteTranscodingSession:transcodingSession];
        }
        if (frame != NULL) {
            free(frame);
        }
        else {
            self.ios_event.enqueue(FORMS_EVENT_VIDEO_TRANSCODE, sessionID, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0);
        }
    }
    else {
        if (i < self.transcodingSessions.count) {
            transcodingSession = (TranscodingSession *)[self.transcodingSessions objectAtIndex:i];
        }
        else {
            transcodingSession = [[TranscodingSession alloc] initWithID:sessionID ios_event:self.ios_event videoDispatchQueue:self.videoDispatchQueue];
            [self.transcodingSessions addObject:transcodingSession];

            [transcodingSession start];
        }

        [transcodingSession transcode:timestamp duration:duration sequenceDiff:sequenceDiff frame:frame length:length width:width height:height stride:stride flags:flags from:from to:to];
    }
}

- (void) safariViewControllerDidFinish:(SFSafariViewController *)controller
{
    [((WebViewController *)(self.window.rootViewController)) dismissViewControllerAnimated:true completion:nil];
    self.fixCertViewController = nil;
    if (self.fix_cert_running) {
        self.fix_cert_running = false;
        if (self.got_certificate) {
            self.got_certificate = false;
            [self on_webserver_started];
        }
    }
}

- (int) formsCxCallErrorMap:(NSError *)error
{
    if ([error.domain isEqualToString:CXErrorDomainIncomingCall]) {
        switch (error.code) {
        case CXErrorCodeIncomingCallErrorUnentitled: return FORMS_CX_CALL_ERROR_UNENTITLED;
        case CXErrorCodeIncomingCallErrorCallUUIDAlreadyExists: return FORMS_CX_CALL_ERROR_DUPLICATE_UUID;
        case CXErrorCodeIncomingCallErrorFilteredByDoNotDisturb: return FORMS_CX_CALL_ERROR_INCOMING_DO_NOT_DISTURB;
        case CXErrorCodeIncomingCallErrorFilteredByBlockList: return FORMS_CX_CALL_ERROR_INCOMING_BLOCKED;
        default: break;
        }
    }
    else if ([error.domain isEqualToString:CXErrorDomainRequestTransaction]) {
        switch (error.code) {
        case CXErrorCodeRequestTransactionErrorUnentitled: return FORMS_CX_CALL_ERROR_UNENTITLED;
        case CXErrorCodeRequestTransactionErrorCallUUIDAlreadyExists: return FORMS_CX_CALL_ERROR_DUPLICATE_UUID;
        case CXErrorCodeRequestTransactionErrorUnknownCallProvider: return FORMS_CX_CALL_ERROR_NO_PROVIDER;
        case CXErrorCodeRequestTransactionErrorUnknownCallUUID: return FORMS_CX_CALL_ERROR_UNKNOWN_UUID;
        case CXErrorCodeRequestTransactionErrorInvalidAction: return FORMS_CX_CALL_ERROR_REQUEST_INVALID;
        case CXErrorCodeRequestTransactionErrorMaximumCallGroupsReached: return FORMS_CX_CALL_ERROR_TOO_MANY_GROUPS;
        default: break;
        }
    }
    return FORMS_CX_CALL_ERROR_UNSPECIFIED;
}

static int fontSizeTable[][FORMS_CONTENT_SIZE_CATEGORY_VALUES] =
{
    /*XS S  M  L XL XXL XXXL*/
    {13,14,15,17,19,21,24},
    {12,13,14,15,17,19,21},
    {13,14,15,17,19,21,24},
    {12,12,12,13,14,16,18},
    {11,11,11,12,13,14,16},
    {11,11,11,11,12,13,14},
};

- (UIFont *) fontForCategory_:(int)fontCategory
{
#ifdef SUBSTITUTE_FONT_NAME
    switch(fontCategory) {
    case FONT_CATEGORY_HEADLINE:
        return [UIFont fontWithName:@SUBSTITUTE_FONT_NAME_BOLD size:fontSizeTable[fontCategory][self.content_size_category]];
    default:
        return [UIFont fontWithName:@SUBSTITUTE_FONT_NAME size:fontSizeTable[fontCategory][self.content_size_category]];
    }
#else
    if ([UIFont respondsToSelector:@selector(preferredFontForTextStyle:)]) {
        switch(fontCategory) {
        case FONT_CATEGORY_HEADLINE:
            return [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
        case FONT_CATEGORY_SUBHEADLINE:
            return [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
        case FONT_CATEGORY_FOOTNOTE:
            return [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
        case FONT_CATEGORY_CAPTION1:
            return [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1];
        case FONT_CATEGORY_CAPTION2:
            return [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2];
        default:
            return [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
        }
    }
    else {
        switch(fontCategory) {
        case FONT_CATEGORY_HEADLINE:
        case FONT_CATEGORY_SUBHEADLINE:
            return [UIFont boldSystemFontOfSize:fontSizeTable[fontCategory][self.content_size_category]];
        default:
            return [UIFont systemFontOfSize:fontSizeTable[fontCategory][self.content_size_category]];
        }
    }
#endif
}

- (void) updateContentSizeCategory
{
    if ([UIFont respondsToSelector:@selector(preferredFontForTextStyle:)]) {
        NSString *contentSize = [UIApplication sharedApplication].preferredContentSizeCategory;
        self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_L;

        if ([contentSize isEqualToString:UIContentSizeCategoryExtraSmall]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_XS;
        }
        else if ([contentSize isEqualToString:UIContentSizeCategorySmall]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_S;
        }
        else if ([contentSize isEqualToString:UIContentSizeCategoryMedium]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_M;
        }
        else if ([contentSize isEqualToString:UIContentSizeCategoryLarge]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_L;
        }
        else if ([contentSize isEqualToString:UIContentSizeCategoryExtraLarge]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_XL;
        }
        else if ([contentSize isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_XXL;
        }
        else if ([contentSize isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
            self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_XXXL;
        }
    }
}

- (void) didChangePreferredContentSize:(NSNotification *)notification
{
    [self updateContentSizeCategory];
    [self updateContentSize];
}

- (const char *) tone_title_from_ident:(const char *)ident tones:(const char *)tones title_buffer:(char *)title_buffer
{
    const char *p;
    unsigned i, ident_len;

    if (!ident) {
        return NULL;
    }
    ident_len = strlen(ident);
    p = tones;
    i = 0;
    do {
        p = strstr(p, "<tune tag='");
        if (p) {
            p += 11;
            i = 0;
            while (p[i] != '\'') {
                i++;
            }
        }
    }
    while (p && ((i != ident_len) || (memcmp(p, ident, ident_len) != 0)));
    if (!p) {
        return NULL;
    }
    p = strstr(p, "' title='");
    if (!p) {
        return NULL;
    }
    p += 9;
    i = 0;
    while (p[i] != '\'') {
        i++;
    }
    memcpy(title_buffer, p, i);
    title_buffer[i] = '\0';
    [XmlIo xml_to_str:title_buffer in:title_buffer in_len:i];
    return title_buffer;
}

- (NSString *) tone_path_from_ident:(const char *)ident notification_tone:(bool)notification_tone
{
    char title_buffer[256];
    const char *melody;
    NSString *melodyString = nil;
    NSString *ringtonePath = nil;
    int i;

    if (!ident || (ident[0] == '\0')) {
        return @"silent_ringtone.mp3";
    }
    melody = [self tone_title_from_ident:ident tones:notification_tone ? default_tones : default_tunes title_buffer:title_buffer];
    if (melody != nil) {
        melodyString = [NSString stringWithCString:melody encoding:NSUTF8StringEncoding];
    }

    NSFileManager *fileMan = [NSFileManager defaultManager];
    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *ringtonesPath = [documentDir stringByAppendingPathComponent:notification_tone ? @"NotificationTones" : @"Ringtones"];
    NSArray *files = [fileMan contentsOfDirectoryAtPath:ringtonesPath error:NULL];
    i = 0;
    while ((i < files.count) && (ringtonePath == nil)) {
        NSString *s = (NSString *)[files objectAtIndex:i];
        NSUInteger start = s.length;
        NSUInteger end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        if ((melodyString == nil) || [melodyString isEqualToString:[s substringWithRange:NSMakeRange(start, end - start)]]) {
            ringtonePath = [ringtonesPath stringByAppendingPathComponent:s];
        }
        i++;
    }

    NSArray *tunesPaths = [[NSBundle mainBundle] pathsForResourcesOfType:nil inDirectory:notification_tone ? @"NotificationTones" : @"Ringtones"];
    i = 0;
    while ((i < tunesPaths.count) && (ringtonePath == nil)) {
        NSString *s = (NSString *)[tunesPaths objectAtIndex:i];
        NSUInteger start = s.length;
        NSUInteger end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        if (![[s substringFromIndex:end] isEqualToString:@".mak"]
         && ((melodyString == nil) || [melodyString isEqualToString:[s substringWithRange:NSMakeRange(start, end - start)]])) {
            ringtonePath = (s.length > [NSBundle mainBundle].resourcePath.length) ? [s substringFromIndex:[NSBundle mainBundle].resourcePath.length + 1] : s;
        }
        i++;
    }
    if (ringtonePath == nil) {
        NSString *s = [tunesPaths objectAtIndex:0];
        ringtonePath = (s.length > [NSBundle mainBundle].resourcePath.length) ? [s substringFromIndex:[NSBundle mainBundle].resourcePath.length + 1] : s;
    }
    return ringtonePath;
}

- (void) formsSetFormsProperty:(NSString *)name value:(NSString *)value
{
    if([name isEqualToString:@"FORMS/ORIENTATION-ADAPTATION-OFF"]) {
        self.orientation_adaptation_off = (value.length == 0) ? false : ![value isEqualToString:@"0"];
//        PhoneAndroidActivity.instance().on_orientation_adaptation();
    }
    else if([name isEqualToString:@"FORMS/DISPLAY-ON-IF-CHARGING"]) {
        self.display_on_if_charging = (value.length == 0) ? FORMS_DISPLAY_ON_IF_CHARGING_NO : value.intValue;
        self.on_display_on_if_charging();
    }
    else if([name isEqualToString:@"FORMS/NEW-CONTACT-NAME"]) {
        self.add_contact_name = value;
    }
    else if([name isEqualToString:@"FORMS/NEW-CONTACT-H323"]) {
        self.add_contact_h323 = value;
    }
    else if([name isEqualToString:@"FORMS/NEW-CONTACT-E164"]) {
        self.add_contact_e164 = value;
        [self add_contact:self.add_contact_name h323:self.add_contact_h323 e164:self.add_contact_e164];
    }
    else if([name isEqualToString:@"CPU/LOG-BROWSER"]) {
        phone_ios_log_browser = (value.length == 0) ? false : ![value isEqualToString:@"0"];
        if (self.window.rootViewController != nil) {
            ((WebViewController *)(self.window.rootViewController)).logBrowser(phone_ios_log_browser);
        }
    }
    else if([name isEqualToString:@"CPU/SAVE-LOG"]) {
        [self save_log:(value.length == 0) ? false : ![value isEqualToString:@"0"]];
    }
    else if([name isEqualToString:@"CPU/EMAIL-LOGS"]) {
        [self email_logs];
    }
    else if([name isEqualToString:@"CPU/CLEAR-LOGS"]) {
        [self clearLogs];
    }
    else if([name isEqualToString:@"ANDROID/DIALER-CLAIM"]) {
        self.dialer_claim = (value.length == 0) ? FORMS_DIALER_CLAIM_MANUAL : value.intValue;
    }
    else if([name isEqualToString:@"ANDROID/GSM-USE"]) {
        self.gsm_use = (value.length == 0) ? FORMS_GSM_USE_NONE : value.intValue;
        [self updateNoMediaCall];
    }
    else if([name isEqualToString:@"FORMS/GSM-CALL-METHOD"]) {
        self.gsm_call_method = (value.length == 0) ? FORMS_GSM_CALL_METHOD_DIRECT : value.intValue;
    }
    else if([name isEqualToString:@"FORMS/MOBILITY-DEVICE-ID"]) {
        self.mobility_device_id = value;
    }
    else if([name isEqualToString:@"FORMS/MOBILITY-PHONE-NUMBER"]) {
        self.mobility_phone_number = value;
    }
    else if([name isEqualToString:@"ANDROID/DOCKING-SWITCHES-HOOK"]) {
        self.docking_switches_hook = (value.length == 0) ? false : ![value isEqualToString:@"0"];
    }
    else if([name isEqualToString:@"FORMS/RING-IN-HEADSET"]) {
        phone_ios_ring_in_headset = (value.length == 0) ? false : ![value isEqualToString:@"0"];
    }
    else if([name isEqualToString:@"FORMS/HIDE-FROM-NATIVE-CALL-LIST"]) {
        self.cxHideFromNativeCallList = (value.length == 0) ? false : ![value isEqualToString:@"0"];
        if (self.cxProvider != nil) {
            if ([self.cxProvider.configuration respondsToSelector:@selector(includesCallsInRecents)]) {
                self.cxProvider.configuration.includesCallsInRecents = !self.cxHideFromNativeCallList;
            }
        }
    }
    else if([name isEqualToString:@"CPU/SHUTDOWN"]) {
        bool cpu_shutdown = (value.length == 0) ? false : ![value isEqualToString:@"0"];
        if (cpu_shutdown) {
            self.forms_event_shutdown(true);
        }
    }
    else if([name isEqualToString:@"PHONE/DIAL-LOCATION"]) {
        self.dial_location = value;
        self.dial_location_country_code = @"";
        self.dial_location_area_code = @"";
        self.dial_location_national_prefix = @"";
        self.dial_location_international_prefix = @"";
        self.dial_location_subscriber_numbers = @"";
        self.dial_location_internal_length = 3;
        NSString *arg_name, *arg_value;
        int i, n;
        i = 0;
        while((i < self.dial_location.length) && ([self.dial_location characterAtIndex:i] == ' ')) {
            i++;
        }
        while(i < self.dial_location.length) {
            n = i;
            do {
                i++;
            }
            while((i < self.dial_location.length) && ([self.dial_location characterAtIndex:i] != ' '));
            arg_name = [self.dial_location substringWithRange:NSMakeRange(n, i-n)];
            while((i < self.dial_location.length) && ([self.dial_location characterAtIndex:i] == ' ')) {
                i++;
            }
            if(i < self.dial_location.length) {
                n = i;
                do {
                    i++;
                }
                while((i < self.dial_location.length) && ([self.dial_location characterAtIndex:i] != ' '));
                arg_value = [self.dial_location substringWithRange:NSMakeRange(n, i-n)];
                if([arg_name caseInsensitiveCompare:@"cc"] == NSOrderedSame) {
                    self.dial_location_country_code = arg_value;
                }
                else if([arg_name caseInsensitiveCompare:@"ac"] == NSOrderedSame) {
                    self.dial_location_area_code = arg_value;
                }
                else if([arg_name caseInsensitiveCompare:@"ntp"] == NSOrderedSame) {
                    self.dial_location_national_prefix = arg_value;
                }
                else if([arg_name caseInsensitiveCompare:@"itp"] == NSOrderedSame) {
                    self.dial_location_international_prefix = arg_value;
                }
                else if([arg_name caseInsensitiveCompare:@"pbx"] == NSOrderedSame) {
                    self.dial_location_subscriber_numbers = arg_value;
                }
                else if([arg_name caseInsensitiveCompare:@"len"] == NSOrderedSame) {
                    self.dial_location_internal_length = arg_value.intValue;
                    if (self.dial_location_internal_length < 3) {
                        self.dial_location_internal_length = 3;
                    }
                }
                while((i < self.dial_location.length) && ([self.dial_location characterAtIndex:i] == ' ')) {
                    i++;
                }
            }
        }
    }
    else if([name isEqualToString:@"PHONE/ACTIVE-USER-REGSTATE"]) {
        self.registrationStateValue = 1;
        [self registrationState:self.registrationStateValue];
        if (value.intValue == 1) {
            unsigned i;
            i = 0;
            while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).claimed || ((CxCall *)[self.cxCalls objectAtIndex:i]).released)) {
                i++;
            }
            if (i < self.cxCalls.count) {
                [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(clear_unclaimed_calls_runnable:) object:nil];
                [self performSelector:@selector(clear_unclaimed_calls_runnable:) withObject:nil afterDelay:2];
            }
            for (i = 0; i < self.cxCalls.count; i++) {
                if (!((CxCall *)[self.cxCalls objectAtIndex:i]).claimed && !((CxCall *)[self.cxCalls objectAtIndex:i]).check_incoming_call && !((CxCall *)[self.cxCalls objectAtIndex:i]).released && (((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id != nil)) {
                    ((CxCall *)[self.cxCalls objectAtIndex:i]).check_incoming_call = true;
                    self.ios_event.enqueue(FORMS_EVENT_CX_CHECK_INCOMING_CALL, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id]);
                }
            }
        }
//        update_notif();
    }
    else if([name isEqualToString:@"PHONE/RAS-REGISTRATION-CHECK-CONFIRM"]) {
        NSLog(@"Registration check confirm");
        if (!self.callPresentation) {
            [self stopReachabilityTask];
        }
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(registration_check_confirm_runnable:) object:nil];
        if (self.registration_check_request != self.registration_check_confirm) {
            self.registration_check_confirm++;
        }
        NSLog(@"Registration check confirm %u %u", self.registration_check_request, self.registration_check_confirm);
        if (self.registration_check_request != self.registration_check_confirm) {
            self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/RAS-REGISTRATION-CHECK", @"");
            [self performSelector:@selector(registration_check_confirm_runnable:) withObject:nil afterDelay:2];
        }
    }
    else if([name isEqualToString:@"PHONE/DIAL-GSM"]) {
        int i;
        i = 0;
        while([value characterAtIndex:i] != ';') {
            i++;
        }
        [self dialGSM:[value substringToIndex:i] numberStringPBX:[value substringFromIndex:i+1]];
    }
    else if([name isEqualToString:@"PHONE/DIAL-DIRECT-GSM"]) {
        [self dialDirectGSM:value];
    }
    else if([name isEqualToString:@"FORMS/CERTIFICATE"]) {
        self.cert_string = value;
        if (self.fix_cert_running) {
            self.got_certificate = true;
        }
        else if (self.webserver_started) {
            [self on_webserver_started];
        }
    }
    else if([name isEqualToString:@"FORMS/FIX-CERT"]) {
        if (fresh_install && NSClassFromString(@"SFSafariViewController")) {
            self.fixCertViewController = [[SFSafariViewController alloc] initWithURL:[NSURL URLWithString:value]];
            self.fixCertViewController.delegate = self;
            [((WebViewController *)(self.window.rootViewController)) presentViewController:self.fixCertViewController animated:YES completion:nil];
        }
        else {
            [PhoneIosDelegate openURL:[NSURL URLWithString:value]];
        }
    }
    else if([name isEqualToString:@"FORMS/SHOW-IF-LOCKED"] || [name isEqualToString:@"FORMS/HIDE-IF-LOCKED"]) {
    }
    else if([name isEqualToString:@"FORMS/SHOW-UNLOCK"]) {
    }
    else if([name isEqualToString:@"FORMS/PUSH-PASSWORD"]) {
        NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@PHONE_IOS_APP_GROUP_ID];
        [userDefaults setObject:value forKey:@"pushPassword"];
        [userDefaults synchronize];
    }
    else if([name isEqualToString:@"FORMS/SET-PREVIEW-CODER"]) {
        ActiveCamera *activeCamera;
        unsigned i;
        
        self.previewCoder = (value.length == 0) ? 0 : value.intValue;
        for (i = 0; i < self.activeCameras.count; i++) {
            activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:i];
            [activeCamera changePreviewCoder:self.previewCoder];
        }
    }
    else if([name isEqualToString:@"FORMS/SET-WEBCAM-CODER"]) {
        ActiveCamera *activeCamera;
        unsigned i;
        
        self.webcamCoder = (value.length == 0) ? 0 : value.intValue;
        for (i = 0; i < self.activeCameras.count; i++) {
            activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:i];
            [activeCamera changeWebcamCoder:self.webcamCoder];
        }
    }
    else if([name isEqualToString:@"FORMS/SCREEN-CAPTURE-RUNNING"]) {
        if (((value.length == 0) ? false : ![value isEqualToString:@"0"]) ^ self.screen_capture_running) {
            self.screen_capture_stopped = self.screen_capture_started && self.screen_capture_running;
            self.screen_capture_running = !self.screen_capture_running;
            NSLog(@"ScreenCaptureChange %u %u %u", self.screen_capture_running, self.screen_capture_stopped, self.didBecomeActive);
            if (self.screen_capture_stopped && self.didBecomeActive) {
                self.screen_capture_stopped = false;
                self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/SCREEN-CAPTURE-STOPPED", @"");
            }
        }
    }
    else if([name isEqualToString:@"FORMS/STATUS-BAR-APPEARANCE"]) {
        unsigned i = (value.length == 0) ? 0 : value.intValue;
        self.statusBarHidden = ((i & FORMS_STATUS_BAR_APPEARANCE_HIDDEN) != 0);
        self.statusBarStyle = ((i & FORMS_STATUS_BAR_APPEARANCE_BLACK_TEXT) ? UIStatusBarStyleDefault : UIStatusBarStyleLightContent);
        self.statusBarColor = [PhoneIosDelegate colorRGB:i & 0x00ffffff];
        self.statusBarTextColor = [PhoneIosDelegate colorRGB:(i & FORMS_STATUS_BAR_APPEARANCE_BLACK_TEXT) ? COLOR_TEXT_LIGHT_SCHEME : COLOR_TEXT_DARK_SCHEME];
        if ([[UIApplication sharedApplication] respondsToSelector:@selector(setStatusBarStyle:)]) {
            [[UIApplication sharedApplication] setStatusBarStyle:(i & FORMS_STATUS_BAR_APPEARANCE_BLACK_TEXT) ? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent];
        }
        [[UINavigationBar appearance] setBarStyle:(i & FORMS_STATUS_BAR_APPEARANCE_BLACK_TEXT) ? UIBarStyleDefault : UIBarStyleBlack];
        [[UINavigationBar appearance] setTintColor:[PhoneIosDelegate colorRGB:(i & FORMS_STATUS_BAR_APPEARANCE_BLACK_TEXT) ? COLOR_TEXT_LIGHT_SCHEME : COLOR_TEXT_DARK_SCHEME]];
        if (self.window.rootViewController != nil) {
            ((WebViewController *)(self.window.rootViewController)).setStatusBarAppearance(self.statusBarHidden, self.statusBarStyle, self.statusBarColor, self.statusBarTextColor);
            self.statusBarBlurViewLight.hidden = self.statusBarHidden || (self.statusBarStyle == UIStatusBarStyleLightContent);
            self.statusBarBlurViewDark.hidden = self.statusBarHidden || (self.statusBarStyle != UIStatusBarStyleLightContent);
        }
    }
}

- (void) reload_webview_runnable:(id)sender
{
    [((WebViewController *)(self.window.rootViewController)).webView stopLoading];
    [[NSURLCache sharedURLCache] removeAllCachedResponses];
    [self on_webserver_started];
}

- (void) registration_check_confirm_runnable:(id)sender
{
    self.registration_check_confirm = self.registration_check_request;
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
    if (self.webserver_started && self.uiStarted) {
        NSLog(@"on_webserver_started stop loading, load about:blank");
        [((WebViewController *)(self.window.rootViewController)).webView stopLoading];
        [((WebViewController *)(self.window.rootViewController)).webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]];
        [self performSelector:@selector(reload_webview_runnable:) withObject:nil afterDelay:0.5];
    }
#endif
}

- (void) forms_event_registration_check
{
    if (self.registration_check_request == self.registration_check_confirm) {
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/RAS-REGISTRATION-CHECK", @"");
        [self performSelector:@selector(registration_check_confirm_runnable:) withObject:nil afterDelay:2];
    }
    self.registration_check_request++;
    NSLog(@"Registration check request %u %u", self.registration_check_request, self.registration_check_confirm);
}

- (void) formsSetLanguage
{
}

- (void) formsWebserverStarted:(NSString *)url_to_load user_agent:(NSString *)user_agent webserver_url:(NSString *)webserver_url start_fix_cert_url:(NSString *)start_fix_cert_url
{
    self.start_fix_cert_url = start_fix_cert_url;
    self.webserver_url = webserver_url;
    self.user_agent = user_agent;
    self.registrationStateValue = 1;
    [self registrationState:self.registrationStateValue];
    self.webserver_started = true;
    if ((self.apnToken != nil) || (self.apnTokenVoIP != nil)) {
        self.forms.event_push_token(self.apnToken, self.apnTokenVoIP);
    }
    self.url_to_load = url_to_load;
    [self on_webserver_started];
    unsigned i;
    i = 0;
    while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).claimed || ((CxCall *)[self.cxCalls objectAtIndex:i]).released)) {
        i++;
    }
    if (i < self.cxCalls.count) {
        self.ios_event.enqueue(FORMS_EVENT_WAKE_PHONE);
    }
//    [self email_cert];
}

- (NSString *) fileFromURL:(NSURL *)url error:(NSError * _Nullable *)error
{
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
/*
    NSString *userAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"; // user agent for iPhone browser
    [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; //setting user agent
*/
    request.timeoutInterval = 1.0;
    NSURLResponse *response = nil;
    NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:error];
    if (*error != nil) {
        return nil;
    }
    NSMutableString *fileName = [[NSMutableString alloc] init];
    if (response.suggestedFilename) {
        [fileName appendString:response.suggestedFilename];
        if (![response.suggestedFilename containsString:@"."]) {
            [fileName appendString:@".png"];
        }
    }
    else {
        [fileName appendString:request.URL.lastPathComponent];
        if (![request.URL.lastPathComponent containsString:@"."]) {
            [fileName appendString:@".png"];
        }
    }
    NSString *filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *downloadsDir = [filesDir stringByAppendingPathComponent:@"Downloads"];
    NSString *path = [downloadsDir stringByAppendingPathComponent:fileName];
    if (![data writeToFile:path options:NSDataWritingAtomic error:error]) {
        return nil;
    }
    return path;
}

- (void) formsAddLocalNotification:(NSString *)ref type:(int)type title:(NSString *)title text:(NSString *)text smallIcon:(NSString *)smallIcon largeIcon:(NSString *)largeIcon
    soundUrl:(NSString *)soundUrl buttonTitle:(NSString *)buttonTitle acceptTitle:(NSString *)acceptTitle rejectTitle:(NSString *)rejectTitle link:(NSString *)link
{
    NSString *category = nil;
    if ((acceptTitle != nil) && (acceptTitle.length != 0)) {
        category = ((type == FORMS_LOCAL_NOTIFICATION_TYPE_DEFAULT) || (type == FORMS_LOCAL_NOTIFICATION_TYPE_PHONE_CALL)) ? @CALL_CATEGORY : @DECISION_CATEGORY;
    }
    else if ((buttonTitle != nil) && (buttonTitle.length != 0)) {
        NSString *last = @"";
        if (ref != nil) {
            NSRange r = [ref rangeOfString:@"\n" options:NSBackwardsSearch];
            if (r.location != NSNotFound) {
                last = [ref substringFromIndex:r.location + 1];
            }
        }
        category = ((type == FORMS_LOCAL_NOTIFICATION_TYPE_CHAT_MESSAGE) || (type == FORMS_LOCAL_NOTIFICATION_TYPE_REPLY)) ? @REPLY_CATEGORY :
            (((ref == nil) || (ref.length == 0) || [ref hasPrefix:@"sip:"] || [ref hasPrefix:@"num:"] || [last hasPrefix:@" sip:"] || [last hasPrefix:@" num:"]) ? @MISSED_CATEGORY : @CONN_CATEGORY);
    }
    NSString *body = ((text != nil) && (text.length != 0)) ? text : @" ";

    if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

        UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
        content.categoryIdentifier = category;
        if ((title != nil) && (title.length != 0)) {
            content.title = title;
        }
        content.body = body;
        if ((soundUrl != nil) && (soundUrl.length != 0)) {
            content.sound = [soundUrl isEqualToString:@"*"] ? [UNNotificationSound defaultSound] : [UNNotificationSound soundNamed:soundUrl];
        }
        if ((largeIcon != nil) && (largeIcon.length != 0)) {
            NSError *error = nil;
            NSString *path = [self fileFromURL:[NSURL URLWithString:largeIcon] error:&error];
            if (error != nil) {
                NSLog(@"formsAddLocalNotification failed to download image error=%@", error);
            }
            if (path != nil) {
                NSURL *fileURL = [NSURL fileURLWithPath:path isDirectory:false];
                UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:fileURL options:nil error:&error];
                if (error != nil) {
                    NSLog(@"formsAddLocalNotification failed to attach image error=%@", error);
                }
                if (attachment != nil) {
                    content.attachments = @[attachment];
                }
            }
        }
        if ((link != nil) && (link.length != 0)) {
            content.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:ref, @"callRef", link, @"msgLink", nil];
        }
        else {
            content.userInfo = [NSDictionary dictionaryWithObject:ref forKey:@"callRef"];
        }
        UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.001 repeats:false];
        UNNotificationRequest *localNotif = [UNNotificationRequest requestWithIdentifier:ref content:content trigger:trigger];
        [self.pendingLocalNotifications addObject:localNotif];
        [self.presentedLocalNotifications addObject:localNotif];

        [center addNotificationRequest:localNotif withCompletionHandler:^(NSError * _Nullable error) {
            if (error) {
                NSLog(@"formsAddLocalNotification failed to add notification request error=%@", error);
            }
        }];
    }
    else {
        UILocalNotification *localNotif = [[UILocalNotification alloc] init];
        localNotif.fireDate = [NSDate date];
        // Set the category to the category that contains our action
        if ([localNotif respondsToSelector:@selector(category)]) {
            localNotif.category = category;
        }
        if ((title != nil) && (title.length != 0)) {
            localNotif.alertTitle = title;
        }
        localNotif.alertBody = [body stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
        if ((soundUrl != nil) && (soundUrl.length != 0)) {
            localNotif.soundName = [soundUrl isEqualToString:@"*"] ? UILocalNotificationDefaultSoundName : soundUrl;
        }
        if ((link != nil) && (link.length != 0)) {
            localNotif.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:ref, @"callRef", link, @"msgLink", nil];
        }
        else {
            localNotif.userInfo = [NSDictionary dictionaryWithObject:ref forKey:@"callRef"];
        }
        [self.pendingLocalNotifications addObject:localNotif];
        [self.presentedLocalNotifications addObject:localNotif];

        [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];
    }
}

- (void) formsRemoveLocalNotification:(NSString *)ref
{
    [self removeLocalNotification:ref];
    if (self.pendingLocalNotifications.count == 0) {
        [self cancelAllLocalNotifications];
        [self badgeUpdate];
    }
}

- (void) formsSetFocus:(NSString *)app title:(NSString *)title
{
}

- (void) formsSetResize:(NSString *)app width:(int)width height:(int)height
{
}

- (void) formsAppWindowOpened:(NSString *)app title:(NSString *)title
{
}

- (void) formsSetAutostart:(bool)autostart
{
}

- (void) formsSetOffline:(int)offline
{
}

- (void) formsSetDocking:(int)docking
{
}

- (void) formsSetHotkey:(int)type hotkey:(NSString *)hotkey
{
}

- (void) formsSetDesktopNotifications:(bool)show
{
}

- (void) formsStartExternalApp:(NSString *)app param:(NSString *)param
{
}

- (void) formsSetBadgeCount:(NSString *)ref count:(int)count text:(NSString *)text
{
    NSString *r;
    unsigned i;
    i = 0;
    if ((count != 0) || ((text != nil) && (text.length != 0))) {
        while ((i < self.pendingBadgeCounts.count)
         && (((r = ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).ref) == nil) || ![r hasPrefix:ref])) {
            i++;
        }
        if (i < self.pendingBadgeCounts.count) {
            ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).ref = ref;
            ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).count = count;
            ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).text = text;
        }
        else {
            BadgeCount *b = [BadgeCount badgeCountWithRef:ref count:count text:text];
            [self.pendingBadgeCounts addObject:b];
        }
        i++;
    }
    while (i < self.pendingBadgeCounts.count) {
        if (((r = ((BadgeCount *)[self.pendingBadgeCounts objectAtIndex:i]).ref) != nil) && [r hasPrefix:ref]) {
            [self.pendingBadgeCounts removeObjectAtIndex:i];
        }
        else {
            i++;
        }
    }
    [self badgeUpdate];
}

- (void) ReportWebcams:(NSString *)context
{
    NSArray * webcams = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    unsigned i;

    for(i = 0 ; i < webcams.count; i++) {
        AVCaptureDevice * webcam = [webcams objectAtIndex:i];
        if (webcam.position == AVCaptureDevicePositionFront) {
            self.forms_event_webcam_added(webcam.uniqueID, true, 0, context, webcam.localizedName);
        }
    }
    for(i = 0 ; i < webcams.count; i++) {
        AVCaptureDevice * webcam = [webcams objectAtIndex:i];
        if (webcam.position != AVCaptureDevicePositionFront) {
            self.forms_event_webcam_added(webcam.uniqueID, false, 0, context, webcam.localizedName);
        }
    }
}

- (void) formsDetectWebcams:(NSString *)context
{
    if ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) {
        [self ReportWebcams:context];
    }
    self.forms_event_webcam_query_result(context);
}

- (void) formsStartWebcam:(NSString *)id context:(NSString *)context width:(int)width height:(int)height hdVideo:(bool)hdVideo
{
    ActiveCamera *activeCamera;
    unsigned i;

    i = 0;
    while ((i < self.activeCameras.count) && ![((ActiveCamera *)[self.activeCameras objectAtIndex:i]).uniqueID isEqualToString:id]) {
        i++;
    }
    if (i < self.activeCameras.count) {
        activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:i];
    }
    else {
        activeCamera = [[ActiveCamera alloc] initWithUniqueID:id ios_event:self.ios_event videoDispatchQueue:self.videoDispatchQueue];
        if ([self cameraFacingFront:activeCamera.uniqueID]) {
            [activeCamera blankOutVideo:self.proximitySensorOn && self.proximityNearby];
        }
        [activeCamera changePreviewCoder:self.previewCoder];
        [activeCamera changeWebcamCoder:self.webcamCoder];
        [self.activeCameras addObject:activeCamera];
    }
    activeCamera.context = context;
    activeCamera.preferredWidth = width;
    activeCamera.preferredHeight = height;
    activeCamera.hdVideo = hdVideo;
    activeCamera.startFlag = true;
    self.webcam_active = true;
    /*
     Setup the capture session.
     In general, it is not safe to mutate an AVCaptureSession or any of its
     inputs, outputs, or connections from multiple threads at the same time.
     
     Don't perform these tasks on the main queue because
     AVCaptureSession.startRunning() is a blocking call, which can
     take a long time. We dispatch session setup to the sessionQueue, so
     that the main queue isn't blocked, which keeps the UI responsive.
    */
    [self configureVideoSessions];
}

- (void) formsStopWebcam:(NSString *)id
{
    ActiveCamera *activeCamera;
    unsigned i;
    
    i = 0;
    while ((i < self.activeCameras.count) && ![((ActiveCamera *)[self.activeCameras objectAtIndex:i]).uniqueID isEqualToString:id]) {
        i++;
    }
    if (i < self.activeCameras.count) {
        activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:i];
        activeCamera.startFlag = false;
        i = 0;
        while ((i < self.activeCameras.count) && !((ActiveCamera *)[self.activeCameras objectAtIndex:i]).startFlag) {
            i++;
        }
        if (i == self.activeCameras.count) {
            self.webcam_active = false;
        }
        [self configureVideoSessions];
    }
}

- (void) formsWebcamFullIntraRequest:(NSString *)id
{
    ActiveCamera *activeCamera;
    unsigned i;
    
    i = 0;
    while ((i < self.activeCameras.count) && ![((ActiveCamera *)[self.activeCameras objectAtIndex:i]).uniqueID isEqualToString:id]) {
        i++;
    }
    if (i < self.activeCameras.count) {
        activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:i];
        [activeCamera requestFullIntra];
    }
}

- (void) formsKeyboardEvent:(int)code keyDown:(bool)keyDown
{
}

- (void) formsMouseEvent:(int)x y:(int)y right:(bool)right keyDown:(bool)keyDown
{
}

- (void) formsMouseWheelEvent:(bool)direction
{
}

- (void) forms_event_contacts_search_result:(int)resultEvent ref:(NSString *)ref
{
    NSMutableString *str = [NSMutableString stringWithCapacity:1024];
    JsonIo *json_io = [JsonIo jsonIo];
    int base = json_io.add_object(JSON_ID_ROOT, nil);
    json_io.add_string(base, @"mt", (resultEvent == FORMS_EVENT_CONTACTS_LOOKUP) ? @"LookupResult" : @"SearchResult");
    json_io.encode(str);
    self.ios_event.enqueue(resultEvent, ref, (NSString *)str);
}

#define CONTACTS_SEARCH_CHUNK_SIZE 100
#define CONTACTS_SEARCH_STATE_IDLE 0
#define CONTACTS_SEARCH_STATE_PLUS 1
#define CONTACTS_SEARCH_STATE_INTL 2
#define CONTACTS_SEARCH_STATE_NTL  3
#define CONTACTS_SEARCH_STATE_SUBS 4

- (void) formsContactsSearch:(NSString *)ref pattern:(NSString *)pattern
{
    ContactsSearchData *s = [[ContactsSearchData alloc] initWithRef:ref resultEvent:FORMS_EVENT_CONTACTS_SEARCH state:CONTACTS_SEARCH_STATE_IDLE];
    s.pattern = pattern;
    [self.contactsSearchQueue addObject:s];
    if (self.contactsSearchResults < CONTACTS_SEARCH_CHUNK_SIZE) {
        [self formsContactsSearchMore];
    }
}

- (void) formsContactsLookup:(NSString *)ref number:(NSString *)number prefixIntl:(NSString *)prefixIntl prefixNtl:(NSString *)prefixNtl prefixSubs:(NSString *)prefixSubs areaCode:(NSString *)areaCode countryCode:(NSString *)countryCode
{
    ContactsSearchData *s = [[ContactsSearchData alloc] initWithRef:ref resultEvent:FORMS_EVENT_CONTACTS_LOOKUP state:CONTACTS_SEARCH_STATE_PLUS];
    s.number = number;
    s.prefixIntl = prefixIntl;
    s.prefixNtl = prefixNtl;
    s.prefixSubs = prefixSubs;
    s.areaCode = areaCode;
    s.countryCode = countryCode;
    [self.contactsSearchQueue addObject:s];
    if (self.contactsSearchResults < CONTACTS_SEARCH_CHUNK_SIZE) {
        [self formsContactsSearchMore];
    }
}

- (void) formsContactsSearchMore
{
    int i;

    if (self.contactsSearchResults == CONTACTS_SEARCH_CHUNK_SIZE) {
        self.contactsSearchResults = 0;
    }
    while ((self.contactsSearchResults < CONTACTS_SEARCH_CHUNK_SIZE) && ((self.contactsSearchResult != nil) || (self.contactsSearchQueue.count != 0))) {
        if (self.contactsSearchResult == nil) {
            ContactsSearchData *q = (ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0];
            if (q.resultEvent == FORMS_EVENT_CONTACTS_LOOKUP) {
                NSString *num = q.number;
                NSString *prefixIntl = ((q.prefixIntl != nil) && (q.prefixIntl.length != 0) &&
                    ((q.prefixSubs == nil) || [q.prefixIntl hasPrefix:q.prefixSubs])) ?
                    ((q.prefixSubs == nil) ? q.prefixIntl : [q.prefixIntl substringFromIndex:q.prefixSubs.length]) : nil;
                NSString *prefixNtl = ((q.prefixNtl != nil) && (q.prefixNtl.length != 0) &&
                    ((q.prefixSubs == nil) || [q.prefixNtl hasPrefix:q.prefixSubs])) ?
                    ((q.prefixSubs == nil) ? q.prefixNtl : [q.prefixNtl substringFromIndex:q.prefixSubs.length]) : nil;

                if ((q.state == CONTACTS_SEARCH_STATE_PLUS)
                 && (q.prefixIntl != nil) && (q.prefixIntl.length != 0) && [num hasPrefix:q.prefixIntl]) {
                    num = [NSString stringWithFormat:@"+%@", [num substringFromIndex:q.prefixIntl.length]];
                    q.state = CONTACTS_SEARCH_STATE_INTL;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_PLUS)
                 && (q.prefixNtl != nil) && (q.prefixNtl.length != 0) && [num hasPrefix:q.prefixNtl]
                 && (q.countryCode != nil) && (q.countryCode.length != 0)) {
                    num = [NSString stringWithFormat:@"+%@%@", q.countryCode, [num substringFromIndex:q.prefixNtl.length]];
                    q.state = CONTACTS_SEARCH_STATE_INTL;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_PLUS)
                 && (q.prefixSubs != nil) && (q.prefixSubs.length != 0) && [num hasPrefix:q.prefixSubs]
                 && (q.countryCode != nil) && (q.countryCode.length != 0)
                 && (q.areaCode != nil) && (q.areaCode.length != 0)) {
                    num = [NSString stringWithFormat:@"+%@%@%@", q.countryCode, q.areaCode, [num substringFromIndex:q.prefixSubs.length]];
                    q.state = CONTACTS_SEARCH_STATE_INTL;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_INTL)
                 && (prefixIntl != nil)
                 && (q.prefixIntl != nil) && (q.prefixIntl.length != 0) && [num hasPrefix:q.prefixIntl]) {
                    num = [NSString stringWithFormat:@"%@%@", prefixIntl, [num substringFromIndex:q.prefixIntl.length]];
                    q.state = CONTACTS_SEARCH_STATE_IDLE;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_INTL)
                 && (prefixIntl != nil)
                 && (q.prefixNtl != nil) && (q.prefixNtl.length != 0) && [num hasPrefix:q.prefixNtl]
                 && (q.countryCode != nil) && (q.countryCode.length != 0)) {
                    num = [NSString stringWithFormat:@"%@%@%@", prefixIntl, q.countryCode, [num substringFromIndex:q.prefixNtl.length]];
                    q.state = CONTACTS_SEARCH_STATE_NTL;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_INTL)
                 && (prefixIntl != nil)
                 && (q.prefixSubs != nil) && (q.prefixSubs.length != 0) && [num hasPrefix:q.prefixSubs]
                 && (q.countryCode != nil) && (q.countryCode.length != 0)
                 && (q.areaCode != nil) && (q.areaCode.length != 0)) {
                    num = [NSString stringWithFormat:@"%@%@%@%@", prefixIntl, q.countryCode, q.areaCode, [num substringFromIndex:q.prefixSubs.length]];
                    q.state = CONTACTS_SEARCH_STATE_NTL;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_NTL)
                 && (prefixNtl != nil)
                 && (q.prefixNtl != nil) && (q.prefixNtl.length != 0) && [num hasPrefix:q.prefixNtl]) {
                    num = [NSString stringWithFormat:@"%@%@", prefixNtl, [num substringFromIndex:q.prefixNtl.length]];
                    q.state = CONTACTS_SEARCH_STATE_IDLE;
                }
                else if ((q.state == CONTACTS_SEARCH_STATE_NTL)
                 && (prefixNtl != nil)
                 && (q.prefixSubs != nil) && (q.prefixSubs.length != 0) && [num hasPrefix:q.prefixSubs]
                 && (q.areaCode != nil) && (q.areaCode.length != 0)) {
                    num = [NSString stringWithFormat:@"%@%@%@", prefixNtl, q.areaCode, [num substringFromIndex:q.prefixSubs.length]];
                    q.state = CONTACTS_SEARCH_STATE_SUBS;
                }
                else {
                    q.state = CONTACTS_SEARCH_STATE_IDLE;
                }
                NSPredicate *predicate = [NSPredicate predicateWithFormat:@"numbers contains %@", num];
                NSArray *contacts = [self allContactsFromAddressBook_];
                self.contactsSearchResult = [contacts filteredArrayUsingPredicate:predicate];
            }
            else {
                self.contactsSearchResult = self.contactsForSearchString(q.pattern);
            }
            self.contactsSearchPos = 0;
        }
        if (self.contactsSearchResult != nil) {
            if (self.contactsSearchPos < self.contactsSearchResult.count) {
                Contact *personContact = (Contact *)[self.contactsSearchResult objectAtIndex:self.contactsSearchPos];
                ABRecordRef person = self.personForRecordID(personContact.recordID);
                NSMutableString *str = [NSMutableString stringWithCapacity:1024];
                JsonIo *json_io = [JsonIo jsonIo];
                int base = json_io.add_object(JSON_ID_ROOT, nil);

                json_io.add_string(base, @"mt", (((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).resultEvent == FORMS_EVENT_CONTACTS_LOOKUP) ? @"LookupInfo" : @"SearchInfo");
                json_io.add_string(base, @"dn", [NSString stringWithFormat:@"%@%@%@", personContact.firstName, ((personContact.firstName.length != 0) && (personContact.lastName.length != 0)) ? @" " : @"", personContact.lastName]);
                json_io.add_unsigned(base, @"relevance", 2000);
                json_io.add_string(base, @"type", @"contact");
                json_io.add_bool(base, @"adjust", true);

                if ([self ABPersonHasImageData:person]) {
                    json_io.add_string(base, @"photourl", [NSString stringWithFormat:@"%@/contactsphoto%u.png", self.webserver_url, personContact.recordID]);
                }

                int contact = json_io.add_object(base, @"contact");
                int base_fax = JSON_ID_NONE;
                int base_mobile = JSON_ID_NONE;
                int base_homephone = JSON_ID_NONE;
                int base_telephone = JSON_ID_NONE;
                int base_email = JSON_ID_NONE;
                int base_sip = JSON_ID_NONE;
                json_io.add_string(contact, @"givenname", personContact.firstName);
                json_io.add_string(contact, @"sn", personContact.lastName);
                json_io.add_string(contact, @"company", (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonOrganizationProperty));
                json_io.add_string(contact, @"position", (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonJobTitleProperty));

                ABMultiValueRef urls = ABRecordCopyValue(person, kABPersonURLProperty);
                if (urls != nil) {
                    NSInteger urlCount = ABMultiValueGetCount(urls);
                    i = 0;
                    while ((i < urlCount) && ![(__bridge_transfer NSString *) ABMultiValueCopyLabelAtIndex(urls, i) isEqualToString:(NSString *)kABWorkLabel]) {
                        i++;
                    }
                    if (i < urlCount) {
                        json_io.add_string(contact, @"url", (__bridge_transfer NSString *) ABMultiValueCopyValueAtIndex(urls, i));
                    }
                    CFRelease(urls);
                }
                ABMultiValueRef addresses = ABRecordCopyValue(person, kABPersonAddressProperty);
                if (addresses != nil) {
                    NSInteger addresseCount = ABMultiValueGetCount(addresses);
                    i = 0;
                    while ((i < addresseCount) && ![(__bridge_transfer NSString *) ABMultiValueCopyLabelAtIndex(addresses, i) isEqualToString:(NSString *)kABWorkLabel]) {
                        i++;
                    }
                    if (i < addresseCount) {
                        NSDictionary *address = (__bridge_transfer NSDictionary *) ABMultiValueCopyValueAtIndex(addresses, i);
                        json_io.add_string(contact, @"street", address[(NSString *)kABPersonAddressStreetKey]);
                        json_io.add_string(contact, @"city", address[(NSString *)kABPersonAddressCityKey]);
                        json_io.add_string(contact, @"country", address[(NSString *)kABPersonAddressCountryKey]);
                        json_io.add_string(contact, @"postalcode", address[(NSString *)kABPersonAddressZIPKey]);
                    }
                    CFRelease(addresses);
                }
                ABMultiValueRef phoneNumbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
                if (phoneNumbers != nil) {
                    NSInteger phoneNumberCount = ABMultiValueGetCount(phoneNumbers);
                    for (i = 0; i < phoneNumberCount; i++) {
                        NSString *phoneLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumbers, i);
                        if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) {
                            if (base_fax == JSON_ID_NONE) {
                                base_fax = json_io.add_array(contact, @"facsimiletelephonenumber");
                            }
                            json_io.add_string(base_fax, nil, (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i));
                        }
                    }
                    for (i = 0; i < phoneNumberCount; i++) {
                        NSString *phoneLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumbers, i);
                        if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) {
                        }
                        else if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) {
                            if (base_mobile == JSON_ID_NONE) {
                                base_mobile = json_io.add_array(contact, @"mobile");
                            }
                            json_io.add_string(base_mobile, nil, (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i));
                        }
                    }
                    for (i = 0; i < phoneNumberCount; i++) {
                        NSString *phoneLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumbers, i);
                        if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) {
                        }
                        else if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) {
                        }
                        else if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeLabel]) {
                            if (base_homephone == JSON_ID_NONE) {
                                base_homephone = json_io.add_array(contact, @"homephone");
                            }
                            json_io.add_string(base_homephone, nil, (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i));
                        }
                    }
                    for (i = 0; i < phoneNumberCount; i++) {
                        NSString *phoneLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumbers, i);
                        if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) {
                        }
                        else if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]
                         || [phoneLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) {
                        }
                        else if ([phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeLabel]) {
                        }
                        else {
                            if (base_telephone == JSON_ID_NONE) {
                                base_telephone = json_io.add_array(contact, @"telephonenumber");
                            }
                            json_io.add_string(base_telephone, nil, (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i));
                        }
                    }
                    CFRelease(phoneNumbers);
                }
                ABMultiValueRef emails = ABRecordCopyValue(person, kABPersonEmailProperty);
                if (emails != nil) {
                    NSInteger emailCount = ABMultiValueGetCount(emails);
                    for (i = 0; i < emailCount; i++) {
                        if (base_email == JSON_ID_NONE) {
                            base_email = json_io.add_array(contact, @"email");
                        }
                        json_io.add_string(base_email, nil, (__bridge_transfer NSString *) ABMultiValueCopyValueAtIndex(emails, i));
                    }
                    CFRelease(emails);
                }
                ABMultiValueRef sips = ABRecordCopyValue(person, kABPersonInstantMessageProperty);
                if (sips != nil) {
                    NSInteger sipCount = ABMultiValueGetCount(sips);
                    for (i = 0; i < sipCount; i++) {
                        NSDictionary *sip = (__bridge_transfer NSDictionary *) ABMultiValueCopyValueAtIndex(sips, i);
                        NSString *sipUser = sip[(NSString *)kABPersonInstantMessageUsernameKey];
                        if (sipUser != nil) {
                            if (base_sip == JSON_ID_NONE) {
                                base_sip = json_io.add_array(contact, @"sip");
                            }
                            json_io.add_string(base_sip, nil, sipUser);
                        }
                    }
                    CFRelease(sips);
                }

                json_io.encode(str);
                self.ios_event.enqueue(((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).resultEvent, ((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).ref, (NSString *)str);
                (self.contactsSearchPos)++;
                (self.contactsSearchResults)++;
            }
            else {
                self.contactsSearchResult = nil;
                if (((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).state == CONTACTS_SEARCH_STATE_IDLE) {
                    [self forms_event_contacts_search_result:((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).resultEvent ref:((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).ref];
                    [self.contactsSearchQueue removeObjectAtIndex:0];
                }
            }
        }
        else {
            [self forms_event_contacts_search_result:((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).resultEvent ref:((ContactsSearchData *)[self.contactsSearchQueue objectAtIndex:0]).ref];
            [self.contactsSearchQueue removeObjectAtIndex:0];
        }
    }
    if (self.contactsSearchResults == CONTACTS_SEARCH_CHUNK_SIZE) {
        self.ios_event.enqueue(FORMS_EVENT_CONTACTS_SEARCH_MORE);
    }
}

#define WEBSERVER_GET_CHUNK_SIZE 64000

- (void) webserver_get_next
{
    if (self.webserverGetData == nil) {
        NSString *resourceName = ((WebserverGetData *)[self.webserverGetQueue objectAtIndex:0]).resourceName;
        if ([resourceName hasPrefix:@"/contactsphoto"]) {
            int i = 14;
            while((i < resourceName.length) && ([resourceName characterAtIndex:i] != '.')) {
                i++;
            }
            NSString *contactId = [resourceName substringWithRange:NSMakeRange(14, i-14)];
            int32_t recordID = contactId.intValue;
            ABRecordRef person = self.personForRecordID(recordID);
            // Get the photo of the contact
            if ([self ABPersonHasImageData:person]) {
                self.webserverGetData = (__bridge_transfer NSData *)[self ABPersonCopyImageDataWithFormat:person format:kABPersonImageFormatThumbnail];
                if (self.webserverGetData == nil) {
                    self.webserverGetData = (__bridge_transfer NSData *)[self ABPersonCopyImageData:person];
                }
            }
        }
        self.webserverGetPos = 0;
    }
    int n = (self.webserverGetData != nil) ? self.webserverGetData.length - self.webserverGetPos : 0;
    bool complete = (n <= WEBSERVER_GET_CHUNK_SIZE);
    if (!complete) {
        n = WEBSERVER_GET_CHUNK_SIZE;
    }
    self.ios_event.enqueue(FORMS_EVENT_WEBSERVER_GET, ((WebserverGetData *)[self.webserverGetQueue objectAtIndex:0]).context, complete, self.webserverGetPos, n, self.webserverGetData);
    self.webserverGetPos += n;
}

- (void) formsWebserverGet:(NSString *)context resourceName:(NSString *)resourceName
{
    WebserverGetData *w = [[WebserverGetData alloc] initWithContext:context resourceName:resourceName];
    [self.webserverGetQueue addObject:w];
    if (self.webserverGetQueue.count == 1) {
        [self webserver_get_next];
    }
}

- (void) formsWebserverGetSent:(NSString *)context
{
    if ((self.webserverGetData == nil) || (self.webserverGetPos == self.webserverGetData.length)) {
        self.webserverGetData = nil;
        [self.webserverGetQueue removeObjectAtIndex:0];
    }
    if (self.webserverGetQueue.count != 0) {
        [self webserver_get_next];
    }
}

- (NSUUID *) UUIDWithString:(NSString *)uuid_string
{
    const char *c = uuid_string.UTF8String;
    byte b[16];
    unsigned i;

    for (i = 0; i < sizeof(b); i++) {
        b[i] = ((isdigit(c[2*i]) ? c[2*i] - '0' : tolower(c[2*i]) - 'a' + 10) << 4) | (isdigit(c[2*i+1]) ? c[2*i+1] - '0' : tolower(c[2*i+1]) - 'a' + 10);
    }
    return ([[NSUUID alloc] initWithUUIDBytes:b]);
}

- (NSString *) stringWithUUID:(NSUUID *)uuid
{
    byte b[16];
    if (uuid == nil) {
        return (@"null");
    }
    [uuid getUUIDBytes:b];
    return ([NSString stringWithFormat:
        @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
        b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]]);
}

- (void) formsCxUse:(bool)use ringtone:(NSString *)ringtone supportsVideo:(bool)supportsVideo maximumCallGroups:(int)maximumCallGroups maximumCallsPerCallGroup:(int)maximumCallsPerCallGroup
{
    int i;

    NSString *countryCode = [NSLocale currentLocale].countryCode;
    if (use && ![countryCode isEqualToString: @"CN"] && ![countryCode isEqualToString: @"CHN"]) {
        CXProviderConfiguration *cxProviderConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:@PHONE_IOS_LOG_APP_NAME];
        cxProviderConfiguration.ringtoneSound = [self tone_path_from_ident:ringtone ? ringtone.UTF8String : nil notification_tone:false];
        cxProviderConfiguration.iconTemplateImageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"CallKitIconTemplate" ofType:@"png"]];
        cxProviderConfiguration.supportsVideo = supportsVideo;
        cxProviderConfiguration.maximumCallGroups = maximumCallGroups;
        cxProviderConfiguration.maximumCallsPerCallGroup = (maximumCallsPerCallGroup < 2) ? 2 : maximumCallsPerCallGroup;
        cxProviderConfiguration.supportedHandleTypes = [NSSet setWithObjects:@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber), @(CXHandleTypeEmailAddress), nil];
        if ([cxProviderConfiguration respondsToSelector:@selector(includesCallsInRecents)]) {
            if (self.cxProvider == nil) {
                self.cxHideFromNativeCallList = [self hide_from_native_call_list];
            }
            cxProviderConfiguration.includesCallsInRecents = !self.cxHideFromNativeCallList;
        }
        if (self.cxProvider == nil) {
            self.cxProvider = [[CXProvider alloc] initWithConfiguration:cxProviderConfiguration];
            [self.cxProvider setDelegate:self queue:dispatch_get_main_queue()];
            self.cxCallController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];
        }
        else {
            i = 0;
            while ((i < self.cxCalls.count) && ((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
                i++;
            }
            if (i == self.cxCalls.count) {
                self.cxProvider.configuration = cxProviderConfiguration;
            }
        }
    }
    else {
        self.cxProvider = nil;
        self.cxCallController = nil;
    }
}

- (void) formsCxReportNewIncomingCall:(NSString *)conf_id_string number:(NSString *)number name:(NSString *)name transfer:(NSString *)transfer called:(NSString *)called flags:(int)flags
{
    byte b[16], conf_id_buf[16];
    int i;

    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];
    [self obsoleteCxCalls];
    i = 0;
    while ((i < self.cxCalls.count)
     && (!((CxCall *)[self.cxCalls objectAtIndex:i]).released
      || (((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil)
      || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i < self.cxCalls.count) {
        [self.cxCalls removeObjectAtIndex:i];
    }
    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i == self.cxCalls.count) {
        i = 0;
        while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).released || (((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id != nil))) {
            i++;
        }
        if (i < self.cxCalls.count) {
            ((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id = conf_id;
            ((CxCall *)[self.cxCalls objectAtIndex:i]).number = number;
            ((CxCall *)[self.cxCalls objectAtIndex:i]).name = name;
            ((CxCall *)[self.cxCalls objectAtIndex:i]).video = ((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0);
        }
    }
    if (i == self.cxCalls.count) {
        [self.cxCalls addObject:[CxCall cxCallWithUUID:conf_id conf_id:conf_id number:number claimed:true incoming:true name:name video:((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0)]];

        CXCallUpdate *cxCallUpdate = [[CXCallUpdate alloc] init];
        cxCallUpdate.remoteHandle = [[CXHandle alloc] initWithType:
            ((number != nil) && [number containsString:@"@"]) ? CXHandleTypeEmailAddress : (((number != nil) && [self number_is_h323:number]) ? CXHandleTypeGeneric : CXHandleTypePhoneNumber) value:
            (number != nil) ? (([number containsString:@"@"] || [self number_is_h323:number]) ? number : [self stripSeparators:number]) : @""];
        cxCallUpdate.localizedCallerName = name;
        cxCallUpdate.hasVideo = ((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0);
        cxCallUpdate.supportsDTMF = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_DTMF) != 0);
        cxCallUpdate.supportsHolding = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_HOLDING) != 0);
        cxCallUpdate.supportsGrouping = true/*((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_GROUPING) != 0)*/;
        cxCallUpdate.supportsUngrouping = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_UNGROUPING) != 0);
        [self.cxProvider reportNewIncomingCallWithUUID:conf_id update:cxCallUpdate completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            unsigned i;
            if (error) {
                byte b[16], conf_id_buf[16];
                [conf_id getUUIDBytes:b];
                i = 0;
                while ((i < phoneIosDelegate.cxCalls.count) && ((((CxCall *)[phoneIosDelegate.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[phoneIosDelegate.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
                    i++;
                }
                if (i < phoneIosDelegate.cxCalls.count) {
                    [((CxCall *)[phoneIosDelegate.cxCalls objectAtIndex:i]) released_now];
                }
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxReportNewIncomingCall failed %@", error]);
                phoneIosDelegate.ios_event.enqueue(FORMS_EVENT_CX_END_CALL_ACTION, conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }];
    }
    else {
        ((CxCall *)[self.cxCalls objectAtIndex:i]).claimed = true;
        [self formsCxReportCallUpdated:conf_id_string number:number name:name transfer:transfer called:called flags:flags];
        if (((CxCall *)[self.cxCalls objectAtIndex:i]).accept) {
            self.ios_event.enqueue(FORMS_EVENT_CX_ANSWER_CALL_ACTION, conf_id_string);
        }
        if (((CxCall *)[self.cxCalls objectAtIndex:i]).reject) {
            self.ios_event.enqueue(FORMS_EVENT_CX_END_CALL_ACTION, conf_id_string, FORMS_CX_CALL_ERROR_SUCCESS);
        }
    }
}

- (void) formsCxReportOutgoingCallConnecting:(NSString *)conf_id_string
{
    [self.cxProvider reportOutgoingCallWithUUID:[self UUIDWithString:conf_id_string] startedConnectingAtDate:nil];
}

- (void) formsCxReportOutgoingCallConnected:(NSString *)conf_id_string
{
    byte b[16], conf_id_buf[16];
    int i;

    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];
    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if ((i < self.cxCalls.count) && ((CxCall *)[self.cxCalls objectAtIndex:i]).incoming && !((CxCall *)[self.cxCalls objectAtIndex:i]).connected && !((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
        ((CxCall *)[self.cxCalls objectAtIndex:i]).connected = true;
        [self.cxProvider reportCallWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid endedAtDate:nil reason:CXCallEndedReasonAnsweredElsewhere];
        [((CxCall *)[self.cxCalls objectAtIndex:i]).uuid getUUIDBytes:b];
        b[0] ^= 1;
        ((CxCall *)[self.cxCalls objectAtIndex:i]).uuid = [[NSUUID alloc] initWithUUIDBytes:b];
        NSString *number = ((CxCall *)[self.cxCalls objectAtIndex:i]).number;
        CXHandle *handle = [[CXHandle alloc] initWithType:
            [number containsString:@"@"] ? CXHandleTypeEmailAddress : ([self number_is_h323:number] ? CXHandleTypeGeneric : CXHandleTypePhoneNumber) value:
            ([number containsString:@"@"] || [self number_is_h323:number]) ? number : [self stripSeparators:number]];
        CXStartCallAction *cxStartCallAction = [[CXStartCallAction alloc] initWithCallUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid handle:handle];
        cxStartCallAction.contactIdentifier = ((CxCall *)[self.cxCalls objectAtIndex:i]).name;
        cxStartCallAction.video = ((CxCall *)[self.cxCalls objectAtIndex:i]).video;

        CXTransaction *cxTransaction = [[CXTransaction alloc] init];
        [cxTransaction addAction:cxStartCallAction];
        [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestStartCall activate failed %@", error]);
                if (![error.domain isEqualToString:CXErrorDomainRequestTransaction] || (error.code != CXErrorCodeRequestTransactionErrorCallUUIDAlreadyExists)) {
                    phoneIosDelegate.ios_event.enqueue(FORMS_EVENT_CX_END_CALL_ACTION, conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
                }
            }
        }];
    }
    else {
        if (i < self.cxCalls.count) {
            ((CxCall *)[self.cxCalls objectAtIndex:i]).connected = true;
        }
        [self.cxProvider reportOutgoingCallWithUUID:[self UUIDWithString:conf_id_string] connectedAtDate:nil];
    }
}

- (void) formsCxReportCallUpdated:(NSString *)conf_id_string number:(NSString *)number name:(NSString *)name transfer:(NSString *)transfer called:(NSString *)called flags:(int)flags
{
    byte b[16], conf_id_buf[16];
    int i;

    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];
    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i < self.cxCalls.count) {
        CXCallUpdate *cxCallUpdate = [[CXCallUpdate alloc] init];
        cxCallUpdate.remoteHandle = [[CXHandle alloc] initWithType:
            ((number != nil) && [number containsString:@"@"]) ? CXHandleTypeEmailAddress : (((number != nil) && [self number_is_h323:number]) ? CXHandleTypeGeneric : CXHandleTypePhoneNumber) value:
            (number != nil) ? (([number containsString:@"@"] || [self number_is_h323:number]) ? number : [self stripSeparators:number]) : @""];
        cxCallUpdate.localizedCallerName = name;
        cxCallUpdate.hasVideo = ((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0);
        cxCallUpdate.supportsDTMF = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_DTMF) != 0);
        cxCallUpdate.supportsHolding = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_HOLDING) != 0);
        cxCallUpdate.supportsGrouping = true/*((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_GROUPING) != 0)*/;
        cxCallUpdate.supportsUngrouping = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_UNGROUPING) != 0);
        [self.cxProvider reportCallWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid updated:cxCallUpdate];
    }
}

- (void) formsCxReportCallEndedRemoteEnded:(id)object
{
    self.trc([NSString stringWithFormat:@"cxReportCallEnded delayed %@ %@", [self stringWithUUID:(NSUUID *)object], @"RemoteEnded"]);
    [self audioSessionMayDeactivate];
    [self.cxProvider reportCallWithUUID:(NSUUID *)object endedAtDate:nil reason:CXCallEndedReasonRemoteEnded];
}

- (void) formsCxReportCallEndedUnanswered:(id)object
{
    self.trc([NSString stringWithFormat:@"cxReportCallEnded delayed %@ %@", [self stringWithUUID:(NSUUID *)object], @"Unanswered"]);
    [self audioSessionMayDeactivate];
    [self.cxProvider reportCallWithUUID:(NSUUID *)object endedAtDate:nil reason:CXCallEndedReasonUnanswered];
}

- (void) formsCxReportCallEndedAnsweredElsewhere:(id)object
{
    self.trc([NSString stringWithFormat:@"cxReportCallEnded delayed %@ %@", [self stringWithUUID:(NSUUID *)object], @"AnsweredElsewhere"]);
    [self audioSessionMayDeactivate];
    [self.cxProvider reportCallWithUUID:(NSUUID *)object endedAtDate:nil reason:CXCallEndedReasonAnsweredElsewhere];
}

- (void) formsCxReportCallEndedDeclinedElsewhere:(id)object
{
    self.trc([NSString stringWithFormat:@"cxReportCallEnded delayed %@ %@", [self stringWithUUID:(NSUUID *)object], @"DeclinedElsewhere"]);
    [self audioSessionMayDeactivate];
    [self.cxProvider reportCallWithUUID:(NSUUID *)object endedAtDate:nil reason:CXCallEndedReasonDeclinedElsewhere];
}

- (void) obsoleteCxCalls
{
    NSTimeInterval oldest_time;
    unsigned i, released_calls, oldest_call;

    do {
        released_calls = 0;
        oldest_time = 0;
        oldest_call = 0;
        for (i = 0; i < self.cxCalls.count; i++) {
            if (((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
                released_calls++;
                if (-((CxCall *)[self.cxCalls objectAtIndex:i]).release_time.timeIntervalSinceNow > oldest_time) {
                    oldest_time = -((CxCall *)[self.cxCalls objectAtIndex:i]).release_time.timeIntervalSinceNow;
                    oldest_call = i;
                }
            }
        }
        if ((released_calls > 2) && (oldest_time > 30)) {
            [self.cxCalls removeObjectAtIndex:oldest_call];
        }
    }
    while ((released_calls > 2) && (oldest_time > 30));
}

- (void) formsCxReportCallEnded:(NSString *)conf_id_string reason:(int)reason
{
    byte b[16], conf_id_buf[16];
    NSTimeInterval duration;
    unsigned i;
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];

    duration = 2;
    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i == self.cxCalls.count) {
        i = 0;
        while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).released || (((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id != nil))) {
            i++;
        }
        if (i < self.cxCalls.count) {
            ((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id = conf_id;
            ((CxCall *)[self.cxCalls objectAtIndex:i]).claimed = true;
        }
    }
    if (i < self.cxCalls.count) {
        duration = -((CxCall *)[self.cxCalls objectAtIndex:i]).create_time.timeIntervalSinceNow;
        [((CxCall *)[self.cxCalls objectAtIndex:i]) released_now];
        if (duration < 2) {
            [self performSelector:
                (reason == FORMS_CX_CALL_ENDED_REASON_REMOTE_ENDED) ? @selector(formsCxReportCallEndedRemoteEnded:) :
                ((reason == FORMS_CX_CALL_ENDED_REASON_UNANSWERED) ? @selector(formsCxReportCallEndedUnanswered:) :
                ((reason == FORMS_CX_CALL_ENDED_REASON_ANSWERED_ELSEWHERE) ? @selector(formsCxReportCallEndedAnsweredElsewhere:) :
                ((reason == FORMS_CX_CALL_ENDED_REASON_DECLINED_ELSEWHERE) ? @selector(formsCxReportCallEndedDeclinedElsewhere:) : @selector(formsCxReportCallEndedRemoteEnded:))))
                withObject:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid afterDelay:2-duration];
        }
        else {
            NSLog(@"cxReportCallEnded %@ %@", [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid],
                (reason == FORMS_CX_CALL_ENDED_REASON_REMOTE_ENDED) ? @"RemoteEnded" :
                ((reason == FORMS_CX_CALL_ENDED_REASON_UNANSWERED) ? @"Unanswered" :
                ((reason == FORMS_CX_CALL_ENDED_REASON_ANSWERED_ELSEWHERE) ? @"AnsweredElsewhere" :
                ((reason == FORMS_CX_CALL_ENDED_REASON_DECLINED_ELSEWHERE) ? @"DeclinedElsewhere" : @"RemoteEnded"))));
            [self audioSessionMayDeactivate];
            [self.cxProvider reportCallWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid endedAtDate:nil reason:
                (reason == FORMS_CX_CALL_ENDED_REASON_REMOTE_ENDED) ? CXCallEndedReasonRemoteEnded :
                ((reason == FORMS_CX_CALL_ENDED_REASON_UNANSWERED) ? CXCallEndedReasonUnanswered :
                ((reason == FORMS_CX_CALL_ENDED_REASON_ANSWERED_ELSEWHERE) ? CXCallEndedReasonAnsweredElsewhere :
                ((reason == FORMS_CX_CALL_ENDED_REASON_DECLINED_ELSEWHERE) ? CXCallEndedReasonDeclinedElsewhere : CXCallEndedReasonRemoteEnded)))];
        }
    }
}

- (void) formsCxRequestStartCall:(NSString *)conf_id_string number:(NSString *)number name:(NSString *)name video:(bool)video
{
    if (number == nil) {
        number = @"";
    }
    CXHandle *handle = [[CXHandle alloc] initWithType:
        [number containsString:@"@"] ? CXHandleTypeEmailAddress : ([self number_is_h323:number] ? CXHandleTypeGeneric : CXHandleTypePhoneNumber) value:
        ([number containsString:@"@"] || [self number_is_h323:number]) ? number : [self stripSeparators:number]];
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [self obsoleteCxCalls];
    CXStartCallAction *cxStartCallAction = [[CXStartCallAction alloc] initWithCallUUID:conf_id handle:handle];
    cxStartCallAction.contactIdentifier = name;
    cxStartCallAction.video = video;

    CXTransaction *cxTransaction = [[CXTransaction alloc] init];
    [cxTransaction addAction:cxStartCallAction];
    [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
        PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
        if (error) {
            phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestStartCall failed %@", error]);
            if (![error.domain isEqualToString:CXErrorDomainRequestTransaction] || (error.code != CXErrorCodeRequestTransactionErrorCallUUIDAlreadyExists)) {
                phoneIosDelegate.ios_event.enqueue(FORMS_EVENT_CX_END_CALL_ACTION, conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }
        else {
            [phoneIosDelegate.cxCalls addObject:[CxCall cxCallWithUUID:conf_id conf_id:conf_id number:number claimed:true incoming:false name:name video:video]];
        }
    }];
}

- (void) formsCxRequestEndCall:(NSString *)conf_id_string
{
    byte b[16], conf_id_buf[16];
    unsigned i;
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i < self.cxCalls.count) {
        [((CxCall *)[self.cxCalls objectAtIndex:i]) released_now];
        [self audioSessionMayDeactivate];
        CXTransaction *cxTransaction = [[CXTransaction alloc] init];
        [cxTransaction addAction:[[CXEndCallAction alloc] initWithCallUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid]];
        [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestEndCall failed %@", error]);
                phoneIosDelegate.forms_event_call_error(conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }];
    }
}

- (void) formsCxRequestSetHeldCall:(NSString *)conf_id_string held:(bool)held
{
    byte b[16], conf_id_buf[16];
    unsigned i;
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if ((i < self.cxCalls.count) && (held ^ ((CxCall *)[self.cxCalls objectAtIndex:i]).held)) {
        ((CxCall *)[self.cxCalls objectAtIndex:i]).held = held;
        CXTransaction *cxTransaction = [[CXTransaction alloc] init];
        [cxTransaction addAction:[[CXSetHeldCallAction alloc] initWithCallUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid onHold:held]];
        [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestSetHeldCall failed %@", error]);
                phoneIosDelegate.forms_event_call_error(conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }];
    }
}

- (void) formsCxRequestSetMutedCall:(NSString *)conf_id_string muted:(bool)muted
{
    byte b[16], conf_id_buf[16];
    unsigned i;
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if ((i < self.cxCalls.count) && (muted ^ ((CxCall *)[self.cxCalls objectAtIndex:i]).muted)) {
        ((CxCall *)[self.cxCalls objectAtIndex:i]).muted = muted;
        CXTransaction *cxTransaction = [[CXTransaction alloc] init];
        [cxTransaction addAction:[[CXSetMutedCallAction alloc] initWithCallUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid muted:muted]];
        [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestSetMutedCall failed %@", error]);
//                phoneIosDelegate.forms_event_call_error(conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }];
    }
}

- (void) formsCxRequestSetGroupCall:(NSString *)conf_id_string with:(NSString *)with
{
    byte b[16], conf_id_buf[16];
    unsigned i;
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];

    self.trc([NSString stringWithFormat:@"cxRequestSetGroupCall %@ %@", conf_id_string, with]);
    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i < self.cxCalls.count) {
        CXTransaction *cxTransaction = [[CXTransaction alloc] init];
        [cxTransaction addAction:[[CXSetGroupCallAction alloc] initWithCallUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid callUUIDToGroupWith:[self UUIDWithString:with]]];
        [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestSetGroupCall failed %@", error]);
                phoneIosDelegate.forms_event_call_error(conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }];
    }
}

- (void) formsCxRequestPlayDTMF:(NSString *)conf_id_string digits:(NSString *)digits
{
    byte b[16], conf_id_buf[16];
    unsigned i;
    NSUUID *conf_id = [self UUIDWithString:conf_id_string];
    [conf_id getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
        i++;
    }
    if (i < self.cxCalls.count) {
        CXTransaction *cxTransaction = [[CXTransaction alloc] init];
        [cxTransaction addAction:[[CXPlayDTMFCallAction alloc] initWithCallUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid digits:digits type:CXPlayDTMFCallActionTypeSingleTone]];
        [self.cxCallController requestTransaction:cxTransaction completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                phoneIosDelegate.trc([NSString stringWithFormat:@"cxRequestPlayDTMFCall failed %@", error]);
                phoneIosDelegate.forms_event_call_error(conf_id_string, [phoneIosDelegate formsCxCallErrorMap:error]);
            }
        }];
    }
}

- (void) onScreenCapture:(UIImage * _Nullable)snapshotImage
{
    if (snapshotImage != nil) {
        CGImageRef cgImage = [snapshotImage CGImage];
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);
        size_t stride = CGImageGetBytesPerRow(cgImage); // Each pixel contains four bytes of RGBA
//        size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);

        CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
        CFDataRef dataRef = CGDataProviderCopyData(dataProvider);
//        UInt8 *buffer = (UInt8 *)CFDataGetBytePtr(dataRef);
        NSData *data = (__bridge NSData *) dataRef;

        struct timeval tv;
        gettimeofday(&tv, (struct timezone *)NULL);
        int64_t captureTime = (((int64_t)(tv.tv_sec)) * 1000) + (tv.tv_usec / 1000);

        self.ios_event.enqueue(FORMS_EVENT_SCREEN_CAPTURE_SAMPLE, captureTime * 90,
            0, height * stride, data,
            width,
            height,
            stride,
            FORMS_VIDEO_FORMAT_RGB32);

        CFRelease(dataRef);
    }
    [self performSelector:@selector(screen_capture_runnable:) withObject:nil afterDelay:0.1];
}

- (void) screen_capture_runnable:(id)sender
{
    [((WebViewController *)(self.window.rootViewController)).webView takeSnapshotWithConfiguration:self.snapshotConfiguration completionHandler:^(UIImage * _Nullable snapshotImage, NSError * _Nullable error) {
        if (self.screen_capture_started) {
            dispatch_async(dispatch_get_main_queue(), ^
                { [self onScreenCapture:snapshotImage]; });
        }
    }];
/*
	UIGraphicsBeginImageContextWithOptions(((WebViewController *)(self.window.rootViewController)).webView.bounds.size, NO, UIScreen.mainScreen.scale);
	[((WebViewController *)(self.window.rootViewController)).webView.layer renderInContext:UIGraphicsGetCurrentContext()];
	UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
	[self onScreenCapture:snapshotImage];
*/
}

- (void) broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController didFinishWithBroadcastController:(RPBroadcastController *)broadcastController error:(NSError *)error
{
    [broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil];
}

- (void) screenCaptureChange:(NSNotificationCenter *)notification
{
    if ([UIScreen mainScreen].isCaptured != self.screen_capture_running) {
        self.screen_capture_stopped = self.screen_capture_started && self.screen_capture_running;
        self.screen_capture_running = !self.screen_capture_running;
        NSLog(@"ScreenCaptureChange %u %u %u", self.screen_capture_running, self.screen_capture_stopped, self.didBecomeActive);
        if (self.screen_capture_stopped && self.didBecomeActive) {
            self.screen_capture_stopped = false;
            self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/PHONE/SCREEN-CAPTURE-STOPPED", @"");
        }
    }
}

- (void) formsStartScreenCapture
{
    if (NSClassFromString(@"RPSystemBroadcastPickerView")) {
        RPSystemBroadcastPickerView *broadcastPicker = [[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
        broadcastPicker.preferredExtension = [NSString stringWithFormat:@"%@.broadcast-extension", @PHONE_IOS_BUNDLE_ID];
        broadcastPicker.preferredExtension = @"com.innovaphone.myapps.broadcast-extension";
        broadcastPicker.showsMicrophoneButton = false;
        broadcastPicker.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
        broadcastPicker.hidden = true;
        broadcastPicker.center = self.window.rootViewController.view.center;
        [self.window.rootViewController.view addSubview:broadcastPicker];
        [self.window.rootViewController.view bringSubviewToFront:broadcastPicker];
        SEL buttonPress = NSSelectorFromString(@"buttonPressed:");
        if ([broadcastPicker respondsToSelector:buttonPress]) {
            [broadcastPicker performSelector:buttonPress withObject:nil];
        }
        self.screen_capture_started = true;
        self.updateProximitySensor();
        self.screen_capture_stopped = false;
//        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenCaptureChange:) name:UIScreenCapturedDidChangeNotification object:nil];
    }
    else {
/*
        if ([RPBroadcastActivityViewController respondsToSelector:@selector(loadBroadcastActivityViewControllerWithPreferredExtension:handler:)]) {
            [RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithPreferredExtension:[NSString stringWithFormat:@"%@.broadcast-ui-extension", @PHONE_IOS_BUNDLE_ID] handler:^(RPBroadcastActivityViewController *broadcastActivityViewController, NSError *error) {
                if (error) {
                    NSLog(@"formsStartScreenCapture loadBroadcastActivityViewControllerWithPreferredExtension failed %@", error);
                }
                else {
                    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                    broadcastActivityViewController.delegate = phoneIosDelegate;
                    [phoneIosDelegate.window.rootViewController presentViewController:broadcastActivityViewController animated:YES completion:nil];
                }
            }];
            self.screen_capture_started = true;
            self.updateProximitySensor();
        }
        else if (NSClassFromString(@"RPBroadcastActivityViewController")) {
            [RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController *broadcastActivityViewController, NSError *error) {
                if (error) {
                    NSLog(@"formsStartScreenCapture loadBroadcastActivityViewControllerWithHandler failed %@", error);
                }
                else {
                    PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                    broadcastActivityViewController.delegate = phoneIosDelegate;
                    [phoneIosDelegate.window.rootViewController presentViewController:broadcastActivityViewController animated:YES completion:nil];
                }
            }];
            self.screen_capture_started = true;
            self.updateProximitySensor();
        }
*/
#ifdef CLIENT_IOS_USE_WKWEBVIEW
        if ([((WebViewController *)(self.window.rootViewController)).webView respondsToSelector:@selector(takeSnapshotWithConfiguration:completionHandler:)]) {
            [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(screen_capture_runnable:) object:nil];
            self.screen_capture_started = true;
            self.updateProximitySensor();
            self.snapshotConfiguration = [[WKSnapshotConfiguration alloc] init];
            if ([self.snapshotConfiguration respondsToSelector:@selector(afterScreenUpdates)]) {
                self.snapshotConfiguration.afterScreenUpdates = false;
            }
            [self performSelector:@selector(screen_capture_runnable:) withObject:nil afterDelay:0.1];
        }
#endif
    }
}

- (void) formsStopScreenCapture
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(screen_capture_runnable:) object:nil];
    self.screen_capture_started = false;
    self.updateProximitySensor();
    if (NSClassFromString(@"RPSystemBroadcastPickerView")) {
        self.screen_capture_stopped = false;
//        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenCapturedDidChangeNotification object:nil];
        UIView *view = [self.window.rootViewController.view.subviews objectAtIndex:self.window.rootViewController.view.subviews.count - 1];
        if ([view isKindOfClass:RPSystemBroadcastPickerView.class]) {
            [view removeFromSuperview];
        }
    }
}

- (void) formsSetConnInfo:(bool)up standby:(bool)standby cur:(NSString *)cur pri:(NSString *)pri sec:(NSString *)sec master:(NSString *)master
{
    self.connInfoUp = up;
    self.connInfoStandby = standby;
    self.connInfoCur = cur;
    self.connInfoPri = pri;
    self.connInfoSec = sec;
    self.connInfoMaster = master;
}

- (bool) calls_to_display_
{
    return false;
}

- (bool) messages_to_display_
{
    return false;
}

- (void) shutdown_no_response_runnable:(id)sender
{
    do_shutdown();
    [self cancelAllLocalNotifications];
    poll_log();
    close_log();
    exit(0);
}

- (void) forms_event_shutdown_:(bool)user_shutdown
{
    self.isShutdown = true;
    self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"CPU/SHUTDOWN", @"");
    [self performSelector:@selector(shutdown_no_response_runnable:) withObject:nil afterDelay:2];
}

- (void) forms_event_external_call_:(CTCall *)call
{
    if([call.callState isEqualToString:CTCallStateIncoming]) {
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/EXTERNAL-CALL-RINGING", @"");
    }
    else if([call.callState isEqualToString:CTCallStateDialing]) {
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/EXTERNAL-CALL-OFFHOOK", @"");
    }
    else if([call.callState isEqualToString:CTCallStateConnected]) {
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/EXTERNAL-CALL-OFFHOOK", @"");
    }
    else {
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/EXTERNAL-CALL-IDLE", @"");
    }
    if (self.internetReachability == nil) {
        self.internetReachability = [Reachability reachabilityForInternetConnection];
        [self.internetReachability startNotifier];
    }
    [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
}

- (bool) ABPersonHasImageData:(ABRecordRef)person
{
    bool ret = false;

    @try {
        ret = ABPersonHasImageData(person);
    }
    @catch (NSException *exception) {
    }
    @finally {
    }
    return ret;
}

- (CFDataRef) ABPersonCopyImageData:(ABRecordRef)person
{
    CFDataRef ret = nil;

    @try {
        ret = ABPersonCopyImageData(person);
    }
    @catch (NSException *exception) {
    }
    @finally {
    }
    return ret;
}

- (CFDataRef) ABPersonCopyImageDataWithFormat:(ABRecordRef)person format:(ABPersonImageFormat)format
{
    CFDataRef ret = nil;

    @try {
        ret = ABPersonCopyImageDataWithFormat(person, format);
    }
    @catch (NSException *exception) {
    }
    @finally {
    }
    return ret;
}

- (void) addressBookReload
{
    int personNumber, i, contactNumber;
    CFIndex sourceNumber, emailNumber;
    
    self.contacts = [[NSMutableArray alloc] init];
    if ((staticAddressBook == nil) || (ABAddressBookGetAuthorizationStatus() != kABAuthorizationStatusAuthorized)) {
        return;
    }

//    ABAddressBookRevert(staticAddressBook);
    CFArrayRef sourcesArray = ABAddressBookCopyArrayOfAllSources(staticAddressBook);
    for (sourceNumber = 0; sourceNumber < CFArrayGetCount(sourcesArray); sourceNumber++) {
        ABRecordRef source = (ABRecordRef)CFArrayGetValueAtIndex(sourcesArray, sourceNumber);
        NSArray *allPeople = (__bridge_transfer NSArray *)ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering(staticAddressBook, source, kABPersonSortByFirstName);
        contactNumber = 0;
        
        for (personNumber = 0; personNumber < allPeople.count; personNumber++) {
            ABRecordRef person = (__bridge ABRecordRef)(allPeople[personNumber]);
            Contact *contact = [[Contact alloc] init];
            
            contact.recordID = ABRecordGetRecordID(person);
            
            // Get the name of the contact
            contact.firstName = (__bridge_transfer NSString*)ABRecordCopyValue(person, kABPersonFirstNameProperty);
            contact.lastName = (__bridge_transfer NSString*)ABRecordCopyValue(person, kABPersonLastNameProperty);
            if ((contact.firstName == nil) && (contact.lastName == nil)) {
                ABMultiValueRef emails = ABRecordCopyValue(person, kABPersonEmailProperty);
                if (emails != nil) {
                    for (emailNumber = 0; emailNumber < ABMultiValueGetCount(emails); emailNumber++) {
                        NSString *label = (__bridge_transfer NSString *) ABMultiValueCopyLabelAtIndex(emails, emailNumber);
                        if ([label isEqualToString:(NSString *)kABWorkLabel]) {
                            contact.firstName = (__bridge_transfer NSString *) ABMultiValueCopyValueAtIndex(emails, emailNumber);
                        }
                    }
                    if ((contact.firstName == nil) && (ABMultiValueGetCount(emails) != 0)) {
                        contact.firstName = (__bridge_transfer NSString *) ABMultiValueCopyValueAtIndex(emails, 0);
                    }
                    CFRelease(emails);
                }
            }
            if (contact.firstName == nil) {
                contact.firstName = @"";
            }
            if (contact.lastName == nil) {
                contact.lastName = @"";
            }
            contact.company = (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonOrganizationProperty);
            if (contact.company == nil) {
                contact.company = @"";
            }

            // Get all phone numbers of the contact
            ABMultiValueRef phoneNumbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
            NSInteger phoneNumberCount = (phoneNumbers != nil) ? ABMultiValueGetCount(phoneNumbers) : 0;
            NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:phoneNumberCount];
            if (phoneNumbers != nil) {
                // If the contact has multiple phone numbers, iterate on each of them
                for (i = 0; i < phoneNumberCount; i++) {
                    NSString *phoneLabel = (__bridge_transfer NSString*)ABMultiValueCopyLabelAtIndex(phoneNumbers, i);
                    if (![phoneLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]
                     && ![phoneLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]
                     && ![phoneLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]
                     && ![phoneLabel isEqualToString:(NSString *)kABPersonPhonePagerLabel]) {
                        NSString *phoneNumberFromAB = (__bridge_transfer NSString*)ABMultiValueCopyValueAtIndex(phoneNumbers, i);
                        [tempArray addObject:[self stripSeparators:phoneNumberFromAB]];
                    }
                }
                CFRelease(phoneNumbers);
            }
            contact.numbers = tempArray;
            
            while((contactNumber != 0)
                && (([((Contact *)([self.contacts objectAtIndex:contactNumber-1])).firstName localizedStandardCompare:contact.firstName] == NSOrderedDescending)
                 || (([((Contact *)([self.contacts objectAtIndex:contactNumber-1])).firstName localizedStandardCompare:contact.firstName] == NSOrderedSame)
                  && ([((Contact *)([self.contacts objectAtIndex:contactNumber-1])).lastName localizedStandardCompare:contact.lastName] == NSOrderedDescending)))) {
                contactNumber--;
            }
            while((contactNumber < self.contacts.count)
                && (([((Contact *)([self.contacts objectAtIndex:contactNumber])).firstName localizedStandardCompare:contact.firstName] == NSOrderedAscending)
                 || (([((Contact *)([self.contacts objectAtIndex:contactNumber])).firstName localizedStandardCompare:contact.firstName] == NSOrderedSame)
                  && !([((Contact *)([self.contacts objectAtIndex:contactNumber])).lastName localizedStandardCompare:contact.lastName] == NSOrderedDescending)))) {
                contactNumber++;
            }
            [self.contacts insertObject:contact atIndex:contactNumber];
        }
    }
    CFRelease(sourcesArray);
}

- (ABAddressBookRef) addressBook
{
    return staticAddressBook;
}

- (NSArray *) allContactsFromAddressBook_
{
    if (self.addressBookChangeCounter != self.addressBookChangeAck) {
        self.addressBookChangeAck = self.addressBookChangeCounter;
        [self addressBookReload];
    }
    return self.contacts;
}

- (void) addressBookChanged:(ABAddressBookRef)addressBook info:(CFDictionaryRef)info context:(void *)context
{
    self.addressBookChangeCounter++;
}

- (Contact *) findContactWithPhoneNumber_:(NSString *)number
{
    NSArray *contacts = [self allContactsFromAddressBook_];
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"numbers contains %@", number];
    NSArray *filteredArray = [contacts filteredArrayUsingPredicate:predicate];
    if (((filteredArray == nil) || (filteredArray.count == 0))
     &&(self.dial_location_country_code != nil)
     &&(self.dial_location_country_code.length != 0)
     &&(self.dial_location_national_prefix != nil)
     &&(self.dial_location_international_prefix != nil)
     &&[number hasPrefix:@"+"]) {
        if(self.dial_location_international_prefix.length != 0) {
            predicate = [NSPredicate predicateWithFormat:@"numbers contains %@",
                [NSString stringWithFormat:@"%@%@", self.dial_location_international_prefix, [number substringFromIndex:1]]];
            filteredArray = [contacts filteredArrayUsingPredicate:predicate];
        }
        if (((filteredArray == nil) || (filteredArray.count == 0))
         &&[number hasPrefix:[NSString stringWithFormat:@"+%@", self.dial_location_country_code]]) {
            if ([[number substringFromIndex:1 + self.dial_location_country_code.length] hasPrefix:self.dial_location_national_prefix]) {
                predicate = [NSPredicate predicateWithFormat:@"numbers contains %@",
                    [number substringFromIndex:1 + self.dial_location_country_code.length]];
            }
            else {
                predicate = [NSPredicate predicateWithFormat:@"numbers contains %@",
                    [NSString stringWithFormat:@"%@%@", self.dial_location_national_prefix, [number substringFromIndex:1 + self.dial_location_country_code.length]]];
            }
            filteredArray = [contacts filteredArrayUsingPredicate:predicate];
        }
    }
    if ((filteredArray != nil) && (filteredArray.count != 0)) {
        return [filteredArray objectAtIndex:0];
    }
    return nil;
}

- (NSArray *) contactsForSearchString_:(NSString *)searchString
{
    NSArray *contacts = [self allContactsFromAddressBook_];
    NSPredicate *predicate;
    NSString *first_part, *last_part;
    int i, n;

    i = 0;
    while((i < searchString.length) && ([searchString characterAtIndex:i] != ' ')) {
        i++;
    }
    n = i;
    while((i < searchString.length) && ([searchString characterAtIndex:i] == ' ')) {
        i++;
    }
    first_part = [searchString substringToIndex:n];
    if (i < searchString.length) {
        last_part = [searchString substringFromIndex:i];
        predicate = [NSPredicate predicateWithFormat:@"firstName beginswith[cd] %@ AND lastName beginswith[cd] %@ OR firstName beginswith[cd] %@ AND lastName beginswith[cd] %@ OR company beginswith[cd] %@ OR numbers contains %@",
            first_part, last_part, last_part, first_part, first_part, first_part];
    }
    else if (n != 0) {
        predicate = [NSPredicate predicateWithFormat:@"firstName beginswith[cd] %@ OR lastName beginswith[cd] %@ OR company beginswith[cd] %@ OR numbers contains %@",
            first_part, first_part, first_part, first_part];
    }
    else {
        predicate = [NSPredicate predicateWithFormat:@"TRUEPREDICATE"];
    }
    return [contacts filteredArrayUsingPredicate:predicate];
}

- (ABRecordRef) personForRecordID_:(int32_t)recordID
{
    if ((staticAddressBook == nil) || (ABAddressBookGetAuthorizationStatus() != kABAuthorizationStatusAuthorized)) {
        return nil;
    }
    return ABAddressBookGetPersonWithRecordID(staticAddressBook, recordID);
}

- (void) forms_event_webcam_added_:(NSString *)id facing_front:(bool)facing_front orientation:(int)orientation context:(NSString *)context description:(NSString *)description
{
    self.ios_event.enqueue(FORMS_EVENT_WEBCAM_ADDED, id, facing_front, orientation, context, description);
}

- (void) forms_event_webcam_query_result_:(NSString *)context
{
    self.ios_event.enqueue(FORMS_EVENT_WEBCAM_QUERY_RESULT, context);
}

- (void) forms_event_webcam_failed_:(NSString *)id context:(NSString *)context
{
    self.ios_event.enqueue(FORMS_EVENT_WEBCAM_FAILED, id, context);
}

- (void) forms_event_call_error_:(NSString *)uuid error:(int)error
{
    self.ios_event.enqueue(FORMS_EVENT_CX_CALL_ERROR, uuid, error);
}

- (void) forms_event_open_permalink_:(NSString *)link
{
    self.ios_event.enqueue(FORMS_EVENT_OPEN_PERMALINK, link);
}

- (void) blankCamerasTimeout:(id)context
{
    self.camerasBlanked = true;
    [self configureVideoSessions];
}

- (void) updateContentSize
{
    [[UINavigationBar appearance] setTitleTextAttributes:
        [NSDictionary dictionaryWithObjectsAndKeys:
            [PhoneIosDelegate colorRGB:COLOR_TEXT_DARK_SCHEME],
            UITextAttributeTextColor,
            nil,
            UITextAttributeTextShadowColor,
            nil,
            UITextAttributeTextShadowOffset,
            self.fontForCategory(FONT_CATEGORY_HEADLINE),
            UITextAttributeFont,
            nil]];

    if (self.window == nil) {
        return;
    }
    if (self.window.rootViewController != nil) {
        [(WebViewController *)(self.window.rootViewController) updateTextZoomFactor:((float)(fontSizeTable[FONT_CATEGORY_BODY][self.content_size_category])) / ((float)(fontSizeTable[FONT_CATEGORY_BODY][FORMS_CONTENT_SIZE_CATEGORY_L]))];
    }
}

- (void) proximityStateChange:(NSNotificationCenter *)notification
{
    bool nearby = [[UIDevice currentDevice] proximityState];
    NSLog(@"ProximityStateChange nearby %u", nearby);
    if (nearby) {
        if(self.hookstateOffhook && !self.headsetSwitched) {
            if(self.speakerphoneSwitched) {
                self.speakerphoneSwitched = false;
                self.trc(@"proximityStateChange: OFFHOOK");
//                self.forms.event_keypress(FORMS_KEY_OFFHOOK);
            }
        }
    }
    else {
        if(self.hookstateOffhook && !self.headsetSwitched) {
            if(self.speakerphoneEnabled && !self.speakerphoneSwitched) {
                self.speakerphoneSwitched = true;
//                self.forms.event_keypress(FORMS_KEY_LOUD);
            }
        }
    }
    self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"DSP/EARPIECE-PROXIMITY", nearby ? @"1" : @"0");
    self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/STOP_FEED_CONTAINERS", ((self.proximitySensorOn && nearby) || !self.didBecomeActive) ? @"1" : @"0");
    if (nearby != self.proximityNearby) {
        ActiveCamera *activeCamera;
        unsigned i;

        self.proximityNearby = nearby;
        for (i = 0; i < self.activeCameras.count; i++) {
            activeCamera = (ActiveCamera *)[self.activeCameras objectAtIndex:i];
            if ([self cameraFacingFront:activeCamera.uniqueID]) {
                [activeCamera blankOutVideo:self.proximitySensorOn && nearby];
            }
        }
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(blankCamerasTimeout:) object:nil];
        if (nearby) {
            if (!self.camerasBlanked) {
                [self performSelector:@selector(blankCamerasTimeout:) withObject:nil afterDelay:2];
            }
        }
        else {
            if (self.camerasBlanked) {
                self.camerasBlanked = false;
                [self configureVideoSessions];
            }
        }
    }
}

- (void) startProximitySensor_
{
    // set up proximity sensor
    if (!self.proximitySensorOn) {
        self.proximitySensorOn = true;
        NSLog(@"startProximitySensor");
        [[UIDevice currentDevice] setProximityMonitoringEnabled:YES];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(proximityStateChange:)
            name:UIDeviceProximityStateDidChangeNotification object:nil];
    }
}

- (void) stopProximitySensor_
{
    // disable proximity sensor
    if (self.proximitySensorOn) {
        self.proximitySensorOn = false;
        NSLog(@"stopProximitySensor");
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceProximityStateDidChangeNotification object:nil];
        [[UIDevice currentDevice] setProximityMonitoringEnabled:NO];
        if(self.hookstateOffhook && !self.headsetSwitched) {
            if(self.speakerphoneEnabled && !self.speakerphoneSwitched) {
                self.speakerphoneSwitched = true;
//                self.forms.event_keypress(FORMS_KEY_LOUD);
            }
        }
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"DSP/EARPIECE-PROXIMITY", @"0");
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/STOP_FEED_CONTAINERS", !self.didBecomeActive ? @"1" : @"0");
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(blankCamerasTimeout:) object:nil];
        if (self.camerasBlanked) {
            self.camerasBlanked = false;
            [self configureVideoSessions];
        }
    }
}

- (void) updateProximitySensor_
{
    if(self.hookstateOffhook && !self.headsetSwitched && !self.screen_capture_started && (UIDevice.currentDevice.orientation != UIDeviceOrientationLandscapeLeft) && (UIDevice.currentDevice.orientation != UIDeviceOrientationLandscapeRight)) {
        self.startProximitySensor();
    }
    else {
        self.stopProximitySensor();
    }
/*
    if(self.hookstateOffhook || self.callPresentation) {
        self.registerRemoteControl();
    }
    else {
        self.unregisterRemoteControl();
        [self checkNotifyObsolete];
    }
*/
}

- (void) remoteControlRunnable:(id)context
{
    self.updateProximitySensor();
}

- (void) onRemoteControl:(UIEvent *)theEvent
{
    NSLog(@"onRemoteControl");
    if(self.hookstateOffhook) {
        self.speakerphoneEnabled = false;
        self.microphoneMuted = false;
        if(self.headsetSwitched) {
            self.headsetSwitched = false;
//            self.forms.event_keypress(FORMS_KEY_HEADSET);
        }
        if(self.speakerphoneSwitched) {
            self.speakerphoneSwitched = false;
//            self.forms.event_keypress(FORMS_KEY_LOUD);
        }
        self.trc(@"onRemoteControl: ONHOOK");
//        self.forms.event_keypress(FORMS_KEY_ONHOOK);
    }
    else {
        self.trc(@"onRemoteControl: OFFHOOK");
//        self.forms.event_keypress(FORMS_KEY_OFFHOOK);
        if(self.headsetEnabled && !self.headsetSwitched) {
            self.headsetSwitched = true;
//            self.forms.event_keypress(FORMS_KEY_HEADSET);
        }
    }
    self.hookstateOffhook = !self.hookstateOffhook;
    self.dtmfActive = self.hookstateOffhook;
    [self performSelectorOnMainThread:@selector(remoteControlRunnable:) withObject:nil waitUntilDone:NO];
}

- (void) remoteControlReceivedWithEvent:(UIEvent *)theEvent 
{
    NSLog(@"remoteControlReceivedWithEvent %d at delegate", (int)(theEvent.type));
    if (theEvent.type == UIEventTypeRemoteControl)
    {
        switch(theEvent.subtype) {
        case UIEventSubtypeRemoteControlTogglePlayPause:
            [self onRemoteControl:theEvent];
            break;
        case UIEventSubtypeRemoteControlPlay:
            break;
        case UIEventSubtypeRemoteControlPause:
            break;
        case UIEventSubtypeRemoteControlStop:
            break;
        default:
            return;
        }
    }
}

- (void) registerRemoteControl_
{
    if (!self.uiStarted || self.remoteControlReceiverRegistered) {
        return;
    }
    self.remoteControlReceiverRegistered = true;
/*
    if ([MPRemoteCommandCenter respondsToSelector:@selector(sharedCommandCenter)]) {
        NSLog(@"register togglePlayPauseCommand");
        [[MPRemoteCommandCenter sharedCommandCenter].togglePlayPauseCommand addTarget:self action:@selector(onRemoteControl:)];
        [MPRemoteCommandCenter sharedCommandCenter].togglePlayPauseCommand.enabled = true;
    }
    else
*/
    {
        NSLog(@"beginReceivingRemoteControlEvents");
        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    }
}

- (void) unregisterRemoteControl_
{
    if (!self.remoteControlReceiverRegistered) {
        return;
    }
    self.remoteControlReceiverRegistered = false;
/*
    if ([MPRemoteCommandCenter respondsToSelector:@selector(sharedCommandCenter)]) {
        NSLog(@"unregister togglePlayPauseCommand");
        [[MPRemoteCommandCenter sharedCommandCenter].togglePlayPauseCommand removeTarget:self action:@selector(onRemoteControl)];
        [MPRemoteCommandCenter sharedCommandCenter].togglePlayPauseCommand.enabled = false;
    }
    else
*/
    {
        NSLog(@"endReceivingRemoteControlEvents");
        [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    }
}

- (void) on_display_on_if_charging_
{
    [UIApplication sharedApplication].idleTimerDisabled = false;
    if ((self.display_on_if_charging != FORMS_DISPLAY_ON_IF_CHARGING_NO) && self.powerPlugged) {
        [UIApplication sharedApplication].idleTimerDisabled = true;
    }
}

- (void) batteryStateChange:(NSNotificationCenter *)notification
{
    if ([[UIDevice currentDevice] batteryState] == UIDeviceBatteryStateUnknown) {
        return;
    }
    if (([[UIDevice currentDevice] batteryState] != UIDeviceBatteryStateUnplugged) ^ self.powerPlugged) {
        self.powerPlugged = !self.powerPlugged;
        if (self.powerPlugged) {
            self.trc(@"onPowerConnected");
            if (self.docking_switches_hook) {
                if(self.hookstateOffhook) {
                    self.hookstateOffhook = false;
                    self.speakerphoneEnabled = false;
                    self.microphoneMuted = false;
                    if(self.headsetSwitched) {
                        self.headsetSwitched = false;
//                        self.forms.event_keypress(FORMS_KEY_HEADSET);
                    }
                    if(self.speakerphoneSwitched) {
                        self.speakerphoneSwitched = false;
//                        self.forms.event_keypress(FORMS_KEY_LOUD);
                    }
                    self.trc(@"onPowerConnected: ONHOOK");
//                    self.forms.event_keypress(FORMS_KEY_ONHOOK);
                    self.dtmfActive = false;
//                    update_phone_screen();
                    self.updateProximitySensor();
                }
            }
        }
        else {
            self.trc(@"onPowerDiscnnected");
            if (self.docking_switches_hook) {
                if(self.callPresentation) {
                    if(!self.hookstateOffhook) {
                        self.hookstateOffhook = true;
                        self.trc(@"onPowerDisconnected: OFFHOOK");
//                        self.forms.event_keypress(FORMS_KEY_OFFHOOK);
//                        update_phone_screen();
                        if(self.headsetEnabled && !self.headsetSwitched) {
                            self.headsetSwitched = true;
//                            self.forms.event_keypress(FORMS_KEY_HEADSET);
                        }
                    }
                }
            }
        }
        self.on_display_on_if_charging();
    }
}

- (void) startBatteryMonitoring_
{
    // set up battery monitoring
    if (!self.batteryMonitoringOn) {
        NSLog(@"Enable Battery Monitoring");
        self.batteryMonitoringOn = true;
        [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryStateChange:)
            name:UIDeviceBatteryStateDidChangeNotification object:nil];
    }
}

- (void) stopBatteryMonitoring_
{
    if (self.batteryMonitoringOn) {
        self.batteryMonitoringOn = false;
        // disable battery monitoring
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceBatteryStateDidChangeNotification object:nil];
        [[UIDevice currentDevice] setBatteryMonitoringEnabled:NO];
    }
}

extern bool isBluetoothActive();

- (void) pollHeadset
{
    bool bluetooth = isBluetoothActive();
    if (!self.headsetPresent) {
        self.headsetPresent = true;
        self.forms.event_headset_plugged(true);
    }
    if (bluetooth ^ self.headsetEnabled) {
        self.headsetEnabled = bluetooth;
    }
}

- (void) pollTimeout:(id)context
{
    if ((self.forms != nil) && !self.didEnterBackground) {
        [self pollHeadset];
    }
    [self pollReachability];
    [self performSelector:@selector(pollTimeout:) withObject:nil afterDelay:1];
}

- (void) startPolling
{
    [self stopPolling];
    [self pollTimeout:nil];
}

- (void) stopPolling
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(pollTimeout:) object:nil];
}

- (void) on_webserver_started
{
    NSLog(@"on_webserver_started %d %d", self.webserver_started, self.uiStarted);
    if (self.webserver_started && self.uiStarted) {
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
        UIViewController *mainViewController = [[WebViewController alloc] initWithURL:[NSURL URLWithString:
//            @"http://localhost:10008/appproxy/ext/pbx.innovaphone.com:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"http://localhost:10008/appproxy/ext/172.16.0.10:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"http://localhost:10008/appproxy/ext/172.16.99.217:80/PBX0/APPCLIENT/BUILD/appclient.htm"]]];
//            @"http://localhost:10008/appproxy/ext/172.16.99.217:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"http://172.16.99.217:80/PBX0/APPCLIENT/BUILD/appclient.htm"]]];
//            @"http://localhost:10008/ext/pbx.innovaphone.com:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"https://webrtc.github.io/samples/src/content/peerconnection/pc1/"]]];
//            @"http://172.16.19.210/PBX0/APPCLIENT/appclient.htm?lang=de"]]];
//            @"https://codepen.io/anon/pen/xEdwop"]]];
            self.url_to_load]];
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        self.window.rootViewController = mainViewController;
        ((WebViewController *)(self.window.rootViewController)).statusBarHidden = self.statusBarHidden;
        ((WebViewController *)(self.window.rootViewController)).statusBarStyle = self.statusBarStyle;
        ((WebViewController *)(self.window.rootViewController)).statusBarColor = self.statusBarColor;
        ((WebViewController *)(self.window.rootViewController)).statusBarTextColor = self.statusBarTextColor;
        [self.window makeKeyAndVisible];
#else
        [((WebViewController *)(self.window.rootViewController)).webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:
//            @"http://localhost:10008/appproxy/ext/pbx.innovaphone.com:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"http://localhost:10008/appproxy/ext/172.16.0.10:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"http://localhost:10008/appproxy/ext/172.16.99.217:80/PBX0/APPCLIENT/BUILD/appclient.htm"]]];
//            @"http://localhost:10008/appproxy/ext/172.16.99.217:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"http://172.16.99.217:80/PBX0/APPCLIENT/BUILD/appclient.htm"]]];
//            @"http://localhost:10008/ext/pbx.innovaphone.com:80/PBX0/APPCLIENT/appclient.htm"]]];
//            @"https://webrtc.github.io/samples/src/content/peerconnection/pc1/"]]];
//            @"http://172.16.19.210/PBX0/APPCLIENT/appclient.htm?lang=de"]]];
//            @"https://codepen.io/anon/pen/xEdwop"]]];
            self.url_to_load]]];
#endif
    }
}

- (void) startFixCert_
{
    if (!self.fix_cert_running) {
        self.fix_cert_running = true;
#ifdef CLIENT_IOS_USE_SFSAFARIVIEW
#else
        [((WebViewController *)(self.window.rootViewController)).webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.start_fix_cert_url]]];
#endif
    }
}

- (WebViewController *) webViewController_
{
    return ((WebViewController *)(self.window.rootViewController));
}

+ (void) registerUNUserNotificationSettings:(UNUserNotificationCenter *)center
{
    // Setup each action thar will appear in the notification
    UNNotificationAction *acceptAction = ([UNNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:)]) ?
        [UNNotificationAction actionWithIdentifier:@ACCEPT_ACTION title:@"Accept"/*self.forms.localized_texts[FORMS_TXT_ACCEPT]*/ options:UNNotificationActionOptionNone icon:[UNNotificationActionIcon iconWithSystemImageName:@"phone.fill"]] :
        [UNNotificationAction actionWithIdentifier:@ACCEPT_ACTION title:@"Accept"/*self.forms.localized_texts[FORMS_TXT_ACCEPT]*/ options:UNNotificationActionOptionNone];
    UNNotificationAction *rejectAction = ([UNNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:)]) ?
        [UNNotificationAction actionWithIdentifier:@REJECT_ACTION title:@"Reject"/*self.forms.localized_texts[FORMS_TXT_REJECT]*/ options:UNNotificationActionOptionDestructive icon:[UNNotificationActionIcon iconWithSystemImageName:@"phone.down.fill"]] :
        [UNNotificationAction actionWithIdentifier:@REJECT_ACTION title:@"Reject"/*self.forms.localized_texts[FORMS_TXT_REJECT]*/ options:UNNotificationActionOptionDestructive];
    UNNotificationCategory *callCat = [UNNotificationCategory categoryWithIdentifier:@CALL_CATEGORY actions:@[acceptAction, rejectAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction | UNNotificationCategoryOptionAllowInCarPlay];

    UNNotificationAction *disconnectAction = ([UNNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:)]) ?
        [UNNotificationAction actionWithIdentifier:@DISCONNECT_ACTION title:@"Disconnect"/*self.forms.localized_texts[FORMS_TXT_DISCONNECT]*/ options:UNNotificationActionOptionDestructive icon:[UNNotificationActionIcon iconWithSystemImageName:@"phone.down.fill"]] :
        [UNNotificationAction actionWithIdentifier:@DISCONNECT_ACTION title:@"Disconnect"/*self.forms.localized_texts[FORMS_TXT_DISCONNECT]*/ options:UNNotificationActionOptionDestructive];
    UNNotificationCategory *connCat = [UNNotificationCategory categoryWithIdentifier:@CONN_CATEGORY actions:@[disconnectAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction | UNNotificationCategoryOptionAllowInCarPlay];

    UNNotificationAction *callbackAction = ([UNNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:)]) ?
        [UNNotificationAction actionWithIdentifier:@CALLBACK_ACTION title:@"Callback"/*self.forms.localized_texts[FORMS_TXT_CALLBACK]*/ options:UNNotificationActionOptionNone icon:[UNNotificationActionIcon iconWithSystemImageName:@"phone.fill"]] :
        [UNNotificationAction actionWithIdentifier:@CALLBACK_ACTION title:@"Callback"/*self.forms.localized_texts[FORMS_TXT_CALLBACK]*/ options:UNNotificationActionOptionNone];
    UNNotificationCategory *missedCat = [UNNotificationCategory categoryWithIdentifier:@MISSED_CATEGORY actions:@[callbackAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction | UNNotificationCategoryOptionAllowInCarPlay | UNNotificationActionOptionAuthenticationRequired];

    UNNotificationAction *yesAction = ([UNNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:)]) ?
        [UNNotificationAction actionWithIdentifier:@YES_ACTION title:@"Yes"/*self.forms.localized_texts[FORMS_TXT_YES]*/ options:UNNotificationActionOptionNone icon:[UNNotificationActionIcon iconWithSystemImageName:@"checkmark"]] :
        [UNNotificationAction actionWithIdentifier:@YES_ACTION title:@"Yes"/*self.forms.localized_texts[FORMS_TXT_YES]*/ options:UNNotificationActionOptionNone];
    UNNotificationAction *noAction = ([UNNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:)]) ?
        [UNNotificationAction actionWithIdentifier:@NO_ACTION title:@"No"/*self.forms.localized_texts[FORMS_TXT_NO]*/ options:UNNotificationActionOptionNone icon:[UNNotificationActionIcon iconWithSystemImageName:@"xmark"]] :
        [UNNotificationAction actionWithIdentifier:@NO_ACTION title:@"No"/*self.forms.localized_texts[FORMS_TXT_NO]*/ options:UNNotificationActionOptionNone];
    UNNotificationCategory *decisionCat = [UNNotificationCategory categoryWithIdentifier:@DECISION_CATEGORY actions:@[yesAction, noAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction | UNNotificationCategoryOptionAllowInCarPlay];

    UNTextInputNotificationAction *replyAction = ([UNTextInputNotificationAction respondsToSelector:@selector(actionWithIdentifier:title:options:icon:textInputButtonTitle:textInputPlaceholder:)]) ?
        [UNTextInputNotificationAction actionWithIdentifier:@REPLY_ACTION title:@"Reply"/*self.forms.localized_texts[FORMS_TXT_REPLY]*/ options:UNNotificationActionOptionNone icon:[UNNotificationActionIcon iconWithSystemImageName:@"bubble.fill"]
            textInputButtonTitle:@"Reply"/*self.forms.localized_texts[FORMS_TXT_REPLY]*/ textInputPlaceholder:@""] :
        [UNTextInputNotificationAction actionWithIdentifier:@REPLY_ACTION title:@"Reply"/*self.forms.localized_texts[FORMS_TXT_REPLY]*/ options:UNNotificationActionOptionNone
            textInputButtonTitle:@"Reply"/*self.forms.localized_texts[FORMS_TXT_REPLY]*/ textInputPlaceholder:@""];
    UNNotificationCategory *replyCat = [UNNotificationCategory categoryWithIdentifier:@REPLY_CATEGORY actions:@[replyAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction | UNNotificationCategoryOptionAllowInCarPlay];

    NSSet *categories = [NSSet setWithObjects:callCat, connCat, missedCat, decisionCat, replyCat, nil];
    [center setNotificationCategories:categories];
}

- (void) start_system
{
    int i;
    PhoneIosDelegate *__weak weakSelf = self;

    self.trc = ^ void (NSString *text) { weakSelf.ios_event.enqueue(FORMS_EVENT_TRACE, text); };
    self.calls_to_display = ^ bool () { return [weakSelf calls_to_display_]; };
    self.messages_to_display = ^ bool () { return [weakSelf messages_to_display_]; };
    self.no_media_call_mode = ^ bool () { return [weakSelf no_media_call_mode_]; };
    self.forms_event_shutdown = ^ void (bool user_shutdown) { [weakSelf forms_event_shutdown_:user_shutdown]; };
    self.dialContact = ^ void (NSString *phoneNumber) { [weakSelf dialContact_:phoneNumber]; };
    self.transferToContact = ^ void (NSString *phoneNumber) { [weakSelf dialContact_:phoneNumber]; };
    self.startProximitySensor = ^ void () { [weakSelf startProximitySensor_]; };
    self.stopProximitySensor = ^ void () { [weakSelf stopProximitySensor_]; };
    self.updateProximitySensor = ^ void () { [weakSelf updateProximitySensor_]; };
    self.startFixCert = ^ void () { [weakSelf startFixCert_]; };
    self.showToast = ^ void (NSString *text) { [weakSelf showToast_:text]; };
    self.webViewController = ^ WebViewController * () { return [weakSelf webViewController_]; };
    self.registerRemoteControl = ^ void () { [weakSelf registerRemoteControl_]; };
    self.unregisterRemoteControl = ^ void () { [weakSelf unregisterRemoteControl_]; };
    self.startBatteryMonitoring = ^ void () { [weakSelf startBatteryMonitoring_]; };
    self.stopBatteryMonitoring = ^ void () { [weakSelf stopBatteryMonitoring_]; };
    self.startOrientationMonitoring = ^ void () { [weakSelf startOrientationMonitoring_]; };
    self.stopOrientationMonitoring = ^ void () { [weakSelf stopOrientationMonitoring_]; };
    self.on_display_on_if_charging = ^ void () { [weakSelf on_display_on_if_charging_]; };
    self.contactsForSearchString = ^ NSArray * (NSString *searchString) { return [weakSelf contactsForSearchString_:searchString]; };
    self.personForRecordID = ^ ABRecordRef (int32_t recordID) { return [weakSelf personForRecordID_:recordID]; };
    self.forms_event_external_call = ^ void (CTCall *call) { [weakSelf forms_event_external_call_:call]; };
    self.forms_event_webcam_added = ^ void (NSString *id, bool facing_front, int orientation, NSString *context, NSString *description)
        { [weakSelf forms_event_webcam_added_:id facing_front:facing_front orientation:orientation context:context description:description]; };
    self.forms_event_webcam_query_result = ^ void (NSString *context) { [weakSelf forms_event_webcam_query_result_:context]; };
    self.forms_event_webcam_failed = ^ void (NSString *id, NSString *context) { [weakSelf forms_event_webcam_failed_:id context:context]; };
    self.forms_event_call_error = ^ void (NSString *uuid, int error) { [weakSelf forms_event_call_error_:uuid error:error]; };
    self.forms_event_open_permalink = ^ void (NSString *link) { [weakSelf forms_event_open_permalink_:link]; };
    self.fontForCategory = ^ UIFont * (int fontCategory) { return [weakSelf fontForCategory_:fontCategory]; };

    self.dtmfActive = false;
    self.hookstateOffhook = false;
    self.microphoneMuted = false;
    self.headsetPresent = false;
    self.headsetEnabled = false;
    self.headsetSwitched = false;
    self.speakerphoneEnabled = false;
    self.speakerphoneSwitched = false;

    unsigned int handler_count;
    handler_count = 0;
    while (ios_async_handler_table[handler_count].format) {
        handler_count++;
    }
    self.ios_async = [IosAsync iosAsyncWithHandlerTable:ios_async_handler_table handlerCount:handler_count iosAsyncSignal:self];
    handler_count = 0;
    while (ios_event_handler_table[handler_count].format) {
        handler_count++;
    }
    self.ios_event = [IosEvent iosEventWithHandlerTable:ios_event_handler_table handlerCount:handler_count];
    self.forms = [Forms formsWithId:0 ios_event:self.ios_event];
    self.forms.formsDelegate = self;
    self.formsCP = [FormsCP formsCPWithId:1 forms:self.forms];
    self.orientation_adaptation_off = false;
    self.display_on_if_charging = 0;
    self.addressBookAccessRequested = false;
    self.addressBookInaccessibleWarned = [self address_book_inaccessible_warned];
    self.dialer_claim = 0;
    self.gsm_use = 0;
    self.docking_switches_hook = false;
    self.registrationStateValue = 0/*REGISTRATION_NONE*/;
    self.dialingGSM = false;
    self.registrationStateUp = false;
    self.addressBookChangeCounter = 0;
    self.addressBookChangeAck = 0;
    self.callPresentation = false;
    self.lockedPresentation = false;
    self.restrictedUse = false;
    self.videoUp = false;
    self.recordPermissionRequested = false;
    self.recordPermissionGranted = false;
    self.didEnterBackground = false;
    self.didBecomeActive = false;
    self.proximitySensorOn = false;
    self.remoteControlReceiverRegistered = false;
    self.batteryMonitoringOn = false;
    self.orientationMonitoringOn = false;
    self.powerPlugged = false;
    self.internetReachability = nil;
    self.connectivityUp = false;
    self.signallingConnected = false;
    self.reachability_background_task = UIBackgroundTaskInvalid;
    self.two_factor_background_task = UIBackgroundTaskInvalid;
    self.reachability_ip_address = 0;
    self.uiStarted = false;
    self.manuallyStarted = false;
    self.remoteLocalNotifications = [[NSMutableArray alloc] init];
    self.pendingLocalNotifications = [[NSMutableArray alloc] init];
    self.presentedLocalNotifications = [[NSMutableArray alloc] init];
    self.pendingBadgeCounts = [[NSMutableArray alloc] init];
    self.notify_active_calls = false;
    self.notify_missed_calls = 0;
    self.notify_missed_msgs = 0;
    self.notify_voice_msgs = 0;
    self.audio_focus_loss = false;
    self.isShutdown = false;
    phone_ios_ring_in_headset = false;
    phone_ios_ios_phone = self.ios_phone = [[IosPhone alloc] init];
    self.webserver_started = false;
    self.statusBarHidden = false;
    self.statusBarStyle = UIStatusBarStyleLightContent;
    self.statusBarColor = [UIColor blackColor];
    self.statusBarTextColor = [PhoneIosDelegate colorRGB:COLOR_TEXT_DARK_SCHEME];
    self.user_agent = nil;
    self.webserver_url = nil;
    self.cert_string = nil;
    self.got_certificate = false;
    self.fix_cert_running = false;
    self.fixCertViewController = nil;
    phone_ios_log_browser = false;
    self.videoPermissionGranted = false;
    // Communicate with the session and other session objects on this queue.
    self.videoDispatchQueue = dispatch_queue_create("videoDispatchQueue", DISPATCH_QUEUE_SERIAL);
    self.webcam_active = false;
    self.activeCameras = [[NSMutableArray alloc] init];
    self.transcodingSessions = [[NSMutableArray alloc] init];
    self.previewCoder = FORMS_WEBCAM_CODER_H264;
    self.webcamCoder = FORMS_WEBCAM_CODER_VP8;
    self.camerasBlanked = false;
    self.proximityNearby = false;
    self.contactsSearchResults = 0;
    self.contactsSearchResult = nil;
    self.contactsSearchQueue = [[NSMutableArray alloc] init];
    self.webserverGetData = nil;
    self.webserverGetQueue = [[NSMutableArray alloc] init];
    self.cxCalls = [[NSMutableArray alloc] init];
    self.cxHideFromNativeCallList = false;
    self.cxProvider = nil;
    self.cxCallController = nil;
    self.evaluatingJavaScript = false;
    self.audioSessionActive = false;
    self.audioSessionActivated = false;
    self.screen_capture_started = false;
    self.screen_capture_running = false;
    self.screen_capture_stopped = false;
    self.snapshotConfiguration = nil;
    self.content_size_category = FORMS_CONTENT_SIZE_CATEGORY_L;
    [self updateContentSizeCategory];

    [[UINavigationBar appearance] setTitleTextAttributes:
        [NSDictionary dictionaryWithObjectsAndKeys:
            [PhoneIosDelegate colorRGB:COLOR_TEXT_DARK_SCHEME],
            UITextAttributeTextColor,
            nil,
            UITextAttributeTextShadowColor,
            nil,
            UITextAttributeTextShadowOffset,
            self.fontForCategory(FONT_CATEGORY_HEADLINE),
            UITextAttributeFont,
            nil]];

    self.ringtone_table = [[NSMutableArray alloc] init];
    NSMutableString *defaultTunes = [NSMutableString stringWithCapacity:1024];
    [defaultTunes appendString:@"<tunes>"];

    NSFileManager *fileMan = [NSFileManager defaultManager];
    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *ringtonesPath = [documentDir stringByAppendingPathComponent:@"Ringtones"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:ringtonesPath]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:ringtonesPath withIntermediateDirectories:NO attributes:nil error:nil];
    }
    NSArray *files = [fileMan contentsOfDirectoryAtPath:ringtonesPath error:NULL];
    for (i = 0; i < files.count; i++) {
        NSString *s = (NSString *)[files objectAtIndex:i];
        int start = s.length;
        int end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        [self.ringtone_table addObject:[s substringWithRange:NSMakeRange(start, end - start)]];
        [defaultTunes appendFormat:@"<tune tag='%d' title='%@'/>", i,
            [s substringWithRange:NSMakeRange(start, end - start)]];
    }

    NSArray *tunesPaths = [[NSBundle mainBundle] pathsForResourcesOfType:nil inDirectory:@"Ringtones"];
    for (i = 0; i < tunesPaths.count; i++) {
        NSString *s = (NSString *)[tunesPaths objectAtIndex:i];
        int start = s.length;
        int end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        if (![[s substringFromIndex:end] isEqualToString:@".mak"]) {
            [self.ringtone_table addObject:[s substringWithRange:NSMakeRange(start, end - start)]];
            [defaultTunes appendFormat:@"<tune tag='%d' title='%@'/>", i,
                [s substringWithRange:NSMakeRange(start, end - start)]];
        }
    }
    [defaultTunes appendString:@"</tunes>"];
    self.defaultTunesData = [NSData dataWithBytes:[defaultTunes cStringUsingEncoding:NSUTF8StringEncoding]
        length:[defaultTunes lengthOfBytesUsingEncoding:NSUTF8StringEncoding]+1];
    default_tunes = (const char *)(self.defaultTunesData.bytes);

    NSMutableString *defaultTones = [NSMutableString stringWithCapacity:1024];
    [defaultTones appendString:@"<tones>"];

    documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *tonesPath = [documentDir stringByAppendingPathComponent:@"NotificationTones"];
    if (![[NSFileManager defaultManager] fileExistsAtPath:tonesPath]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:tonesPath withIntermediateDirectories:NO attributes:nil error:nil];
    }
    files = [fileMan contentsOfDirectoryAtPath:tonesPath error:NULL];
    for (i = 0; i < files.count; i++) {
        NSString *s = (NSString *)[files objectAtIndex:i];
        int start = s.length;
        int end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        [self.ringtone_table addObject:[s substringWithRange:NSMakeRange(start, end - start)]];
        [defaultTones appendFormat:@"<tone tag='%d' title='%@'/>", i,
            [s substringWithRange:NSMakeRange(start, end - start)]];
    }

    NSArray *tonesPaths = [[NSBundle mainBundle] pathsForResourcesOfType:nil inDirectory:@"NotificationTones"];
    for (i = 0; i < tonesPaths.count; i++) {
        NSString *s = (NSString *)[tonesPaths objectAtIndex:i];
        int start = s.length;
        int end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        if (![[s substringFromIndex:end] isEqualToString:@".mak"]) {
            [self.ringtone_table addObject:[s substringWithRange:NSMakeRange(start, end - start)]];
            [defaultTones appendFormat:@"<tone tag='%d' title='%@'/>", i,
                [s substringWithRange:NSMakeRange(start, end - start)]];
        }
    }
    [defaultTones appendString:@"</tones>"];
    self.defaultTonesData = [NSData dataWithBytes:[defaultTones cStringUsingEncoding:NSUTF8StringEncoding]
        length:[defaultTones lengthOfBytesUsingEncoding:NSUTF8StringEncoding]+1];
    default_tones = (const char *)(self.defaultTonesData.bytes);

    self.playing_ringtone = -1;

    start_sys();

    NSString *countryCode = [NSLocale currentLocale].countryCode;
    if (![countryCode isEqualToString: @"CN"] && ![countryCode isEqualToString: @"CHN"] && NSClassFromString(@"CXProvider")) {
        NSString *ringtone = [self selected_ringtone];
        CXProviderConfiguration *cxProviderConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:@PHONE_IOS_LOG_APP_NAME];
        cxProviderConfiguration.ringtoneSound = [self tone_path_from_ident:ringtone ? ringtone.UTF8String : nil notification_tone:false];
        cxProviderConfiguration.iconTemplateImageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"CallKitIconTemplate" ofType:@"png"]];
        cxProviderConfiguration.supportsVideo = true;
        cxProviderConfiguration.maximumCallGroups = 2;
        cxProviderConfiguration.maximumCallsPerCallGroup = 2;
        cxProviderConfiguration.supportedHandleTypes = [NSSet setWithObjects:@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber), @(CXHandleTypeEmailAddress), nil];
        if ([cxProviderConfiguration respondsToSelector:@selector(includesCallsInRecents)]) {
            if (self.cxProvider == nil) {
                self.cxHideFromNativeCallList = [self hide_from_native_call_list];
            }
            cxProviderConfiguration.includesCallsInRecents = !self.cxHideFromNativeCallList;
        }
        self.cxProvider = [[CXProvider alloc] initWithConfiguration:cxProviderConfiguration];
        [self.cxProvider setDelegate:self queue:dispatch_get_main_queue()];
        self.cxCallController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];
    }
    self.ios_event.enqueue(FORMS_EVENT_CX_SUPPORTED,
#if TARGET_IPHONE_SIMULATOR
        false
#else
        (self.cxProvider != nil)
#endif
        );
    phone_ios_callCenter = self.callCenter = [[CTCallCenter alloc] init];
    if (self.cxProvider == nil) {
        // self.callCenter.callEventHandler = self.forms_event_external_call;
        if (self.callCenter.currentCalls != nil) {
            for (i = 0; i < self.callCenter.currentCalls.count; i++) {
                self.forms_event_external_call((CTCall *)[self.callCenter.currentCalls.allObjects objectAtIndex:i]);
            }
        }
    }

    // set up incoming call notification
    if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [PhoneIosDelegate registerUNUserNotificationSettings:center];
        NSLog(@"registerUserNotificationSettings");

        [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionCarPlay) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (error) {
                NSLog(@"UNUserNotificationCenter requestAuthorizationWithOptions failed %@", error);
            }
            else {
                NSLog(@"UNUserNotificationCenter requestAuthorizationWithOptions succeeded.");
            }
            // required to get the app to do anything at all about push notifications
            dispatch_async(dispatch_get_main_queue(), ^{
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate registerForRemoteNotification];
            });
        }];
    }
    else if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        // Setup each action thar will appear in the notification
        UIMutableUserNotificationAction *acceptAction = [[UIMutableUserNotificationAction alloc] init];
        acceptAction.identifier = @ACCEPT_ACTION; // Identifier is returned in handleActionWithIdentifier: after the user taps on an action
        acceptAction.title = @"Accept"/*self.forms.localized_texts[FORMS_TXT_ACCEPT]*/;
        acceptAction.authenticationRequired = false;
        acceptAction.destructive = false;
        acceptAction.activationMode = UIUserNotificationActivationModeBackground; //Allows app to remain the background when action tapped

        UIMutableUserNotificationAction *rejectAction = [[UIMutableUserNotificationAction alloc] init];
        rejectAction.identifier = @REJECT_ACTION;
        rejectAction.title = @"Reject"/*self.forms.localized_texts[FORMS_TXT_REJECT]*/;
        rejectAction.authenticationRequired = false;
        rejectAction.destructive = true;
        rejectAction.activationMode = UIUserNotificationActivationModeBackground; //Allows app to remain the background when action tapped

        UIMutableUserNotificationCategory *callCat = [[UIMutableUserNotificationCategory alloc] init];
        callCat.identifier = @CALL_CATEGORY;

        [callCat setActions:@[acceptAction, rejectAction] forContext:UIUserNotificationActionContextDefault];
        [callCat setActions:@[acceptAction, rejectAction] forContext:UIUserNotificationActionContextMinimal];

        UIMutableUserNotificationAction *disconnectAction = [[UIMutableUserNotificationAction alloc] init];
        disconnectAction.identifier = @DISCONNECT_ACTION;
        disconnectAction.title = @"Disconnect"/*self.forms.localized_texts[FORMS_TXT_DISCONNECT]*/;
        disconnectAction.authenticationRequired = false;
        disconnectAction.destructive = true;
        disconnectAction.activationMode = UIUserNotificationActivationModeBackground; //Allows app to remain the background when action tapped

        UIMutableUserNotificationCategory *connCat = [[UIMutableUserNotificationCategory alloc] init];
        connCat.identifier = @CONN_CATEGORY;

        [connCat setActions:@[disconnectAction] forContext:UIUserNotificationActionContextDefault];
        [connCat setActions:@[disconnectAction] forContext:UIUserNotificationActionContextMinimal];

        UIMutableUserNotificationAction *callbackAction = [[UIMutableUserNotificationAction alloc] init];
        callbackAction.identifier = @CALLBACK_ACTION;
        callbackAction.title = @"Callback"/*self.forms.localized_texts[FORMS_TXT_CALLBACK]*/;
        callbackAction.authenticationRequired = false;
        callbackAction.destructive = false;
        callbackAction.activationMode = UIUserNotificationActivationModeBackground; //Allows app to remain the background when action tapped

        UIMutableUserNotificationCategory *missedCat = [[UIMutableUserNotificationCategory alloc] init];
        missedCat.identifier = @MISSED_CATEGORY;

        [missedCat setActions:@[callbackAction] forContext:UIUserNotificationActionContextDefault];
        [missedCat setActions:@[callbackAction] forContext:UIUserNotificationActionContextMinimal];

        UIMutableUserNotificationAction *yesAction = [[UIMutableUserNotificationAction alloc] init];
        yesAction.identifier = @YES_ACTION; // Identifier is returned in handleActionWithIdentifier: after the user taps on an action
        yesAction.title = @"Yes"/*self.forms.localized_texts[FORMS_TXT_YES]*/;
        yesAction.authenticationRequired = false;
        yesAction.destructive = false;
        yesAction.activationMode = UIUserNotificationActivationModeBackground; //Allows app to remain the background when action tapped

        UIMutableUserNotificationAction *noAction = [[UIMutableUserNotificationAction alloc] init];
        noAction.identifier = @NO_ACTION;
        noAction.title = @"No"/*self.forms.localized_texts[FORMS_TXT_NO]*/;
        noAction.authenticationRequired = false;
        noAction.destructive = false;
        noAction.activationMode = UIUserNotificationActivationModeBackground; //Allows app to remain the background when action tapped

        UIMutableUserNotificationCategory *decisionCat = [[UIMutableUserNotificationCategory alloc] init];
        decisionCat.identifier = @DECISION_CATEGORY;

        [decisionCat setActions:@[yesAction, noAction] forContext:UIUserNotificationActionContextDefault];
        [decisionCat setActions:@[yesAction, noAction] forContext:UIUserNotificationActionContextMinimal];

        UIMutableUserNotificationCategory *replyCat = [[UIMutableUserNotificationCategory alloc] init];
        replyCat.identifier = @REPLY_CATEGORY;

        NSSet *categories = [NSSet setWithObjects:callCat, connCat, missedCat, decisionCat, replyCat, nil];

        [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:categories]];
        NSLog(@"registerUserNotificationSettings");
        [self registerForRemoteNotification];
    }
    else {
        [self registerForRemoteNotification];
    }

    [self try_start_ui:nil];
    self.forms.event_pause_subscriptions(self.didEnterBackground);
    [self configureVideoSessions];

    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
    [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
    [[UINavigationBar appearance] setTintColor:[PhoneIosDelegate colorRGB:COLOR_TEXT_DARK_SCHEME]];
}

#ifdef PHONE_IOS_USE_CRASHREPORTER

- (void) handleCrashReport
{
    NSData *crashData;
    NSError *error;
    int i, CrashFileCount;

    // Try loading the crash report
    crashData = [self.crashReporter loadPendingCrashReportDataAndReturnError: &error];
    if (crashData != nil) {
        // We could send the report from here, but we'll just print out
        // some debugging info instead
//      PLCrashReport *report = [[[PLCrashReport alloc] initWithData: crashData error: &error] autorelease];
        PLCrashReport *report = [[PLCrashReport alloc] initWithData: crashData error: &error];
        if (report != nil) {
/*
            NSLog(@"Crashed on %@", report.systemInfo.timestamp);
            NSLog(@"Crashed with signal %@ (code %@, address=0x%" PRIx64 ")", report.signalInfo.name,
            report.signalInfo.code, report.signalInfo.address);
*/
            error = nil;
            NSFileManager *fileManager = [NSFileManager defaultManager];
            NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];

            NSArray *dirs = [fileManager contentsOfDirectoryAtPath:documentDir error:NULL];
            do {
				NSString *oldestCrash = nil;
				CrashFileCount=0;
				for (i = 0; i < dirs.count; i++) {
					NSString *filename = (NSString *)[dirs objectAtIndex:i];
					if ([filename hasPrefix:@CRASH_FILE_NAME  ]) {
						if ((oldestCrash == nil) || ([filename compare:oldestCrash] == NSOrderedAscending)) {
							oldestCrash = filename;
						}
						CrashFileCount++;
					}
				}
				if (CrashFileCount >= 2 * MAX_CRASH_COUNT){
					CrashFileCount--;
					if (![fileManager removeItemAtPath:[documentDir stringByAppendingPathComponent:oldestCrash] error:&error]) {
						NSLog(@"Failed to remove '%@': %@", oldestCrash, error.localizedDescription);
						CrashFileCount = 0;
					}
				}
			}
			while (CrashFileCount >= 2 * MAX_CRASH_COUNT);

            NSString *humanReadable = [PLCrashReportTextFormatter stringValueForCrashReport:report withTextFormat:PLCrashReportTextFormatiOS];
            NSLog(@"Report: %@", humanReadable);

            struct timeval tv;
            time_t tlocal;
            struct tm tm;
            gettimeofday(&tv, (struct timezone *)NULL);
            tlocal = tv.tv_sec;
            localtime_r(&tlocal, &tm);

            NSString *crashFilename = [NSString stringWithFormat:@"%s-%04d-%02d-%02d-%02d_%02d_%02d%s",
                CRASH_FILE_NAME, 1900+tm.tm_year, 1+tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, CRASH_FILE_EXT];
            NSString *crashPath = [documentDir stringByAppendingPathComponent:crashFilename];
            if (![humanReadable writeToFile:crashPath atomically:YES encoding:NSUTF8StringEncoding error:&error]) {
                NSLog(@"Failed to write '%@': %@", crashPath, error.localizedDescription);
            }

            NSString *crashbinFilename = [NSString stringWithFormat:@"%s-%04d-%02d-%02d-%02d_%02d_%02d-bin%s",
                CRASH_FILE_NAME, 1900+tm.tm_year, 1+tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, CRASH_FILE_EXT];
            NSString *crashbinPath = [documentDir stringByAppendingPathComponent:crashbinFilename];
            [crashData writeToFile:crashbinPath atomically:YES];

            // Purge the report
            [self.crashReporter purgePendingCrashReport];
        }
    }
}


PLCrashReporterCallbacks CallBacks={0};

void CrashCallBack(siginfo_t *info, ucontext_t *uap, void *context)
{
    bool restart_via_redirect;
    NSLog(@"==============CrashCallBack");
    restart_via_redirect = false;
    if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"com.innovaphone.clientiosrestart:"]]) {
        [PhoneIosDelegate openURL:[NSURL URLWithString:@"com.innovaphone.clientiosrestart:"]];
    }
    else {
        restart_via_redirect = true;
    }
    poll_log();
    close_log();
    if (restart_via_redirect) {
        [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate restartViaRedirect];
    }
}

#endif

- (bool) push_autostart
{
    NSString *filesDir, *content;

    filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [filesDir stringByAppendingPathComponent:@CONFIG_FILE_NAME];
    content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    if (content == nil) {
        return nil;
    }
    JsonIo *json_io = [JsonIo jsonIo];
    json_io.decode(content);
    word root = json_io.get_object(JSON_ID_ROOT, nil);
    word noPushAutostart = json_io.get_object(root, @"noPushAutostart");
    NSString *value = (noPushAutostart != JSON_ID_NONE) ? json_io.get_value(noPushAutostart, 0, @"value") : nil;
    return ((value == nil) || ![value isEqualToString:@"true"]);
}

- (NSString *) push_password
{
    NSString *filesDir, *content;

    filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [filesDir stringByAppendingPathComponent:@CONFIG_FILE_NAME];
    content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    if (content == nil) {
        NSLog(@"no password");
        return nil;
    }
    JsonIo *json_io = [JsonIo jsonIo];
    json_io.decode(content);
    word root = json_io.get_object(JSON_ID_ROOT, nil);
    NSString *pushPassword = json_io.get_string(root, @"pushPassword");
    if (pushPassword == nil) {
        NSLog(@"no password %@", content);
    }
    return (pushPassword);
}

- (NSString *) selected_ringtone
{
    NSString *filesDir, *content;

    filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [filesDir stringByAppendingPathComponent:@CONFIG_FILE_NAME];
    content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    if (content == nil) {
        return nil;
    }
    JsonIo *json_io = [JsonIo jsonIo];
    json_io.decode(content);
    word root = json_io.get_object(JSON_ID_ROOT, nil);
    word localStorage = json_io.get_object(root, @"localStorage");
    NSString *ringerSelectedRingtones = json_io.get_string(localStorage, @"ringerSelectedRingtones");
    if (ringerSelectedRingtones == nil) {
        return nil;
    }
    JsonIo *ringtones_json_io = [JsonIo jsonIo];
    ringtones_json_io.decode(ringerSelectedRingtones);
    word ringtones_root = ringtones_json_io.get_object(JSON_ID_ROOT, nil);
    word ringtones = ringtones_json_io.get_array(ringtones_root, @"ringtones");
    word ringtone = ringtones_json_io.get_first(JSON_TYPE_ANY, ringtones);
    return (ringtones_json_io.get_string(ringtone, @"ringtone"));
}

- (bool) hide_from_native_call_list
{
    NSString *filesDir, *content;

    filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [filesDir stringByAppendingPathComponent:@CONFIG_FILE_NAME];
    content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    if (content == nil) {
        return nil;
    }
    JsonIo *json_io = [JsonIo jsonIo];
    json_io.decode(content);
    word root = json_io.get_object(JSON_ID_ROOT, nil);
    word hideFromNativeCallList = json_io.get_object(root, @"hideFromNativeCallList");
    NSString *value = (hideFromNativeCallList != JSON_ID_NONE) ? json_io.get_value(hideFromNativeCallList, 0, @"value") : nil;
    return ((value != nil) && [value isEqualToString:@"true"]);
}

- (bool) address_book_inaccessible_warned
{
    NSString *filesDir, *content;

    filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *path = [filesDir stringByAppendingPathComponent:@CONFIG_FILE_NAME];
    content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    if (content == nil) {
        return nil;
    }
    JsonIo *json_io = [JsonIo jsonIo];
    json_io.decode(content);
    word root = json_io.get_object(JSON_ID_ROOT, nil);
    NSString *addressBookInaccessibleWarned = json_io.get_value(root, 0, @"addressBookInaccessibleWarned");
    return ((addressBookInaccessibleWarned != nil) && [addressBookInaccessibleWarned isEqualToString:@"true"]);
}

- (void) registerForRemoteNotification
{
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert;
        UIUserNotificationSettings *notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings];
    }
    else {
        UIRemoteNotificationType types = UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert;
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types];
    }
}

- (BOOL) isPushNotificationOn
{
    if ([UIApplication instancesRespondToSelector:@selector(isRegisteredForRemoteNotifications)]) {
        return ([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
    } else {
        UIRemoteNotificationType types = [UIApplication sharedApplication].enabledRemoteNotificationTypes;
        return ((types & UIRemoteNotificationTypeAlert) != 0);
    }
}

- (void) application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    NSLog(@"didRegisterUserNotificationSettings %lx", (unsigned long)([application currentUserNotificationSettings].types));
    if (notificationSettings.types != UIUserNotificationTypeNone) {
        [application registerForRemoteNotifications];

        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        // Create a push registry object
        PKPushRegistry * voipRegistry = [[PKPushRegistry alloc] initWithQueue: mainQueue];
        // Set the registry's delegate to self
        voipRegistry.delegate = self;
        // Set the push type to VoIP
        voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
    }
}

- (void) application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"didFailToRegisterForRemoteNotificationsWithError: %@", error);
}

- (void) application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
static char hex_digit_table[] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
    char dump_buffer[256];
    int i;
    
    self.apnToken = deviceToken;
    for (i = 0; i < deviceToken.length; i++) {
        dump_buffer[i<<1] = hex_digit_table[((byte *)(deviceToken.bytes))[i] >> 4];
        dump_buffer[(i<<1) | 1] = hex_digit_table[((byte *)(deviceToken.bytes))[i] & 0xf];
    }
    dump_buffer[i<<1] = '\0';
    NSLog(@"didRegisterForRemoteNotificationsWithDeviceToken: %s%@%@%@", dump_buffer,
        (application.enabledRemoteNotificationTypes & UIRemoteNotificationTypeBadge) ? @" Badge" : @"",
        (application.enabledRemoteNotificationTypes & UIRemoteNotificationTypeSound) ? @" Sound" : @"",
        (application.enabledRemoteNotificationTypes & UIRemoteNotificationTypeAlert) ? @" Alert" : @"");
    if (self.webserver_started) {
        self.forms.event_push_token(self.apnToken, self.apnTokenVoIP);
    }
}

- (void) pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(NSString *)type
{
    // Register VoIP push token (a property of PKPushCredentials) with server
    NSString *countryCode = [NSLocale currentLocale].countryCode;
    NSLog(@"didUpdatePushCredentials: %@ %@ %@", type, countryCode, pushCredentials.token);
    if ([type isEqualToString:PKPushTypeVoIP]) {
        self.apnTokenVoIP = (![countryCode isEqualToString: @"CN"] && ![countryCode isEqualToString: @"CHN"]) ? pushCredentials.token : nil;
        if (self.webserver_started) {
            self.forms.event_push_token(self.apnToken, self.apnTokenVoIP);
        }
    }
}

- (void) pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(NSString *)type
{
    NSLog(@"didInvalidatePushTokenForType: %@", type);
}

- (BOOL) application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    if (![self shouldOpenURL:[launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]
        sourceApplication:[launchOptions objectForKey:UIApplicationLaunchOptionsSourceApplicationKey]]) {
        return NO;
    }
    return YES;
}

- (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSError *error;

    open_log();
    self.launchOptions = launchOptions;
    if (![self shouldOpenURL:[launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]
        sourceApplication:[launchOptions objectForKey:UIApplicationLaunchOptionsSourceApplicationKey]]) {
        return NO;
    }
    [[UISegmentedControl appearance] setTintColor:[UIColor clearColor]];	  

    NSString *filesDir, *downloadsDir;
    NSError *errorC;
    BOOL isDir;

    NSFileManager *fileManager = [NSFileManager defaultManager];

    filesDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    if (![fileManager fileExistsAtPath:filesDir isDirectory:&isDir]) {
        if (![fileManager createDirectoryAtPath:filesDir withIntermediateDirectories:YES attributes:nil error:&errorC]) {
            NSLog(@"Create files directory %@ failed: %@", filesDir, errorC.description);
        }
    }
    downloadsDir = [filesDir stringByAppendingPathComponent:@"Downloads"];
    if (![fileManager fileExistsAtPath:downloadsDir isDirectory:&isDir]) {
        if (![fileManager createDirectoryAtPath:downloadsDir withIntermediateDirectories:YES attributes:nil error:&errorC]) {
            NSLog(@"Create downloads directory %@ failed: %@", downloadsDir, errorC.description);
        }
    }

#if TARGET_IPHONE_SIMULATOR
    NSLog(@"currentDirectoryPath %@", fileManager.currentDirectoryPath);
    if (![fileManager changeCurrentDirectoryPath:filesDir]) {
        NSLog(@"Change current directory %@ failed", filesDir);
    }
    NSLog(@"Changed currentDirectoryPath %@", fileManager.currentDirectoryPath);
#endif
    strcpy(phone_ios_files_dir_container, filesDir.UTF8String);

#ifdef PHONE_IOS_USE_CRASHREPORTER
    PLCrashReporterConfig *crashConfig = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
        symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
    self.crashReporter = [[PLCrashReporter alloc] initWithConfiguration:crashConfig];

    // Check if we previously crashed
    if ([self.crashReporter hasPendingCrashReport]) {
        [self handleCrashReport];
    }
    CallBacks.handleSignal = &CrashCallBack;
    [self.crashReporter setCrashCallbacks:&CallBacks];

    // Enable the Crash Reporter
    if (![self.crashReporter enableCrashReporterAndReturnError:&error]){
        NSLog(@"Warning: Could not enable crash reporter: %@", error);
    }
#endif

    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) objectAtIndex:0];
    [[NSUserDefaults standardUserDefaults] setObject:[documentDir stringByAppendingPathComponent:@"LocalStorage"] forKey:@"WebKitLocalStorageDatabasePathPreferenceKey"];
    [[NSUserDefaults standardUserDefaults] setObject:[documentDir stringByAppendingPathComponent:@"WebDatabase"] forKey:@"WebDatabaseDirectory"];
    [[NSUserDefaults standardUserDefaults] synchronize];

//    [[NSURLCache sharedURLCache] removeAllCachedResponses];
/*
    //Set Cache
    NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
    [NSURLCache setSharedURLCache:sharedCache];
*/
/*
    //Clear All Cookies
    for(NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
    }
*/
    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];

    if ([UIFont respondsToSelector:@selector(preferredFontForTextStyle:)]) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangePreferredContentSize:)
            name:UIContentSizeCategoryDidChangeNotification object:nil];
    }

    UIApplicationState state = [UIApplication sharedApplication].applicationState;
    if ((state == UIApplicationStateInactive) || (state == UIApplicationStateBackground))
    {
        [self start_system];
        [self startReachabilityTask];
        [self updateKeepAlive];
    }
    // Override point for customization after application launch.
    NSLog(@"didFinishLaunchingWithOptions %ld %d", (long)state, [self push_autostart]);
    return YES;
}

- (void) reachabilityTaskTimeout:(id)context
{
    if (self.reachability_background_task != UIBackgroundTaskInvalid) {
        if (self.callPresentation) {
            [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
        }
        else {
            [self updateKeepAlive];
            [[UIApplication sharedApplication] endBackgroundTask:self.reachability_background_task];
            self.reachability_background_task = UIBackgroundTaskInvalid;
            NSLog(@"reachability background timeout");
            [self stopPolling];
        }
    }
}

- (void) startReachabilityTask
{
    if (self.internetReachability == nil) {
        self.internetReachability = [Reachability reachabilityForInternetConnection];
        [self.internetReachability startNotifier];
    }
    int reachability_timeout = self.connectivityUp ? 13 : 1;

    if (self.reachability_background_task == UIBackgroundTaskInvalid) {
        UIApplication *application = [UIApplication sharedApplication];
        if (application.applicationState == UIApplicationStateBackground) {
            NSLog(@"reachability background start");
            self.reachability_background_task = [application beginBackgroundTaskWithName:@"ReachabilityBackgroundTask" expirationHandler: ^{
                PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
                NSLog(@"reachability background ExpirationHandler");
                //Clean up code. Tell the system that we are done.
                [NSObject cancelPreviousPerformRequestsWithTarget:phoneIosDelegate selector:@selector(reachabilityTaskTimeout:) object:nil];
                [phoneIosDelegate updateKeepAlive];
                [[UIApplication sharedApplication] endBackgroundTask:phoneIosDelegate.reachability_background_task];
                phoneIosDelegate.reachability_background_task = UIBackgroundTaskInvalid;
            }];
            [self performSelector:@selector(reachabilityTaskTimeout:) withObject:nil afterDelay:reachability_timeout];
        }
    }
    else {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(reachabilityTaskTimeout:) object:nil];
        [self performSelector:@selector(reachabilityTaskTimeout:) withObject:nil afterDelay:reachability_timeout];
    }
}

- (void) stopReachabilityTask
{
    if (self.reachability_background_task != UIBackgroundTaskInvalid) {
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(reachabilityTaskTimeout:) object:nil];
        [self updateKeepAlive];
        [[UIApplication sharedApplication] endBackgroundTask:self.reachability_background_task];
        self.reachability_background_task = UIBackgroundTaskInvalid;
        NSLog(@"reachability background stop");
    }
}

- (void) pollReachability
{
    dword addr = getIPv4Address();
    if (addr != self.reachability_ip_address) {
        [self updateInterfaceWithReachability:self.internetReachability ipAddress:addr];
    }
}

/*
 * Called by Reachability whenever status changes.
 */
- (void) reachabilityChanged:(NSNotification *)note
{
    Reachability *curReach = note.object;
    NSParameterAssert([curReach isKindOfClass:Reachability.class]);
    [self updateInterfaceWithReachability:curReach ipAddress:getIPv4Address()];
}

- (void) updateInterfaceWithReachability:(Reachability *)reachability ipAddress:(dword)ipAddress
{
    NetworkStatus netStatus = [reachability currentReachabilityStatus];
    bool prevConnectivityUp = self.connectivityUp;
    self.connectivityUp = (netStatus != NotReachable);
    self.reachability_ip_address = ipAddress;
    NSLog(@"updateInterfaceWithReachability %d->%d %d %d.%d.%d.%d", prevConnectivityUp, self.connectivityUp, ((netStatus != NotReachable) && (netStatus != ReachableViaWWAN)),
        ipAddress & 0xff, (ipAddress >> 8) & 0xff, (ipAddress >> 16) & 0xff, ipAddress >> 24);
    if (self.uiStarted) {
        connectivity_changed(self.connectivityUp, ((netStatus != NotReachable) && (netStatus != ReachableViaWWAN)));
        if (self.connectivityUp) {
            [self forms_event_registration_check];
        }
        [self updateNoMediaCall];
    }
    if (self.connectivityUp) {
        //start task to refresh registration
        [self startReachabilityTask];
    }
}

- (void) updateKeepAlive
{
}

- (void) applicationDidReceiveMemoryWarning:(UIApplication *)application
{
    NSLog(@"applicationDidReceiveMemoryWarning");
}

- (void) dealloc
{
    if (staticAddressBook != nil) {
        ABAddressBookUnregisterExternalChangeCallback(staticAddressBook, addressBookChanged, (__bridge void *)self);
        CFRelease(staticAddressBook);
        staticAddressBook = nil;
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kReachabilityChangedNotification object:nil];
    self.stopProximitySensor();
//    self.unregisterRemoteControl();
    self.stopBatteryMonitoring();
    self.stopOrientationMonitoring();
    if ([UIFont respondsToSelector:@selector(preferredFontForTextStyle:)]) {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
    }
    default_tunes = nil;
    self.defaultTunesData = nil;
    default_tones = nil;
    self.defaultTonesData = nil;
}
							
- (void) onSignallingConnected_:(id)context
{
    NSLog(@"Signalling connected");
    if (!self.signallingConnected) {
        self.signallingConnected = true;
        if (self.connectivityUp) {
            [self startReachabilityTask];
        }
    }
}

- (void) onSignallingDisconnected_:(id)context
{
    NSLog(@"Signalling disconnected");
    if (self.signallingConnected) {
        self.signallingConnected = false;
        if (self.connectivityUp) {
            [self startReachabilityTask];
        }
        else {
            [self stopReachabilityTask];
        }
    }
}

- (void) onAudioSessionActive_:(id)context
{
    self.audioSessionActive = true;
}

- (void) onAudioSessionInactive_:(id)context
{
    self.audioSessionActive = false;
}

- (void) audioSessionMayDeactivate
{
    unsigned i;
    i = 0;
    while ((i < self.cxCalls.count) && ((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
        i++;
    }
    if (i == self.cxCalls.count) {
        phone_audio_session_will_deactivate();
    }
}

- (void) applicationWillResignActive:(UIApplication *)application
{
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    NSLog(@"applicationWillResignActive");
    self.didBecomeActive = false;
    if (self.forms != nil) {
        [self configureVideoSessions];
    }
    if (self.two_factor_background_task == UIBackgroundTaskInvalid) {
        NSLog(@"two factor background start");
        self.two_factor_background_task = [application beginBackgroundTaskWithName:@"TwoFactorBackgroundTask" expirationHandler: ^{
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            NSLog(@"two factor background ExpirationHandler");
            //Clean up code. Tell the system that we are done.
            [[UIApplication sharedApplication] endBackgroundTask:phoneIosDelegate.two_factor_background_task];
            phoneIosDelegate.two_factor_background_task = UIBackgroundTaskInvalid;
        }];
    }
}

- (void) applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    NSLog(@"applicationDidEnterBackground %d %d", self.signallingConnected, self.connectivityUp);
    self.didEnterBackground = true;
    [self.pendingLocalNotifications removeAllObjects];
    [self.presentedLocalNotifications removeAllObjects];
    [self badgeUpdate];
    [self updateKeepAlive];
    if (self.forms != nil) {
        self.forms.event_pause_subscriptions(true);
        while (self.transcodingSessions.count != 0) {
            [self deleteTranscodingSession:((TranscodingSession *)[self.transcodingSessions objectAtIndex:0])];
        }
    }
    if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [PhoneIosDelegate registerUNUserNotificationSettings:center];
    }
}

- (void) applicationWillEnterForeground:(UIApplication *)application
{
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    NSLog(@"applicationWillEnterForeground");
}

- (void) applicationDidBecomeActive:(UIApplication *)application
{
    unsigned i;

    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    NSLog(@"applicationDidBecomeActive %d %d", self.signallingConnected, self.connectivityUp);
    self.didBecomeActive = true;
/*
    dispatch_async(dispatch_get_main_queue(), ^
        {
            if ([NSClassFromString(@"ATTrackingManager") respondsToSelector:@selector(requestTrackingAuthorizationWithCompletionHandler:)]) {
                [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
                NSLog(@"Tracking authorization status %@", (status == ATTrackingManagerAuthorizationStatusAuthorized) ? @"authorized" :
                    ((status == ATTrackingManagerAuthorizationStatusDenied) ? @"denied" :
                    ((status == ATTrackingManagerAuthorizationStatusRestricted) ? @"restricted" : @"not determined")));
                }];
            }
        });
*/
    while (self.remoteLocalNotifications.count != 0) {
        if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
            UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
            [center removePendingNotificationRequestsWithIdentifiers:@[((UNNotificationRequest *)[self.remoteLocalNotifications objectAtIndex:0]).identifier]];
            [center removeDeliveredNotificationsWithIdentifiers:@[((UNNotificationRequest *)[self.remoteLocalNotifications objectAtIndex:0]).identifier]];
        }
        else {
            [application cancelLocalNotification:((UILocalNotification *)[self.remoteLocalNotifications objectAtIndex:0])];
        }
        [self.remoteLocalNotifications removeObjectAtIndex:0];
    }
    if (self.didEnterBackground) {
        self.didEnterBackground = false;
        [self updateKeepAlive];
        [self stopReachabilityTask];
        restart_listen_sockets();
        i = 0;
        while ((i < self.cxCalls.count) && ((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
            i++;
        }
        NSString *countryCode = [NSLocale currentLocale].countryCode;
        if ((i == self.cxCalls.count) && (self.two_factor_background_task == UIBackgroundTaskInvalid) && ![countryCode isEqualToString: @"CN"] && ![countryCode isEqualToString: @"CHN"]) {
            NSLog(@"close TCP sockets");
            close_tcp_sockets();
        }
        NetworkStatus netStatus = [self.internetReachability currentReachabilityStatus];
        connectivity_changed(self.connectivityUp, ((netStatus != NotReachable) && (netStatus != ReachableViaWWAN)));
        if (self.connectivityUp) {
            [self forms_event_registration_check];
        }
        if (self.forms != nil) {
            self.forms.event_pause_subscriptions(false);
            if (self.fixCertViewController == nil) {
                if (self.fix_cert_running) {
                    self.fix_cert_running = false;
                    if (self.got_certificate) {
                        self.got_certificate = false;
                        [self on_webserver_started];
                    }
                }
            }
        }
    }
    else {
        NetworkStatus netStatus = [self.internetReachability currentReachabilityStatus];
        connectivity_changed(self.connectivityUp, ((netStatus != NotReachable) && (netStatus != ReachableViaWWAN)));
        if (self.connectivityUp) {
            [self forms_event_registration_check];
        }
    }
    if (self.two_factor_background_task != UIBackgroundTaskInvalid) {
        [application endBackgroundTask:self.two_factor_background_task];
        self.two_factor_background_task = UIBackgroundTaskInvalid;
        NSLog(@"two factor background stop");
    }
    if ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized) {
        [self ReportWebcams:nil];
    }
    [self configureVideoSessions];
    if (self.screen_capture_stopped) {
        self.screen_capture_stopped = false;
        self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"PHONE/SCREEN-CAPTURE-STOPPED", @"");
    }
    if (self.uiStarted && !self.manuallyStarted && ([UIApplication sharedApplication].applicationState == UIApplicationStateActive)) {
        self.manuallyStarted = true;
        set_manually_started(true);
    }
    [self startPolling];
}

- (void) applicationWillTerminate:(UIApplication *)application
{
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    NSLog(@"applicationWillTerminate");
}

- (void) application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void (^)(void))completionHandler
{
    NSString *link = [notification.userInfo valueForKey:@"msgLink"];
    if (link != nil) {
        link = [NSString stringWithFormat:@"%@%c%@=%@", link, [link containsString:@"?"] ? '&' : '?', notification.category, identifier];
        self.forms_event_open_permalink(link);
    }
    NSString *currentCall = [notification.userInfo valueForKey:@"callRef"];
    if (currentCall != nil) {
        NSLog(@"handleActionWithIdentifier %@ call %@", identifier, currentCall);
        self.ios_event.enqueue(([identifier isEqualToString:@ACCEPT_ACTION] || [identifier isEqualToString:@CALLBACK_ACTION]) ? FORMS_EVENT_LOCAL_NOTIFICATION_ACCEPT :
            ([identifier isEqualToString:@REJECT_ACTION] ? FORMS_EVENT_LOCAL_NOTIFICATION_REJECT : FORMS_EVENT_LOCAL_NOTIFICATION_CLICKED), currentCall);
    }
    NSString *currentMsg = [notification.userInfo valueForKey:@"msgRef"];
    if (currentMsg != nil) {
        NSLog(@"handleActionWithIdentifier %@ msg %@", identifier, currentMsg);
    }
    //must call completion handler when finished
    completionHandler();
    [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
}

- (void) application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)(void))completionHandler
{
    [self application:application handleActionWithIdentifier:identifier forLocalNotification:notification completionHandler:completionHandler];
}

- (void) application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(void(^)(void))completionHandler
{
    userInfo = [self decryptNotificationPayload:userInfo pushPassword:[self push_password]];
    NSString *link = [userInfo valueForKey:@"msgLink"];
    if (link != nil) {
        link = [NSString stringWithFormat:@"%@%c%@=%@", link, [link containsString:@"?"] ? '&' : '?', @DECISION_CATEGORY, identifier];
        self.forms_event_open_permalink(link);
    }
    NSString *currentCall = [userInfo valueForKey:@"callRef"];
    if (currentCall != nil) {
        NSLog(@"handleActionWithIdentifier %@ call %@ remote", identifier, currentCall);
        self.ios_event.enqueue(([identifier isEqualToString:@ACCEPT_ACTION] || [identifier isEqualToString:@CALLBACK_ACTION]) ? FORMS_EVENT_LOCAL_NOTIFICATION_ACCEPT :
            ([identifier isEqualToString:@REJECT_ACTION] ? FORMS_EVENT_LOCAL_NOTIFICATION_REJECT : FORMS_EVENT_LOCAL_NOTIFICATION_CLICKED), currentCall);
    }
    NSString *currentMsg = [userInfo valueForKey:@"msgRef"];
    if (currentMsg != nil) {
        NSLog(@"handleActionWithIdentifier %@ msg %@ remote", identifier, currentMsg);
    }
    //must call completion handler when finished
    completionHandler();
    [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
}

- (void) application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)(void))completionHandler
{
    [self application:application handleActionWithIdentifier:identifier forRemoteNotification:userInfo completionHandler:completionHandler];
}

- (void) application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
    NSLog(@"didReceiveLocalNotification");
}

- (void) userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
    NSLog(@"willPresentNotification");
    center.delegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
    [PhoneIosDelegate registerUNUserNotificationSettings:center];
//    completionHandler(UNNotificationPresentationOptionNone);
    completionHandler(UNNotificationPresentationOptionBadge | (1 << 4)/*UNNotificationPresentationOptionBanner*/ | (1 << 3)/*UNNotificationPresentationOptionList*/ | UNNotificationPresentationOptionSound);
}

- (void) userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
    UNNotificationContent *notification = response.notification.request.content;
    if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
        if ([notification.categoryIdentifier isEqualToString:@CALL_CATEGORY] || [notification.categoryIdentifier isEqualToString:@CONN_CATEGORY]) {
            NSDictionary *userInfo = [self decryptNotificationPayload:notification.userInfo pushPassword:[self push_password]];
            NSString *currentCall = [userInfo valueForKey:@"callRef"];
            if ((currentCall != nil) && [currentCall containsString:@"\n"]) {
                NSLog(@"handleActionWithIdentifier %@ call %@", response.actionIdentifier, currentCall);
                self.ios_event.enqueue(FORMS_EVENT_LOCAL_NOTIFICATION_REJECT, currentCall);
            }
            NSString *currentMsg = [userInfo valueForKey:@"msgRef"];
            if (currentMsg != nil) {
                NSLog(@"handleActionWithIdentifier %@ msg %@", response.actionIdentifier, currentMsg);
            }
        }
    }
    else {
        NSDictionary *userInfo = [self decryptNotificationPayload:notification.userInfo pushPassword:[self push_password]];
        NSString *link = [userInfo valueForKey:@"msgLink"];
        if (link != nil) {
            if (![response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) {
                if ([response isKindOfClass:UNTextInputNotificationResponse.class]) {
                    link = [NSString stringWithFormat:@"%@%c%@=%@", link, [link containsString:@"?"] ? '&' : '?', response.actionIdentifier,
                        [((UNTextInputNotificationResponse *) response).userText stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]]];
                }
                else {
                    link = [NSString stringWithFormat:@"%@%c%@=%@", link, [link containsString:@"?"] ? '&' : '?', notification.categoryIdentifier, response.actionIdentifier];
                }
            }
            self.forms_event_open_permalink(link);
        }
        NSString *currentCall = [userInfo valueForKey:@"callRef"];
        if (currentCall != nil) {
            NSLog(@"handleActionWithIdentifier %@ call %@", response.actionIdentifier, currentCall);
            if ([response isKindOfClass:UNTextInputNotificationResponse.class]) {
                self.ios_event.enqueue(FORMS_EVENT_LOCAL_NOTIFICATION_REPLY, currentCall, ((UNTextInputNotificationResponse *) response).userText);
            }
            else {
                self.ios_event.enqueue(([response.actionIdentifier isEqualToString:@ACCEPT_ACTION] || [response.actionIdentifier isEqualToString:@CALLBACK_ACTION]) ? FORMS_EVENT_LOCAL_NOTIFICATION_ACCEPT :
                    ([response.actionIdentifier isEqualToString:@REJECT_ACTION] ? FORMS_EVENT_LOCAL_NOTIFICATION_REJECT : FORMS_EVENT_LOCAL_NOTIFICATION_CLICKED), currentCall);
            }
        }
        NSString *currentMsg = [userInfo valueForKey:@"msgRef"];
        if (currentMsg != nil) {
            NSLog(@"handleActionWithIdentifier %@ msg %@", response.actionIdentifier, currentMsg);
        }
    }
    completionHandler();
}

- (void) clear_unclaimed_calls_runnable:(id)sender
{
    unsigned i;

    for (i = 0; i < self.cxCalls.count; i++) {
        if (!((CxCall *)[self.cxCalls objectAtIndex:i]).claimed && !((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
            self.trc([NSString stringWithFormat:@"Clear unclaimed call cxReportCallEnded %@", [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid]]);
            [((CxCall *)[self.cxCalls objectAtIndex:i]) released_now];
            [self audioSessionMayDeactivate];
            [self.cxProvider reportCallWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid endedAtDate:nil reason:CXCallEndedReasonAnsweredElsewhere];
        }
    }
}

- (void) keepWkWebViewActive:(id)object
{
    unsigned i;
    i = 0;
    while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).claimed || ((CxCall *)[self.cxCalls objectAtIndex:i]).released)) {
        i++;
    }
    if ((i < self.cxCalls.count) && ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) && !self.evaluatingJavaScript) {
        self.evaluatingJavaScript = true;
        [((WebViewController *)(self.window.rootViewController)).webView evaluateJavaScript:@"1+1" completionHandler:nil];
        self.evaluatingJavaScript = false;
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(keepWkWebViewActive:) object:nil];
        [self performSelector:@selector(keepWkWebViewActive:) withObject:nil afterDelay:self.hookstateOffhook ? 0.1 : 0.01];
    }
}

static byte hex_to_byte(char hi, char lo)
{
    return ((byte)(((hi - (((hi >= '0') && (hi <= '9')) ? '0' : (((hi >= 'a') && (hi <= 'z')) ? 'a' - 10 : 'A' - 10))) << 4) |
        (lo - (((lo >= '0') && (lo <= '9')) ? '0' : (((lo >= 'a') && (lo <= 'z')) ? 'a' - 10 : 'A' - 10)))));
}

- (NSDictionary *) decryptNotificationPayload:(NSDictionary *)userInfo pushPassword:(NSString *)pushPassword
{
    unsigned i;

    if ([userInfo valueForKey:@"callRef"] != nil) {
        return userInfo;
    }
    NSString *type = nil;
    NSDictionary *userData = userInfo[@"data"];
    NSString *payloadData = userData[@"payloadData"];
    if (payloadData == nil) {
        NSLog(@"decryptNotificationPayload no payload data");
    }
    else {
        JsonIo *payloadDataJson = [JsonIo jsonIo];
        if (!payloadDataJson.decode(payloadData)) {
            NSLog(@"decryptNotificationPayload payload data not json");
        }
        else {
            word dataRoot = payloadDataJson.get_object(JSON_ID_ROOT, nil);
            type = payloadDataJson.get_string(dataRoot, @"type");
            NSString *salt = payloadDataJson.get_string(dataRoot, @"salt");
            NSString *dataString = payloadDataJson.get_string(dataRoot, @"data");
            if ((type == nil) || (salt == nil) || (dataString == nil)) {
                NSLog(@"decryptNotificationPayload no type salt data");
            }
            else {
                if (pushPassword == nil) {
                    NSLog(@"decryptNotificationPayload no password");
                }
                else {
                    unsigned passwordLen = (unsigned)(pushPassword.length / 2);
                    NSMutableData *key = [NSMutableData dataWithLength:salt.length + 1 + passwordLen];
                    for (i = 0; i < salt.length; i++) {
                        ((byte *)(key.mutableBytes))[i] = (byte)([salt characterAtIndex:i]);
                    }
                    ((byte *)(key.mutableBytes))[salt.length] = ':';
                    for (i = 0; i < passwordLen; i++) {
                        ((byte *)(key.mutableBytes))[salt.length + 1 + i] = hex_to_byte([pushPassword characterAtIndex:2*i], [pushPassword characterAtIndex:2*i+1]);
                    }
                    unsigned dataLen = (unsigned)(dataString.length / 2);
                    NSMutableData *data = [NSMutableData dataWithLength:dataLen];
                    for (i = 0; i < dataLen; i++) {
                        ((byte *)(data.mutableBytes))[i] = hex_to_byte([dataString characterAtIndex:2*i], [dataString characterAtIndex:2*i+1]);
                    }
                    Cipher *cipher = [[Cipher alloc] init];
                    cipher.Init(CIPHER_RC4, (byte *)(key.mutableBytes), (int)(key.length), false);
                    NSMutableData *payload = [NSMutableData dataWithLength:dataLen];
                    cipher.Crypt((byte *)(data.mutableBytes), (byte *)(payload.mutableBytes), dataLen);
                    NSString *payloadString = [[NSString alloc] initWithData:payload encoding:NSUTF8StringEncoding];
                    if (payloadString == nil) {
                        NSLog(@"decryptNotificationPayload cannot decrypt payload");
                    }
                    else {
                        JsonIo *payloadJson = [JsonIo jsonIo];
                        if (!payloadJson.decode(payloadString)) {
                            NSLog(@"decryptNotificationPayload failed decrypt payload");
                        }
                        else {
                            word root = payloadJson.get_object(JSON_ID_ROOT, nil);
                            if ([type isEqualToString:@"notification"]) {
                                NSString *ref = payloadJson.get_string(root, @"ref");
                                if (ref == nil) {
                                    ref = @"";
                                }
                                NSString *link = payloadJson.get_string(root, @"link");

                                if ((link != nil) && (link.length != 0)) {
                                    return ([NSDictionary dictionaryWithObjectsAndKeys:ref, @"callRef", link, @"msgLink", nil]);
                                }
                                else {
                                    return ([NSDictionary dictionaryWithObject:ref forKey:@"callRef"]);
                                }
                            }
                            else {
                                NSLog(@"decryptNotificationPayload unknown type");
                            }
                        }
                    }
                }
            }
        }
    }
    return nil;
}

- (bool) onMessageReceived:(NSDictionary *)userInfo withCompletionHandler:(void (^)(void))completion
{
    byte b[16], conf_id_buf[16];
    NSString *r;
    unsigned i;

    NSString *type = nil;
    NSUUID *conf_id = nil;
    NSString *number = nil;
    NSString *h323 = nil;
    NSMutableString *name = nil;
    NSDictionary *userData = userInfo[@"data"];
    NSString *payloadData = userData[@"payloadData"];
    if (payloadData == nil) {
        NSLog(@"onMessageReceived no payload data");
    }
    else {
        JsonIo *payloadDataJson = [JsonIo jsonIo];
        if (!payloadDataJson.decode(payloadData)) {
            NSLog(@"onMessageReceived payload data not json");
        }
        else {
            word dataRoot = payloadDataJson.get_object(JSON_ID_ROOT, nil);
            type = payloadDataJson.get_string(dataRoot, @"type");
            NSString *salt = payloadDataJson.get_string(dataRoot, @"salt");
            NSString *dataString = payloadDataJson.get_string(dataRoot, @"data");
            if ((type == nil) || (![type isEqualToString:@"badge"] && ((salt == nil) || (dataString == nil)))) {
                NSLog(@"onMessageReceived no type salt data");
            }
            else {
                JsonIo *payloadJson = [JsonIo jsonIo];
                word root = JSON_ID_NONE;
                NSString *pushPassword = nil;
                if ((type != nil) && (salt != nil) && (dataString != nil)) {
                    pushPassword = [self push_password];
                    if (pushPassword == nil) {
                        NSLog(@"onMessageReceived no password");
                    }
                    else {
                        unsigned passwordLen = (unsigned)(pushPassword.length / 2);
                        NSMutableData *key = [NSMutableData dataWithLength:salt.length + 1 + passwordLen];
                        for (i = 0; i < salt.length; i++) {
                            ((byte *)(key.mutableBytes))[i] = (byte)([salt characterAtIndex:i]);
                        }
                        ((byte *)(key.mutableBytes))[salt.length] = ':';
                        for (i = 0; i < passwordLen; i++) {
                            ((byte *)(key.mutableBytes))[salt.length + 1 + i] = hex_to_byte([pushPassword characterAtIndex:2*i], [pushPassword characterAtIndex:2*i+1]);
                        }
                        unsigned dataLen = (unsigned)(dataString.length / 2);
                        NSMutableData *data = [NSMutableData dataWithLength:dataLen];
                        for (i = 0; i < dataLen; i++) {
                            ((byte *)(data.mutableBytes))[i] = hex_to_byte([dataString characterAtIndex:2*i], [dataString characterAtIndex:2*i+1]);
                        }
                        Cipher *cipher = [[Cipher alloc] init];
                        cipher.Init(CIPHER_RC4, (byte *)(key.mutableBytes), (int)(key.length), false);
                        NSMutableData *payload = [NSMutableData dataWithLength:dataLen];
                        cipher.Crypt((byte *)(data.mutableBytes), (byte *)(payload.mutableBytes), dataLen);
                        NSString *payloadString = [[NSString alloc] initWithData:payload encoding:NSUTF8StringEncoding];
                        if (payloadString == nil) {
                            NSLog(@"onMessageReceived cannot decrypt payload");
                        }
                        else {
                            if (!payloadJson.decode(payloadString)) {
                                NSLog(@"onMessageReceived failed decrypt payload");
                            }
                            else {
                                root = payloadJson.get_object(JSON_ID_ROOT, nil);
                            }
                        }
                    }
                }
                if ((salt == nil) || (dataString == nil) || (root != JSON_ID_NONE)) {
                    NSString *badge = payloadDataJson.get_value(dataRoot, 0, @"badge");
                    if (badge != nil) {
                        unsigned count = badge.intValue;
                        [UIApplication sharedApplication].applicationIconBadgeNumber = count;
                    }
                    if ([type isEqualToString:@"call"]) {
                        NSString *conf_id_string = payloadJson.get_string(root, @"confID");
                        if (conf_id_string == nil) {
                            NSLog(@"onMessageReceived no conference ID");
                        }
                        else {
                            number = payloadJson.get_string(root, @"e164");
                            h323 = payloadJson.get_string(root, @"h323");
                            NSString *prefix_intl = payloadJson.get_string(root, @"prefixIntl");
                            NSString *prefix_ntl = payloadJson.get_string(root, @"prefixNtl");
                            NSString *prefix_subs = payloadJson.get_string(root, @"prefixSubs");
//                            NSString *area_code = payloadJson.get_string(root, @"area");
//                            NSString *country_code = payloadJson.get_string(root, @"country");
//                            NSString *domain = payloadJson.get_string(root, @"domain");
                            if ((prefix_subs != nil) && (prefix_subs.length != 0) && (number != nil) && [number hasPrefix:prefix_subs]
                             && ((number.length >= prefix_subs.length + 7)
                              || ((prefix_ntl != nil) && [number hasPrefix:prefix_ntl])
                              || ((prefix_intl != nil) && [number hasPrefix:prefix_intl]))) {
                                number = [number substringFromIndex:prefix_subs.length];
                            }
                            name = [NSMutableString stringWithCapacity:1024];
                            NSString *n;
                            n = payloadJson.get_string(root, @"dn");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"cn");
                            if ((n == nil) || (n.length == 0)) n = h323;
                            if ((n == nil) || (n.length == 0)) n = number;
                            if ((n != nil) && (n.length != 0)) {
                                [name appendString:n];
                            }
                            n = payloadJson.get_string(root, @"transDN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"transCN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"transH323");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"transE164");
                            if ((n != nil) && (n.length != 0)) {
                                [name appendString:@" by "];
                                [name appendString:n];
                            }
                            n = payloadJson.get_string(root, @"divDN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"divCN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"divH323");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"divE164");
                            if ((n != nil) && (n.length != 0)) {
                                [name appendString:@" via "];
                                [name appendString:n];
                            }
                            n = payloadJson.get_string(root, @"calledDN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"calledCN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"calledH323");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"calledE164");
                            if ((n != nil) && (n.length != 0)) {
                                [name appendString:@" for "];
                                [name appendString:n];
                            }
/*
                            n = payloadJson.get_string(root, @"localDN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"localCN");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"localH323");
                            if ((n == nil) || (n.length == 0)) n = payloadJson.get_string(root, @"localE164");
                            if ((n != nil) && (n.length != 0)) {
                                [name appendString:@" to "];
                                [name appendString:n];
                            }
*/
                            conf_id = [self UUIDWithString:conf_id_string];
                        }
                    }
                    else if ([type isEqualToString:@"notification"] || [type isEqualToString:@"obsolete"]) {
                        word obsolete = [type isEqualToString:@"obsolete"] ? root : payloadJson.get_object(root, @"obsolete");
                        if (obsolete != JSON_ID_NONE) {
                            NSString *ref = payloadJson.get_string(obsolete, @"ref");
                            if (ref == nil) {
                                ref = @"";
                            }
                            i = 0;
                            while (i < self.remoteLocalNotifications.count) {
                                if (((r = [((UILocalNotification *)[self.remoteLocalNotifications objectAtIndex:i]).userInfo valueForKey:@"callRef"]) != nil) && [r hasPrefix:ref]) {
                                    if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
                                        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
                                        [center removePendingNotificationRequestsWithIdentifiers:@[((UNNotificationRequest *)[self.remoteLocalNotifications objectAtIndex:0]).identifier]];
                                        [center removeDeliveredNotificationsWithIdentifiers:@[((UNNotificationRequest *)[self.remoteLocalNotifications objectAtIndex:0]).identifier]];
                                    }
                                    else {
                                        [[UIApplication sharedApplication] cancelLocalNotification:((UILocalNotification *)[self.remoteLocalNotifications objectAtIndex:i])];
                                    }
                                    [self.remoteLocalNotifications removeObjectAtIndex:i];
                                }
                                else {
                                    i++;
                                }
                            }
                            if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
                                UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
                                [center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> *notifications) {
                                    NSMutableArray<NSString *> *identifiers = [NSMutableArray array];
                                    int i;
                                    for (i = 0; i < notifications.count; i++) {
                                        NSDictionary *userInfo = [self decryptNotificationPayload:[notifications objectAtIndex:i].request.content.userInfo pushPassword:pushPassword];
                                        if (userInfo) {
                                            NSString *r = [userInfo valueForKey:@"callRef"];
                                            if ((r != nil) && [r hasPrefix:ref]) {
                                                [identifiers addObject:[notifications objectAtIndex:i].request.identifier];
                                            }
                                        }
                                    }
                                    [center removeDeliveredNotificationsWithIdentifiers:identifiers];
                                }];
                            }
                        }
                        if ([type isEqualToString:@"obsolete"]) {
                            if (completion != nil) {
                                completion();
                            }
                            return false;
                        }
                        NSString *ref = payloadJson.get_string(root, @"ref");
                        if (ref == nil) {
                            ref = @"";
                        }
                        NSString *typeString = payloadJson.get_string(root, @"type");
                        int typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_DEFAULT;
                        if (typeString != nil) {
                            if ([typeString isEqualToString:@"PhoneCall"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_PHONE_CALL;
                            }
                            else if ([typeString isEqualToString:@"HoldPhoneCall"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_HOLD_PHONE_CALL;
                            }
                            else if ([typeString isEqualToString:@"EndPhoneCall"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_END_PHONE_CALL;
                            }
                            else if ([typeString isEqualToString:@"ChatMessage"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_CHAT_MESSAGE;
                            }
                            else if ([typeString isEqualToString:@"decision"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_DECISION;
                            }
                            else if ([typeString isEqualToString:@"reply"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_REPLY;
                            }
                            else if ([typeString isEqualToString:@"AppNotify"]) {
                                typeValue = FORMS_LOCAL_NOTIFICATION_TYPE_APP_NOTIFY;
                            }
                        }
                        NSString *title = payloadJson.get_string(root, @"title");
                        NSString *text = payloadJson.get_string(root, @"text");
//                        NSString *smallIcon = payloadJson.get_string(root, @"smallIcon");
//                        NSString *largeIcon = payloadJson.get_string(root, @"largeIcon");
                        NSString *soundUrl = payloadJson.get_string(root, @"soundUrl");
                        NSString *buttonTitle = payloadJson.get_string(root, @"buttonTitle");
                        NSString *acceptTitle = payloadJson.get_string(root, @"acceptTitle");
//                        NSString *rejectTitle = payloadJson.get_string(root, @"rejectTitle");
                        NSString *link = payloadJson.get_string(root, @"link");
                        NSString *category = nil;
                        if ((acceptTitle != nil) && (acceptTitle.length != 0)) {
                            category = ((typeValue == FORMS_LOCAL_NOTIFICATION_TYPE_DEFAULT) || (typeValue == FORMS_LOCAL_NOTIFICATION_TYPE_PHONE_CALL)) ? @CALL_CATEGORY : @DECISION_CATEGORY;
                        }
                        else if ((buttonTitle != nil) && (buttonTitle.length != 0)) {
                            NSString *last = @"";
                            if (ref != nil) {
                                NSRange r = [ref rangeOfString:@"\n" options:NSBackwardsSearch];
                                if (r.location != NSNotFound) {
                                    last = [ref substringFromIndex:r.location + 1];
                                }
                            }
                            category = ((typeValue == FORMS_LOCAL_NOTIFICATION_TYPE_CHAT_MESSAGE) || (typeValue == FORMS_LOCAL_NOTIFICATION_TYPE_REPLY)) ? @REPLY_CATEGORY :
                                (((ref == nil) || (ref.length == 0) || [ref hasPrefix:@"sip:"] || [ref hasPrefix:@"num:"] || [last hasPrefix:@" sip:"] || [last hasPrefix:@" num:"]) ? @MISSED_CATEGORY : @CONN_CATEGORY);
                        }
                        NSString *body = ((text != nil) && (text.length != 0)) ? text : @" ";

                        if ([NSClassFromString(@"UNUserNotificationCenter") respondsToSelector:@selector(currentNotificationCenter)]) {
                            UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

                            UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
                            content.categoryIdentifier = category;
                            if ((title != nil) && (title.length != 0)) {
                                content.title = title;
                            }
                            content.body = body;
                            if ((soundUrl != nil) && (soundUrl.length != 0)) {
                                content.sound = [soundUrl isEqualToString:@"*"] ? [UNNotificationSound defaultSound] : [UNNotificationSound soundNamed:soundUrl];
                            }
/* Not permitted to stall until image is loaded
                            if ((largeIcon != nil) && (largeIcon.length != 0)) {
                                NSError *error = nil;
                                NSString *path = [self fileFromURL:[NSURL URLWithString:largeIcon] error:&error];
                                if (error != nil) {
                                    NSLog(@"onMessageReceived failed to download image error=%@", error);
                                }
                                if (path != nil) {
                                    NSURL *fileURL = [NSURL fileURLWithPath:path isDirectory:false];
                                    UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:fileURL options:nil error:&error];
                                    if (error != nil) {
                                        NSLog(@"onMessageReceived failed to attach image error=%@", error);
                                    }
                                    if (attachment != nil) {
                                        content.attachments = @[attachment];
                                    }
                                }
                            }
*/
                            if ((link != nil) && (link.length != 0)) {
                                content.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:ref, @"callRef", link, @"msgLink", nil];
                            }
                            else {
                                content.userInfo = [NSDictionary dictionaryWithObject:ref forKey:@"callRef"];
                            }
                            UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.001 repeats:false];
                            UNNotificationRequest *localNotif = [UNNotificationRequest requestWithIdentifier:ref content:content trigger:trigger];
                            [self.remoteLocalNotifications addObject:localNotif];

                            [center addNotificationRequest:localNotif withCompletionHandler:^(NSError * _Nullable error) {
                                if (error) {
                                    NSLog(@"onMessageReceived failed to add notification request error=%@", error);
                                }
                            }];
                        }
                        else {
                            UILocalNotification *localNotif = [[UILocalNotification alloc] init];
                            localNotif.fireDate = [NSDate date];
                            // Set the category to the category that contains our action
                            if ([localNotif respondsToSelector:@selector(category)]) {
                                localNotif.category = category;
                            }
                            if ((title != nil) && (title.length != 0)) {
                                localNotif.alertTitle = title;
                            }
                            localNotif.alertBody = [body stringByReplacingOccurrencesOfString:@"%" withString:@"%%"];
                            if ((soundUrl != nil) && (soundUrl.length != 0)) {
                                localNotif.soundName = [soundUrl isEqualToString:@"*"] ? UILocalNotificationDefaultSoundName : soundUrl;
                            }
                            if ((link != nil) && (link.length != 0)) {
                                localNotif.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:ref, @"callRef", link, @"msgLink", nil];
                            }
                            else {
                                localNotif.userInfo = [NSDictionary dictionaryWithObject:ref forKey:@"callRef"];
                            }
                            [self.remoteLocalNotifications addObject:localNotif];

                            [[UIApplication sharedApplication] presentLocalNotificationNow:localNotif];
                        }
                        if (completion != nil) {
                            completion();
                        }
                        return false;
                    }
                    else if ([type isEqualToString:@"badge"]) {
                        if (completion != nil) {
                            completion();
                        }
                        return false;
                    }
                    else {
                        NSLog(@"onMessageReceived unknown type");
                    }
                }
            }
        }
    }

    if ((type == nil) || [type isEqualToString:@"call"]) {
        int flags = FORMS_CX_CALL_UPDATE_SUPPORTS_HOLDING;
        bool tel_number = ((number != nil) && (number.length != 0));
        NSString *contact_number = tel_number ? number : ((h323 != nil) ? h323 : @"");

        NSString *countryCode = [NSLocale currentLocale].countryCode;
        if ([countryCode isEqualToString: @"CN"] || [countryCode isEqualToString: @"CHN"] || !NSClassFromString(@"CXProvider")) {
            NSLog(@"onMessageReceived no CallKit");
            if (completion != nil) {
                completion();
            }
            return true;
        }
        if (self.cxProvider == nil) {
            NSString *ringtone = [self selected_ringtone];
            CXProviderConfiguration *cxProviderConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:@PHONE_IOS_LOG_APP_NAME];
            cxProviderConfiguration.ringtoneSound = [self tone_path_from_ident:ringtone ? ringtone.UTF8String : nil notification_tone:false];
            cxProviderConfiguration.iconTemplateImageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"CallKitIconTemplate" ofType:@"png"]];
            cxProviderConfiguration.supportsVideo = true;
            cxProviderConfiguration.maximumCallGroups = 2;
            cxProviderConfiguration.maximumCallsPerCallGroup = 2;
            cxProviderConfiguration.supportedHandleTypes = [NSSet setWithObjects:@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber), @(CXHandleTypeEmailAddress), nil];
            if ([cxProviderConfiguration respondsToSelector:@selector(includesCallsInRecents)]) {
                self.cxHideFromNativeCallList = [self hide_from_native_call_list];
                cxProviderConfiguration.includesCallsInRecents = !self.cxHideFromNativeCallList;
            }
            self.cxProvider = [[CXProvider alloc] initWithConfiguration:cxProviderConfiguration];
            [self.cxProvider setDelegate:self queue:dispatch_get_main_queue()];
            self.cxCallController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];
        }
        CXCallUpdate *cxCallUpdate = [[CXCallUpdate alloc] init];
        cxCallUpdate.remoteHandle = [[CXHandle alloc] initWithType:
            (tel_number ? CXHandleTypePhoneNumber : ([contact_number containsString:@"@"] ? CXHandleTypeEmailAddress : CXHandleTypeGeneric)) value:contact_number];
        cxCallUpdate.localizedCallerName = name;
        cxCallUpdate.hasVideo = ((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0);
        cxCallUpdate.supportsDTMF = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_DTMF) != 0);
        cxCallUpdate.supportsHolding = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_HOLDING) != 0);
        cxCallUpdate.supportsGrouping = true/*((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_GROUPING) != 0)*/;
        cxCallUpdate.supportsUngrouping = ((flags & FORMS_CX_CALL_UPDATE_SUPPORTS_UNGROUPING) != 0);
        [self obsoleteCxCalls];
        i = 0;
        if (conf_id != nil) {
            [conf_id getUUIDBytes:b];
            while ((i < self.cxCalls.count)
             && (!((CxCall *)[self.cxCalls objectAtIndex:i]).released
              || (((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil)
              || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
                i++;
            }
            if (i < self.cxCalls.count) {
                [self.cxCalls removeObjectAtIndex:i];
            }
            i = 0;
            while ((i < self.cxCalls.count) && ((((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id == nil) || (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getConfIdBytes:conf_id_buf], sizeof(b)) != 0))) {
                i++;
            }
            if (i == self.cxCalls.count) {
                i = 0;
                while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).released || (((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id != nil))) {
                    i++;
                }
                if (i < self.cxCalls.count) {
                    ((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id = conf_id;
                    ((CxCall *)[self.cxCalls objectAtIndex:i]).number = number;
                    ((CxCall *)[self.cxCalls objectAtIndex:i]).name = name;
                    ((CxCall *)[self.cxCalls objectAtIndex:i]).video = ((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0);
                    if (!((CxCall *)[self.cxCalls objectAtIndex:i]).claimed && !((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
                        NSLog(@"onMessageReceived cxReportCallUpdated %@", [self stringWithUUID:conf_id]);
                        [self.cxProvider reportCallWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid updated:cxCallUpdate];
                    }
                }
                else {
                    [self.cxCalls addObject:[CxCall cxCallWithUUID:conf_id conf_id:conf_id number:number claimed:false incoming:true name:name video:((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0)]];
                }
            }
        }
        else {
            while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).released || !((CxCall *)[self.cxCalls objectAtIndex:i]).incoming)) {
                i++;
            }
            if (i == self.cxCalls.count) {
                [self.cxCalls addObject:[CxCall cxCallWithUUID:[[NSUUID alloc] init] conf_id:nil number:number claimed:false incoming:true name:name video:((flags & FORMS_CX_CALL_UPDATE_HAS_VIDEO) != 0)]];
            }
            conf_id = ((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id;
        }
        NSUUID *uuid = ((CxCall *)[self.cxCalls objectAtIndex:i]).uuid;
        NSLog(@"onMessageReceived cxReportNewIncomingCall %@ %@", [self stringWithUUID:conf_id], [self stringWithUUID:uuid]);
        [self.cxProvider reportNewIncomingCallWithUUID:uuid update:cxCallUpdate completion:^(NSError * _Nullable error) {
            PhoneIosDelegate *phoneIosDelegate = (PhoneIosDelegate *)[UIApplication sharedApplication].delegate;
            if (error) {
                NSLog(@"onMessageReceived cxReportNewIncomingCall failed %@", error);
                if (contact_number.length != 0) {
                    [phoneIosDelegate.cxProvider reportCallWithUUID:uuid updated:cxCallUpdate];
                }
            }
            else {
                [NSObject cancelPreviousPerformRequestsWithTarget:phoneIosDelegate selector:@selector(clear_unclaimed_calls_runnable:) object:nil];
                [phoneIosDelegate performSelector:@selector(clear_unclaimed_calls_runnable:) withObject:nil afterDelay:(phoneIosDelegate.hookstateOffhook || phoneIosDelegate.callPresentation) ? 2 : 10];
#ifdef CLIENT_IOS_USE_WKWEBVIEW
                if (phoneIosDelegate.uiStarted && ([UIApplication sharedApplication].applicationState != UIApplicationStateActive)) {
                    [NSObject cancelPreviousPerformRequestsWithTarget:phoneIosDelegate selector:@selector(keepWkWebViewActive:) object:nil];
                    [phoneIosDelegate performSelector:@selector(keepWkWebViewActive:) withObject:nil afterDelay:0.01];
                }
#endif
            }
            if (completion != nil) {
                completion();
            }
        }];
    }
    else {
        if (completion != nil) {
            completion();
        }
    }
    return true;
}

- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    bool autostart = [self push_autostart];
    NSLog(@"didReceiveRemoteNotification %d %@", autostart, userInfo);
    if (!autostart) {
        exit (0);
    }
    if ([self onMessageReceived:userInfo withCompletionHandler:nil]) {
        [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
    }
}

- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
    bool autostart = [self push_autostart];
    NSLog(@"didReceiveRemoteNotification fetch %d %@", autostart, userInfo);
    completionHandler(UIBackgroundFetchResultNewData);
    if (!autostart) {
        exit (0);
    }
    if ([self onMessageReceived:userInfo withCompletionHandler:nil]) {
        [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
    }
}

- (void) pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type
{
    // Process the received push
    bool autostart = [self push_autostart];
    NSLog(@"didReceiveIncomingPushWithPayload %d %@ %@", autostart, type, payload.dictionaryPayload);
    if (!autostart) {
        exit (0);
    }
    if ([self onMessageReceived:payload.dictionaryPayload withCompletionHandler:nil]) {
        [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
    }
}

- (void) pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion
{
    // Process the received push
    bool autostart = [self push_autostart];
    NSLog(@"didReceiveIncomingPushWithPayload withCompletionHandler %d %@ %@", autostart, type, payload.dictionaryPayload);
    if (!autostart) {
        completion();
        exit (0);
    }
    if ([self onMessageReceived:payload.dictionaryPayload withCompletionHandler:completion]) {
        [self updateInterfaceWithReachability:self.internetReachability ipAddress:getIPv4Address()];
    }
}

- (NSString *) stripNonASCII:(NSString *)in
{
    NSMutableString *out = [NSMutableString stringWithCapacity:in.length];
    int i;

    for (i = 0; i < in.length; i++) {
        if ([in characterAtIndex:i] <= 0x7f) {
            [out appendString:[in substringWithRange:NSMakeRange(i,1)]];
        }
    }
    return out;
}

- (id) application:(UIApplication *)application handlerForIntent:(INIntent *)intent
{
    return (([intent isKindOfClass:[INStartCallIntent class]]
     || [intent isKindOfClass:[INStartAudioCallIntent class]]
     || [intent isKindOfClass:[INStartVideoCallIntent class]]
     || [intent isKindOfClass:[INSearchCallHistoryIntent class]]) ? self : nil);
}

#pragma mark - INStartCallIntentHandling

-  (void) resolveCallCapabilityForStartCall:(INStartCallIntent *)intent withCompletion:(void (^)(INStartCallCallCapabilityResolutionResult *resolutionResult))completion
{
    completion([INStartCallCallCapabilityResolutionResult successWithResolvedCallCapability:intent.callCapability]);
}

- (void) resolveDestinationTypeForStartCall:(INStartCallIntent *)intent withCompletion:(void (^)(INCallDestinationTypeResolutionResult *resolutionResult))completion
{
    completion([INCallDestinationTypeResolutionResult successWithResolvedCallDestinationType:INCallDestinationTypeNormal]);
}

- (void) resolveCallRecordToCallBackForStartCall:(INStartCallIntent *)intent withCompletion:(void (^)(INCallRecordResolutionResult *resolutionResult))completion
{
    int i;
    NSMutableArray<INCallRecord *> *records = [NSMutableArray array];

    for (i = 0; i < intent.contacts.count; i++) {
        INCallRecord *record = [[INCallRecord alloc] initWithIdentifier:[NSString stringWithFormat:@"%lu", records.count] dateCreated:[NSDate date] caller:[intent.contacts objectAtIndex:i]
            callRecordType:INCallRecordTypeOutgoing callCapability:INCallCapabilityUnknown callDuration:[NSNumber numberWithInt:0] unseen:false];
        [records addObject:record];
    }
    if (records.count == 1) {
        completion([NSClassFromString(@"INCallRecordResolutionResult") performSelector:NSSelectorFromString(@"successWithResolvedCallRecord:") withObject:records.firstObject]);
    }
    else if (records.count > 1) {
        completion([NSClassFromString(@"INCallRecordResolutionResult") performSelector:NSSelectorFromString(@"disambiguationWithCallRecordsToDisambiguate:") withObject:records]);
    }
    else {
        completion([NSClassFromString(@"INCallRecordResolutionResult") performSelector:NSSelectorFromString(@"needsValue")]);
    }
}

-  (void) resolveContactsForStartCall:(INStartCallIntent *)intent withCompletion:(void (^)(NSArray<INStartCallContactResolutionResult *> *resolutionResults))completion
{
    NSMutableArray<INStartCallContactResolutionResult *> *resolutionResults = [NSMutableArray array];
    int i, j;

    if (intent.contacts.count == 0) {
        [resolutionResults addObject:[INStartCallContactResolutionResult needsValue]];
    }
    else {
        NSMutableArray<INPerson *> *recipients = [NSMutableArray array];
        for (i = 0; i < intent.contacts.count; i++) {
            INPerson *contact = [intent.contacts objectAtIndex:i];
            if ([contact respondsToSelector:@selector(siriMatches)] && contact.siriMatches && (contact.siriMatches.count != 0)) {
                for (j = 0; j < contact.siriMatches.count; j++) {
                    [recipients addObject:[contact.siriMatches objectAtIndex:j]];
                }
            }
            else {
                [recipients addObject:contact];
            }
        }
        if (recipients.count == 1) {
            [resolutionResults addObject:[INStartCallContactResolutionResult successWithResolvedPerson:recipients.firstObject]];
        }
        else if (recipients.count > 1) {
            [resolutionResults addObject:[INStartCallContactResolutionResult disambiguationWithPeopleToDisambiguate:recipients]];
        }
        else {
            [resolutionResults addObject:[INStartCallContactResolutionResult unsupported]];
        }
    }
    completion(resolutionResults);
}

- (void) confirmStartCall:(INStartCallIntent *)intent completion:(void (^)(INStartCallIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartCallIntent class])];
    INStartCallIntentResponse *response = [[INStartCallIntentResponse alloc] initWithCode:INStartCallIntentResponseCodeReady userActivity:userActivity];
    completion(response);
}

- (void) handleStartCall:(INStartCallIntent *)intent completion:(void (^)(INStartCallIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartCallIntent class])];
    INStartCallIntentResponse *response = [[INStartCallIntentResponse alloc] initWithCode:INStartCallIntentResponseCodeContinueInApp userActivity:userActivity];
    completion(response);
}

#pragma mark - INStartAudioCallIntentHandling

- (void) resolveContactsForStartAudioCall:(INStartAudioCallIntent *)intent withCompletion:(void (^)(NSArray<INPersonResolutionResult *> *resolutionResults))completion
{
    NSMutableArray<INPersonResolutionResult *> *resolutionResults = [NSMutableArray array];
    int i, j;

    if (intent.contacts.count == 0) {
        [resolutionResults addObject:[INPersonResolutionResult needsValue]];
    }
    else {
        NSMutableArray<INPerson *> *recipients = [NSMutableArray array];
        for (i = 0; i < intent.contacts.count; i++) {
            INPerson *contact = [intent.contacts objectAtIndex:i];
            if ([contact respondsToSelector:@selector(siriMatches)] && contact.siriMatches && (contact.siriMatches.count != 0)) {
                for (j = 0; j < contact.siriMatches.count; j++) {
                    [recipients addObject:[contact.siriMatches objectAtIndex:j]];
                }
            }
            else {
                [recipients addObject:contact];
            }
        }
        if (recipients.count == 1) {
            [resolutionResults addObject:[INPersonResolutionResult successWithResolvedPerson:recipients.firstObject]];
        }
        else if (recipients.count > 1) {
            [resolutionResults addObject:[INPersonResolutionResult disambiguationWithPeopleToDisambiguate:recipients]];
        }
        else {
            [resolutionResults addObject:[INPersonResolutionResult unsupported]];
        }
    }
    completion(resolutionResults);
}

- (void) confirmStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartAudioCallIntent class])];
    INStartAudioCallIntentResponse *response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeReady userActivity:userActivity];
    completion(response);
}

- (void) handleStartAudioCall:(INStartAudioCallIntent *)intent completion:(void (^)(INStartAudioCallIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartAudioCallIntent class])];
    INStartAudioCallIntentResponse *response = [[INStartAudioCallIntentResponse alloc] initWithCode:INStartAudioCallIntentResponseCodeContinueInApp userActivity:userActivity];
    completion(response);
}

#pragma mark - INStartVideoCallIntentHandling

- (void) resolveContactsForStartVideoCall:(INStartVideoCallIntent *)intent withCompletion:(void (^)(NSArray<INPersonResolutionResult *> *resolutionResults))completion
{
    NSMutableArray<INPersonResolutionResult *> *resolutionResults = [NSMutableArray array];
    int i, j;

    if (intent.contacts.count == 0) {
        [resolutionResults addObject:[INPersonResolutionResult needsValue]];
    }
    else {
        NSMutableArray<INPerson *> *recipients = [NSMutableArray array];
        for (i = 0; i < intent.contacts.count; i++) {
            INPerson *contact = [intent.contacts objectAtIndex:i];
            if ([contact respondsToSelector:@selector(siriMatches)] && contact.siriMatches && (contact.siriMatches.count != 0)) {
                for (j = 0; j < contact.siriMatches.count; j++) {
                    [recipients addObject:[contact.siriMatches objectAtIndex:j]];
                }
            }
            else {
                [recipients addObject:contact];
            }
        }
        if (recipients.count == 1) {
            [resolutionResults addObject:[INPersonResolutionResult successWithResolvedPerson:recipients.firstObject]];
        }
        else if (recipients.count > 1) {
            [resolutionResults addObject:[INPersonResolutionResult disambiguationWithPeopleToDisambiguate:recipients]];
        }
        else {
            [resolutionResults addObject:[INPersonResolutionResult unsupported]];
        }
    }
    completion(resolutionResults);
}

- (void) confirmStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartVideoCallIntent class])];
    INStartVideoCallIntentResponse *response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeReady userActivity:userActivity];
    completion(response);
}

- (void) handleStartVideoCall:(INStartVideoCallIntent *)intent completion:(void (^)(INStartVideoCallIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INStartVideoCallIntent class])];
    INStartVideoCallIntentResponse *response = [[INStartVideoCallIntentResponse alloc] initWithCode:INStartVideoCallIntentResponseCodeContinueInApp userActivity:userActivity];
    completion(response);
}

#pragma mark - INSearchCallHistoryIntentHandling

- (void) resolveCallTypesForSearchCallHistory:(INSearchCallHistoryIntent *)intent withCompletion:(void (^)(INCallRecordTypeOptionsResolutionResult *resolutionResult))completion
{
    completion([INCallRecordTypeOptionsResolutionResult successWithResolvedCallRecordTypeOptions:intent.callTypes]);
}

- (void) resolveCallTypeForSearchCallHistory:(INSearchCallHistoryIntent *)intent withCompletion:(void (^)(INCallRecordTypeResolutionResult *resolutionResult))completion
{
    completion([INCallRecordTypeResolutionResult successWithResolvedCallRecordType:intent.callType]);
}

- (void) resolveRecipientForSearchCallHistory:(INSearchCallHistoryIntent *)intent withCompletion:(void (^)(INPersonResolutionResult *resolutionResult))completion
{
    completion([INPersonResolutionResult successWithResolvedPerson:intent.recipient]);
}

- (void) resolveDateCreatedForSearchCallHistory:(INSearchCallHistoryIntent *)intent withCompletion:(void (^)(INDateComponentsRangeResolutionResult *resolutionResult))completion
{
    completion([INDateComponentsRangeResolutionResult successWithResolvedDateComponentsRange:intent.dateCreated]);
}

- (void) resolveUnseenForSearchCallHistory:(INSearchCallHistoryIntent *)intent withCompletion:(void (^)(INBooleanResolutionResult *resolutionResult))completion
{
    completion([INBooleanResolutionResult successWithResolvedValue:true]);
}

- (void) confirmSearchCallHistory:(INSearchCallHistoryIntent *)intent completion:(void (^)(INSearchCallHistoryIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSearchCallHistoryIntent class])];
    INSearchCallHistoryIntentResponse *response = [[INSearchCallHistoryIntentResponse alloc] initWithCode:INSearchCallHistoryIntentResponseCodeReady userActivity:userActivity];
    completion(response);
}

- (void) handleSearchCallHistory:(INSearchCallHistoryIntent *)intent completion:(void (^)(INSearchCallHistoryIntentResponse *response))completion
{
    NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSearchCallHistoryIntent class])];
    INSearchCallHistoryIntentResponse *response = [[INSearchCallHistoryIntentResponse alloc] initWithCode:INSearchCallHistoryIntentResponseCodeFailureRequiringAppLaunch userActivity:userActivity];
    completion(response);
}

- (BOOL) application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> *restorableObjects))restorationHandler
{
    int i;

    NSLog(@"continueUserActivity %@", userActivity);
    if (userActivity.activityType && [userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        return [self application:[UIApplication sharedApplication] openURL:userActivity.webpageURL sourceApplication:userActivity.referrerURL.absoluteString annotation:nil];
    }
    else if ([userActivity.interaction.intent isKindOfClass:[INStartVideoCallIntent class]]) {
        INPerson *person = [[(INStartVideoCallIntent *)userActivity.interaction.intent contacts] firstObject];
        if ([person respondsToSelector:@selector(siriMatches)] && (person.personHandle.value.length == 0) && person.siriMatches && (person.siriMatches.count != 0)) {
            person = [person.siriMatches firstObject];
        }
        NSString *number = [self stripNonASCII:person.personHandle.value];
        i = 0;
        while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).released || ![number isEqualToString:((CxCall *)[self.cxCalls objectAtIndex:i]).number])) {
            i++;
        }
        if (i < self.cxCalls.count) {
            NSLog(@"continueUserActivity INStartVideoCallIntent released_now %u %lu", i, self.cxCalls.count);
            [((CxCall *)[self.cxCalls objectAtIndex:i]) released_now];
        }
        else {
            NSLog(@"continueUserActivity INStartVideoCallIntent %u intent=%@ person=%@ number=%@ numtype=%ld", (self.forms != NULL), userActivity.interaction.intent, person, number, person.personHandle.type);
            if (person.personHandle.type == INPersonHandleTypePhoneNumber) {
                self.forms.event_dial_number(number, person.displayName, FORMS_DIAL_NUMBER_VIDEO_MODE_ON);
            }
            else {
                self.forms.event_dial_sip(number, person.displayName, FORMS_DIAL_NUMBER_VIDEO_MODE_ON);
            }
        }
    }
    else if ([userActivity.interaction.intent isKindOfClass:[INStartAudioCallIntent class]]) {
        INPerson *person = [[(INStartAudioCallIntent *)userActivity.interaction.intent contacts] firstObject];
        if ([person respondsToSelector:@selector(siriMatches)] && (person.personHandle.value.length == 0) && person.siriMatches && (person.siriMatches.count != 0)) {
            person = [person.siriMatches firstObject];
        }
        NSString *number = [self stripNonASCII:person.personHandle.value];
        NSLog(@"continueUserActivity INStartAudioCallIntent %u intent=%@ person=%@ number=%@ numtype=%ld", (self.forms != NULL), userActivity.interaction.intent, person, number, person.personHandle.type);
        if (person.personHandle.type == INPersonHandleTypePhoneNumber) {
            self.forms.event_dial_number(number, person.displayName, FORMS_DIAL_NUMBER_VIDEO_MODE_OFF);
        }
        else {
            self.forms.event_dial_sip(number, person.displayName, FORMS_DIAL_NUMBER_VIDEO_MODE_OFF);
        }
    }
    else if ([userActivity.interaction.intent isKindOfClass:NSClassFromString(@"INStartCallIntent")]) {
        NSObject *intent = userActivity.interaction.intent;
        INPerson *person = [[intent performSelector:@selector(contacts)] firstObject];
        if ([person respondsToSelector:@selector(siriMatches)] && (person.personHandle.value.length == 0) && person.siriMatches && (person.siriMatches.count != 0)) {
            person = [person.siriMatches firstObject];
        }
        NSString *number = [self stripNonASCII:person.personHandle.value];
        int callCapability = [[intent valueForKey:@"callCapability"] intValue];
        i = 0;
        while ((i < self.cxCalls.count) && (((CxCall *)[self.cxCalls objectAtIndex:i]).released || ![number isEqualToString:((CxCall *)[self.cxCalls objectAtIndex:i]).number])) {
            i++;
        }
        if ((callCapability == INCallCapabilityVideoCall) && (i < self.cxCalls.count)) {
            NSLog(@"continueUserActivity INStartCallIntent released_now %u %u %lu", callCapability, i, self.cxCalls.count);
            [((CxCall *)[self.cxCalls objectAtIndex:i]) released_now];
        }
        else {
            NSLog(@"continueUserActivity INStartCallIntent %u intent=%@ person=%@ number=%@ numtype=%ld", (self.forms != NULL), intent, person, number, person.personHandle.type);
            if (person.personHandle.type == INPersonHandleTypePhoneNumber) {
                self.forms.event_dial_number(number, person.displayName, (callCapability == INCallCapabilityVideoCall) ? FORMS_DIAL_NUMBER_VIDEO_MODE_ON :
                    ((callCapability == INCallCapabilityAudioCall) ? FORMS_DIAL_NUMBER_VIDEO_MODE_OFF : FORMS_DIAL_NUMBER_VIDEO_MODE_DEFAULT));
            }
            else {
                self.forms.event_dial_sip(number, person.displayName, (callCapability == INCallCapabilityVideoCall) ? FORMS_DIAL_NUMBER_VIDEO_MODE_ON :
                    ((callCapability == INCallCapabilityAudioCall) ? FORMS_DIAL_NUMBER_VIDEO_MODE_OFF : FORMS_DIAL_NUMBER_VIDEO_MODE_DEFAULT));
            }
        }
    }
    return YES;
}

#pragma mark - CXProviderDelegate

- (void) providerDidReset:(CXProvider *)provider
{
}

// Called when the provider has been fully created and is ready to send actions and receive updates
- (void) providerDidBegin:(CXProvider *)provider
{
}

- (void) provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
{
    byte b[16], uuid[16];
    int i, j;
    [action.callUUID getUUIDBytes:b];

    phone_configure_audio_session();
    [action fulfill];
    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if ((i < self.cxCalls.count) && !((CxCall *)[self.cxCalls objectAtIndex:i]).released && ((CxCall *)[self.cxCalls objectAtIndex:i]).incoming) {
        [self.cxProvider reportOutgoingCallWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).uuid connectedAtDate:nil];
    }
    else {
        self.ios_event.enqueue(FORMS_EVENT_CX_START_CALL_ACTION, [self stringWithUUID:action.callUUID], action.handle.value, action.contactIdentifier, action.video);
        j = 0;
        while ((j < self.cxCalls.count) && ((CxCall *)[self.cxCalls objectAtIndex:j]).released) {
            j++;
        }
        if (j + 1 < self.cxCalls.count) {
            [self formsCxRequestSetGroupCall:[self stringWithUUID:action.callUUID] with:[self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:j]).uuid]];
        }
    }
}

- (void) provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
{
    byte b[16], uuid[16];
    int i;
    [action.callUUID getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if (i < self.cxCalls.count) {
        if (!((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
            ((CxCall *)[self.cxCalls objectAtIndex:i]).connected = true;
    		if (((CxCall *)[self.cxCalls objectAtIndex:i]).claimed) {
                if (!((CxCall *)[self.cxCalls objectAtIndex:i]).accept) {
                    phone_configure_audio_session();
                }
                self.ios_event.enqueue(FORMS_EVENT_CX_ANSWER_CALL_ACTION, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id]);
            }
            else {
                if (!((CxCall *)[self.cxCalls objectAtIndex:i]).accept) {
                    phone_configure_audio_session();
                    ((CxCall *)[self.cxCalls objectAtIndex:i]).accept = true;
                }
            }
        }
    }
    [action fulfill];
}

- (void) provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
{
    byte b[16], uuid[16];
    int i;
    [action.callUUID getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if (i < self.cxCalls.count) {
        if (((CxCall *)[self.cxCalls objectAtIndex:i]).claimed) {
            [((CxCall *)[self.cxCalls objectAtIndex:i]) released_now];
            [self audioSessionMayDeactivate];
            self.ios_event.enqueue(FORMS_EVENT_CX_END_CALL_ACTION, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id], FORMS_CX_CALL_ERROR_SUCCESS);
        }
        else {
            ((CxCall *)[self.cxCalls objectAtIndex:i]).reject = true;
        }
    }
    [action fulfill];
}

- (void) provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action
{
    byte b[16], uuid[16];
    int i;
    [action.callUUID getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if ((i < self.cxCalls.count) && (action.onHold ^ ((CxCall *)[self.cxCalls objectAtIndex:i]).held)) {
        ((CxCall *)[self.cxCalls objectAtIndex:i]).held = action.onHold;
        self.ios_event.enqueue(FORMS_EVENT_CX_SET_HELD_CALL_ACTION, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id], action.onHold);
    }
    [action fulfill];
}

- (void) provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action
{
    byte b[16], uuid[16];
    int i;
    [action.callUUID getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if (i < self.cxCalls.count && (action.muted ^ ((CxCall *)[self.cxCalls objectAtIndex:i]).muted)) {
        ((CxCall *)[self.cxCalls objectAtIndex:i]).muted = action.muted;
        self.ios_event.enqueue(FORMS_EVENT_CX_SET_MUTED_CALL_ACTION, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id], action.muted);
    }
    [action fulfill];
}

- (void) provider:(CXProvider *)provider performSetGroupCallAction:(CXSetGroupCallAction *)action
{
    byte b[16], uuid[16];
    int i;
    [action.callUUID getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if (i < self.cxCalls.count) {
//        self.ios_event.enqueue(FORMS_EVENT_CX_SET_GROUP_CALL_ACTION, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id], [self stringWithUUID:action.callUUIDToGroupWith]);
    }
    [action fulfill];
}

- (void) provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action
{
    byte b[16], uuid[16];
    int i;
    [action.callUUID getUUIDBytes:b];

    i = 0;
    while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
        i++;
    }
    if (i < self.cxCalls.count) {
        self.ios_event.enqueue(FORMS_EVENT_CX_PLAY_DTMF_CALL_ACTION, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id], action.digits);
    }
    [action fulfill];
}

- (void) performAudioRouteChanged_:(NSUUID *)UUID deviceId:(NSString *)deviceId
{
    byte b[16], uuid[16];
    int i;

    i = 0;
    if (UUID != nil) {
        [UUID getUUIDBytes:b];

        while ((i < self.cxCalls.count) && (memcmp(b, [((CxCall *)[self.cxCalls objectAtIndex:i]) getUUIDBytes:uuid], sizeof(b)) != 0)) {
            i++;
        }
    }
    else {
        while ((i < self.cxCalls.count) && ((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
            i++;
        }
    }
    bool speakerphone = [deviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSFREE];
    bool headset = (!speakerphone && ![deviceId isEqualToString:@AUDIO_DEVICE_ID_DEFAULT] && ![deviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSET]);
    if (i < self.cxCalls.count) {
        if (speakerphone
		 || headset
		 || (self.speakerphoneEnabled && self.speakerphoneSwitched && !speakerphone)
		 || (self.headsetEnabled && !headset)) {
            self.ios_event.enqueue(FORMS_EVENT_SET_PROPERTY, @"DSP/AUDIO-DEVICES-UPDATED", @"");
            self.ios_event.enqueue(FORMS_EVENT_CX_AUDIO_ROUTE_CHANGED, [self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id], deviceId);
        }
        self.speakerphoneEnabled = speakerphone;
        self.headsetEnabled = headset;        
    }
}

- (void) setMicrophoneMute_:(id)context
{
    int i;

    for (i = 0; i < self.cxCalls.count; i++) {
        if (!((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
            [self formsCxRequestSetMutedCall:[self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id] muted:true];
        }
    }
}

- (void) setMicrophoneUnmute_:(id)context
{
    int i;

    for (i = 0; i < self.cxCalls.count; i++) {
        if (!((CxCall *)[self.cxCalls objectAtIndex:i]).released) {
            [self formsCxRequestSetMutedCall:[self stringWithUUID:((CxCall *)[self.cxCalls objectAtIndex:i]).conf_id] muted:false];
        }
    }
}

// If provider:executeTransaction:error: returned NO, each perform*CallAction method is called sequentially for each action in the transaction
/*
- (BOOL) provider:(CXProvider *)provider executeTransaction:(CXTransaction *)transaction
{
}
*/

// Called when an action was not performed in time and has been inherently failed. Depending on the action, this timeout may also force the call to end. An action that has already timed out should not be fulfilled or failed by the provider delegate
- (void) provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
{
    // React to the action timeout if necessary, such as showing an error UI.
}

// Called when the provider's audio session activation state changes.
- (void) provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
{
    NSLog(@"cxDidActivateAudioSession");
    // Start call audio media, now that the audio session has been activated after having its priority boosted.
    self.audioSessionActivated = true;
    phone_audio_session_activated(true);
}

- (void) provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
{
    NSLog(@"cxDidDeactivateAudioSession");
    // Restart any non-call related audio now that the app's audio session has been
    // de-activated after having its priority restored to normal.
    phone_audio_session_activated(false);
    self.audioSessionActivated = false;
}

- (void) formsCPCreate:(int)new_id
{
    [self.formsCP formsCPCreate:new_id];
}

- (void) application:(UIApplication *)application didConnectCarInterfaceController:(CPInterfaceController *)interfaceController toWindow:(CPWindow *)window
{
    if (self.cpSessionConfiguration == nil) {
        self.cpSessionConfiguration = [[CPSessionConfiguration alloc] initWithDelegate:self];
    }
    [self.formsCP didConnectCarInterfaceController:interfaceController toWindow:window];
}

- (void) application:(UIApplication *)application didDisconnectCarInterfaceController:(CPInterfaceController *)interfaceController fromWindow:(CPWindow *)window
{
    [self.formsCP didDisconnectCarInterfaceController:interfaceController fromWindow:window];
}

- (void) sessionConfiguration:(CPSessionConfiguration *)sessionConfiguration limitedUserInterfacesChanged:(CPLimitableUserInterface)limitedUserInterfaces
{
}

/*
- (UISceneConfiguration *) application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options
{
    UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:nil sessionRole:connectingSceneSession.role];
    configuration.delegateClass = SceneDelegate.class;
    return configuration;
}

- (void) application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions
{
}
*/

@end

void ns_log(const char *format, ...)
{
    va_list args;

    va_start(args, format);
    NSLogv([NSString stringWithCString:format encoding:NSUTF8StringEncoding], args);
    va_end(args);
}

CFDictionaryRef UserDefaultsCopyDictionaryForKey(const char *key)
{
    return ((CFDictionaryRef)(CFBridgingRetain([[NSUserDefaults standardUserDefaults] dictionaryForKey:[NSString stringWithCString:key encoding:NSUTF8StringEncoding]])));
}

void getISOCountry(char *buf)
{
    NSString *country_code = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
    if (country_code.length >= 2) {
        buf[0] = [country_code characterAtIndex:0];
        buf[1] = [country_code characterAtIndex:1];
        buf[2] = '\0';
    }
    else {
        buf[0] = '\0';
    }
}

void getISO3Language(char *buf)
{
    NSDictionary *language_code_to_ios639_2_map =
    @{
        @"aa" : @"aar",
        @"ab" : @"abk",
        @"ae" : @"ave",
        @"af" : @"afr",
        @"ak" : @"aka",
        @"am" : @"amh",
        @"an" : @"arg",
        @"ar" : @"ara",
        @"as" : @"asm",
        @"av" : @"ava",
        @"ay" : @"aym",
        @"az" : @"aze",
        @"ba" : @"bak",
        @"be" : @"bel",
        @"bg" : @"bul",
        @"bh" : @"bih",
        @"bi" : @"bis",
        @"bm" : @"bam",
        @"bn" : @"ben",
        @"bo" : @"bod",
        @"br" : @"bre",
        @"bs" : @"bos",
        @"ca" : @"cat",
        @"ce" : @"che",
        @"ch" : @"cha",
        @"co" : @"cos",
        @"cr" : @"cre",
        @"cs" : @"ces",
        @"cu" : @"chu",
        @"cv" : @"chv",
        @"cy" : @"cym",
        @"da" : @"dan",
        @"de" : @"deu",
        @"dv" : @"div",
        @"dz" : @"dzo",
        @"ee" : @"ewe",
        @"el" : @"ell",
        @"en" : @"eng",
        @"eo" : @"epo",
        @"es" : @"spa",
        @"et" : @"est",
        @"eu" : @"eus",
        @"fa" : @"fas",
        @"ff" : @"ful",
        @"fi" : @"fin",
        @"fj" : @"fij",
        @"fo" : @"fao",
        @"fr" : @"fra",
        @"fy" : @"fry",
        @"ga" : @"gle",
        @"gd" : @"gla",
        @"gl" : @"glg",
        @"gn" : @"grn",
        @"gu" : @"guj",
        @"gv" : @"glv",
        @"ha" : @"hau",
        @"he" : @"heb",
        @"hi" : @"hin",
        @"ho" : @"hmo",
        @"hr" : @"hrv",
        @"ht" : @"hat",
        @"hu" : @"hun",
        @"hy" : @"hye",
        @"hz" : @"her",
        @"ia" : @"ina",
        @"id" : @"ind",
        @"ie" : @"ile",
        @"ig" : @"ibo",
        @"ii" : @"iii",
        @"ik" : @"ipk",
        @"io" : @"ido",
        @"is" : @"isl",
        @"it" : @"ita",
        @"iu" : @"iku",
        @"ja" : @"jpn",
        @"jv" : @"jav",
        @"ka" : @"kat",
        @"kg" : @"kon",
        @"ki" : @"kik",
        @"kj" : @"kua",
        @"kk" : @"kaz",
        @"kl" : @"kal",
        @"km" : @"khm",
        @"kn" : @"kan",
        @"ko" : @"kor",
        @"kr" : @"kau",
        @"ks" : @"kas",
        @"ku" : @"kur",
        @"kv" : @"kom",
        @"kw" : @"cor",
        @"ky" : @"kir",
        @"la" : @"lat",
        @"lb" : @"ltz",
        @"lg" : @"lug",
        @"li" : @"lim",
        @"ln" : @"lin",
        @"lo" : @"lao",
        @"lt" : @"lit",
        @"lu" : @"lub",
        @"lv" : @"lav",
        @"mg" : @"mlg",
        @"mh" : @"mah",
        @"mi" : @"mri",
        @"mk" : @"mkd",
        @"ml" : @"mal",
        @"mn" : @"mon",
        @"mr" : @"mar",
        @"ms" : @"msa",
        @"mt" : @"mlt",
        @"my" : @"mya",
        @"na" : @"nau",
        @"nb" : @"nob",
        @"nd" : @"nde",
        @"ne" : @"nep",
        @"ng" : @"ndo",
        @"nl" : @"nld",
        @"nn" : @"nno",
        @"no" : @"nor",
        @"nr" : @"nbl",
        @"nv" : @"nav",
        @"ny" : @"nya",
        @"oc" : @"oci",
        @"oj" : @"oji",
        @"om" : @"orm",
        @"or" : @"ori",
        @"os" : @"oss",
        @"pa" : @"pan",
        @"pi" : @"pli",
        @"pl" : @"pol",
        @"ps" : @"pus",
        @"pt" : @"por",
        @"qu" : @"que",
        @"rm" : @"roh",
        @"rn" : @"run",
        @"ro" : @"ron",
        @"ru" : @"rus",
        @"rw" : @"kin",
        @"sa" : @"san",
        @"sc" : @"srd",
        @"sd" : @"snd",
        @"se" : @"sme",
        @"sg" : @"sag",
        @"si" : @"sin",
        @"sk" : @"slk",
        @"sl" : @"slv",
        @"sm" : @"smo",
        @"sn" : @"sna",
        @"so" : @"som",
        @"sq" : @"sqi",
        @"sr" : @"srp",
        @"ss" : @"ssw",
        @"st" : @"sot",
        @"su" : @"sun",
        @"sv" : @"swe",
        @"sw" : @"swa",
        @"ta" : @"tam",
        @"te" : @"tel",
        @"tg" : @"tgk",
        @"th" : @"tha",
        @"ti" : @"tir",
        @"tk" : @"tuk",
        @"tl" : @"tgl",
        @"tn" : @"tsn",
        @"to" : @"ton",
        @"tr" : @"tur",
        @"ts" : @"tso",
        @"tt" : @"tat",
        @"tw" : @"twi",
        @"ty" : @"tah",
        @"ug" : @"uig",
        @"uk" : @"ukr",
        @"ur" : @"urd",
        @"uz" : @"uzb",
        @"ve" : @"ven",
        @"vi" : @"vie",
        @"vo" : @"vol",
        @"wa" : @"wln",
        @"wo" : @"wol",
        @"xh" : @"xho",
        @"yi" : @"yid",
        @"yo" : @"yor",
        @"za" : @"zha",
        @"zh" : @"zho",
        @"zu" : @"zul",
    };

    NSString *language_code = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
    NSString *ios639_2_code = [language_code_to_ios639_2_map objectForKey:language_code];
    if (ios639_2_code == nil) {
        ios639_2_code = language_code;
    }
    if (ios639_2_code.length >= 3) {
        buf[0] = [ios639_2_code characterAtIndex:0];
        buf[1] = [ios639_2_code characterAtIndex:1];
        buf[2] = [ios639_2_code characterAtIndex:2];
        buf[3] = '\0';
    }
    else {
        buf[0] = '\0';
    }
}

void dialNumber(const char *number)
{
    NSString * numberString = [NSString stringWithCString:number encoding:NSUTF8StringEncoding];
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate dialNumber_:numberString]; });
}

void dialNormalizedNumber(const char *number)
{
    dialNumber(number);
}

void onCallPresentationStart()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onCallPresentationStart_:nil]; });
}

void onCallPresentationEnd()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onCallPresentationEnd_:nil]; });
}

void onCommunicationStart()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onCommunicationStart_:nil]; });
}

void onCommunicationEnd()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onCommunicationEnd_:nil]; });
}

void onHandsetStart()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onHandsetStart_:nil]; });
}

void onHandsetEnd()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onHandsetEnd_:nil]; });
}

void onSpeakerphoneStart()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onSpeakerphoneStart_:nil]; });
}

void onSpeakerphoneEnd()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onSpeakerphoneEnd_:nil]; });
}

void onHeadsetStart()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onHeadsetStart_:nil]; });
}

void onHeadsetEnd()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onHeadsetEnd_:nil]; });
}

void onVideoStart()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onVideoStart_:nil]; });
}

void onVideoEnd()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onVideoEnd_:nil]; });
}

void onResetNotify()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onResetNotify_:nil]; });
}

void onSignallingConnected()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onSignallingConnected_:nil]; });
}

void onSignallingDisconnected()
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onSignallingDisconnected_:nil]; });
}

bool isExternalCallPending()
{
    return ((NSClassFromString(@"CXProvider") == nil) && (phone_ios_callCenter.currentCalls != nil));
/*
    int i;

    if (phone_ios_callCenter.currentCalls != nil) {
        for (i = 0; i < phone_ios_callCenter.currentCalls.count; i++) {
            CTCall *call = (CTCall *)[phone_ios_callCenter.currentCalls.allObjects objectAtIndex:i];
            if([call.callState isEqualToString:CTCallStateIncoming]) {
                return 'R';
            }
            else if([call.callState isEqualToString:CTCallStateDialing]) {
                return 'O';
            }
            else if([call.callState isEqualToString:CTCallStateConnected]) {
                return 'O';
            }
        }
    }
    return 'I';
*/
}

void audioFocusLoss(bool audio_focus_loss)
{
    dispatch_async(dispatch_get_main_queue(), ^
        {
            if (audio_focus_loss)
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate audioFocusLoss_:nil];
            else
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate audioFocusGain_:nil];
        });
}

void onAudioSessionActive(bool active)
{
    dispatch_async(dispatch_get_main_queue(), ^
        {
            if (active)
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onAudioSessionActive_:nil];
            else
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate onAudioSessionInactive_:nil];
        });
}

void performAudioRouteChanged(const char *uuid, const char * deviceId)
{
    NSUUID * UUID = uuid ? [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate UUIDWithString:[NSString stringWithCString:uuid encoding:NSUTF8StringEncoding]] : nil;
    NSString * deviceIdString = [NSString stringWithCString:deviceId encoding:NSUTF8StringEncoding];
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate performAudioRouteChanged_:UUID deviceId:deviceIdString]; });
}

void setMicrophoneMute(bool mute)
{
    dispatch_async(dispatch_get_main_queue(), ^
        {
            if (mute)
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate setMicrophoneMute_:nil];
            else
                [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate setMicrophoneUnmute_:nil];
        });
}

void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void *context)
{
    dispatch_async(dispatch_get_main_queue(), ^
        { [(PhoneIosDelegate *)[UIApplication sharedApplication].delegate addressBookChanged:addressBook info:info context:context]; });
}

#define ASL_KEY_MESSAGE_ID      "ASLMessageID"
#define ASL_KEY_TIME_NANO_SEC   "TimeNanoSec"

static int pipe_stderr_fd[2] = {-1,-1};
static int old_stderr_fd = -1;
static int save_log_fd = -1;

void open_log()
{
#if !TARGET_IPHONE_SIMULATOR && !defined(DEBUG)
    long flags;
    int i;

    if (atoi([UIDevice currentDevice].systemVersion.UTF8String) >= 10) {
        if (pipe_stderr_fd[0] == -1) {
            old_stderr_fd = dup(fileno(stderr));
            if(pipe(pipe_stderr_fd) >= 0) {
                for(i = 0; i < sizeof(pipe_stderr_fd) / sizeof(pipe_stderr_fd[0]); i++) {
                    flags = fcntl(pipe_stderr_fd[i], F_GETFL);
                    if(flags == -1) {
                        flags = 0;
                    }
                    flags |= O_NONBLOCK;
                    fcntl(pipe_stderr_fd[i], F_SETFL, flags);
                }
                dup2(pipe_stderr_fd[1], fileno(stderr));
            }
        }
    }
#endif
}

#if !TARGET_IPHONE_SIMULATOR

static time_t last_time_sec = 0;
static char last_time_string[20] = {'\n','0','1','-','0','1',' ','0','0',':','0','0',':','0','0','.','0','0','0',' '};
static dword next_asl_message_id = 0;

static void save_aslmsg(aslmsg msg)
{
#if 0
static char asl_level_to_char_table[] = {
    'F', /* Emergency */
    'A', /* Alert */
    'C', /* Critical */
    'E', /* Error */
    'W', /* Warning */
    'N', /* Notice */
    'I', /* Info */
    'D', /* Debug */
    '8', '9', '0', '1', '2', '3', '4', '5',
};
#endif
    struct tm tm_local;
    dword ns, i;
    const char *m;
    
    time_t s = atol(asl_get(msg, ASL_KEY_TIME));
    if (s != last_time_sec) {
        last_time_sec = s;
        localtime_r(&s, &tm_local);
        sprintf(last_time_string, "\n%02d-%02d %02d:%02d:%02d", tm_local.tm_mon + 1, tm_local.tm_mday,
            tm_local.tm_hour, tm_local.tm_min, tm_local.tm_sec);
        last_time_string[15] = '.';
    }
    ns = atol(asl_get(msg, ASL_KEY_TIME_NANO_SEC));
    i = ns / 100000000;
    last_time_string[16] = (char)('0' + i);
    ns -= i * 100000000;
    i = ns / 10000000;
    last_time_string[17] = (char)('0' + i);
    ns -= i * 10000000;
    i = ns / 1000000;
    last_time_string[18] = (char)('0' + i);

    write(save_log_fd, last_time_string, 20);
    m = asl_get(msg, ASL_KEY_MSG);
    write(save_log_fd, m, strlen(m));
}

#endif

void poll_log()
{
    char path[PATH_MAX];
    char text_line[80];
    int i;

    if (save_log_fd == -1) {
        sprintf(path, "%s/%s%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, SAVE_LOG_FILE_EXT);
        save_log_fd = open(path, O_WRONLY | O_CREAT | O_APPEND, S_IREAD | S_IWRITE);
        if (save_log_fd != -1) {
            i = sprintf(text_line, "\n--------- version %s%s", _BUILD_STRING_, (pipe_stderr_fd[0] != -1) ? "\n" : "");
            write(save_log_fd, text_line, i);
        }
    }
#if !TARGET_IPHONE_SIMULATOR
    if (save_log_fd != -1) {
        if (pipe_stderr_fd[0] != -1) {
            byte buf[1024];
            i = read(pipe_stderr_fd[0], buf, sizeof(buf));
            while(i > 0) {
                write(save_log_fd, buf, i);
                write(old_stderr_fd, buf, i);
                i = read(pipe_stderr_fd[0], buf, sizeof(buf));
            }
        }
        else {
            aslmsg q, l, m;
            aslresponse r;
            dword d;
            char num[16];

            q = asl_new(ASL_TYPE_QUERY);
            sprintf(num, "%u", next_asl_message_id);
            asl_set_query(q, ASL_KEY_MESSAGE_ID, num, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC);
            r = asl_search(NULL, q);
            l = NULL;
            m = aslresponse_next(r);
            if (m != NULL) {
                d = atol(asl_get(m, ASL_KEY_MESSAGE_ID));
                if (d != next_asl_message_id) {
                    i = sprintf(text_line, "\n--------- missing sequence %u-%u\n", next_asl_message_id, d);
                    write(save_log_fd, text_line, i);
                }
                do {
                    l = m;
                    save_aslmsg(m);
                    m = aslresponse_next(r);
                }
                while (m != NULL);
            }
            if (l != NULL) {
                next_asl_message_id = atol(asl_get(l, ASL_KEY_MESSAGE_ID)) + 1;
            }
            aslresponse_free(r);
            asl_free(q);
        }
    }
#endif
    if ((save_log_fd != -1) && (lseek(save_log_fd, 0, SEEK_END) >= SAVE_LOG_FILE_LIMIT)) {
        close (save_log_fd);
        save_log_fd = -1;
#if (SAVE_LOG_FILE_COUNT != 0)
        char path2[PATH_MAX];
        i = SAVE_LOG_FILE_COUNT-1;
        sprintf(path, "%s/%s%d%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, i, SAVE_LOG_FILE_EXT);
        remove(path);
        while (i != 0) {
            strcpy(path2, path);
            i--;
            sprintf(path, "%s/%s%d%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, i, SAVE_LOG_FILE_EXT);
            rename(path, path2);
        }
        strcpy(path2, path);
        sprintf(path, "%s/%s%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, SAVE_LOG_FILE_EXT);
        rename(path, path2);
#else
        sprintf(path, "%s/%s%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, SAVE_LOG_FILE_EXT);
        remove(path);
#endif
    }
}

void close_log()
{
    int i;
    
    if (save_log_fd != -1) {
        write(save_log_fd, "\n", 1);
        close(save_log_fd);
        save_log_fd = -1;
    }
    if (old_stderr_fd != -1) {
        dup2(old_stderr_fd, fileno(stderr));
        old_stderr_fd = -1;
    }
    for(i = 0; i < sizeof(pipe_stderr_fd) / sizeof(pipe_stderr_fd[0]); i++) {
        close(pipe_stderr_fd[i]);
        pipe_stderr_fd[i] = -1;
    }
}

void clear_logs()
{
    char path[PATH_MAX];
    int i;
    
    if (save_log_fd != -1) {
        write(save_log_fd, "\n", 1);
        close(save_log_fd);
        save_log_fd = -1;
    }
    sprintf(path, "%s/%s%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, SAVE_LOG_FILE_EXT);
    remove(path);
    for (i = 0; i < SAVE_LOG_FILE_COUNT; i++) {
        sprintf(path, "%s/%s%d%s", phone_ios_files_dir_container, SAVE_LOG_FILE_NAME, i, SAVE_LOG_FILE_EXT);
        remove(path);
    }
}

void nslog_print(const char *line)
{
    NSLog(@"%@", [NSString stringWithCString:line encoding:NSUTF8StringEncoding]);
}

void nslog_printv(const char *format, va_list ap)
{
    NSLogv([NSString stringWithCString:format encoding:NSUTF8StringEncoding], ap);
}

