飘易博客(作者:Flymorn)
订阅《飘易博客》RSS,第一时间查看最新文章!
飘易首页 | 留言本 | 关于我 | 订阅Feed

Hbuilder离线打包及蓝牙插件开发Android注意事项

Author:飘易 Source:飘易
Categories:移动开发 PostTime:2018-1-22 11:02:32
正 文:

本来只想使用HBuilder在线方式开发一个管理BLE蓝牙的小型APP,研究了下dcloud上所有和蓝牙有关的文档,发现经典蓝牙的可以实现,而对于BLE蓝牙并没有适当的文档。


经典蓝牙使用下述方式即可连接:

bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid);

bluetoothSocket.connect();


但是对于BLE蓝牙,会报错如下:

Uncaught java.io.IOException: read failed, socket might closed or timeout, read ret: -1;at android.bluetooth.BluetoothSocket.connect 


原因如下:

The problem is with the socket.mPort parameter. When you create your socket using socket = device.createRfcommSocketToServiceRecord(SERIAL_UUID); , the mPort gets integer value "-1", and this value seems doesn't work for android >=4.2 , so you need to set it to "1". The bad news is that createRfcommSocketToServiceRecord only accepts UUID as parameter and not mPort so we have to use other aproach : socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);. We need to use both socket attribs , the second one as a fallback.


我们可以使用:

socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);
socket.connect();

但这种使用方式目前用html5plus的 Native.js for Android 方式应该是无法实现的,Native.js无法反射抽象类。


请注意:Android 4.3(API Level 18)才开始引入Bluetooth Low Energy(BLE,低功耗蓝牙4.0)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。也就是说蓝牙4.0只有android4.3或4.3以上才支持


Android使用蓝牙之前先引入权限:

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"/>

另外要注意,蓝牙也需要 Geolocation(位置信息)权限,加上否则搜不出来设备列表。


说到安卓的权限问题,飘易特别提醒一下:

安卓6.0API >= 23)开始实行权限的动态管理,而目前5+SDK离线版并未实现动态授权管理,因此建议安卓离线打包时设置较低的targetSdkVersion来解决这个问题,目前建议编译目标设置为Android 5.0API 21)。

如果需要开发插件, io.dcloud.PandoraEntry作为apk入口时,必须设置 targetSDKVersion>=21 沉浸式才生效。

 

以下为危险的权限,需要动态授权:

身体传感器/日历/摄像头/通讯录/地理位置/麦克风/电话/短信/存储空间

 其他的权限不受影响,所以在做这些危险操作的时候需要提示用户授权。否则报错如下:

java.lang.SecurityException:Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTUREflg=0x3 cmp=com.android.camera/.Camera clip={text/uri-listU:file:///storage/emulated/0/bdc97b284f5549d5b9d89fe6f7fcc7ba.jpg} (has extras)} from ProcessRecord{382b57 16353:cn.xzkj.chihuo/u0a189} (pid=16353, uid=10189)with revoked permission android.permission.CAMERA

 

 飘易使用的Android Studio 3.0进行插件开发,过程中需要安装不少依赖包,而GOOGLE的安卓资源在国内基本是被qiang的,因此要开发安卓,先把路打通:

先挂上v-p-n(建议思科的anyConnect),然后必须在系统的hosts文件里手动绑定dl.google.com的ip地址到正确的海外ip(国内ip污染严重):

172.217.*.*       dl.google.com

可以到 http://ping.chinaz.com/  获取dl.google.com 的海外ip。如何判断绑定的ip有效呢?在浏览器中打开:

https://dl.google.com/android/repository/repository2-1.xml,如果该网址可以打开,说明绑定的ip是有效的。

 

在Android Studio 3.0 里build.gradle添加阿里云的国内镜像:

allprojects {
    repositories {
        maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
        jcenter()
        google()
    }
}

其中,google() 默认使用了google自家的maven库:https://maven.google.com,而该地址默认301跳转到https://dl.google.com/dl/android/maven2/ 了。


蓝牙原生部分,飘易使用的是开源项目FastBle:https://github.com/Jasonchenlijian/FastBle

使用Gradle安装方式:

compile 'com.clj.fastble:FastBleLib:2.2.2'

下面就是如何编写原生代码实现蓝牙相关的状态获取,扫描,连接,读写等操作了:

package com.plugin.bluetooth;
...

//蓝牙状态
public void state(IWebview pWebview, JSONArray array){
    boolean ble_support = BleManager.getInstance().isSupportBle();//是否支持蓝牙BLE
    boolean ble_enable = BleManager.getInstance().isBlueEnable();//是否打开蓝牙
    final String CallBackID = array.optString(0);//回调通知id
    String ReturnString = null;
    if(ble_support){
        if(ble_enable){
            // 调用方法将原生代码的执行结果返回给js层并触发相应的JS层回调函数
            JSUtil.execCallback(pWebview, CallBackID, "Bluetooth is open", JSUtil.OK, false);
        }else{
            // 调用方法将原生代码的执行结果返回给js层并触发相应的JS层回调函数
            JSUtil.execCallback(pWebview, CallBackID, "Bluetooth is closed", JSUtil.ERROR, false);
            // 可以手动打开蓝牙
            //BleManager.getInstance().enableBluetooth();
        }
    }else{
        // 调用方法将原生代码的执行结果返回给js层并触发相应的JS层回调函数
        JSUtil.execCallback(pWebview, CallBackID, "Bluetooth is not supported", JSUtil.ERROR, false);
    }
}

//scan扫描 第1个参数是:回调id ;
public void scan(final IWebview pWebview, JSONArray array){
    final String CallBackID = array.optString(0);
    //扫描规则
    BleScanRuleConfig scanRuleConfig = new BleScanRuleConfig.Builder()
            //.setServiceUuids(serviceUuids)      // 只扫描指定的服务的设备,可选
            //.setDeviceName(true, names)         // 只扫描指定广播名的设备,可选
            //.setDeviceMac(mac)                  // 只扫描指定mac的设备,可选
            .setAutoConnect(false)      // 连接时的autoConnect参数,可选,默认false
            .setScanTimeOut(30000)              // 扫描超时时间,可选,默认10秒;小于等于0表示不限制扫描时间
            .build();
    BleManager.getInstance().initScanRule(scanRuleConfig);
    BleManager.getInstance().scan(new BleScanCallback() {
        @Override
        public void onScanStarted(boolean success) {
            // 开始(主线程)
            deviceMap = new HashMap<String, JSONArray>();//传递给js层
            bleDeviceMap = new HashMap<String, BleDevice>();//内部持有
            deviceMap.clear();
            bleDeviceMap.clear();
            Log.d("BLE","scan started");
            JSUtil.execCallback(pWebview, CallBackID, "scan started", JSUtil.OK, true);//回调通知js层
        }
        @Override
        public void onScanning(BleDevice bleDevice) {
            // 扫描到一个符合扫描规则的BLE设备(主线程)
            JSONArray newArray = new JSONArray();
            newArray.put(bleDevice.getName());
            newArray.put(bleDevice.getRssi());
            newArray.put(bleDevice.getMac());
            if(!deviceMap.containsKey(bleDevice.getMac())){
                deviceMap.put(bleDevice.getMac(), newArray);
                bleDeviceMap.put(bleDevice.getMac(), bleDevice);
            }
            JSONObject jsonObj = new JSONObject(deviceMap);//转JSONObject
            //日志
            Log.d("BLE", jsonObj.toString());
            JSUtil.execCallback(pWebview, CallBackID, jsonObj, JSUtil.OK, true);//回调通知js层
        }
        @Override
        public void onScanFinished(List<BleDevice> scanResultList) {
            // 扫描结束,列出所有扫描到的符合扫描规则的BLE设备(主线程)
            Log.d("BLE", "scan finished");
            JSUtil.execCallback(pWebview, CallBackID, "scan finished", JSUtil.OK, true);//回调通知js层
        }
    });
}

//连接 第1个参数是:回调id ; 第2个参数:设备的 mac 地址
public void connect(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    String mac = array.optString(1);//这里是指 mac address
    if(!bleDeviceMap.containsKey(mac)) {
        JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
    }
    BleDevice bleDevice = bleDeviceMap.get(mac);
    if(BleManager.getInstance().isConnected(bleDevice)){
        JSUtil.execCallback(pWebview, CallBackID, "device is already connected", JSUtil.OK, true);//回调通知js层
    }else {
        //先取消扫描
        BleManager.getInstance().cancelScan();
        //初始化变量
        final HashMap<String, HashMap> mapS = new HashMap<String, HashMap>();
        //连接
        BleManager.getInstance().connect(bleDevice, new BleGattCallback() {
            @Override
            public void onStartConnect() {
                // 开始连接
                JSUtil.execCallback(pWebview, CallBackID, "start connect", JSUtil.OK, true);//回调通知js层
            }
            @Override
            public void onConnectFail(BleException exception) {
                // 连接失败
                JSUtil.execCallback(pWebview, CallBackID, "connect fail: "+exception.getDescription()+" Code:"+String.valueOf(exception.getCode()), JSUtil.ERROR, true);//回调通知js层
            }
            @Override
            public void onConnectSuccess(BleDevice bleDevice, final BluetoothGatt gatt, int status) {
                // 连接成功,BleDevice即为所连接的BLE设备
                JSUtil.execCallback(pWebview, CallBackID, "device connected", JSUtil.OK, true);//回调通知js层
                //安卓下蓝牙操作必须延时
                new Timer().schedule(new TimerTask() {
                    @Override
                    public void run() {
                        //延时操作的run方法
                        mapS.clear();
                        for (BluetoothGattService service : gatt.getServices()) {
                            List<BluetoothGattCharacteristic> characteristicList = service.getCharacteristics();
                            HashMap<String, JSONArray> mapC =  new HashMap<String, JSONArray>();
                            for(BluetoothGattCharacteristic c : service.getCharacteristics()){// 特征
                                JSONArray arr = new JSONArray();
                                arr.put(String.valueOf(c.getProperties()));
                                arr.put(c.getUuid().toString());
                                mapC.put(c.getUuid().toString(), arr);
                            }
                            if(!mapS.containsKey(service.getUuid().toString())){
                                mapS.put(service.getUuid().toString(), mapC);
                            }
                        }
                        JSONObject jsonObj = new JSONObject(mapS);//转JSONObject
                        //日志
                        Log.d("BLE", jsonObj.toString());
                        JSUtil.execCallback(pWebview, CallBackID, jsonObj, JSUtil.OK, true);//回调通知js层
                    }
                },600);//延时多少ms执行
            }
            @Override
            public void onDisConnected(boolean isActiveDisConnected, BleDevice bleDevice, BluetoothGatt gatt, int status) {
                // 连接中断,isActiveDisConnected表示是否是主动调用了断开连接方法
                //gatt.connect();//断开自动重新连接
                JSUtil.execCallback(pWebview, CallBackID, "device disconnected", JSUtil.ERROR, true);//回调通知js层
            }
        });
    }
}


//断开某个连接 第1个参数是:回调id ; 第2个参数:设备的 mac 地址
public void cancelConnect(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    String mac = array.optString(1);//这里是指 mac address
    if(!bleDeviceMap.containsKey(mac)) {
        JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
    }
    BleDevice bleDevice = bleDeviceMap.get(mac);
    if(BleManager.getInstance().isConnected(bleDevice)) {
        BleManager.getInstance().disconnect(bleDevice);
    }
    JSUtil.execCallback(pWebview, CallBackID, "connect cancelled", JSUtil.OK, false);//回调通知js层
}

//断开所有连接 第1个参数是:回调id ;
public void cancelAllConnect(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    BleManager.getInstance().disconnectAllDevice();
    JSUtil.execCallback(pWebview, CallBackID, "all connect cancelled", JSUtil.OK, false);//回调通知js层
}

//取消扫描 第1个参数是:回调id ;
public void cancelScan(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    BleManager.getInstance().cancelScan();
    JSUtil.execCallback(pWebview, CallBackID, "scan cancelled", JSUtil.OK, false);//回调通知js层
}

//读取特征 - 订阅通知notify | 第1个参数是:回调id ; Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
public void notify(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    String mac = array.optString(1);//这里是指 mac address
    String uuid_service = array.optString(2);//service uuid
    String uuid_characteristic_notify = array.optString(3);//characteristic uuid
    if(!bleDeviceMap.containsKey(mac)) {
        JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
    }
    BleDevice bleDevice = bleDeviceMap.get(mac);
    if(!BleManager.getInstance().isConnected(bleDevice)){
        JSUtil.execCallback(pWebview, CallBackID, "device not connected", JSUtil.ERROR, false);//回调通知js层
    }
    BleManager.getInstance().notify(
            bleDevice, uuid_service, uuid_characteristic_notify,
            new BleNotifyCallback() {
                @Override
                public void onNotifySuccess() {
                    // 打开通知操作成功
                    Log.d("BLE", "notify success");
                }
                @Override
                public void onNotifyFailure(BleException exception) {
                    // 打开通知操作失败
                    JSUtil.execCallback(pWebview, CallBackID, "notify fail: "+exception.getDescription(), JSUtil.ERROR, false);//回调通知js层
                }
                @Override
                public void onCharacteristicChanged(byte[] data) {
                    // 打开通知后,设备发过来的数据将在这里出现
                    String s = HexUtil.formatHexString(data, false);
                    Log.d("BLE", s);
                    JSUtil.execCallback(pWebview, CallBackID, s, JSUtil.OK, true);//回调通知js层
                }
            }
    );
    //over
}

//取消读取特征 - 取消订阅通知notify  | 第1个参数是:回调id ; Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
public void cancelNotify(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    String mac = array.optString(1);//这里是指 mac address
    String uuid_service = array.optString(2);//service uuid
    String uuid_characteristic_notify = array.optString(3);//characteristic uuid
    if(!bleDeviceMap.containsKey(mac)) {
        JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
    }
    BleDevice bleDevice = bleDeviceMap.get(mac);
    if(!BleManager.getInstance().isConnected(bleDevice)){
        JSUtil.execCallback(pWebview, CallBackID, "device not connected", JSUtil.ERROR, false);//回调通知js层
    }
    //取消订阅通知notify,并移除数据接收的回调监听
    if(BleManager.getInstance().stopNotify(bleDevice, uuid_service, uuid_characteristic_notify)){
        JSUtil.execCallback(pWebview, CallBackID, "notify cancelled", JSUtil.OK, false);//回调通知js层
    }else{
        JSUtil.execCallback(pWebview, CallBackID, "notify cancel fail", JSUtil.ERROR, false);//回调通知js层
    }
}

//写入特征 | 第1个参数是:回调id ; Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid;Argus4:写入的16进制字符串
public void write(final IWebview pWebview, JSONArray array) {
    final String CallBackID = array.optString(0);
    String mac = array.optString(1);//这里是指 mac address
    String uuid_service = array.optString(2);//service uuid
    String uuid_characteristic_notify = array.optString(3);//characteristic uuid
    String dataHex = array.optString(4);//要写入的16进制字符串
    if(!bleDeviceMap.containsKey(mac)) {
        JSUtil.execCallback(pWebview, CallBackID, "device not found", JSUtil.ERROR, false);//回调通知js层
    }
    BleDevice bleDevice = bleDeviceMap.get(mac);
    if(!BleManager.getInstance().isConnected(bleDevice)){
        JSUtil.execCallback(pWebview, CallBackID, "device not connected", JSUtil.ERROR, false);//回调通知js层
    }
    //写入
    byte[] data = HexUtil.hexStringToBytes(dataHex);
    BleManager.getInstance().write(
            bleDevice, uuid_service, uuid_characteristic_notify, data,
            new BleWriteCallback() {
                @Override
                public void onWriteSuccess() {
                    // 发送数据到设备成功
                    JSUtil.execCallback(pWebview, CallBackID, "write success", JSUtil.OK, false);//回调通知js层
                }
                @Override
                public void onWriteFailure(BleException exception) {
                    // 发送数据到设备失败
                    JSUtil.execCallback(pWebview, CallBackID, "write fail", JSUtil.ERROR, false);//回调通知js层
                }
            }
    );
    //over
}


接下来编写原生类和js层的对应关系

开发者在实现JS层API时首先要定义一个插件类的别名,并需要在Android工程的assets\data\dcloud_properties.xml文件中声明插件类别名和Native层扩展插件类的对应关系:

<properties>
    <features>
        <feature name="bluetooth" value="com.plugin.bluetooth.Bluetooth"></feature>
    </features> 
</properties>


再接下来就是js层的事了:

document.addEventListener( "plusready",  function()
{
    var PluginJSName = 'bluetooth', B = window.plus.bridge;
    var bluetooth = 
    {
        //判断蓝牙状态是否可用
        state : function (successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "state", [callbackID]);
        },

        //扫描
        scan : function (successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "scan", [callbackID]);
        },

        //停止扫描
        cancelScan : function (successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "cancelScan", [callbackID]);
        },

        //选择一个设备连接
        connect : function (Argus1, successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "connect", [callbackID, Argus1]);
        },

        //断开一个设备连接
        cancelConnect : function (Argus1, successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "cancelConnect", [callbackID, Argus1]);
        },

        //断开所有设备连接
        cancelAllConnect : function (successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "cancelAllConnect", [callbackID]);
        },

        //读数据 Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
        notify : function (Argus1, Argus2, Argus3, successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "notify", [callbackID, Argus1, Argus2, Argus3]);
        },

        //写数据 Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid;Argus4:写入的16进制字符串
        write : function (Argus1, Argus2, Argus3, Argus4, successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "write", [callbackID, Argus1, Argus2, Argus3, Argus4]);
        },

        //取消读数据 Argus1:设备mac地址 Argus2:服务uuid Argus3:特征uuid
        cancelNotify : function (Argus1, Argus2, Argus3, successCallback, errorCallback )
        {
            var success = typeof successCallback !== 'function' ? null : function(args)
            {
                successCallback(args);
            },
            fail = typeof errorCallback !== 'function' ? null : function(code)
            {
                errorCallback(code);
            };
            callbackID = B.callbackId(success, fail);
            return B.exec(PluginJSName, "cancelNotify", [callbackID, Argus1, Argus2, Argus3]);
        },

    };
    window.plus.bluetooth = bluetooth;
}, true );



