Uniconnect

Uniconnect服务是一个广义的互联服务,可以基于蓝牙、wifi、NFC等实现设备间的互联。目前只实现了基于蓝牙的互联功能。

基本概念

Adaptor(适配器)

代表互联的射频适配器,是所有互联操作的基础。通过它可以扫描设备、获取绑定的设备和创建链接。它的子类 AndroidBtAdapter是classic蓝牙适配器(只有android系统下的实现); 子类 BleAdapter是BLE蓝牙适配器(在android和IOS系统下都有实现)。

RemoteDevice(远程设备)

代表互联的远程设备,内含设备的类型、名称、地址等信息。它的子类 AndroidBtDevice是classic蓝牙设备(只有android系统下的实现);子类 BleDevice是BLE蓝牙设备(在android和IOS系统下都有实现)。

Link(链接)

代表两个设备的适配器之间建立起来的物理通路。一个适配器可以同时和多个设备的适配器建立链接。

Connection(连接)

代表在链接的基础上建立的逻辑通路。它的读写操作是同步的。一个链接上可以同时建立多个连接。

ConnectionHelper(连接帮助器)

帮助处理 Connection相关的状态变化(包括处理和 Uniconnect服务的连接和断开,并负责通知 Connection和服务的连接状态变化或已建立链接的设备变化),它使 Connection的使用更加方便。

DataTransactor(数据传输器)

基于 Connection和 ConnectionHelper实现,读写操作是异步的,使用较 Connection更方便。DataTransactor还支持紧急传输,紧急DataTransactor的数据发送优先于普通DataTransactor,适用于简短消息或命令的传输

CameraTransactionModel(摄像头传输模型)

基于 DataTransactor实现的针对摄像头应用的传输模型。

ProviderTransactionModel(通用数据传输模型)

基于 DataTransactor实现的通用数据传输模型,可以在它的基础上派生出针对各种应用的数据传输模型,包括 HealthTransactionModel、LocaleTransactionModel、MusicControlTransactionModel、NotificationTransactionModel、ScheduleTransactionModel、SyncTimeTransactionModel、WatchFaceTransactionModel和WeatherTransactionModel。

FileSender/FileReceiver(文件传输模型)

基于 Connection和 ConnectionHelper实现。具体功能如下:

FileSender只处理文件的发送,相比较于 FileTransactionModel功能更明确,需要配合 FileReceiver一起使用。

FileReceiver只处理文件的接收,区别于 FileTransactionModel(已废弃),它不需要使用者自己记录断点位置,还可以选择自动接收和用户确认接收两种方式,并且设置接收文件的存放位置(默认存放位置为 Android: "sdcard/Download/iwds/",iOS: "Documents/iwds/"),使用起来较 FileTransactionModel更为灵活。

启动服务

启动 Uniconnect服务的工作只需要在客户端和服务端设备的服务程序里进行,编写一般的应用程序不需要关注。启动服务的步骤包括: 使能互联、扫描设备(可选)和建立链接并启动。

使能互联

对于手表系统,我们默认在iwds-device.apk中使能了互联服务;如果决定不使能互联服务,可以在系统的init.board.private.rc里,设置setprop ro.iwds.enableUniconnect false。

ANDROID CODE:

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

IOS CODE:

    IWDSAdapterManager *adapterManager = [IWDSAdapterManager shareInstance];

classic蓝牙适配器的使能方法(只有android系统下的实现)

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

BLE蓝牙适配器的使能方法:

ANDROID CODE:

  Adapter m_bleAdapter = m_manager.getAdapter(Adapter.TAG_BLE_DATA_CHANNEL);
  m_bleAdapter.enable();

IOS CODE:

  IWDSAdapter *adapter = [adapterManager getAdapter:IWDSLinkTagBleDataChannel];
  // IOS 下app没有开关蓝牙的权限

手表端BLE参数的设置方法:

    private void setUniconnectParameters() {
        BleAdapter adapter = (BleAdapter) m_bleAdapter;
        adapter.setUniconnectServiceUuid(0xfe02);
        adapter.setUniconnectMasterWriteCharUuid(0xff01);
        adapter.setUniconnectSlaveWriteCharUuid(0xff02);
        adapter.setUniconnectControlCharUuid(0xff03);
        setDefaultAdvertisement(adapter);
    }

    private void setDefaultAdvertisement(BleAdapter adapter) {
        AdvertiseData.Builder advDataBuilder = new AdvertiseData.Builder();
        advDataBuilder.addServiceUuid(ParcelUuid.fromString(DEFAULT_SERVICE_UUID));
        byte[] serviceData = getLastBytesFromAddress(((Adapter)adapter)
                .getLocalAddress());
        advDataBuilder.addServiceData(ParcelUuid.fromString(DEFAULT_SERVICE_UUID),
                serviceData);

        advDataBuilder.setIncludeDeviceName(true);
        AdvertiseData advertiseData = advDataBuilder.build();
        
        AdvertiseSettings settings = new AdvertiseSettings.Builder()
                .setConnectable(true).setTimeout(0).build();
        AdvertiseData scanResponse = null;
        adapter.setBleAdvertisement(settings, advertiseData, scanResponse);
    }

