Uniconnect

Uniconnect service is a universal service for device inter-connection, and can be based on bluetooth, wifi, NFC, etc. At the present only the bluetooth is supported.

(Wifi method is being implemented.)

Basic Concept

Adaptor

Represents the adapter for inter-connection, is the foundation of all inter-connected operation.Through the adapter, you can scan device, obtain the bonded devices and create links. Its subclass AndroidBtAdapter is classic bluetooth adapter; subclass BleAdapter is BLE adaptor.

RemoteDevice

Represents the remote device for inter-connection, containing device type, name, address and other information.Its subclasses AndroidBtDevice represents classic bluetooth device; subclasses BleDevice represents BLE device.

Link

Represents physical pathway between the adapter.

Connection

Represents the logical channel based on the link. Its read and write operations are synchronous. A link can establish multiple connections at the same time.

ConnectionHelper

Help to handle the related connection state changes, it makes the use of the connection more convenient.

DataTransactor

Based on the Connection and ConnectionHelper implementation, read and write operations are asynchronous, it is more convenient to use than Connection.

FileTransactionModel

Transmission model focus on file system based on DataTransactor implemented .

CameraTransactionModel

Transmission model focus on camera application based on DataTransactor implemented.

ProviderTransactionModel

The common data transmission model based on DataTransactor implemented, can derived different data transmission model focus on various application , including HealthTransactionModel, LocaleTransactionModel, MusicControlTransactionModel, NotificationTransactionModel, ScheduleTransactionModel, SyncTimeTransactionModel, WatchFaceTransactionModel and WeatherTransactionModel.

Start the Service

Launch the service, only need to operate the client and server equipment service program, don't need to pay attention to the general application.

The Steps of Service-Initiation: Enable uniconnection, scanning device (optional in the phone), and establish link and start link.

Enable service

ANDROID CODE:

    AdapterManager m_manager = AdapterManager.getInstance(getApplicationContext());

IOS CODE:

    IWDSAdapterManager *adapterManager = [IWDSAdapterManager shareInstance];

Enable method of classic bluetooth adapter:

    m_adapter = m_manager.getAdapter(Adapter.TAG_ANDROID_BT_DATA_CHANNEL);
    m_adapter.enable();

Enable method of BLE bluetooth adapter:

ANDROID CODE:

    m_manager.getAdapter(Adapter.TAG_BLE_DATA_CHANNEL);
    m_adapter.enable();

IOS CODE:

    IWDSAdapter *adapter = [adapterManager getAdapter:IWDSLinkTagBleDataChannel];
    [adapter enable];

 

Scanning equipment

    Adapter.DeviceDiscoveryCallbacks callback = new Adapter.DeviceDiscoveryCallbacks() {
        public void onDiscoveryStarted() {
        
        }

        public void onDeviceFound(AndroidBtDevice device) {
                
        }

        public void onDiscoveryFinished() {
                
        }
    };
        
    m_adapter.startDiscovery(callback);

Establish links and Initiation

Before the two devices (watch and phone) establish connection and transmit data, you need to establish the link.

Generally, smartwatch as the server, mobile phone as the client, if you want establish the link between the two device, you need make both initiate the links: the server call startServer (), client call bondAddress (address). One phone can connect only one watche, and one watch can only connect to one mobile phone.

Sample code for starting the server (watch) link :

    initialize(DeviceDescriptor.DEVICE_CLASS_WEARABLE,
            DeviceDescriptor.WEARABLE_DEVICE_SUBCLASS_WATCH);

    m_link = m_adapter.createLink(new DeviceDescriptor(m_adapter
            .getLocalAddress(), m_adapter.getLinkTag(),
            DeviceDescriptor.DEVICE_CLASS_WEARABLE,
            DeviceDescriptor.WEARABLE_DEVICE_SUBCLASS_WATCH));

    m_link.startServer();

Sample code for starting the client (phone) link :

