【转】Android4.4(MT8685)源码蓝牙解析--BLE搜索

原文网址:http://blog.csdn.net/u013467735/article/details/41962075

BLE:全称为Bluetooth Low Energy。蓝牙规范4.0最重要的一个特性就是低功耗。BLE使得蓝牙设备可通过一粒纽扣电池供电以维持续工作数年之久。很明显,BLE使得蓝牙设备在钟表、远程控制、医疗保健及运动感应器等市场具有极光明的应用场景。

Google从Android 4.3开始添加了对蓝牙4.0的支持。本文一个demo为入口分析 BLE 搜索的流程。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. package com.dy.ble;  
  2.   
  3. import android.annotation.SuppressLint;  
  4. import android.app.Activity;  
  5. import android.bluetooth.BluetoothAdapter;  
  6. import android.bluetooth.BluetoothDevice;  
  7. import android.os.Bundle;  
  8. import android.util.Log;  
  9. import android.view.View;  
  10. import android.view.View.OnClickListener;  
  11. import android.widget.Button;  
  12.   
  13. public class MainActivity extends Activity {  
  14.     private static final String TAG = "BLE";  
  15.     private Button scanBtn;  
  16.     private BluetoothAdapter bluetoothAdapter;  
  17.       
  18.     @Override  
  19.     protected void onCreate(Bundle savedInstanceState) {  
  20.         super.onCreate(savedInstanceState);  
  21.         setContentView(R.layout.main);  
  22.           
  23.         bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();  
  24.         if(!bluetoothAdapter.isEnabled()){  
  25.             bluetoothAdapter.enable();  
  26.         }  
  27.         scanBtn = (Button) this.findViewById(R.id.btn_scan);  
  28.         scanBtn.setOnClickListener(new OnClickListener(){  
  29.   
  30.             @SuppressLint("NewApi")  
  31.             @Override  
  32.             public void onClick(View arg0) {  
  33.                 if(bluetoothAdapter.isEnabled()){  
  34.                     bluetoothAdapter.startLeScan(callback);  
  35.                 }  
  36.             }  
  37.               
  38.         });  
  39.           
  40.     }  
  41.       
  42.     @SuppressLint("NewApi")  
  43.     private BluetoothAdapter.LeScanCallback callback = new BluetoothAdapter.LeScanCallback(){  
  44.   
  45.         @Override  
  46.         public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {  
  47.             Log.d(TAG, "onLeScan device = " + device + ",rssi = " + rssi + "scanRecord = " + scanRecord);  
  48.         }  
  49.     };  
  50.   
  51. }  

点击按钮就会开始扫描,扫描到设备时,就会触发onLeScan这个回调方法,并且可以从参数中获取扫描到的蓝牙设备信息。下面分析BluetoothAdapter中的startLeScan方法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public boolean startLeScan(LeScanCallback callback) {  
  2.        return startLeScan(null, callback);  
  3.    }  


这里调用了一个同名的方法,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public boolean startLeScan(UUID[] serviceUuids, LeScanCallback callback) {  
  2.         if (DBG) Log.d(TAG, "startLeScan(): " + serviceUuids);  
  3.   
  4.         if (callback == null) {  
  5.             if (DBG) Log.e(TAG, "startLeScan: null callback");  
  6.             return false;  
  7.         }  
  8.   
  9.         synchronized(mLeScanClients) {  
  10.             if (mLeScanClients.containsKey(callback)) {  
  11.                 if (DBG) Log.e(TAG, "LE Scan has already started");  
  12.                 return false;  
  13.             }  
  14.   
  15.             try {  
  16.                 IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();  
  17.                 if (iGatt == null) {  
  18.                      if (DBG) Log.e("BluetoothAdapterReceiver", "iGatt == null");  
  19.                     // BLE is not supported  
  20.                     return false;  
  21.                 }  
  22.   
  23.                 UUID uuid = UUID.randomUUID();  
  24.                 GattCallbackWrapper wrapper = new GattCallbackWrapper(this, callback, serviceUuids);  
  25.                 iGatt.registerClient(new ParcelUuid(uuid), wrapper);  
  26.                 if (wrapper.scanStarted()) {  
  27.                     if (DBG) Log.e("BluetoothAdapterReceiver", "wrapper.scanStarted()==true");  
  28.                     mLeScanClients.put(callback, wrapper);  
  29.                     return true;  
  30.                 }  
  31.             } catch (RemoteException e) {  
  32.                 Log.e(TAG,"",e);  
  33.             }  
  34.         }  
  35.         return false;  
  36.     }  