android手机端BLE参数的设置方法:

    private BleScanDeviceMatchRule mDeviceMatchRule = new BleScanDeviceMatchRule() {
        @Override
        public boolean matchDevice(byte[] scanRecord) {
            BleAdvertisedData advertiseData = BleUtil
                    .parseAdertisedData(scanRecord);
            byte[] serviceData = advertiseData.getServiceData();
            String remoteAddress = getRemoteDeviceAddress();
            byte[] remoteAddressData = getLastBytesFromAddress(remoteAddress);

            return Arrays.equals(serviceData, remoteAddressData);
        }
    };

    private void setUniconnectParameters() {
        AndroidBleAdapter adapter = (AndroidBleAdapter) m_bleAdapter;
        adapter.setUniconnectServiceUuid(0xfe02);
        adapter.setUniconnectMasterWriteCharUuid(0xff01);
        adapter.setUniconnectSlaveWriteCharUuid(0xff02);
        adapter.setUniconnectControlCharUuid(0xff03);

        adapter.setBleDeviceMatchRule(mDeviceMatchRule);
    }

iOS手机端BLE参数的设置方法:

    - (BOOL)matchDevice:(NSDictionary <NSString *, id> *)advertisementData
    {
        NSDictionary *serviceDatas =
            [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];

        if (!serviceDatas) {
            return NO;
        }

        NSString *serviceUuid = [self getServiceUuid];
        NSData *serviceData =
            [serviceDatas objectForKey:[CBUUID UUIDWithString:serviceUuid]];

        if (!serviceData) {
            return NO;
        }

        NSString *remoteDeviceAddress = [self getRemoteDeviceAddress];
        NSData  *remoteAddressData = [self getLastBytesFromAddress:remoteDeviceAddress];

        return [remoteAddressData isEqualToData:serviceData];
    }

    IWDSAdapterManager *adapterManager = [IWDSAdapterManager shareInstance];
    _adapter = (IWDSBleAdapter *)[adapterManager getAdapter:IWDSLinkTagBleDataChannel];
    [_adapter setUniconnectServiceUuid:@"FE02"];
    [_adapter setUniconnectMasterWriteCharUuid:@"FF01"];
    [_adapter setUniconnectSlaveWriteCharUuid:@"FF02"];
    [_adapter setUniconnectControlCharUuid:@"FF03"];

获取可用Adapter信息

通过ConnectionServiceManager获取可用的Adapter信息: 
格式: linkTag0,address0,isEnabled0,linkTag1,address1,isEnabled1,...
比如: Android BT data channel,AC:38:70:74:7C:28,true,BLE data channel,AC:38:70:74:7C:28,true

    ServiceClient client = new ServiceClient(context,
            ConnectionServiceManager.SERVICE_CONNECTION, this);
    client.connect();

    @Override
    public void onConnected(ServiceClient serviceClient) {
        mConnectionManager = (ConnectionServiceManager) serviceClient
                .getServiceManagerContext();
    }

    @Override
    public void onDisconnected(ServiceClient serviceClient,
            boolean unexpected) {
        mConnectionManager = null;
    }

    @Override
    public void onConnectFailed(ServiceClient serviceClient,
            ConnectFailedReason reason) {
    }

    mConnectionManager.getAvailableAdaptersInfo();

注:手表端可以根据以上信息生成二维码,以便手机在扫描二维码绑定设备时,选择需要的链路类型进行连接(比如:BT/BLE/WiFi)

扫描设备

只有android系统下的实现:

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

        public void onDeviceFound(AndroidBtDevice device) {
                
        }

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

 

建立链接并启动

在两个设备建立连接(connection)传输数据之前,需要建立好链接(link)。在使能互联服务之后,iwds-device.apk默认使能了classic蓝牙link和BLE蓝牙link,如果决定不使用君正BLE蓝牙link,需要 在init.board.private.rc里设置setprop ro.iwds.uniconnect.enableBle false,以免和自定义的BLE服务冲突。

一般手表作为服务端,手机作为客户端,要使两者之间的链接建立成功,需要两者都启动链接:服务端调用 startServer(),客户端调用 bondAddress(address)。一个手机可以连接多个手表,一个手表只能连接一个手机。

启动服务端(手表)链接的示例代码:

    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();