ANDROID CODE:

    initialize(DeviceDescriptor.DEVICE_CLASS_MOBILE,
            DeviceDescriptor.MOBILE_DEVICE_SUBCLASS_SMARTPHONE);

    List<String> addresses = getBondedAddresses();
    DeviceDescriptor descriptor = new DeviceDescriptor(
            m_adapter.getLocalAddress(), m_adapter.getLinkTag(),
            DeviceDescriptor.DEVICE_CLASS_MOBILE,
            DeviceDescriptor.MOBILE_DEVICE_SUBCLASS_SMARTPHONE);
    if (addresses != null) {
        // 遍历所有已经绑定的手表,依次创建链接并启动服务
        for (String address : addresses) {
            Link link = m_adapter.createLink(descriptor);
            link.bondAddress(address);
            mLinks.add(link);
        }
    }

IOS CODE:

    [self initializeDevices:IWDS_DEVICE_CLASS_MOBILE
            deviceSubClass:IWDS_MOBILE_DEVICE_SUBCLASS_SMARTPHONE];

    NSArray *addresses = [self getBondedAddresses];
    if (addresses) {
        DeviceDescriptor *descriptor = [[DeviceDescriptor alloc]
            initWithDeviceAddress:[_adapter localAddress]
            deviceLinkTag:[_adapter linkTag]
            deviceClass:IWDS_DEVICE_CLASS_MOBILE
            deviceSubClass:IWDS_MOBILE_DEVICE_SUBCLASS_SMARTPHONE];

        for (NSString *address in addresses) {
            IWDSLink *link = [_adapter createLink:descriptor];
            [link bondAddress:address];
            [self addLink:link];
        }
    }

Data transmission

You can use the Connection and DataTransactor classes for data transmission between the two devices(watch and phone).Connection is underlying class, DataTransactor is encapsulation of Connection, provides a more convenient interface; IN comparison, Connection class is more flexibility, DataTransactor class is more usability.If there are no special requirements, we recommended choose DataTransactor.

Use Connection

When you Use the Connection to transmission between watch and phone, the main points as following:

1. Create an instance of ConnectionHelper subclass in server and client according to their demand, implement the four callback functions as requirement:

a. onServiceConnected(ConnectionServiceManager) 

It means you succeeded in binding Uniconnect service (calling start () to request binding).

b. onConnectedDevice(DeviceDescriptor)

If have the remote device before established links, so after the call onServiceConnected it Will call the function for each remote device in turn; If it still haven't link to the remote device after binding Uniconnect service , then if the new device establishing link, this function will be called back.

c. onServiceDisconnected(boolean)

It means succeed unbundling services;Boolean parameter is "true" it said service is abnormal exit to bind, parameters is "false", it said the service unbundling is cause of application active exit(call stop () to apply for the unbundling).

d. onDisconnectedDevice(DeviceDescriptor)

When broken links with remote devices, or unbundling Uniconnect service , it will triggers to call the function.

2. Server and the client would call ConnectionHelper subclasses of the start () to bind to uniconnect service.If binding is successful, onServiceConnected (ConnectionServiceManager) and onConnectedDevice (DeviceDescriptor) will be callback in turn.

3. Server and client need to invoke ConnectionServiceManager The createConnection () to create a Connection.Consider create in the onServiceConnected (ConnectionServiceManager).

4. Server and client need to invoke the Connection of the open () to open the underlying Connection, call handshake () implemente the Connection between the client and the server to handshake.When both handshake () returns successfully at the same time, it means handshake successful, at this time the two sides can calls read (byte [] buffer, int offset, int maxSize) and write (byte [] buffer, int offset, int maxSize) to read and write operations, the operations are blocking type.It Can consider implement open, shaking hands and read and write operations in connection, all these operations in onConnectedDevice (DeviceDescriptor) .

Please notice that when the connection is opened, the device will consume more memory(about 200 KB for class bluetooth, about 10KB for BLE).

5. Server and the client can call the stop () of ConnectionHelper subclasses, to unbundling uniconnect service.If unbundling success, onDisconnectedDevice DeviceDescriptor () and onServiceDisconnected (Boolean) will be callback in turn.