这个方法需要BLUETOOTH_ADMIN权限,第一个参数是各种蓝牙服务的UUID数组,UUID是“Universally Unique Identifier”的简称,通用唯一识别码的意思。对于蓝牙设备,每个服务都有通用、独立、唯一的UUID与之对应。也就是说,在同一时间、同一地点,不可能有两个相同的UUID标识的不同服务。第二个参数是前面传进来的LeScanCallback对象。

接下来分析下mManagerService,它是一个IBluetoothManager对象,IBluetoothManager是一个AIDL,可以实现跨进程通信,其在源码中的路径为:/alps/frameworks/base/core/java/android/bluetooth/IBluetoothManager.aidl。下面来看看mManagerService的实例化,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. BluetoothAdapter(IBluetoothManager managerService) {  
  2.   
  3.        if (managerService == null) {  
  4.            throw new IllegalArgumentException("bluetooth manager service is null");  
  5.        }  
  6.        try {  
  7.            mService = managerService.registerAdapter(mManagerCallback);  
  8.        } catch (RemoteException e) {Log.e(TAG, "", e);}  
  9.        mManagerService = managerService;  
  10.        mLeScanClients = new HashMap<LeScanCallback, GattCallbackWrapper>();  
  11.    }  


直接将BluetoothAdapter构造方法的参数传给了它,来看看这个参数到底是什么?

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public static synchronized BluetoothAdapter getDefaultAdapter() {  
  2.         if (sAdapter == null) {  
  3.             IBinder b = ServiceManager.getService(BLUETOOTH_MANAGER_SERVICE);  
  4.             if (b != null) {  
  5.                 IBluetoothManager managerService = IBluetoothManager.Stub.asInterface(b);  
  6.                 sAdapter = new BluetoothAdapter(managerService);  
  7.             } else {  
  8.                 Log.e(TAG, "Bluetooth binder is null");  
  9.             }  
  10.         }  
  11.         return sAdapter;  
  12.     }  


首先通过Binder机制获取了BLUETOOTH_MANAGER_SERVICE服务的IBinder对象,这个服务是在系统启动的时候添加进去的,在SystemServer.java中

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. <pre name="code" class="java"> bluetooth = new BluetoothManagerService(context);  
  2.  ServiceManager.addService(BluetoothAdapter.BLUETOOTH_MANAGER_SERVICE, bluetooth);  


这里实际就是实例化了一个BluetoothManagerService对象,然后把这个对象通过Binder保存在BLUETOOTH_MANAGER_SERVICE服务中。最后把这个IBinder对象转化为IBluetoothManager对象。所以managerService实际就是一个BluetoothManagerService对象。

现在回到BluetoothAdapter的startLeScan方法中,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();  

这里实际就是调用BluetoothManagerService中的getBluetoothGatt方法了,我们进去看看

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public IBluetoothGatt getBluetoothGatt() {  
  2.         // sync protection  
  3.         return mBluetoothGatt;  
  4.     }  


这里直接返回一个IBluetoothGatt对象,那我们就来看看这个对象时在哪里得到的呢?其实通过对代码的研究发现, 这个对象是在蓝牙开启的时候得到的!

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public boolean enable() {  
  2.         if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&  
  3.             (!checkIfCallerIsForegroundUser())) {  
  4.             Log.w(TAG,"enable(): not allowed for non-active and non system user");  
  5.             return false;  
  6.         }  
  7.   
  8.         mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,  
  9.                                                 "Need BLUETOOTH ADMIN permission");  
  10.         if (DBG) {  
  11.             Log.d(TAG,"enable():  mBluetooth =" + mBluetooth +  
  12.                     " mBinding = " + mBinding);  
  13.         }  
  14.         /// M: MoMS permission check @{  
  15.         if(FeatureOption.MTK_MOBILE_MANAGEMENT) {  
  16.             checkEnablePermission();  
  17.             return true;  
  18.         }  
  19.         /// @}  
  20.         synchronized(mReceiver) {  
  21.             mQuietEnableExternal = false;  
  22.             mEnableExternal = true;  
  23.             // waive WRITE_SECURE_SETTINGS permission check  
  24.             long callingIdentity = Binder.clearCallingIdentity();  
  25.             persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);  
  26.             Binder.restoreCallingIdentity(callingIdentity);  
  27.             sendEnableMsg(false);  
  28.         }  
  29.         return true;  
  30.     }  


这是开启蓝牙的代码,sendEnableMsg(false);这里看来要发送一个消息,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. private void sendEnableMsg(boolean quietMode) {  
  2.         mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_ENABLE,  
  3.                              quietMode ? 1 : 0, 0));  
  4.     }  