启动客户端(手机)链接的示例代码:

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];
        }
    }

 

数据传输

可以使用 Connection和 DataTransactor类进行设备间的数据传输。Connection是更底层的类,DataTransactor是对 Connection的封装,提供了更方便的接口; 两者比较而言,Connection类的灵活性更强,DataTransactor类的易用性更好。如果没有特殊需求,一般建议使用 DataTransactor。

使用Connection

使用 Connection进行设备间传输的要点如下:

1. 服务端和客户端根据各自需求实例化 ConnectionHelper子类,实现它要求的4个回调函数:

a. onServiceConnected(ConnectionServiceManager) 

表示成功绑定 Uniconnect服务(调用start()请求绑定)。

b. onConnectedDevice(DeviceDescriptor)

如果之前已经有远端设备建立好了链接,那么在调用 onServiceConnected(ConnectionServiceManager) 之后,会依次针对每个远端设备调用该函数; 如果绑定成功Uniconnect服务时还没有链接成功的远端设备,你们在之后有新设备建立好链接时,该函数也会被回调。

c. onServiceDisconnected(boolean)

表示解绑服务成功; boolean参数为真表示服务是异常退出导致解绑,参数为假表示服务是应用主动退出导致解绑(调用stop()申请解绑)。

d. onDisconnectedDevice(DeviceDescriptor)

当与远端设备断开链接时,或与Uniconnect服务解绑时,都会触发调用该函数。

2. 服务端和客户端都要调用 ConnectionHelper子类的start()以绑定到 uniconnect服务。如果绑定成功,onServiceConnected(ConnectionServiceManager) 和onConnectedDevice(DeviceDescriptor)会先后被回调。

3. 服务端和客户端都要调用 ConnectionServiceManager 的 createConnection()创建 Connection。可以考虑在onServiceConnected(ConnectionServiceManager) 中创建。

4. 服务端和客户端都要调用 Connection的open()打开底层 connection,调用 handshake()实现服务端和客户端之间的 connection的握手。当两端的 handshake()同时成功返回时,表示握手成功,此时双方可以调用 read(byte[] buffer, int offset, int maxSize)和 write(byte[] buffer, int offset, int maxSize)进行读写操作,读写操作都是阻塞式的。可以考虑在 onConnectedDevice(DeviceDescriptor)中执行 connection的打开、握手和读写操作。注意,当Connection open之后,设备都将多消耗内存资源(经典蓝牙消耗200KB左右,BLE 消耗10KB左右)。

5. 服务端和客户端可以调用 ConnectionHelper子类的stop()以解绑 uniconnect服务。如果解绑成功,onDisconnectedDevice(DeviceDescriptor)和 onServiceDisconnected(boolean)会先后被回调。

6. 服务端和客户端可以调用 Connection的close()关闭底层的 connection, 取消所有读写操作。可以考虑在 onDisconnectedDevice(DeviceDescriptor)中停止对connection的读写并 close connection(对应Connection.open());可以考虑在 onServiceDisconnected(boolean)中销毁 connection(对应 ConnectionServiceManager.createConnection())。当Connection close之后,相关的内存资源也会被释放。

 

使用DataTransactor

使用DataTransactor进行设备间传输的要点如下:

1. 服务端和客户端根据各自的需求,实现 DataTransactorCallback接口(如果双方角色是对等的,可以使用同一套接口),该接口内部的6个回调函数:

a. onLinkConnected(DeviceDescriptor, boolean) 传输服务连接状态的变化时回调;

b. onChannelAvailable(boolean) 数据通道可用情况变化时回调;

c. onSendResult(DataTransactResult) 发送数据完毕时回调以通知发送结果;

d. onDataArrived(Object) 有数据收到时回调;

e. onSendFileProgress(int) 文件发送过程中回调以通知发送进度;

f. onRecvFileProgress(int) 文件接收过程中回调以通知接受进度;

g. onRecvFileInterrupted(int) 文件接收中断时回调;

h. onSendFileInterrupted(int) 文件发送中断时回调。

这些回调运行在创建 DataTransactor 对象所在的线程中。

2. 服务端和客户端设备使用实现的 DataTransactorCallback接口和同一个 UUID创建 DataTransactor对象。

例如:

    DataTransactorCallback callback = new DataTransactorCallback() {

        // implement all callbacks 

        ...

    };

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

    // 最后一个参数表示是否为紧急传输,
    // 紧急传输的DataTransactor数据发送会优先于普通DataTransactor,
    // 因此更适用于简短的消息或命令的传输
    DataTransactor dataTransactor = DataTransactor(context, callback, UUID, true);