6. Server and client can invoke the close () of the connection to close it, all read and write operations will be canceled,  and related memory will be released.

Using DataTransactor

The points of using DataTransactor for data transmission between devices is as follows:

1. Server and client according to their own needs, implement DataTransactorCallback interface, which includes the six callback function:

a. onLinkConnected(DeviceDescriptor, boolean): callback when transmission service connection state changed;

b. onChannelAvailable(boolean): callback when data channel state changed.

c. onSendResult(DataTransactResult): callback when send data over, then notice the results;

d. onDataArrived(Object): callback when there is data received ;

e. onSendFileProgress(int): callback during sending file, to notice the process.

f. onRecvFileProgress(int): callback during reveiving file, to notice the process;

g. onRecvFileInterrupted(int) callback during reveiving file interrupted, to notice the process;

h. onSendFileInterrupted(int) callback during sending file interrupted, to notice the process;

These callback runs in the context of the thread in which the DataTransactor object was created.

2. Server and client devices use the implemented DataTransactorCallback interface and the same UUID to create DataTransactor object.

Such as:

    DataTransactorCallback callback = new DataTransactorCallback() {

        // implement all callbacks 

        ...

    };

    final static String UUID = "a1dc19e2-17a4-0797-9362-68a0dd4bfb6f";

    DataTransactor dataTransactor = DataTransactor(context, callback, UUID);

3. Server and the client would call start () to launch the transmission service (i.e., binding to uniconnect service).This method is asynchronous, when the service started successfully and links are in connection, onLinkConnected (DeviceDescriptor, Boolean) will be called back to notify transmission service start-up successfully;

When both devices' data channel connection handshake successfully, onChannelAvailable (Boolean) will be called in both devices at the same time to notify the data channel is available. If only one device DataTransactor calling start (), and other device not call, so only the device's transmission service was started, but the data channel is not available (onChannelAvailable (Boolean) will not be called), has been in a state of waiting to shake hands. Please notice that when the dataTransactor is started, the device will consume more memory(about 200 KB for class bluetooth, about 10KB for BLE).

4. Server and the client can call the send () sends the object. This method is asynchronous, and the sending result could be got through the callback function onSendResult (DataTransactResult) .If sending the file object (other object types do not have sending or receiving progress notifications), sending progress could be got through callback function onSendFileProgress (int) ; sending at the end of the object, we can get the sending result by the callback function onSendResult (DataTransactResult). The other device will get the file receiving progress through the callback function onRecvFileProgress (int); if the object receiving end, onDataArrived (Object) will be called back.

5. We also provide a data compression method to send objects. The default compression algorithm: ZLIB (COMPRESS_ALGORITHM_GZIP), the compression algorithm also supports are: LZO (COMPRESS_ALGORITHM_LZO); the default compression strategy: balance of speed and compression ratio (COMPRESS_LEVEL_BALANCE), the current compression strategy also supports: fast (COMPRESS_LEVEL_FASTER) and with the highest compression ratio for the target (COMPRESS_LEVEL_HIGH_COMPRESSION). By calling sendCompressedUser() or sendCompressed(), you can achieve the compression of the sending object.

6. Server and the client can use the stop () to stop transmission services (i.e. the unbundling uniconnect services).This method is asynchronous, when service stop onChannelAvailable (Boolean) to be called back to inform data channel is not available, then onLinkConnected (DeviceDescriptor, Boolean) will be called back to notify that transmission service has stopped. Between two connected devices, when only one device called the stop(), the other device of the current data channel will be disconnected, but transmission service will not stop, it will continue to try to open a new data channel and wait for a handshake.When the physical link is disconnected, can also lead to data channel disconnect and transmission service to stop. When the dataTransactor is stopped, the related memory resources will be released.

Notes:

Because each time launch a DataTransactor object consumes more memory, so try to start fewer DataTransactor objects, when not in use, as far as possible through a call to stop () to stop transmission services, and call start () to start the service when needed.

The following example will stop DataTransactor transmission service when the system suspends,  and restart transmission service, send the weather information when the system wakes up(the role of server and client device is equal, so you can run the same set of code for android system):