果然,看看在哪里接收了

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. @Override  
  2.         public void handleMessage(Message msg) {  
  3.             if (DBG) Log.d (TAG, "Message: " + msg.what);  
  4.             switch (msg.what) {  
  5. <span style="white-space:pre">    </span>    case MESSAGE_ENABLE:  
  6.                     if (DBG) {  
  7.                         Log.d(TAG, "MESSAGE_ENABLE: mBluetooth = " + mBluetooth);  
  8.                     }  
  9.                     mHandler.removeMessages(MESSAGE_RESTART_BLUETOOTH_SERVICE);  
  10.                     mEnable = true;  
  11.                     handleEnable(msg.arg1 == 1);  
  12.                     break;  
[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. <span style="white-space:pre">        </span>}  
  2. }  


进入handleEnable方法看看

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. private void handleEnable(boolean quietMode) {  
  2.        mQuietEnable = quietMode;  
  3.   
  4.        synchronized(mConnection) {  
  5.            if (DBG) Log.d(TAG, "handleEnable: mBluetooth = " + mBluetooth +   
  6.                    ", mBinding = " + mBinding + "quietMode = " + quietMode);  
  7.            if ((mBluetooth == null) && (!mBinding)) {  
  8.                if (DBG) Log.d(TAG, "Bind AdapterService");  
  9.                //Start bind timeout and bind  
  10.                Message timeoutMsg=mHandler.obtainMessage(MESSAGE_TIMEOUT_BIND);  
  11.                mHandler.sendMessageDelayed(timeoutMsg,TIMEOUT_BIND_MS);  
  12.                mConnection.setGetNameAddressOnly(false);  
  13.                Intent i = new Intent(IBluetooth.class.getName());  
  14.                if (!doBind(i, mConnection,Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {  
  15.                    mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);  
  16.                    Log.e(TAG, "Fail to bind to: " + IBluetooth.class.getName());  
  17.                } else {  
  18.                    mBinding = true;  
  19.                }  
  20.            } else if (mBluetooth != null) {  
  21.                if (mConnection.isGetNameAddressOnly()) {  
  22.                    // if GetNameAddressOnly is set, we can clear this flag,  
  23.                    // so the service won't be unbind  
  24.                    // after name and address are saved  
  25.                    mConnection.setGetNameAddressOnly(false);  
  26.                    //Register callback object  
  27.                    try {  
  28.                        mBluetooth.registerCallback(mBluetoothCallback);  
  29.                    } catch (RemoteException re) {  
  30.                        Log.e(TAG, "Unable to register BluetoothCallback",re);  
  31.                    }  
  32.                    //Inform BluetoothAdapter instances that service is up  
  33.                    sendBluetoothServiceUpCallback();  
  34.                }  
  35.   
  36.                //Enable bluetooth  
  37.                try {  
  38.                    if (!mQuietEnable) {  
  39.                        if(!mBluetooth.enable()) {  
  40.                            Log.e(TAG,"IBluetooth.enable() returned false");  
  41.                        }  
  42.                    }  
  43.                    else {  
  44.                        if(!mBluetooth.enableNoAutoConnect()) {  
  45.                            Log.e(TAG,"IBluetooth.enableNoAutoConnect() returned false");  
  46.                        }  
  47.                    }  
  48.                } catch (RemoteException e) {  
  49.                    Log.e(TAG,"Unable to call enable()",e);  
  50.                }  
  51.            }  
  52.        }  
  53.    }  


这里会调用doBinder方法来绑定服务,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. boolean doBind(Intent intent, ServiceConnection conn, int flags, UserHandle user) {  
  2.         ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);  
  3.         intent.setComponent(comp);  
  4.         if (comp == null || !mContext.bindServiceAsUser(intent, conn, flags, user)) {  
  5.             Log.e(TAG, "Fail to bind to: " + intent);  
  6.             return false;  
  7.         }  
  8.         return true;  
  9.     }  


这个conn就是mConnection,那么mConnection是什么呢?

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. private BluetoothServiceConnection mConnection = new BluetoothServiceConnection();  
[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. private class BluetoothServiceConnection implements ServiceConnection {  
  2.   
  3.        private boolean mGetNameAddressOnly;  
  4.   
  5.        public void setGetNameAddressOnly(boolean getOnly) {  
  6.            mGetNameAddressOnly = getOnly;  
  7.        }  
  8.   
  9.        public boolean isGetNameAddressOnly() {  
  10.            return mGetNameAddressOnly;  
  11.        }  
  12.   
  13.        public void onServiceConnected(ComponentName className, IBinder service) {  
  14.            if (DBG) Log.d(TAG, "BluetoothServiceConnection: " + className.getClassName());  
  15.            Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_CONNECTED);  
  16.            // TBD if (className.getClassName().equals(IBluetooth.class.getName())) {  
  17.            if (className.getClassName().equals("com.android.bluetooth.btservice.AdapterService")) {  
  18.                msg.arg1 = SERVICE_IBLUETOOTH;  
  19.                // } else if (className.getClassName().equals(IBluetoothGatt.class.getName())) {  
  20.            } else if (className.getClassName().equals("com.android.bluetooth.gatt.GattService")) {  
  21.                msg.arg1 = SERVICE_IBLUETOOTHGATT;  
  22.            } else {  
  23.                Log.e(TAG, "Unknown service connected: " + className.getClassName());  
  24.                return;  
  25.            }  
  26.            msg.obj = service;  
  27.            mHandler.sendMessage(msg);  
  28.        }  
  29.   
  30.        public void onServiceDisconnected(ComponentName className) {  
  31.            // Called if we unexpected disconnected.  
  32.            if (DBG) Log.d(TAG, "BluetoothServiceConnection, disconnected: " +  
  33.                           className.getClassName());  
  34.            Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED);  
  35.            if (className.getClassName().equals("com.android.bluetooth.btservice.AdapterService")) {  
  36.                msg.arg1 = SERVICE_IBLUETOOTH;  
  37.            } else if (className.getClassName().equals("com.android.bluetooth.gatt.GattService")) {  
  38.                msg.arg1 = SERVICE_IBLUETOOTHGATT;  
  39.            } else {  
  40.                Log.e(TAG, "Unknown service disconnected: " + className.getClassName());  
  41.                return;  
  42.            }  
  43.            mHandler.sendMessage(msg);  
  44.        }  
  45.    }  