3. 服务端和客户端都要调用start()以启动传输服务(即绑定到 uniconnect服务)。该方法是异步的,当服务启动成功并且链接处于连接状态时, onLinkConnected(DeviceDescriptor, boolean)会被回调以通知传输服务启动成功; 当双方设备数据通道的连接握手成功时,onChannelAvailable(boolean)会在双方设备同时被回调以通知数据通道可用。如果只有一方设备的 DataTransactor调用start(),而对方设备没有调用,那么该设备只有传输服务会启动成功,但数据通道不能建立(onChannelAvailable(boolean)不会被调),一直处于等待握手状态。注意,当DataTransactor启动之后,设备都将多消耗内存资源(经典蓝牙消耗200KB左右,BLE 消耗10KB左右)。

4. 服务端和客户端可以调用 send()发送对象。该方法是异步的,在每次发送之后,都应该通过 onSendResult(DataTransactResult) 查询发送结果。如果发送的是文件对象(其他对象类型不会有发送或接收进度通知),可以通过回调 onSendFileProgress(int)得知文件的发送进度; 对象发送结束时, onSendResult(DataTransactResult)会被回调,从中可以得知发送结果(是否发送成功,如果失败,具体的失败原因)。对方设备会通过回调函数 onRecvFileProgress(int)得知文件的接收进度; 如果对象接收完毕,onDataArrived(Object)会被回调。

5. 我们还提供了以数据压缩的方式来发送对象。默认的压缩算法为:ZLIB (COMPRESS_ALGORITHM_GZIP),目前还支持的压缩算法有:LZO (COMPRESS_ALGORITHM_LZO);默认的压缩策略为:速度和压缩率平衡 (COMPRESS_LEVEL_BALANCE),目前还支持的压缩策略有:快速 (COMPRESS_LEVEL_FASTER)/以最高压缩比为目标 (COMPRESS_LEVEL_HIGH_COMPRESSION)。通过调用sendCompressed()/sendCompressedUser(),就可以实现压缩发送对象。

6. 服务端和客户端可以使用 stop()停止传输服务(即解绑 uniconnect服务)。该方法是异步的,当服务停止时,onChannelAvailable(boolean)先被回调以通知通道不可用,然后 onLinkConnected(DeviceDescriptor, boolean)会被回调以通知传输服务停止。建立了数据通道的两个设备,当只有一方设备调用 stop()时,对方设备的当前数据通道会断开,但传输服务不会停止,它会继续尝试打开新的数据通道并等待握手。当物理链接断开时,也会导致数据通道断开和传输服务停止。设备在停止DataTransactor之后,将释放相关的内存资源。

注意:

因为每启动一个 DataTransactor对象都会消耗内存,所以尽可能少地启动DataTransactor对象,在不使用的时候,尽量通过调用 stop()停止传输服务,在需要时再调用 start()启动服务。

下面的例子将在系统休眠时断开 DataTransactor的传输服务,系统唤醒时重新启动传输服务,并在唤醒时发送天气信息(服务端和客户端设备的角色是对等的,对于android系统,可以运行同一套代码):

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);
    }

ProviderTransactionModel(ANDROID)和IWDSDataTransactor(IOS)的用法与示例基本一致,这里不再赘述。

使用 FileSender 和 FileReceiver

使用 FileSender 和 FileReceiver 进行设备间的文件传输要点如下:

1.文件发送端需要实现 SenderCallback 接口,该接口内部的7个回调函数如下:

a. onLinkConnected(DeviceDescriptor, boolean) 传输服务连接状态的变化时回调;

b. onChannelAvailable(boolean) 数据通道可用情况变化时回调;

c. onSendProgress(File, int) 文件发送过程中回调以通知发送进度;

d. onSendSuccess(File) 文件发送完成时回调;

e. onSendFailed(File, int) 文件发送失败时回调;

f. onSendDenied(File, int) 文件发送被拒绝时回调;

g. onSendInterruptPoint(File, long) 文件发送开始前回调以通知断点位置,非断点传输时断点位置为0。

这些回调运行在创建 FileSender 对象的线程中。

2.发送端使用send(File) / sendCompressed(File) / sendCompressedUser(File, int, int)发送文件,后面两个方法分别时已默认压缩方式(压缩算法和压缩等级)发送文件和以自定义的压缩方式发送文件。

3.文件接收端需要实现 ReceiverCallback 接口,该接口内部的5个回调函数如下:

a. onLinkConnected(DeviceDescriptor, boolean) 传输服务连接状态的变化时回调;

b. onChannelAvailable(boolean) 数据通道可用情况变化时回调;

c. onFileArrived(File) 文件接收完成时回调;

d. onFileReceiveProgress(File, int) 文件接收过程中回调以通知发送进度;

e. onFileReceiveFailed(File, int) 文件接收失败时回调。