ANDROID CODE:

    // The client and the server using the same UUID create DataTransactor object
    private String m_uuid = "a1dc19e2-17a4-0797-9362-68a0dd4bfb6f";
    DataTransactor m_transactor  m_transactor = new DataTransactor(this, m_callback, m_uuid);

    // Implement DataTransactorCallback interface
    DataTransactorCallback m_callback = new DataTransactorCallback() {
        public void onChannelAvailable(boolean isAvailable) {
            if (isAvailable) {
                IwdsLog.i(this, "Data channel is available.");

                // In the data channel is available to send files
                File file = new File(FILE_PATH);
                m_transactor.send(file);

                // send data compressed
                // m_transactor.sendCompressed(file);
                // m_transactor.sendCompressedUser(file, Connection.CompressAlgorithm.COMPRESS_ALGORITHM_GZIP, COMPRESS_LEVEL_HIGH_COMPRESSION);
            } else {
                IwdsLog.i(this, "Data channel is unavaiable.");
            }
        }

        public void onSendResult(DataTransactResult result) {
            if (result.getResultCode() == DataTransactResult.RESULT_OK) {
                IwdsLog.i(this, "Send success");
            } else {
                IwdsLog.i(this,
                        "Send failed by error code: " + result.getResultCode());
            }
        }

        public void onDataArrived(Object object) {
            IwdsLog.i(this, "Arrived data: " + object);
            // Do something
        }

        public void onLinkConnected(DeviceDescriptor descriptor, boolean isConnected) {
            if (isConnected) {
                IwdsLog.i(this, "Link connected: " + descriptor.toString());
            } else {
                IwdsLog.i(this, "Link disconnected: " + descriptor.toString());
            }
        }

        /**
        * Because the transmission is a file object, so in the process of sending and receiving 
        * the following function will be called back to inform the progress.If the transmission is 
        * not a file object, the function should be empty.
        */
        public void onRecvFileProgress(int progress) {
            IwdsLog.i(this, "receiving progress: " + progress + "%");
        }

        public void onSendFileProgress(int progress) {
            IwdsLog.i(this, "sending progress: " + progress + "%");
        }
    }
  
    /* The callback when system dormancy*/
    protected void onPause() {
        super.onPause();
        m_transactor.stop();
    }

    /* In the wake callback system */
    protected void onResume() {
        super.onResume();
        m_transactor.start();
    }

IOS CODE:

    static NSString *const WEATHER_UUID = @"1dfe1c37-e619-2b74-4d09-03b56990a5fa";
    IWDSProviderTransactionModel _transactor = [[IWDSProviderTransactionModel alloc]
            initWithUuid:WEATHER_UUID creator:[WeatherInfoArray creator] callback:self];

    - (void)startTransaction
    {
        if (!_transactor) {
            IWDSLogD(@"Weather model - startTransaction failed, because the weather transaction model is nil!");
            return;
        }

        if ([_transactor isStarted]) {
            return;
        }

        [_transactor start];
    }

    - (void)stopTransaction
    {
        if (!_transactor) {
            IWDSLogD(@"Weather model - stopTransaction failed, because the weather transaction model is nil!");
            return;
        }

        if (![_transactor isStarted]) {
            return;
        }

        [_transactor stop];
    }

    // IWDSProviderTransactionModelCallback
    - (void)onLinkConnected:(DeviceDescriptor *)descriptor isConnected:(BOOL)isConnected
    {
        IWDSLogI(@"Weather model - link connection is : %@, device descriptor is : %@.", 
                isConnected ? @"YES" : @"NO", descriptor);
    }

    - (void)onChannelAvailable:(BOOL)isAvailable
    {
        IWDSLogI(@"Weather model - channel available is : %@.", isAvailable ? @"YES" : @"NO");
    }

    - (void)onRequest
    {
        IWDSLogI(@"Weather model - receive weather forecasts request.");

        [self refreshWeather];
    }

    - (void)onSendResult:(IWDSDataTransactResult *)result
    {
        int resultCode = result.resultCode;

        if (resultCode == IWDSDataTransactorResultOk) {
            IWDSLogI(@"Weather model - send weather forecasts success");
            id transferedObject = result.transferedObject;

            if ([transferedObject isKindOfClass:([WeatherInfoArray class])]) {
                self.weatherForecasts = ((WeatherInfoArray *)transferedObject).data;
            }
        } else {
            IWDSLogE(@"Weather model - send weather forecasts failed, result code is : %d", resultCode);
        }
    }

    - (void)onRequestFailed
    {
        IWDSLogE(@"Weather model - request failed.");
    }

    - (void)onObjectArrived:(id)object
    {
        IWDSLogI(@"Weather model - data arrived, arrived object is : %@", object);
    }