现在我们就知道原来这个mConnection是一个绑定服务的连接对象,所以现在BluetoothManagerService绑定了一个IBluetooth的AIDL服务,这时onServiceConnected方法会执行,并且会发送一个MESSAGE_BLUETOOTH_SERVICE_CONNECTED消息,来看接收消息的地方

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. case MESSAGE_BLUETOOTH_SERVICE_CONNECTED:  
  2.                 {  
  3.                     if (DBG) Log.d(TAG,"MESSAGE_BLUETOOTH_SERVICE_CONNECTED: " + msg.arg1);  
  4.   
  5.                     IBinder service = (IBinder) msg.obj;  
  6.                     synchronized(mConnection) {  
  7.                         if (msg.arg1 == SERVICE_IBLUETOOTHGATT) {  
  8.                             mBluetoothGatt = IBluetoothGatt.Stub.asInterface(service);  
  9.                             break;  
  10.                         } // else must be SERVICE_IBLUETOOTH  
  11.   
  12.                         //Remove timeout  
  13.                         mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);  
  14.   
  15.                         mBinding = false;  
  16.                         mBluetooth = IBluetooth.Stub.asInterface(service);  
  17.   
  18.                         try {  
  19.                             boolean enableHciSnoopLog = (Settings.Secure.getInt(mContentResolver,  
  20.                                 Settings.Secure.BLUETOOTH_HCI_LOG, 0) == 1);  
  21.                             if (!mBluetooth.configHciSnoopLog(enableHciSnoopLog)) {  
  22.                                 Log.e(TAG,"IBluetooth.configHciSnoopLog return false");  
  23.                             }  
  24.                         } catch (RemoteException e) {  
  25.                             Log.e(TAG,"Unable to call configHciSnoopLog", e);  
  26.                         }  
  27.   
  28.                         if (mConnection.isGetNameAddressOnly()) {  
  29.                             //Request GET NAME AND ADDRESS  
  30.                             Message getMsg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS);  
  31.                             mHandler.sendMessage(getMsg);  
  32.                             if (!mEnable) return;  
  33.                         }  
  34.   
  35.                         mConnection.setGetNameAddressOnly(false);  
  36.                         //Register callback object  
  37.                         try {  
  38.                             mBluetooth.registerCallback(mBluetoothCallback);  
  39.                         } catch (RemoteException re) {  
  40.                             Log.e(TAG, "Unable to register BluetoothCallback",re);  
  41.                         }  
  42.                         //Inform BluetoothAdapter instances that service is up  
  43.                         sendBluetoothServiceUpCallback();  
  44.   
  45.                         //Do enable request  
  46.                         try {  
  47.                             if (mQuietEnable == false) {  
  48.                                 if(!mBluetooth.enable()) {  
  49.                                     Log.e(TAG,"IBluetooth.enable() returned false");  
  50.                                 }  
  51.                             }  
  52.                             else  
  53.                             {  
  54.                                 if(!mBluetooth.enableNoAutoConnect()) {  
  55.                                     Log.e(TAG,"IBluetooth.enableNoAutoConnect() returned false");  
  56.                                 }  
  57.                             }  
  58.                         } catch (RemoteException e) {  
  59.                             Log.e(TAG,"Unable to call enable()",e);  
  60.                         }  
  61.                     }  
  62.   
  63.                     if (!mEnable) {  
  64.                         waitForOnOff(true, false);  
  65.                         handleDisable();  
  66.                         waitForOnOff(false, false);  
  67.                     }  
  68.                     break;  
  69.                 }  