这些回调运行在创建 FileReceiver 对象的线程中。

4.文件接收端还可以实现 ReceiverConfirmCallback 接口,当 FileReceiver 设置了该回调接口后,接收端不再默认允许文件发送端发送文件,而是通过onRequestSendFile(string, long, long)回调使用者,让使用者决定是否接收文件。接收端在接收到该回调后,可以通过以下方法允许或拒绝接收文件:

a. notifyAllowForReceiveFile() 通知(发送端)允许接收文件;

b. notifyDenyForReceiveFile() 通知(发送端)拒绝接收文件;

届时,发送端会在接收到允许发送时开始发送文件,在接收到拒绝时回调onSendDenied(int)。

5.接收端还可以通过setFileReceiveDirectory(String)设置接收文件的存放路径(默认存放位置为 Android: "sdcard/Download/iwds/",iOS: "Documents/iwds/"。如果文件当前正在接收中,该设置会在下次接收到新文件请求时生效,注意,请设置有效的文件存放路径,否则会设置失败。

6.与 DataTransactor 一样,FileSender 和 FileReceiver 需要使用同一个UUID来创建,都要调用 start()以启动文件传输服务,调用 stop()以停止传输服务。相关方法和过程都是类似的。

下面是使用FileSender和FileReceiver的使用示例:

FileReceiver:

ANDROID CODE:

    private final static String UUID = "291c78ca-18bc-427a-a899-6583d75fffd2";

    private FileReceiver mReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ......

        mReceiver = new FileReceiver(this, UUID, this);
        mReceiver.start();
    }

    @Override
    protected void onDestroy() {
        mReceiver.stop();

        super.onDestroy();
    }

    @Override
    public void onLinkConnected(DeviceDescriptor descriptor,
                                boolean isConnected) {
        IwdsLog.i(TAG, "onLinkConnected: " + descriptor + ", " + isConnected);

        setLinkState(isConnected);
    }

    @Override
    public void onChannelAvailable(boolean isAvailable) {
        IwdsLog.i(TAG, "onChannelAvailable: " + isAvailable);

        Toast.makeText(this,
                isAvailable ? R.string.data_channel_available
                        : R.string.data_channel_unavailable,
                Toast.LENGTH_LONG).show();

        setConnectionState(isAvailable);

        // 在传输通道断开时,取消对话框显示
        if (!isAvailable && null != mDialog && mDialog.isShowing()) {
            mDialog.dismiss();
            mDialog = null;
        }
    }

    @Override
    public void onFileArrived(File file) {
        IwdsLog.i(TAG,
                "onFileArrived: " + file.getName() + ", " + file.length());
        Toast.makeText(this,
                "onFileArrived: " + file.getName() + ", " + file.length(),
                Toast.LENGTH_LONG).show();

        setLogger("No." + ++mRecvCount + " New File Arrived, file name: "
                + file.getName() + ", file length: " + file.length()
                + "bytes\n", false);
    }

    @Override
    public void onFileReceiveProgress(File file, int progress) {
        IwdsLog.i(TAG, "onFileReceiveProgress: " + progress);
        setReceiveProgress(progress);
    }

    @Override
    public void onFileReceiveFailed(File file, int failedReason) {
        String reasonString = FileReceiver.failedReasonString(failedReason);
        IwdsLog.e(TAG, "onFileReceiveFailed: " + reasonString);

        Toast.makeText(this,
                "Receive Failed, ReasonString: " + reasonString,
                Toast.LENGTH_LONG).show();

        setLogger("No." + ++mRecvCount + " File receive failed, ReasonString: "
                + reasonString + "\n", false);
    }

    @Override
    public void onRequestSendFile(String name, long length, long interruptedPoint) {
        IwdsLog.i(TAG, "onRequestSendFile: name=" +name + ", length=" + length
                + ", interruptedPoint=" + interruptedPoint);

        mDialog = new AlertDialog.Builder(this)
                .setCancelable(false)
                .setTitle("File send request")
                .setMessage("File name: " + name + ", File length: " + length + "bytes"
                        + ", interruptedPoint: " + interruptedPoint + "bytes")
                .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        mReceiver.notifyAllowForReceiveFile();
                    }
                })
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        mReceiver.notifyDenyForReceiveFile();
                    }
                }).create();

        mDialog.show();
    }