【离线打包注意点】:

1,配置应用的包名及版本号

打开AndroidManifest.xml文件,在代码视图中修改根节点的package属性值,其中package为应用的包名,采用反向域名格式,为应用的标识;versionCode为应用的版本号(整数值),用于各应用市场的升级判断,建议与manifest.json中version -> code值一致;versionName为应用的版本名称(字符串),在系统应用管理程序中显示的版本号,建议与manifest.json中version -> name值一致。


2,配置应用名称

打开res -> values -> strings.xml文件,修改“app_name”字段值,该值为安装到手机上桌面显示的应用名称。


3,配置应用图标和启动界面

将应用的图标(文件名为icon.png)和启动图片按照对应的尺寸拷贝到工程的res -> drawable-XXX目录下。


4,更新应用资源

打开assets -> apps 目录,将下面“HelloH5”目录名称修改为应用manifest.json中的id名称(这步非常重要,否则会导致应用无法正常启动),并将所有应用资源拷贝到其下的www目录中。


5,配置应用信息

打开assets -> data下的control.xml文件,修改appid值;其中appid值为HBuilder应用的appid,必须与应用manifest.json中的id值完全一致;appver为应用的版本号,用于应用资源的升级,必须保持与manifest.json中的version -> name值完全一致;version值为应用基座版本号(plus.runtime.innerVersion返回的值),不要随意修改。



【参考】:

1,关于蓝牙设备搜索和Ble设备的搜索的简单调用方法

2,IOException: read failed, socket might closed - Bluetooth on Android 4.3

3,安卓离线打包

作者:飘易
来源:飘易
版权所有。转载时必须以链接形式注明作者和原始出处及本声明。
上一篇:没有了
下一篇:js版本的crc32计算,修正会出现负数的bug
1条评论 “Hbuilder离线打包及蓝牙插件开发Android注意事项”
2018-2-9 17:43:58
好专业,然而并不懂哈。
发表评论
名称(*必填)
邮件(选填)
网站(选填)

记住我,下次回复时不用重新输入个人信息
© 2007-2019 飘易博客 Www.Piaoyi.Org 原创文章版权由飘易所有