Use FileTransactionModel

FileTransactionModel specially used for the transmission of files.

1. Server and client implementation interface FileTransactionModelCallback, used to update the link, the state of the data path, and the relevant state of the file to send and receive (if the two sides is equal, you can use the same set of interface).Specific include the following correction (former seven callback is the same as DataTransactor) :

onLinkConnected(DeviceDescriptor, boolean)

onChannelAvailable(boolean)

onSendResult(DataTransactResult)

onRecvFileProgress(int)

onSendFileProgress(int)

onRecvFileInterrupted(int)

onSendFileInterrupted(int)

Relative DataTransactor added:

a. onRequestSendFile(FileInfo) Received the remote device file callback when sending a request;

b. onConfirmForReceiveFile() The remote device has confirmed receiving callback when files;

c. onCancelForReceiveFile() The remote device has been cancelled in receiving callback file;

d. onFileArrived(File) A file receiving callback.

e. onFileTransferError(int) Callback when a file transfer error.

These back into dispatching in create FileTransactionModel object's thread.

2. Server and client devices use implementation FileTransactionModelCallback interface and the same UUID create FileTransactionModel object.

Such as:

    FileTransactionModelCallback callback = new FileTransactionModelCallback() {

        // implement all callbacks 

        ...

    };

    final static String UUID = "a1dc19e2-17a4-0797-9362-68a0dd4bfb68";

    fileTransactionModel = new FileTransactionModel(this, callback , UUID);

3. Like DataTransactor, server and the client would call start() to start the transfer files service service, call the stop () to stop the transfer files.The relevant methods and process are similar.

4. The sender device use requestSendFile() or requestSendFileCompressed() to request to send file; receiver device confirms whether to accept the file in onRequestSendFile() , use notifyConfirmForReceiveFile to notify the sender device that it's ready to receive the file and use notifyCancelForReceiveFile to notify the sender device that it has canceled the reception of the file; the sender device could send a file in the callback function onConfirmForReceiveFile(), and cancel sending the file  in the callback function onCancelForReceiveFile(). The callback function onFileArrived will be called in the receiver device to notify the file receiving completed.

5. FileTransactionModel also supports sending a file from the specified index position or in the breakpoint; failed to send, through onSendFileInterrupted (int index) to obtain the breakpoint position, and then by calling requestSendFile (filePath, index) or requestSendFileCompressed (filePath, index) can send files from the specified index position.

Example:

ANDROID CODE:

    /*
     * Connection state changes between the monitor equipment.IsConnected to true said two equipment connection is successful.
     */
    @Override
    public void onLinkConnected(DeviceDescriptor descriptor, boolean isConnected) {
        if (isConnected) {
            IwdsLog.i(this, "Link connected: " + descriptor.toString());
            mHandler.obtainMessage(MSG_LINK_CONNECTED).sendToTarget();
        } else {
            IwdsLog.i(this, "Link disconnected: " + descriptor.toString());
            mHandler.obtainMessage(MSG_LINK_DISCONNECTED).sendToTarget();
        }
    }

    /*
     * Bluetooth transmission channel status monitoring, isAvailable for true said transmission channel is available, then the data transmission.
     */
    @Override
    public void onChannelAvailable(boolean isAvailable) {
        if (isAvailable) {
            IwdsLog.i(this, "Data channel is available.");
            mHandler.obtainMessage(MSG_DATA_CHANNEL_AVAILABLE).sendToTarget();
        } else {
            IwdsLog.i(this, "Data channel is unavaiable.");
            mHandler.obtainMessage(MAS_DATA_CHANNEL_UNAVAILABLE).sendToTarget();
        }
    }

    /*
     * Monitor the transmission was successful.Transfer at the end of the sender and the receiver.
     */
    @Override
    public void onSendResult(DataTransactResult result) {
        mReadyToSend = false;
        if (result.getResultCode() == DataTransactResult.RESULT_OK) {
            IwdsLog.i(this, "Send success");
            mHandler.obtainMessage(MSG_SEND_OK).sendToTarget();
        } else {
            mHandler.obtainMessage(MSG_SEND_ERROR, result.getResultCode())
                    .sendToTarget();
            IwdsLog.i(this,
                    "Send failed by error code: " + result.getResultCode());
        }
    }

    /*
     * Documents received.Only the receiver again.
     */
    @Override
    public void onFileArrived(File file) {
        IwdsLog.i(this, "Recv success");
        mRecvDataType = TYPE_FILE;
        mRecvFileSize = file.length();
        mHandler.obtainMessage(MSG_RECV_OK).sendToTarget();
    }

    /*
     * Documents send schedule.Only the sender again.
     */
    @Override
    public void onSendFileProgress(int progress) {
        mHandler.obtainMessage(MSG_UPDATE_PROGRESS, progress, 0).sendToTarget();
    }

    /*
     * File receiving schedule.Only the receiver again.
     */
    @Override
    public void onRecvFileProgress(int progress) {
        mHandler.obtainMessage(MSG_UPDATE_PROGRESS, progress, 0).sendToTarget();
    }

    /*
     * The receiver to confirm whether receive files.
     */
    @Override
    public void onRequestSendFile(FileInfo info) {
        mIsResponse = false;
        mRecvDataType = TYPE_FILE_INFO;
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.file_transfer);
        builder.setCancelable(false);
        builder.setMessage(FileTransactionActivity.this.getString(R.string.file_name) + ": " + info.name + "\n"
                + FileTransactionActivity.this.getString(R.string.file_size) + ": " + info.length + "bytes");
        builder.setPositiveButton(R.string.confirm,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog,
                            int which) {
                        mFileTransactionModel.notifyConfirmForReceiveFile();
                        mRecvStartTime = System.currentTimeMillis();
                    }
                });
        builder.setNegativeButton(R.string.cancel,
                new DialogInterface.OnClickListener(){
        });
        builder.create().show();
    }

    /*
     * When the receiver call notifyConfirmForReceiveFile confirm receive file.Only in the sender again.
     */
    @Override
    public void onConfirmForReceiveFile() {
        Log.d(TAG, "onConfirmForReceiveFile");
        mReadyToSend = true;
        mIsResponse = true;
        mSendStartTime = System.currentTimeMillis();
    }

    @Override
    public void onCancelForReceiveFile() {
        Log.d(TAG, "onCancelForReceiveFile");
        mIsResponse = true;
        Toast.makeText(FileTransactionActivity.this,
                FileTransactionActivity.this.getString(R.string.unable_to_send) + ": " + mFile.getName(),
                Toast.LENGTH_SHORT).show();
        mHandler.obtainMessage(MSG_SEND_DONE).sendToTarget();
    }