iOS CODE:

    IWDSFileReceiver    *fileReceiver;

    - (void)viewDidLoad
    {
        [super viewDidLoad];

        ......

        fileReceiver = [[IWDSFileReceiver alloc]
                                    initWithUuid:@"291c78ca-18bc-427a-a899-6583d75fffd2"
                                    delegate:self queue:dispatch_get_main_queue()];
    }

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];

        if (![fileReceiver isStarted]) {
            [fileReceiver start];
        }
    }

    - (void)viewWillDisappear:(BOOL)animated
    {
        if ([fileReceiver isStarted]) {
            [fileReceiver stop];
        }

        [super viewWillDisappear:animated];
    }

    - (void)onLinkConnected:(DeviceDescriptor *)descriptor isConnected:(BOOL)isConnected
    {
        IWDSLogI(@"onLinkConnected: %@, isConnected: %@", descriptor,
                      isConnected ? @"YES" : @"NO");

        _linkState.text = [NSString stringWithFormat:@"Link: %@",
                      isConnected ? @"Connected" : @"Disconnected"];
    }

    - (void)onChannelAvailable:(BOOL)isAvailable
    {
        IWDSLogI(@"onChannelAvailable: %@", isAvailable ? @"YES" : @"NO");

        _connectionState.text = [NSString stringWithFormat:@"Connection: %@",
                      isAvailable ? @"Available" : @"Unavailable"];

        [WMUtils showMessage:[NSString stringWithFormat:@"Data channel is %@",
                      isAvailable ? @"available" : @"unavailable"] toView:self.view];

        if (!isAvailable && dialog) {
            [dialog dismissViewControllerAnimated:YES completion:nil];
        }
    }

    - (void)onFileReceiveProgress:(int)progress filePath:(NSString *)filePath
    {
        IWDSLogI(@"onFileReceiveProgress: %d, filePath: %@", progress, filePath);

        _recvProgress.text = [NSString stringWithFormat:@"Recv Progress: %d", progress];
    }

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

        NSString *msg =
                 [NSString stringWithFormat:@"File receive success, filePath: %@", filePath];
        [WMUtils showMessage:msg toView:self.view];

        NSFileManager *fileManager = [NSFileManager defaultManager];

        if (![fileManager fileExistsAtPath:filePath]) {
            msg = [NSString stringWithFormat:@"Received file not exist: %@", filePath];
            [WMUtils showMessage:msg toView:self.view];

            return;
        }

        NSError *fileErr = nil;
        NSDictionary *fileDic = [fileManager attributesOfItemAtPath:filePath error:&fileErr];

        if (fileErr) {
            IWDSLogE(@"Read file length error: %@", fileErr);
            return;
        }

        NSString    *fileName = [[filePath componentsSeparatedByString:@"/"] lastObject];
        long long   fileLength = [[fileDic objectForKey:NSFileSize] longLongValue];

        NSString *log = [NSString stringWithFormat:
                                @"Receive file success, file name: %@, file length: %lld",
                                fileName, fileLength];

        [self updateLogs:log];
    }

    - (void)onFileReceiveFailed:(int)reason
    {
        NSString *reasonString = [IWDSFileReceiver failedReasonString:reason];

        IWDSLogI(@"onFileReceiveFailed: %@", reasonString);

        [WMUtils showMessage:[NSString stringWithFormat:
                                           @"File receive failed, reason: %@",
                                           reasonString] toView:self.view];

        NSString *log = [NSString stringWithFormat:
                                @"File receive failed, reason: %@",
                                reasonString];

        [self updateLogs:log];
    }

    - (void)onRequestSendFile:(NSString *)fileName
               fileLength:(long long)fileLength
         interruptedPoint:(long long)interruptedPoint
    {
        IWDSLogI(@"onRequestSendFile: %@, fileLength: %lld, interruptedPoint: %lld",
                      fileName, fileLength, interruptedPoint);

        NSString *msg = [NSString stringWithFormat:
                                 @"File name: %@, File length: %lld bytes, interruptedPoint: %lld bytes",
                                 fileName, fileLength, interruptedPoint];

        if (!dialog) {
            dialog = [UIAlertController alertControllerWithTitle:
                         @"File send request" message:msg
                         preferredStyle:UIAlertControllerStyleAlert];

            UIAlertAction *allowedAction = [UIAlertAction actionWithTitle:@"OK"
                                style:UIAlertActionStyleDestructive 
                                handler:^(UIAlertAction *_Nonnull action) {
                [fileReceiver notifyAllowForReceiveFile];
            }];

            [dialog addAction:allowedAction];

            UIAlertAction *deniedAction = [UIAlertAction actionWithTitle:
                               @"CANCEL" style:UIAlertActionStyleCancel
                               handler:^(UIAlertAction *_Nonnull action) {
                    [fileReceiver notifyDenyForReceiveFile];
                }];

            [dialog addAction:deniedAction];
        } else {
            dialog.message = msg;
        }

        [self presentViewController:dialog animated:YES completion:nil];
    }

FileSender:

ANDROID CODE:

    private final static String UUID = "291c78ca-18bc-427a-a899-6583d75fffd2";

    private FileSender mSender;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ......

        mSender = new FileSender(this, UUID, this);
        mSender.start();
    }

    @Override
    protected void onDestroy() {
        mSender.stop();

        super.onDestroy();
    }

    @Override
    public void onLinkConnected(DeviceDescriptor descriptor,
                                boolean isConnected) {
        IwdsLog.i(TAG, "onLinkConnected: " + descriptor + ", " + isConnected);

        setLinkState(isConnected);
    }

    @Override
    public void onChannelAvailable(boolean isAvailable) {
        IwdsLog.i(TAG, "onChannelAvailable: " + isAvailable);

        Toast.makeText(this,
                isAvailable ? R.string.data_channel_available
                        : R.string.data_channel_unavailable,
                Toast.LENGTH_LONG).show();

        setConnectionState(isAvailable);
    }

    @Override
    public void onSendSuccess(File file) {
        long time = System.currentTimeMillis() - mSendStartTime;
        long sendLength = (file.length() - mInterruptPoint);
        String logText = "No." + mSendCount + " Send Success, Compressed: "
                + mCompressedEnable
                + ", file name: " + file.getName() + ", file length: " + file.length()
                + "bytes, send length: " + sendLength + "bytes, consume time: "
                + time + "ms, send speed: "
                + sendLength * 1000.0 / 1024 / time + "KB/s\n";

        IwdsLog.i(TAG, "onSendSuccess: " + file.getName() + ", " + file.length()
                + ", " + file.lastModified());

        Toast.makeText(this, "onSendSuccess: " + file.getName() + ", "
                + file.length() + ", " + file.lastModified(), Toast.LENGTH_LONG).show();

        setLogger(logText, false);
        mInterruptPoint = 0;

        // 循环测试
        if (mCircle)
            mSendButton.performClick();
    }

    @Override
    public void onSendProgress(File file, int progress) {
        IwdsLog.i(TAG, "onSendProgress: " + progress);

        setSendProgress(progress);
    }

    @Override
    public void onSendFailed(File file, int failedReason) {
        String reasonString = FileSender.failedReasonString(failedReason);
        IwdsLog.e(TAG, "onSendFailed: " + reasonString);

        Toast.makeText(this, "Send Failed, ReasonString: "
                       + reasonString, Toast.LENGTH_LONG).show();

        setLogger("No." + mSendCount + " Send Failed, ReasonString: "
                       + reasonString + "\n", false);
    }

    @Override
    public void onSendDenied(File file, int deniedReason) {
        String reasonString = FileReceiver.deniedReasonString(deniedReason);
        IwdsLog.e(TAG, "onSendDenied: " + reasonString);

        Toast.makeText(this, "Send Denied, ReasonString: "
                       + reasonString, Toast.LENGTH_LONG).show();

        setLogger("No." + mSendCount + " Send Denied, ReasonString: "
                       + reasonString + "\n", false);
    }

    @Override
    public void onSendInterruptPoint(File file, long interruptPoint) {
        IwdsLog.i(TAG, "onSendInterruptPoint: " + interruptPoint);

        mInterruptPoint = interruptPoint;
    }

    public void onButtonClick(View view) {
        switch (view.getId()) {
            case R.id.send_button: // 发送按钮被点击
                if (mSender == null) {
                    Toast.makeText(this, "FileSender unavailable!", Toast.LENGTH_LONG).show();
                    return;
                }

                mLoggerView.requestFocus();

                if (null == sendFilePath) {
                    Toast.makeText(this, "Unknown File Path!", Toast.LENGTH_LONG).show();
                    return;
                }

                File file = new File(sendFilePath);
                if (!file.exists()) {
                    Toast.makeText(this, "File not found!", Toast.LENGTH_LONG).show();
                    return;
                }

                mSendStartTime = System.currentTimeMillis();
                mSendCount++;

                boolean ok;
                if (isCompressEnable()) {
                    mCompressedEnable = true;
                    ok = mSender.sendCompressed(file);

                } else {
                    mCompressedEnable = false;
                    ok = mSender.send(file);
                }

                if (!ok) {
                    Toast.makeText(this, "Connection is unavailable!",
                                                  Toast.LENGTH_LONG).show();

                    setLogger("No." + mSendCount + " Send failed, can not to send!\n",
                                   false);
                    return;
                }

                mCircle = true;

                break;

            case R.id.stop_button: // 停止循环发送按钮被点击
                mCircle = false;
                break;

            default:
                break;
        }
    }