当msg的参数1为SERVICE_IBLUETOOTHGATT时,实例化mBluetoothGatt对象,至此我们就可以得到mBluetoothGatt。

再一次回到BluetoothAdapter的startLeScan方法中,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public boolean startLeScan(UUID[] serviceUuids, LeScanCallback callback) {  
  2.         if (DBG) Log.d(TAG, "startLeScan(): " + serviceUuids);  
  3.   
  4.         if (callback == null) {  
  5.             if (DBG) Log.e(TAG, "startLeScan: null callback");  
  6.             return false;  
  7.         }  
  8.   
  9.         synchronized(mLeScanClients) {  
  10.             if (mLeScanClients.containsKey(callback)) {  
  11.                 if (DBG) Log.e(TAG, "LE Scan has already started");  
  12.                 return false;  
  13.             }  
  14.   
  15.             try {  
  16.                 IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();  
  17.                 if (iGatt == null) {  
  18.                      if (DBG) Log.e("BluetoothAdapterReceiver", "iGatt == null");  
  19.                     // BLE is not supported  
  20.                     return false;  
  21.                 }  
  22.   
  23.                 UUID uuid = UUID.randomUUID();  
  24.                 GattCallbackWrapper wrapper = new GattCallbackWrapper(this, callback, serviceUuids);  
  25.                 iGatt.registerClient(new ParcelUuid(uuid), wrapper);  
  26.                 if (wrapper.scanStarted()) {  
  27.                     if (DBG) Log.e("BluetoothAdapterReceiver", "wrapper.scanStarted()==true");  
  28.                     mLeScanClients.put(callback, wrapper);  
  29.                     return true;  
  30.                 }  
  31.             } catch (RemoteException e) {  
  32.                 Log.e(TAG,"",e);  
  33.             }  
  34.         }  
  35.         return false;  
  36.     }  


接着创建了一个GattCallbackWrapper对象,这是个BluetoothAdapter的内部类,主要用于获取回调信息,然后iGatt注册一个client,由BluetoothManagerService中的分析可知,iGatt实际是一个GattService内部类BluetoothGattBinder的对象

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public void registerClient(ParcelUuid uuid, IBluetoothGattCallback callback) {  
  2.             GattService service = getService();  
  3.             if (service == null) return;  
  4.             service.registerClient(uuid.getUuid(), callback);  
  5.         }  


这里还是调用GattService的registerClient方法

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. void registerClient(UUID uuid, IBluetoothGattCallback callback) {  
  2.        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");  
  3.   
  4.        if (DBG) Log.d(TAG, "registerClient() - UUID=" + uuid);  
  5.        mClientMap.add(uuid, callback);  
  6.        gattClientRegisterAppNative(uuid.getLeastSignificantBits(),  
  7.                                    uuid.getMostSignificantBits());  
  8.    }  


这里面调用了本地方法,对应的JNI文件是Com_android_bluetooth_gatt.cpp,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. static void gattClientRegisterAppNative(JNIEnv* env, jobject object,  
  2.                                         jlong app_uuid_lsb, jlong app_uuid_msb )  
  3. {  
  4.     bt_uuid_t uuid;  
  5.   
  6.     if (!sGattIf) return;  
  7.     set_uuid(uuid.uu, app_uuid_msb, app_uuid_lsb);  
  8.     sGattIf->client->register_client(&uuid);  
  9. }  


分析到这里其实差不多了,因为这里系统会调用MTK提供的蓝牙库来实现搜索,源码我们无法看到。

至此,蓝牙BLE搜索分析完毕!

原文地址:https://www.cnblogs.com/wi100sh/p/4325213.html