IOS CODE:

    - (void)onLinkConnected:(DeviceDescriptor *)descriptor isConnected:(BOOL)isConnected
    {
    }

    - (void)onChannelAvailable:(BOOL)isAvailable
    {
        _isAvailable = isAvailable;

        if (isAvailable) {
            _startBtn.enabled = YES;
            _stopBtn.enabled = YES;
        } else {
            _startBtn.enabled = NO;
            _stopBtn.enabled = NO;
        }
    }

    - (void)onSendResult:(IWDSDataTransactResult *)result
    {
        int resultCode = result.resultCode;
        id  object = result.transferedObject;

        if (resultCode) {
            _testResultLabel.text = @"Sned Failed";

            if ([object isKindOfClass:[RequestSendFile class]]) {
                NSLog(@"Failed in request, current time: %ld", (unsigned long)_testTime);
            } else if ([object isKindOfClass:[NSString class]]) {
                if ([IWDSFileTransactionConfirmReceiveFile isEqualToString:object]) {
                    NSLog(@"Confirm failed");
                } else if ([IWDSFileTransactionCancelReceiveFile isEqualToString:object]) {
                    NSLog(@"Cancel failed");
                } else {
                    NSLog(@"Send file failed, current time: %ld", (unsigned long)_testTime);
                }
            }
        } else {
            if ([object isKindOfClass:[RequestSendFile class]]) {
                _testResultLabel.text = @"Waiting for reception";
            } else if ([object isKindOfClass:[NSString class]]) {
                if ([IWDSFileTransactionConfirmReceiveFile isEqualToString:object]) {
                    NSLog(@"Confirm succeed");
                } else if ([IWDSFileTransactionCancelReceiveFile isEqualToString:object]) {
                    NSLog(@"Cancel succeed");
                } else {
                    _testResultLabel.text = @"Send successful";
                    _testTimeLabel.text = [NSString stringWithFormat:@"%ld", (unsigned long)++_testTime];

                    _endTime = [[NSDate date] timeIntervalSince1970] * 1000;
                    long long transactionTime = _endTime - _startTime;
                    _testTransactionRates.text = [_testTransactionRates.text stringByAppendingString:
                                  [NSString stringWithFormat:@"No.%ld: Send file time consuming %lld ms, rate %.1f KB/s\n",
                                   (unsigned long)_testTime, transactionTime,
                                   _testFileSize.text.floatValue * 1000 / 1024 / transactionTime]];

                    [self tryToRequestSendFile];
                }
            }
        }
    }

    - (void)onFileArrived:(NSString *)filePath
    {
        IWDSLogI(@"Receive a new file,%@", filePath);

        _testResultLabel.text = @"Receive successful";

        _testTransactionRates.text = [_testTransactionRates.text 
            stringByAppendingString:[NSString stringWithFormat:@"No.%ld: Receive a new file : %@ size : %lld byte\n", 
            (unsigned long)++_recvCounter, _recvFileName, _recvFileSize]];
    }

    - (void)onRequestSendFile:(FileInfo *)info
    {
        _recvFileName = info.name;
        _recvFileSize = info.length;

        _testResultLabel.text = @"Receive file transfer request";

        [[[UIAlertView alloc] initWithTitle:@"File transfer request" 
            message:[NSString stringWithFormat:@"File Name: %@\nSize: %lld",
                 _recvFileName, _recvFileSize]
        delegate:self cancelButtonTitle:@"Refuse" otherButtonTitles:@"Receive", nil] show];
    }

    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        if (buttonIndex == 0) {
            [_fileTransactionModel cancelReceiveFile];
            _testResultLabel.text = @"Have refused to receive";
        } else {
            [_fileTransactionModel confirmReceiveFile];
            _testResultLabel.text = @"Receiving...";
        }
    }

    - (void)onSendFileProgress:(int)progress
    {
        _testResultLabel.text = [NSString stringWithFormat:@"Send progress:%d%%", progress];
    }

    - (void)onSendFileInterrupted:(int)index
    {
        _testResultLabel.text = @"Send file interrupted";
    }

    - (void)onRecvFileProgress:(int)progress
    {
        _testResultLabel.text = [NSString stringWithFormat:@"File receive progress : %d%", progress];
    }

    - (void)onRecvFileInterrupted:(int)index
    {
        _testResultLabel.text = @"Receive file interrupted";
    }

    - (void)onConfirmReceiveFile
    {
        _testResultLabel.text = @"Sending...";
        _startTime = [[NSDate date] timeIntervalSince1970] * 1000;
    }

    - (void)onCancelReceiveFile
    {
        _testResultLabel.text = @"Receiver refused to receive the file";
        _startBtn.enabled = YES;
    }