iOS CODE:

    IWDSFileSender  *_fileSender;

    - (void)viewDidLoad
    {
        [super viewDidLoad];

        ......

        _fileSender = [[IWDSFileSender alloc] 
                               initWithUuid:@"291c78ca-18bc-427a-a899-6583d75fffd2"
                               delegate:self queue:dispatch_get_main_queue()];
    }

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];

        if (![_fileSender isStarted]) {
            [_fileSender start];
        }
    }

    - (void)viewWillDisappear:(BOOL)animated
    {
        if ([_fileSender isStarted]) {
            [_fileSender stop];
        }

        [super viewWillDisappear:animated];
    }

    - (IBAction)send:(id)sender
    {
        [_createTxtFileView resignFirstResponder];

        _testStart = YES;

        [self startTest];
    }

    - (void)startTest
    {
        if (!_testStart) {
            return;
        }

        _isPreCompressEnabled = _isCompressEnabled;

        _startTime = [[NSDate date] timeIntervalSince1970] * 1000;

        if (_isCompressEnabled) {
            [_fileSender sendCompressed:_txtFilePath];
        } else {
            [_fileSender send:_txtFilePath];
        }
    }

    - (IBAction)stop:(id)sender
    {
        [_createTxtFileView resignFirstResponder];

        if (!_testStart) {
            return;
        }

        _testStart = NO;
    }

    - (void)onLinkConnected:(DeviceDescriptor *)descriptor connected:(BOOL)isConnected
    {
        IWDSLogI(@"onLinkConnected: %@, isConnected: %@", descriptor,
                      isConnected ? @"YES" : @"NO");

        _linkState.text = [NSString stringWithFormat:@"Link: %@",
                      isConnected ? @"Connected" : @"Disconnected"];
    }

    - (void)onChannelAvailable:(BOOL)isAvailable
    {
        IWDSLogI(@"onChannelAvailable: %@", isAvailable ? @"YES" : @"NO");

        _connectionState.text = [NSString stringWithFormat:@"Connection: %@",
                       isAvailable ? @"Available" : @"Unavailable"];
    }

    - (void)onSendInterruptPoint:(long long)interruptPoint filePath:(NSString *)filePath
    {
        IWDSLogI(@"onSendInterruptPoint: %lld, filePath: %@", interruptPoint, filePath);

        _interruptedPoint = interruptPoint;
    }

    - (void)onSendProgress:(int)progress filePath:(NSString *)filePath
    {
        IWDSLogI(@"onSendProgress: %d, filePath: %@", progress, filePath);

        _sendProgress.text = [NSString stringWithFormat:@"Recv Progress: %d", progress];
    }

    - (void)onSendSuccess:(NSString *)filePath
    {
        IWDSLogI(@"onSendSuccess, filePath: %@", filePath);

        _endTime = [[NSDate date] timeIntervalSince1970] * 1000;

        NSFileManager *fileManager = [NSFileManager defaultManager];

        if (![fileManager fileExistsAtPath:filePath]) {
            [WMUtils showMessage:[NSString stringWithFormat:
                    @"Received file not exist: %@", filePath] toView:self.view];

            return;
        }

        NSError *fileErr = nil;
        NSDictionary *fileDic = [fileManager attributesOfItemAtPath:filePath error:&fileErr];

        if (fileErr) {
            IWDSLogE(@"Read file length error: %@", fileErr);
            return;
    }

    NSString        *fileName = [[filePath componentsSeparatedByString:@"/"] lastObject];
    long long       fileLength = [[fileDic objectForKey:NSFileSize] longLongValue];
    long long       sendLength = fileLength - _interruptedPoint;
    NSTimeInterval  consumeTime = _endTime - _startTime;

    NSString *log = [NSString stringWithFormat:
            @"Send success, compressed: %@, file name: %@, file length: %lldbytes, send length: %lldbytes, consume time: %.0fms, send speed: %.8fKB/s",
            _isPreCompressEnabled ? @"YES" : @"NO", fileName, fileLength,
            sendLength, consumeTime, (1.0f * sendLength / consumeTime)];

        [self updateLogs:log];

        // 循环测试,点击Stop按钮停止循环
        [self startTest];
    }

    - (void)onSendFailed:(int)failedReason filePath:(NSString *)filePath
    {
        NSString *reasonString = [IWDSFileSender failedReasonString:failedReason];

        IWDSLogI(@"onSendFailed: %@, filePath: %@", reasonString, filePath);

        NSString *log = [NSString stringWithFormat:
                @"Send failed, ReasonString: %@", reasonString];
        [self updateLogs:log];
    }

    - (void)onSendDenied:(int)deniedReason
    {
        NSString *reasonString = [IWDSFileReceiver deniedReasonString:deniedReason];

        IWDSLogI(@"onSendDenied: %@", reasonString);

        NSString *log = [NSString stringWithFormat:
                @"Send denied, ReasonString: %@", reasonString];
        [self updateLogs:log];
    }