#
引言智汀家庭云Android版 是一款结合智慧中心(SA)的Android客户端,支持用户在SA所在局域网环境下实现智能家居的设备控制等功能。
#
1. 快速上手#
1.1 开发工具- Android Studio 2020.3.1
- JDK 1.8
#
1.2 源码地址Git Hub
名称 URL 描述 sa-android-sdk https://github.com/zhiting-tech/sa-android-sdk 安卓源码 gitee
名称 URL 描述 sa-android-sdk https://gitee.com/zhiting-tech/sa-android-sdk 安卓源码
#
1.3 构建版本克隆存储库
$ git clone https://xxxx/sa-android-sdk
同步代码(syn),如果失败,删除app、frameword、provisioning、zxing里面的build文件,重新构建。
#
2. 开发指南#
2.1 设计模式本项目框架设计模式采用MVP
#
2.2 组织架构- 项目代码架构如下图:
架构描述
- app 是开发经常使用到包,我们平时开发一般都是再此目录下。
- frameword是网络请求框架和公用工具类封装
- provisioning设备配网
- zxing 第三方扫码
#
2.3 Sqlite 本地化存储#
封装路径:#
app/com.yctc.zhiting/db#
数据库表:- zhiting_home_company 公司家庭表
- zhiting_user_info 用户信息表
- zhiting_location 房间列表
- zhiting_devices 设备列表
- zhiting_scene 场景列表
以home_company表为例子:
创建表格属性
private void checkHomeCompanyTable() { try { String createRecentContactTableSqlStr = "create table if not exists " + DBConfig.TABLE_HOME_COMPANY + "(h_id INTEGER primary key AUTOINCREMENT," + "name text, sa_user_token text, sa_user_id integer, sa_lan_address text, is_bind_sa bool, user_id integer,ss_id text,mac_address text, cloud_id integer, cloud_user_id integer, area_id integer, sa_id text, area_type integer)"; db.execSQL(createRecentContactTableSqlStr); } catch (Exception e) { e.printStackTrace(); } }
更新家庭信息
public int updateHomeCompanyByCloudId(HomeCompanyBean homeCompanyBean) { int count = 0; try { long id = homeCompanyBean.getId(); ContentValues contentValues = new ContentValues(); contentValues.put("name", homeCompanyBean.getName()); count = db.update(DBConfig.TABLE_HOME_COMPANY, contentValues, "cloud_id=? or area_id=?", new String[]{String.valueOf(id), String.valueOf(id)}); } catch (Exception e) { e.printStackTrace(); } return count; }
关于更多sqlite的初级操作
#
2.4 网络层协议篇#
2.4.1 OkHttp几个特点1) OkHttp的特点
是基于建造者模式(将一个复杂对象的构建与它的表示分离,用于属性参数很多时)。
链式调用,每一个方法的返回值类型都是当前类的对象。
2) OkHttp的优点
- 支持HTTP2/SPDY(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。)
- Socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,减少了请求延迟
- 共享Socket,减少对服务器的请求次数。
- 拥有Interceptors轻松处理请求与响应(自动处理GZip压缩)。
#
2.4.2 OkHttp请求流程 在请求的时候初始化一个Call的实例,然后执行它的execute()方法或enqueue()方法,内部最后都会执行到getResponseWithInterceptorChain()方法,这个方法里面通过拦截器组成的责任链,依次经过用户自定义普通拦截器、重试拦截器、桥接拦截器、缓存拦截器、连接拦截器和用户自定义网络拦截器以及访问服务器拦截器等拦截处理过程,来获取到一个响应并交给用户。OKHttp内部的大致请求流程图如下所示:
#
2.4.3 初始化OkHttpClient OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(httpConfig.getConnectTimeout(), TimeUnit.SECONDS) .writeTimeout(httpConfig.getWriteTimeout(), TimeUnit.SECONDS) .readTimeout(httpConfig.getReadTimeout(), TimeUnit.SECONDS) .addInterceptor(new LoggerInterceptor("ZhiTing", true)) .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager()) .hostnameVerifier((hostname, session) -> { if (hostname.equals(CLOUD_HOST_NAME)) { // SC直接访问 return true; } else { if (session != null) { String cersCacheJson = SpUtil.get(hostname); // 根据主机名获取本地存储的数据 try { Certificate[] certificates = session.getPeerCertificates(); // 证书 String cersJson = byte2Base64String(certificates[0].getEncoded()); // 把证书转为base64存储 if (!TextUtils.isEmpty(cersCacheJson)) { // 如果之前存储过 String ccj = new String(cersCacheJson.getBytes(), "UTF-8"); String cj = new String(cersJson.getBytes(), "UTF-8"); boolean cer = cj.equals(ccj); if (cer) { // 之前存储过的证书和当前证书一样,直接访问 return true; } else {// 之前存储过的证书和当前证书不一样,重新授权 showAlertDialog(LibLoader.getCurrentActivity().getString(R.string.whether_trust_this_certificate_again), hostname, cersJson); return false; } } else {// 之前没存储过,询问是否信任证书 showAlertDialog(LibLoader.getCurrentActivity().getString(R.string.whether_trust_this_certificate), hostname, cersJson); return false; } } catch (SSLPeerUnverifiedException e) { e.printStackTrace(); } catch (CertificateEncodingException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return false; } }) .cookieJar(new CookieJarImpl(PersistentCookieStore.getInstance())) .build();
#
2.4.4 MVP模式1) model层示例代码
public class MainPresenter extends BasePresenterImpl<MainContract.View> implements MainContract.Presenter { MainModel model;
@Override public void init() { model = new MainModel(); }
@Override public void clear() { model = null; }
/** * 获取订单是否 未到时,准时,超时 * * @param request */ @Override public void postOrderCheck(AddRoomRequest request) { mView.showLoadingView(); model.postOrderCheck(request, new RequestDataCallback<HttpResult>() { @Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); ToastUtil.showBottom(errorMessage); if (null == mView) return; mView.hideLoadingView(); }
@Override public void onSuccess(HttpResult datas) { super.onSuccess(datas); if (null == mView) return; mView.hideLoadingView(); } }); }}
2) View接口层示例代码
public interface MainContract { interface Model { void postOrderCheck(AddRoomRequest request, RequestDataCallback<HttpResult> callback); }
interface View extends BaseView { void postOrderCheckSuccess(HttpResult msg); }
interface Presenter extends BasePresenter<View> { void postOrderCheck(AddRoomRequest request); }}
3) Presenter层示例代码
public class MainPresenter extends BasePresenterImpl<MainContract.View> implements MainContract.Presenter { MainModel model;
@Override public void init() { model = new MainModel(); }
@Override public void clear() { model = null; }
/** * 获取订单是否 未到时,准时,超时 * * @param request */ @Override public void postOrderCheck(AddRoomRequest request) { mView.showLoadingView(); model.postOrderCheck(request, new RequestDataCallback<HttpResult>() { @Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); ToastUtil.showBottom(errorMessage); if (null == mView) return; mView.hideLoadingView(); }
@Override public void onSuccess(HttpResult datas) { super.onSuccess(datas); if (null == mView) return; mView.hideLoadingView(); } }); }}
#
2.4.5 网络请求方法1) get请求方式
HTTPCaller.getInstance().get(CheckBindSaBean.class,HttpUrlConfig.getCheckBindSA(), callback);
2) post请求方式
HTTPCaller.getInstance().post(ScopeTokenBean.class, HttpUrlConfig.getScopesToken(), body, callback);
3) delete请求方式
HTTPCaller.getInstance().delete(Object.class, HttpUrlConfig.getCreatePluginList()+"/"+id, "", callback);
4) patch请求方式
HTTPCaller.getInstance().patch(Object.class,HttpUrlConfig.getCreatePluginList()+"/"+id, "", callback);
5) put请求方式
HTTPCaller.getInstance().put(Object.class, HttpUrlConfig.getScene()+"/"+id, body, callback);
#
2.4.6、公用方法1) 添加请求头
public static void addHeader(String name, String value) { if (!TextUtils.isEmpty(value)) { headers.clear(); headers.add(new Header(name, value)); }}
2) 添加请求公共参数
public void addCommonField(String key, String value) { commonField.add(new NameValuePair(key, value)); }
3) 删除公共参数
public void removeCommonField(String key) { for (int i = commonField.size() - 1; i >= 0; i--) { if (commonField.get(i).equals(key)) { commonField.remove(i); } } }
#
2.5 智能设备置网篇Android智能设备的置网协议支持blufi蓝牙和softAP两种。在Android端配网时会先检查本地是否存在对应的插件包,然后进行更新或者下载插件包,配网页面时将之前下载或更新的html静态资源加载到WebView中实现的,而配网中用到的方法则由App端原生实现后,提供接口给h5调用。
#
2.5.1 插件包更新及下载在进入配网页之前会先检查本地插件包是否存在和插件包版本,根据情况下载或者更新插件包。
/** * 发现设备 */public class ScanDevice2Activity extends MVPBaseActivity<AddDeviceContract.View, AddDevicePresenter> implements AddDeviceContract.View { ... /** * 插件详情成功 * * @param pluginDetailBean */ @Override public void getPluginDetailSuccess(PluginDetailBean pluginDetailBean) { if (pluginDetailBean != null) { PluginsBean pluginsBean = pluginDetailBean.getPlugin(); String pluginId = pluginsBean.getId(); String pluginFilePath = ""; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ pluginFilePath = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath()+"/plugins/" + pluginId; }else { pluginFilePath = Constant.PLUGIN_PATH + pluginId; } if (pluginsBean != null) { String downloadUrl = pluginsBean.getDownload_url(); String cachePluginJson = SpUtil.get(pluginId); PluginsBean cachePlugin = GsonConverter.getGson().fromJson(cachePluginJson, PluginsBean.class); String cacheVersion = ""; if (cachePlugin!=null){ cacheVersion = cachePlugin.getVersion(); } String version = pluginsBean.getVersion(); if (mDeviceTypeDeviceBean!=null && BaseFileUtil.isFileExist(pluginFilePath) && !TextUtils.isEmpty(cacheVersion) && !TextUtils.isEmpty(version) && cacheVersion.equals(version)) { // 如果缓存存在且版本相同 String urlPath = "file://"+pluginFilePath+"/"+mDeviceTypeDeviceBean.getProvisioning(); toConfigNetwork(urlPath); } else { if (!TextUtils.isEmpty(downloadUrl)) { String suffix = downloadUrl.substring(downloadUrl.lastIndexOf(".") + 1); BaseFileUtil.deleteFolderFile(pluginFilePath, true); String fileZipPath = pluginFilePath+"."+suffix; File file = new File(fileZipPath); BaseFileUtil.deleteFile(file); List<Header> headers = new ArrayList<>(); headers.add(new Header("Accept-Encoding", "identity")); headers.add(new Header(HttpConfig.TOKEN_KEY, HomeUtil.getSaToken())); String path = ""; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ path = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath()+"/plugins/"; }else { path = Constant.PLUGIN_PATH; } String finalPath = path; String finalPluginFilePath = pluginFilePath; HTTPCaller.getInstance().downloadFile(downloadUrl, path, pluginId, headers.toArray(new Header[headers.size()]), UiUtil.getString(R.string.home_download_plugin_package_fail), new ProgressUIListener() { @Override public void onUIProgressChanged(long numBytes, long totalBytes, float percent, float speed) { LogUtil.e("进度:" + percent); if (percent == 1) { LogUtil.e("下载完成"); try { ZipUtils.decompressFile(new File(fileZipPath), finalPath, true); String pluginsBeanToJson = GsonConverter.getGson().toJson(pluginsBean); SpUtil.put(pluginId, pluginsBeanToJson); String urlPath = "file://"+ finalPluginFilePath +"/"+mDeviceTypeDeviceBean.getProvisioning(); toConfigNetwork(urlPath); } catch (IOException e) { e.printStackTrace(); }
} } }); } } } } } ...}
#
2.5.2 Blufi配网使用Blufi配网前,首先需要引入 com.github.EspressifApp:lib-blufi-android:2.3.4 库,引入该库之后封装BlufiUtil工具类。
import android.bluetooth.BluetoothDevice;import android.bluetooth.BluetoothGattCallback;import android.content.Context;import android.net.wifi.WifiInfo;import android.net.wifi.WifiManager;import android.text.TextUtils;import android.util.Log;
import blufi.espressif.BlufiCallback;import blufi.espressif.BlufiClient;import blufi.espressif.params.BlufiConfigureParams;import blufi.espressif.params.BlufiParameter;
public class BlufiUtil {
public static final int MIN_MTU_LENGTH = 23; public static final int MAX_MTU_LENGTH = 517; public static final int DEFAULT_MTU_LENGTH = 512;
private Context mContext; private WifiManager mWifiManager; private BlufiClient mBlufiClient;
public BlufiUtil(Context context) { mContext = context; mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); }
/** * 连接 * @param device 要连接的设备 * @param gattCallback 连接回调 * @param blufiCallback blufi回调 */ public void connect(BluetoothDevice device, BluetoothGattCallback gattCallback, BlufiCallback blufiCallback){ if (mBlufiClient != null) { mBlufiClient.close(); mBlufiClient = null; } mBlufiClient = new BlufiClient(mContext, device); mBlufiClient.setGattCallback(gattCallback); if (blufiCallback!=null) mBlufiClient.setBlufiCallback(blufiCallback); mBlufiClient.connect(); }
/** * 断开连接 */ public void disconnect(){ if (mBlufiClient != null) { mBlufiClient.requestCloseConnection(); } }
/** * Request to configure station or softap * * @param params configure params */ public void configure(BlufiConfigureParams params) { if (mBlufiClient != null) { mBlufiClient.configure(params); } }
/** * 创建配置信息实体 * @param deviceMode 模式 sta/softap * @param ssid wifi名称 * @param password wifi密码 * @param channel 频道(softap才有 1-13) * @param maxConnection 最大连接数(softap才有 1-4) * @param security 安全级别(softap才有 SOFTAP_SECURITY_OPEN, SOFTAP_SECURITY_WEP, SOFTAP_SECURITY_WPA, SOFTAP_SECURITY_WPA2, SOFTAP_SECURITY_WPA_WPA2) * @return */ public BlufiConfigureParams createParams(int deviceMode, String ssid, String password, int channel, int maxConnection, int security) { BlufiConfigureParams params = new BlufiConfigureParams(); params.setOpMode(deviceMode); switch (deviceMode) { case BlufiParameter.OP_MODE_NULL: return params; case BlufiParameter.OP_MODE_STA: if (checkSta(params, ssid, password)) { return params; } else { return null; } case BlufiParameter.OP_MODE_SOFTAP: if (checkSoftAP(params, ssid, password, channel, maxConnection, security)) { return params; } else { return null; } case BlufiParameter.OP_MODE_STASOFTAP: if (checkSoftAP(params, ssid, password, channel, maxConnection, security) && checkSta(params, ssid, password)) { return params; } else { return null; } }
return null; }
/** * 是否是 sta模式 * @param params 配置参数 * @param ssid wifi名称 * @param password wifi密码 * @return */ private boolean checkSta(BlufiConfigureParams params, String ssid, String password) { if (TextUtils.isEmpty(ssid)) { return false; } params.setStaSSIDBytes(ssid.getBytes()); params.setStaPassword(password); return true; }
/** * * @param params 配置参数 * @param ssid wifi名称 * @param password wifi密码 * @param channel 频道(softap才有 1-13) * @param maxConnection 最大连接数(softap才有 1-4) * @param security 安全级别(softap才有 SOFTAP_SECURITY_OPEN, SOFTAP_SECURITY_WEP, SOFTAP_SECURITY_WPA, SOFTAP_SECURITY_WPA2, SOFTAP_SECURITY_WPA_WPA2) * @return */ public boolean checkSoftAP(BlufiConfigureParams params, String ssid, String password, int channel, int maxConnection, int security) { params.setSoftAPSSID(ssid); params.setSoftAPPAssword(password); if (channel>0) params.setSoftAPChannel(channel); if (maxConnection>0) params.setSoftAPMaxConnection(maxConnection); if (security>0) { params.setSoftAPSecurity(security);
} return true; }
/** * 连接的wifi名称 * @return */ private String getConnectionSSID() { if (!mWifiManager.isWifiEnabled()) { return null; }
WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); if (wifiInfo == null) { return null; }
String ssid = wifiInfo.getSSID(); if (ssid.startsWith("\"") && ssid.endsWith("\"") && ssid.length() >= 2) { ssid = ssid.substring(1, ssid.length() - 1); } return ssid; }
/** * wifi频率 * @return */ private int getConnectionFrequncy() { if (!mWifiManager.isWifiEnabled()) { return -1; }
WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); if (wifiInfo == null) { return -1; }
return wifiInfo.getFrequency(); }
/** * 是否5g * @param freq * @return */ private boolean is5GHz(int freq) { return freq > 4900 && freq < 5900; }
public void releaseSource(){ if (mBlufiClient!=null) { mBlufiClient.close(); } }}
#
2.5.3 SoftAP配网在进行SoftAP配网前需要引入 com.github.espressif:esp-idf-provisioning-android:lib-2.0.11 库或直接下载该库,这里是直接下载的方式引入该库,引入该库之后封装工具类EspressifConfigNetworkUtil。
import android.annotation.SuppressLint;import android.content.Context;import android.util.Log;
import com.espressif.provisioning.ESPConstants;import com.espressif.provisioning.ESPDevice;import com.espressif.provisioning.ESPProvisionManager;import com.espressif.provisioning.listeners.ProvisionListener;import com.yctc.zhiting.utils.confignetwork.ConfigNetworkCallback;
/** * espressif wifi方式配网工具类 */public class EspressifConfigNetworkUtil {
private final String TAG = EspressifConfigNetworkUtil.class.getSimpleName(); private ESPProvisionManager provisionManager;
/** * 初始化 ESPProvisionManager * @param context */ public EspressifConfigNetworkUtil(Context context) { provisionManager = ESPProvisionManager.getInstance(context); }
/** * 创建 ESPDevice设备) */ public ESPDevice createDevice(String proof){ ESPDevice espDevice = provisionManager.createESPDevice(ESPConstants.TransportType.TRANSPORT_SOFTAP, ESPConstants.SecurityType.SECURITY_1); // 创建设备 provisionManager.getEspDevice().setProofOfPossession(proof); // 设置session return espDevice; }
/** * 创建并连接 ESPDevice设备) */ @SuppressLint("MissingPermission") public void connectDevice(){ provisionManager.getEspDevice().connectWiFiDevice(); // 连接设备 }
/** * 配网 * @param ssidValue wifi 名称 * @param passphraseValue wifi 密码 * @param callback 回调 */ public void configNetwork(String ssidValue, String passphraseValue, ConfigNetworkCallback callback){
provisionManager.getEspDevice().provision(ssidValue, passphraseValue, new ProvisionListener() { // 配网操作
@Override public void createSessionFailed(Exception e) { callback.onFailed(ConfigNetworkCallback.CREATE_SESSION_FAILED, e); }
@Override public void wifiConfigSent() { Log.e(TAG, "==========发送配置"); }
@Override public void wifiConfigFailed(Exception e) { callback.onFailed(ConfigNetworkCallback.WIFI_CONFIG_FAILED, e); }
@Override public void wifiConfigApplied() {
Log.e(TAG, "==========申请配置成功"); }
@Override public void wifiConfigApplyFailed(Exception e) {
callback.onFailed(ConfigNetworkCallback.WIFI_CONFIG_APPLIED_FAILED, e); }
@Override public void provisioningFailedFromDevice(final ESPConstants.ProvisionFailureReason failureReason) {
switch (failureReason) { case AUTH_FAILED: callback.onFailed(ConfigNetworkCallback.ERROR_AUTHENTICATION_FAILED, null); break; case NETWORK_NOT_FOUND: callback.onFailed(ConfigNetworkCallback.ERROR_NETWORK_NOT_FOUND, null); break; case DEVICE_DISCONNECTED: case UNKNOWN: callback.onFailed(ConfigNetworkCallback.TO_PROV_DEVICE_FAILED, null); break; } }
@Override public void deviceProvisioningSuccess() { Log.e(TAG, "==========配网成功"); callback.onSuccess(); }
@Override public void onProvisioningFailed(Exception e) { callback.onFailed(ConfigNetworkCallback.TO_PROV_DEVICE_FAILED, e); } }); }
/** * 断开连接 */ public void disconnectDevice(){ if (provisionManager.getEspDevice() != null) { provisionManager.getEspDevice().disconnectDevice(); } }}
#
2.5.4 提供给前端调用的方法根据前端提供的文档,由原生实现对应的方法给前端调用,封装一个JsMethodConstant配网js交互方法类。
import android.bluetooth.BluetoothDevice;import android.bluetooth.BluetoothGatt;import android.bluetooth.BluetoothGattCallback;import android.bluetooth.BluetoothGattCharacteristic;import android.bluetooth.BluetoothGattDescriptor;import android.bluetooth.BluetoothGattService;import android.bluetooth.BluetoothProfile;import android.os.Handler;import android.text.TextUtils;import android.util.Log;import android.webkit.WebView;import android.widget.Toast;
import com.app.main.framework.baseutil.LogUtil;import com.app.main.framework.baseutil.SpConstant;import com.app.main.framework.baseutil.SpUtil;import com.app.main.framework.baseutil.TimeFormatUtil;import com.app.main.framework.baseutil.UiUtil;import com.app.main.framework.baseutil.toast.ToastUtil;import com.app.main.framework.baseview.BaseApplication;import com.app.main.framework.entity.ChannelEntity;import com.app.main.framework.gsonutils.GsonConverter;import com.espressif.provisioning.ESPDevice;import com.espressif.provisioning.ESPProvisionManager;import com.espressif.provisioning.WiFiAccessPoint;import com.espressif.provisioning.listeners.WiFiScanListener;import com.yctc.zhiting.R;import com.yctc.zhiting.application.Application;import com.yctc.zhiting.config.Constant;import com.yctc.zhiting.entity.JsBean;import com.yctc.zhiting.utils.confignetwork.BlufiUtil;import com.yctc.zhiting.utils.confignetwork.ConfigNetworkCallback;import com.yctc.zhiting.utils.confignetwork.WifiUtil;import com.yctc.zhiting.utils.confignetwork.softap.EspressifConfigNetworkUtil;
import java.io.UnsupportedEncodingException;import java.util.ArrayList;import java.util.List;import java.util.Locale;
import blufi.espressif.BlufiCallback;import blufi.espressif.BlufiClient;import blufi.espressif.params.BlufiConfigureParams;import blufi.espressif.params.BlufiParameter;import blufi.espressif.response.BlufiScanResult;import blufi.espressif.response.BlufiStatusResponse;import blufi.espressif.response.BlufiVersionResponse;
/** * 配网js交互方法 */public class JsMethodConstant {
public static final int SUCCESS = 0; // 原生处理结果反馈h5 public static final int FAIL = 1; // 原生处理结果反馈h5
public static final String NETWORK_TYPE = "networkType"; // 查看app连网类型 public static final String GET_USER_INFO = "getUserInfo"; // 获取用户信息 public static final String SET_TITLE = "setTitle"; // 设置标题属性 public static final String IS_PROFESSION = "isProfession"; // 是否是专业版app public static final String CONNECT_DEVICE_BY_BLUETOOTH = "connectDeviceByBluetooth"; // 通过蓝牙连接设备 public static final String CONNECT_NETWORK_BY_BLUETOOTH = "connectNetworkByBluetooth"; // 通过蓝牙发送配网信息 public static final String CONNECT_DEVICE_HOST_SPOT = "connectDeviceHotspot"; // 连接设备热点 public static final String CREATE_DEVICE_BY_HOTSPOT = "createDeviceByHotspot"; // 通过设备热点创建设备 public static final String CONNECT_DEVICE_BY_HOTSPOT = "connectDeviceByHotspot"; // 通过热点连接设备 public static final String CONNECT_NETWORK_BY_HOTSPOT = "connectNetworkByHotspot"; // 通过热点发送配网信息 public static final String REGISTER_DEVICE_BY_HOTSPOT = "registerDeviceByHotspot"; // 通过热点发送设备注册 public static final String REGISTER_DEVICE_BY_BLUETOOTH = "registerDeviceByBluetooth"; // 通过蓝牙发送配网信息 public static final String GET_DEVICE_INFO = "getDeviceInfo"; // 通过热点发送配网信息 public static final String GET_CONNECT_WIFI = "getConnectWifi"; // 获取设备当前连接wifi public static final String TO_SYSTEM_WLAN = "toSystemWlan"; // 去系统设置wlan页 public static final String GET_SYSTEM_WIFI_LIST = "getSystemWifiList"; // 获取wifi列表页 public static final String GET_SOCKET_ADDRESS = "getSocketAddress"; // 获取插件websocket地址
private static String connectBleCallbackID; // 连接蓝牙回调id private static String connectNetworkByBluetoothCallbackID; // 通过蓝牙配网id private static String connectDeviceHotspotCallbackID; // 连接设备热点id private static String createDeviceByHotspotCallbackID; // 通过设备热点创建设备id private static String connectDeviceByHotspotCallbackID; // 通过热点连接设备id private static String connectNetworkByHotspotCallbackID; // 通过热点连接设备id public static List<BluetoothDevice> mBlueLists; // 蓝牙设备列表
private WebView mWebView; // 蓝牙配网 private BlufiUtil mBlufiUtil; // ap配网 private ESPProvisionManager provisionManager; private static EspressifConfigNetworkUtil espressifConfigNetworkUtil; private static ArrayList<WiFiAccessPoint> mWifiAPList; private WifiUtil mWifiUtil; public static boolean dealConnectHotspot; // 是否需要处理连接设备热点结果 public static String mHotspotName; // 要连接wifi热点名称
public JsMethodConstant(WebView mWebView) { this.mWebView = mWebView; }
public JsMethodConstant(WebView webView, BlufiUtil blufiUtil, WifiUtil wifiUtil){ init(webView, blufiUtil, wifiUtil); }
/** * 初始化 * * @param webView * @param blufiUtil */ public void init(WebView webView, BlufiUtil blufiUtil, WifiUtil wifiUtil) { mBlueLists = new ArrayList<>(); mWifiAPList = new ArrayList<>(); mWebView = webView; mWebView.resumeTimers(); mBlufiUtil = blufiUtil; mWifiUtil = wifiUtil; provisionManager = ESPProvisionManager.getInstance(Application.getContext()); espressifConfigNetworkUtil = new EspressifConfigNetworkUtil(Application.getContext());// startWifiScan(); }
/** * 释放资源 */ public void release() { if (mBlueLists!=null) { mBlueLists.clear(); mBlueLists = null; } if (mWifiAPList!=null) { mWifiAPList.clear(); mWifiAPList = null; } if (mBlufiUtil != null) { mBlufiUtil.disconnect(); mBlufiUtil = null; } if (espressifConfigNetworkUtil != null) { espressifConfigNetworkUtil.disconnectDevice(); } provisionManager = null; espressifConfigNetworkUtil = null; if (mWifiUtil != null) { mWifiUtil.release(); mWifiUtil = null; } mHotspotName = null; dealConnectHotspot = false; }
private void startWifiScan() { if (provisionManager == null || mWifiAPList == null) { return; } Log.d("startWifiScan====", "Start Wi-Fi Scan"); mWifiAPList.clear();
provisionManager.getEspDevice().scanNetworks(new WiFiScanListener() { @Override public void onWifiListReceived(final ArrayList<WiFiAccessPoint> wifiList) { mWifiAPList.addAll(wifiList); }
@Override public void onWiFiScanFailed(Exception e) {
// TODO Log.e("onWiFiScanFailed", "onWiFiScanFailed"); e.printStackTrace(); UiUtil.runInMainThread(new Runnable() { @Override public void run() { LogUtil.e("Failed to get Wi-Fi scan list"); } }); } }); }
/** * 拼接回调给h5的js代码 * * @param callbackId * @param status * @param error * @return */ public static String dealJsCallback(String callbackId, int status, String error) { String callbackJs = "zhiting.callBack('" + callbackId + "'," + "'{\"status\":" + status + ",\"error\":\"" + error + "\"}')"; return callbackJs; }
/** * 把结果回调给h5 * * @param js */ public void runOnMainUi(String js) { UiUtil.runInMainThread(new Runnable() { @Override public void run() { if (mWebView != null) mWebView.loadUrl("javascript:" + js); } }); }
/** * 通过蓝牙连接设备 * * @param jsBean */ public void connectDeviceByBluetooth(JsBean jsBean, BlufiCallback blufiCallback) { if (mBlufiUtil == null) { delayBluetoothFail(); return; } if (jsBean != null) { JsBean.JsSonBean jsSonBean = jsBean.getParams(); connectBleCallbackID = jsBean.getCallbackID(); if (jsSonBean != null) { String bluetoothName = jsSonBean.getBluetoothName(); if (!TextUtils.isEmpty(bluetoothName)) { BluetoothDevice bluetoothDevice = null; if (mBlueLists!=null) { for (BluetoothDevice bd : mBlueLists) { String name = bd.getName(); if (name != null && name.equals(bluetoothName)) { bluetoothDevice = bd; break; } } if (bluetoothDevice != null) { // 蓝牙设备不为空 mBlufiUtil.connect(bluetoothDevice, new GattCallback(), blufiCallback); } else { // 蓝牙设备为空 delayBluetoothFail(); } }else { delayBluetoothFail(); } }else { delayBluetoothFail(); } } } }
private void delayBluetoothFail(){ new Handler().postDelayed(new Runnable() { @Override public void run() { connectBluetoothResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.bluetooth_is_not_found)); } }, 10000); }
/** * 连接蓝牙结果 * * @param status * @param error */ private void connectBluetoothResult(int status, String error) { if (!TextUtils.isEmpty(connectBleCallbackID)) { String connectBleJs = dealJsCallback(connectBleCallbackID, status, error); runOnMainUi(connectBleJs); connectBleCallbackID = ""; } }
/** * 通过蓝牙发送配网信息 * * @param jsBean */ public void connectNetworkByBluetooth(JsBean jsBean) { if (mBlufiUtil == null) { delayBluetoothFail(); return; } if (jsBean != null) { JsBean.JsSonBean jsSonBean = jsBean.getParams(); connectNetworkByBluetoothCallbackID = jsBean.getCallbackID(); if (jsSonBean != null) { String wifiName = jsSonBean.getWifiName(); String wifiPwd = jsSonBean.getWifiPass(); if (TextUtils.isEmpty(wifiName)) { connectNetworkByBluetoothResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.home_please_input_wifi_name)); return; } if (TextUtils.isEmpty(wifiPwd)) { connectNetworkByBluetoothResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.home_please_input_wifi_name)); return; } BlufiConfigureParams blufiConfigureParams = mBlufiUtil.createParams(BlufiParameter.OP_MODE_STA, wifiName, wifiPwd, 0, 0, 0); mBlufiUtil.configure(blufiConfigureParams); } } }
/** * 通过蓝牙发送配网信息结果 * * @param status * @param error */ public void connectNetworkByBluetoothResult(int status, String error) { if (!TextUtils.isEmpty(connectNetworkByBluetoothCallbackID)) { String connectNetJs = dealJsCallback(connectNetworkByBluetoothCallbackID, status, error); runOnMainUi(connectNetJs); connectNetworkByBluetoothCallbackID = ""; } }
/** * 连接设备热点 * * @param jsBean */ public void connectDeviceHotspot(JsBean jsBean) { if (jsBean != null) { JsBean.JsSonBean jsSonBean = jsBean.getParams(); connectDeviceHotspotCallbackID = jsBean.getCallbackID(); if (jsSonBean != null) { String hotspotName = jsSonBean.getHotspotName(); mHotspotName = hotspotName; if (TextUtils.isEmpty(hotspotName)) { connectDeviceHotspotResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.please_input_hotspotname)); return; } if (mWifiUtil != null) { if (mWifiUtil.isWifiEnabled()) { dealConnectHotspot = true; mWifiUtil.connectWifiPwd(hotspotName, ""); } else { connectDeviceHotspotResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.wifi_disable)); } }else { connectDeviceHotspotResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.wifi_disable)); }
} } }
/** * 检查要连接的热点是否 * * @param hotspotName * @return */ public static boolean findHotspot(String hotspotName) { for (WiFiAccessPoint wiFiAccessPoint : mWifiAPList) { String name = wiFiAccessPoint.getWifiName(); if (name != null && name.equals(hotspotName)) { return true; } } return false; }
/** * 连接设备热点结果 * * @param status * @param error */ public void connectDeviceHotspotResult(int status, String error) { String connectDeviceHotspot = dealJsCallback(connectDeviceHotspotCallbackID, status, error); System.out.println("连接设备热点回调:"+connectDeviceHotspot); runOnMainUi(connectDeviceHotspot); connectDeviceHotspotCallbackID = ""; }
/** * 通过设备热点创建设备 * * @param jsBean */ public void createDeviceByHotspot(JsBean jsBean) { if (jsBean != null) { JsBean.JsSonBean jsSonBean = jsBean.getParams(); createDeviceByHotspotCallbackID = jsBean.getCallbackID(); if (jsSonBean != null) { String ownership = jsSonBean.getOwnership(); if (TextUtils.isEmpty(ownership)) { createDeviceByHotspotResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.please_input_ownership)); return; } ESPDevice espDevice = espressifConfigNetworkUtil.createDevice(ownership); int status = espDevice == null ? FAIL : SUCCESS; String error = espDevice == null ? UiUtil.getString(R.string.failed) : UiUtil.getString(R.string.success); createDeviceByHotspotResult(status, error); } } }
/** * 通过设备热点创建设备结果 * * @param status * @param error */ private void createDeviceByHotspotResult(int status, String error) { String connectDeviceHotspot = dealJsCallback(createDeviceByHotspotCallbackID, status, error); runOnMainUi(connectDeviceHotspot); createDeviceByHotspotCallbackID = ""; }
/** * 通过设备热点创建设备 * * @param jsBean */ public static void connectDeviceByHotspot(JsBean jsBean) { if (jsBean != null) { connectDeviceByHotspotCallbackID = jsBean.getCallbackID(); espressifConfigNetworkUtil.connectDevice(); } }
/** * 通过热点连接设备结果 * * @param status * @param error */ public void connectDeviceByHotspotResult(int status, String error) { String connectDeviceHotspot = dealJsCallback(connectDeviceByHotspotCallbackID, status, error); runOnMainUi(connectDeviceHotspot); connectDeviceByHotspotCallbackID = ""; }
/** * 通过设备热点创建设备 * * @param jsBean */ public void connectNetworkByHotspot(JsBean jsBean, ConfigNetworkCallback callback) { if (jsBean != null) { JsBean.JsSonBean jsSonBean = jsBean.getParams(); connectNetworkByHotspotCallbackID = jsBean.getCallbackID(); if (jsSonBean != null) { String wifiName = jsSonBean.getWifiName(); String pwd = jsSonBean.getWifiPass(); if (TextUtils.isEmpty(wifiName)) { connectNetworkByHotspotResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.home_please_input_wifi_name)); return; }
if (TextUtils.isEmpty(pwd)) { connectNetworkByHotspotResult(JsMethodConstant.FAIL, UiUtil.getString(R.string.home_please_input_wifi_password)); return; } espressifConfigNetworkUtil.configNetwork(wifiName, pwd, callback); } } }
/** * 通过热点连接设备结果 * * @param status * @param error */ public void connectNetworkByHotspotResult(int status, String error) { String connectDeviceHotspot = dealJsCallback(connectNetworkByHotspotCallbackID, status, error); runOnMainUi(connectDeviceHotspot); connectNetworkByHotspotCallbackID = ""; }
/** * 获取配网的设备信息 * @param jsBean * @param json */ public void getDeviceInfo(JsBean jsBean, String json){ if (jsBean != null) { String getDeviceInfoDCallbackID = jsBean.getCallbackID(); String devInfoJs = "zhiting.callBack('" + getDeviceInfoDCallbackID + "'," + json+")"; LogUtil.e(devInfoJs); runOnMainUi(devInfoJs); } }
/** * 当前wifi名称 * @param jsBean */ public void getConnectWifi(JsBean jsBean){ if (jsBean!=null){ int status = mWifiUtil.isConnectWifi() ? SUCCESS : FAIL; String wifiName = mWifiUtil.isConnectWifi() ? mWifiUtil.getCurrentWifiName() : ""; String wifiNameJs = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{\"status\":" + status + ",\"wifiName\":\"" + wifiName + "\"}')"; runOnMainUi(wifiNameJs); } }
/** * 获取wifi列表页 * @param jsBean */ public void getSystemWifiList(JsBean jsBean){ if (jsBean!=null){ int status = 0; String wifiJson = "\\[]"; if (GpsUtil.isEnabled(BaseApplication.getContext())) { List<WifiUtil.WifiNameSignBean> wifiNames = mWifiUtil.getWifiNameSignList(); status = 0; wifiJson = GsonConverter.getGson().toJson(wifiNames); }else { status = 1; LogUtil.e("请打开Gps"); } String wifiNameJs = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{\"status\":" + status + ",\"list\":" + wifiJson + "}')"; System.out.println("wifi列表:" + wifiNameJs); runOnMainUi(wifiNameJs); } }
/** * 获取插件websocket地址 * @param jsBean */ public void getSocketAddress(JsBean jsBean){ if (jsBean!=null){ int status = 0; String address = ""; if (Constant.CurrentHome!=null && !TextUtils.isEmpty(Constant.CurrentHome.getSa_lan_address())) { status = 0; address = getUrl(); }else { status = 1; } System.out.println("websoket地址:"+address); String socketAddrJs = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{\"status\":" + status + ",\"address\":\"" + address + "\"}')"; runOnMainUi(socketAddrJs); } }
/** * 获取地址 * * @return */ private String getUrl() { String urlSA = ""; if (Constant.CurrentHome!=null && !TextUtils.isEmpty(Constant.CurrentHome.getSa_lan_address())) { String url = "ws://"+Constant.CurrentHome.getSa_lan_address().replace("http://", "")+"/ws"; urlSA = url; } if (HomeUtil.isSAEnvironment()) { return urlSA; } else { if (UserUtils.isLogin()) { return getSCUrl(); } else { return urlSA; } } }
private String getSCUrl() { String newUrlSC = "wss://sc.zhitingtech.com/ws"; long currentTime = TimeFormatUtil.getCurrentTime(); String tokenKey = SpUtil.get(SpConstant.SA_TOKEN); String json = SpUtil.get(tokenKey);
if (!TextUtils.isEmpty(json)) { ChannelEntity channel = GsonConverter.getGson().fromJson(json, ChannelEntity.class); if (channel != null) { LogUtil.e("WSocketManager=getSCUrl=" + GsonConverter.getGson().toJson(channel)); if ((currentTime - channel.getCreate_channel_time()) < channel.getExpires_time()) { String mHostSC = "sc.zhitingtech.com"; newUrlSC = newUrlSC.replace(mHostSC, channel.getHost()); newUrlSC=newUrlSC.replace("wss","ws"); LogUtil.e("WSocketManager=getSCUrl=" + newUrlSC); } } } return newUrlSC; }
/** * 蓝牙连接结果回调 * mBlufiClient call onCharacteristicWrite and onCharacteristicChanged is required */ private class GattCallback extends BluetoothGattCallback { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String devAddr = gatt.getDevice().getAddress(); LogUtil.d(String.format(Locale.ENGLISH, "onConnectionStateChange addr=%s, status=%d, newState=%d", devAddr, status, newState)); int st = JsMethodConstant.FAIL; String error = ""; if (status == BluetoothGatt.GATT_SUCCESS) { switch (newState) { case BluetoothProfile.STATE_CONNECTED: // 蓝牙连接成功 LogUtil.e("蓝牙连接成功"); st = JsMethodConstant.SUCCESS; error = UiUtil.getString(R.string.bluetooth_connect_success); break; case BluetoothProfile.STATE_DISCONNECTED: // 蓝牙连接失败 gatt.close(); st = JsMethodConstant.FAIL; LogUtil.e("蓝牙断开连接"); error = UiUtil.getString(R.string.bluetooth_disconnect); break; } } else { // 蓝牙连接失败 gatt.close(); st = JsMethodConstant.FAIL; LogUtil.e("蓝牙连接失败"); error = UiUtil.getString(R.string.bluetooth_connect_fail); } connectBluetoothResult(st, error); }
@Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { LogUtil.d(String.format(Locale.ENGLISH, "onMtuChanged status=%d, mtu=%d", status, mtu)); if (status == BluetoothGatt.GATT_SUCCESS) {
} else {
}
}
@Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { LogUtil.d(String.format(Locale.ENGLISH, "onServicesDiscovered status=%d", status)); if (status != BluetoothGatt.GATT_SUCCESS) { gatt.disconnect();
} }
@Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { LogUtil.d(String.format(Locale.ENGLISH, "onDescriptorWrite status=%d", status)); if (descriptor.getUuid().equals(BlufiParameter.UUID_NOTIFICATION_DESCRIPTOR) && descriptor.getCharacteristic().getUuid().equals(BlufiParameter.UUID_NOTIFICATION_CHARACTERISTIC)) { String msg = String.format(Locale.ENGLISH, "Set notification enable %s", (status == BluetoothGatt.GATT_SUCCESS ? " complete" : " failed"));
} }
@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status != BluetoothGatt.GATT_SUCCESS) { gatt.disconnect(); LogUtil.d(String.format(Locale.ENGLISH, "WriteChar error status %d", status)); } } }
}
#
2.6 业务功能智汀家庭云Android端,支持对智慧中心(SA)、智能设备的发现及控制。
#
2.6.1 扫描发现智慧中心(SA)智慧中心的发现主要流程为:
1.在局域网中广播hello包;
2.对响应设备进行点对点通信获取加密后的token;
3.利用解密后的token加密用于对设备通讯的消息和解密设备的响应。
具体协议请参照硬件协议文档,下面是主要代码实现:
/** * 发现设备 */public class ScanDevice2Activity extends MVPBaseActivity<AddDeviceContract.View, AddDevicePresenter> implements AddDeviceContract.View { ... /** * 处理udp发现的设备 * * @param address * @param port * @param data * @param length */ private void scanDeviceSuccessByUDP(String address, int port, byte[] data, int length) { byte[] deviceIdData = Arrays.copyOfRange(data, 6, 12); // 设备id try { String deviceId = ByteUtil.bytesToHex(deviceIdData); if (scanMap.containsKey(deviceId)) { // 已经获取到hello数据包信息 ScanDeviceByUDPBean sdb = scanMap.get(deviceId); String token = sdb.getToken(); byte[] dealData = Arrays.copyOfRange(data, 32, length); if (TextUtils.isEmpty(token)) { // 没有获取过token信息 String key = sdb.getPassword(); String decryptKeyMD5 = Md5Util.getMD5(key); byte[] decryptKeyDta = ByteUtil.md5Str2Byte(decryptKeyMD5); byte[] ivData = ByteUtil.byteMergerAll(decryptKeyDta, key.getBytes()); byte[] ivEncryptedData = Md5Util.getMD5(ivData); String tokenFromServer = AESUtil.decryptAES(dealData, decryptKeyDta, ivEncryptedData, AESUtil.PKCS7, true); if (!TextUtils.isEmpty(tokenFromServer)) { sdb.setToken(tokenFromServer); sdb.setId(sendId); String deviceStr = "{\"method\":\"get_prop.info\",\"params\":[],\"id\":" + sendId + "}"; // 获取设备信息体 sendId++; byte[] bodyData = AESUtil.encryptAES(deviceStr.getBytes(), tokenFromServer, AESUtil.PKCS7); // 获取设备信息体转字节加密 int len = bodyData.length + 32; // 包长 byte[] lenData = ByteUtil.intToByte2(len); // 包长用占两位字节 byte[] headData = {(byte) 0x21, (byte) 0x31}; // 包头固定 byte[] preData = {(byte) 0xFF, (byte) 0xFF}; // 预留固定 byte[] serData = {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}; // 序列号固定 byte[] tokenData = sdb.getPassword().getBytes(); // 之前获取设备信息时生成的16位随机密码 byte[] getDeviceInfoData = ByteUtil.byteMergerAll(headData, lenData, preData, deviceIdData, serData, tokenData, bodyData); // 拼接获取设备信息包 System.out.println(address + "获取设备信息发送数据:" + Arrays.toString(getDeviceInfoData)); udpSocket.sendMessage(getDeviceInfoData, address); } } else { // 获取过token信息 GetDeviceInfoBean deviceInfoBean = sdb.getDeviceInfoBean(); byte[] decryptDeviceData = Md5Util.getMD5(ByteUtil.md5Str2Byte(token)); byte[] ivDeviceData = ByteUtil.byteMergerAll(decryptDeviceData, ByteUtil.md5Str2Byte(token)); byte[] ivEncryptedDeviceData = Md5Util.getMD5(ivDeviceData); System.out.println("设备信息字节:" + Arrays.toString(dealData)); System.out.println("=========处理数据长度:" + dealData.length + " " + length); String infoJson = AESUtil.decryptAES(dealData, decryptDeviceData, ivEncryptedDeviceData, AESUtil.PKCS7, false); System.out.println("设备信息:" + infoJson); GetDeviceInfoBean getDeviceInfoBean = new Gson().fromJson(infoJson, GetDeviceInfoBean.class);
if (deviceInfoBean == null && getDeviceInfoBean != null && sdb.getId() == getDeviceInfoBean.getId()) { GetDeviceInfoBean.ResultBean gdifb = getDeviceInfoBean.getResult(); sdb.setDeviceInfoBean(getDeviceInfoBean); DeviceBean deviceBean = new DeviceBean(); deviceBean.setAddress(sdb.getHost()); if (gdifb != null) { deviceBean.setPort(gdifb.getPort()); String model = gdifb.getModel(); if (!TextUtils.isEmpty(model) && model.equals(Constant.SMART_ASSISTANT)) { // 如果时sa设备 deviceBean.setType(Constant.DeviceType.TYPE_SA); deviceBean.setModel(gdifb.getModel()); deviceBean.setName(gdifb.getSa_id()); deviceBean.setSa_id(gdifb.getSa_id()); deviceBean.setSwVersion(gdifb.getSw_ver()); checkIsBind(deviceBean); } } } } } else { // 获取到hello数据包信息 ScanDeviceByUDPBean scanDeviceByUDPBean = new ScanDeviceByUDPBean(address, port, deviceId); String password = StringUtil.getUUid().substring(0,16); scanDeviceByUDPBean.setPassword(password); scanMap.put(deviceId, scanDeviceByUDPBean); getDeviceToken(address, data, password); } } catch (Exception e) { e.printStackTrace(); } }
/** * 检查通过udp发现的sa是否已被绑定 */ private void checkIsBind(DeviceBean deviceBean) { HttpConfig.clearHear(HttpConfig.AREA_ID); String url = Constant.HTTP_HEAD + deviceBean.getAddress() + ":" + deviceBean.getPort() + HttpUrlConfig.API + HttpUrlParams.checkBindSa; HTTPCaller.getInstance().get(CheckBindSaBean.class, url, new RequestDataCallback<CheckBindSaBean>() { @Override public void onSuccess(CheckBindSaBean obj) { super.onSuccess(obj); if (obj != null) { deviceBean.setBind(obj.isIs_bind()); mScanLists.add(deviceBean); addDeviceAdapter.notifyDataSetChanged(); setHasDeviceStatus(); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); LogUtil.e(errorMessage); } }); }
/** * 获取设备token * * @param address * @param data */ private void getDeviceToken(String address, byte[] data, String password) { if (!updAddressSet.contains(address)) { updAddressSet.add(address); byte[] tokenHeadData = {(byte) 0x21, (byte) 0x31, (byte) 0x00, (byte) 0x20, (byte) 0xFF, (byte) 0xFE}; // 包头,包长,预留字节固定 byte[] deviceIdData = Arrays.copyOfRange(data, 6, 12); // 设备id byte[] passwordData = password.getBytes(); // 密码 byte[] serData = {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}; // 序列号 固定 byte[] tokenData = ByteUtil.byteMergerAll(tokenHeadData, deviceIdData, serData, passwordData); // 拼接获取token数据包 udpSocket.sendMessage(tokenData, address); } } ...}
#
2.6.2 SA发现智能设备SA扫描发现设备:
/** * 发现设备 */public class ScanDevice2Activity extends MVPBaseActivity<AddDeviceContract.View, AddDevicePresenter> implements AddDeviceContract.View { ... /** * WebSocket初始化、添加监听 */ private void initWebSocket() { mIWebSocketListener = new IWebSocketListener() { @Override public void onMessage(WebSocket webSocket, String text) { if (!TextUtils.isEmpty(text)) { ScanResultBean scanBean = GsonConverter.getGson().fromJson(text, ScanResultBean.class); if (scanBean != null && scanBean.getResult() != null && scanBean.getResult().getDevice() != null) { DeviceBean bean = scanBean.getResult().getDevice(); mScanLists.add(bean); addDeviceAdapter.notifyDataSetChanged(); setHasDeviceStatus(); } } } @Override public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) { } }; WSocketManager.getInstance().addWebSocketListener(mIWebSocketListener); startScanDevice(); } ... /** * 开始发现设备 */ private void startScanDevice() { tvStatus.setText(UiUtil.getString(R.string.mine_home_scanning)); FindDeviceBean findDeviceBean = new FindDeviceBean(); findDeviceBean.setDomain("plugin"); findDeviceBean.setService("discover"); findDeviceBean.setId(mFindDeviceId); String findDeviceJson = GsonConverter.getGson().toJson(findDeviceBean); UiUtil.postDelayed(() -> WSocketManager.getInstance().sendMessage(findDeviceJson), 1000); mFindDeviceId++; } ...}
添加智能设备:
/** * 添加设备 */public class DeviceConnectPresenter extends BasePresenterImpl<DeviceConnectContract.View> implements DeviceConnectContract.Presenter { ... @Override public void addDevice(DeviceBean bean) { if (model!=null) model.addDevice(bean, new RequestDataCallback<AddDeviceResponseBean>() { @Override public void onSuccess(AddDeviceResponseBean obj) { super.onSuccess(obj); if (mView != null) { mView.addDeviceSuccess(obj); } } @Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (mView != null) { mView.addDeviceFail(errorCode, errorMessage); } } }); } ...}
#
2.6.3 智能设备置网#
2.7 业务功能:场景篇#
2.7.1 添加场景和编辑场景/** * 创建修改场景重构 */public class SceneDetailActivity extends MVPBaseActivity<SceneDetailContract.View, SceneDetailPresenter> implements SceneDetailContract.View{ ... /** * 完成 */ private void complete() { isDel = false; String name = etName.getText().toString().trim(); if (TextUtils.isEmpty(name)) { ToastUtil.show(getResources().getString(R.string.scene_name_not_empty)); return; } if (CollectionUtil.isEmpty(sceneConditionData)) { ToastUtil.show(getResources().getString(R.string.scene_please_add_condition)); return; } if (CollectionUtil.isEmpty(sceneTaskData)) { ToastUtil.show(getResources().getString(R.string.scene_please_add_perform_task)); return; }
if (sceneDetailEntity == null){ // 如果sceneDetailEntity如果为null则创建场景 sceneDetailEntity = new SceneDetailEntity(); } sceneDetailEntity.setName(name); sceneDetailEntity.setAuto_run(auto_run); if (auto_run) { sceneDetailEntity.setCondition_logic(allConditionSubmit ? 1 : 2); sceneDetailEntity.setTime_period(time_period); sceneDetailEntity.setEffect_start_time(effect_start_time); sceneDetailEntity.setEffect_end_time(effect_end_time); sceneDetailEntity.setRepeat_type(repeat_type); sceneDetailEntity.setRepeat_date(repeat_date); }
if (CollectionUtil.isNotEmpty(del_condition_ids)){ // 已删除的条件 sceneDetailEntity.setDel_condition_ids(del_condition_ids); } if (CollectionUtil.isNotEmpty(del_task_ids)){ // 已删除的任务 sceneDetailEntity.setDel_task_ids(del_task_ids); } if (auto_run){ for (SceneConditionEntity sceneConditionEntity: sceneConditionData){ SceneConditionAttrEntity sceneConditionAttrEntity = sceneConditionEntity.getCondition_attr(); if (sceneConditionAttrEntity!=null) { Object object = sceneConditionAttrEntity.getVal(); if (object != null && object instanceof Double) { sceneConditionAttrEntity.setVal(Math.round((Double) object)); } } } } sceneDetailEntity.setScene_conditions(auto_run ? sceneConditionData : null);
for (SceneTaskEntity sceneTaskEntity : sceneTaskData){ if (CollectionUtil.isNotEmpty( sceneTaskEntity.getAttributes())) { for (SceneConditionAttrEntity sceneConditionAttrEntity : sceneTaskEntity.getAttributes()) { if (sceneConditionAttrEntity!=null) { Object object = sceneConditionAttrEntity.getVal(); if (object != null && object instanceof Double) { sceneConditionAttrEntity.setVal(Math.round((Double) object)); } } } } } sceneDetailEntity.setScene_tasks(sceneTaskData); String body = GsonConverter.getGson().toJson(sceneDetailEntity); if (sceneId>0){ // 修改场景 mPresenter.modifyScene(sceneId, body); }else { // 创建场景 mPresenter.createScene(body); } tvFinish.setText(getResources().getString(R.string.scene_saving)); rbFinish.setVisibility(View.VISIBLE); llFinish.setEnabled(false); } ...}
#
2.7.2 场景控制场景的控制,包括手动场景的执行、自动场景的开启/关闭。
/** * 首页-场景 */public class SceneFragment extends MVPBaseFragment<SceneFragmentContract.View, SceneFragmentPresenter> implements SceneFragmentContract.View, ISceneView { ... /** * 手动 */ private void initRvManual() { rvManual.setLayoutManager(new LinearLayoutManager(getContext())); manualAdapter = new SceneAdapter(0); rvManual.setAdapter(manualAdapter);
manualAdapter.setOnItemClickListener((adapter, view, position) -> { SceneBean sceneBean = manualAdapter.getItem(position); if ((WSocketManager.isConnecting && HomeUtil.isSAEnvironment()) || UserUtils.isLogin()) { if (hasUpdateP) { // 有权限才进 if (HomeUtil.notLoginAndSAEnvironment()) { return; } Bundle bundle = new Bundle(); bundle.putInt(IntentConstant.ID, sceneBean.getId()); bundle.putBoolean(IntentConstant.REMOVE_SCENE, hasDelP); switchToActivity(SceneDetailActivity.class, bundle); } else { ToastUtil.show(getResources().getString(R.string.scene_no_modify_permission)); } } }); manualAdapter.setOnItemChildClickListener((adapter, view, position) -> { if (view.getId() == R.id.tvPerform) { // 执行 if (manualAdapter.getItem(position).isControl_permission()) { type = 0; manualAdapter.getItem(position).setPerforming(true); manualAdapter.notifyItemChanged(position); mPresenter.perform(manualAdapter.getItem(position).getId(), true); } else { ToastUtil.show(getResources().getString(R.string.scene_no_control_permission)); } } }); }
/** * 自动 */ private void initRvAutomatic() { rvAutomatic.setLayoutManager(new LinearLayoutManager(getContext())); automaticAdapter = new SceneAdapter(1); rvAutomatic.setAdapter(automaticAdapter); automaticAdapter.setOnItemClickListener((adapter, view, position) -> { SceneBean sceneBean = automaticAdapter.getItem(position); if (WSocketManager.isConnecting) { if (hasUpdateP) { // 有权限才进 if (HomeUtil.notLoginAndSAEnvironment()) { return; } Bundle bundle = new Bundle(); bundle.putInt(IntentConstant.ID, sceneBean.getId()); bundle.putBoolean(IntentConstant.REMOVE_SCENE, hasDelP); switchToActivity(SceneDetailActivity.class, bundle); } else { ToastUtil.show(getResources().getString(R.string.scene_no_modify_permission)); } } }); automaticAdapter.setOnItemChildClickListener((adapter, view, position) -> { if (view.getId() == R.id.ivSwitch) { // 无权限开关 ToastUtil.show(getResources().getString(R.string.scene_no_control_permission)); } else if (view.getId() == R.id.llSwitch) { // 开关 SceneBean sceneBean = automaticAdapter.getItem(position); sceneBean.setIs_on(!sceneBean.isIs_on()); sceneBean.setPerforming(true); automaticAdapter.notifyItemChanged(position); type = 1; checked = sceneBean.isIs_on(); mPresenter.perform(sceneBean.getId(), sceneBean.isIs_on()); } }); } ... /** * 执行成功 */ @Override public void performSuccess() { String tip = UiUtil.getString(R.string.scene_perform_success); if (type == 1) { tip = checked ? UiUtil.getString(R.string.scene_perform_has_been_opened) : UiUtil.getString(R.string.scene_perform_has_been_closed); } getData(true); ToastUtil.show(tip); }
/** * 执行失败 * * @param errorCode * @param msg */ @Override public void performFail(int errorCode, String msg) { if (errorCode == 5012) {// showRemovedTipsDialog(); findSAToken(); } else { ToastUtil.show(msg); } } ...}
#
2.7.3 删除场景/** * 创建修改场景重构 */public class SceneDetailActivity extends MVPBaseActivity<SceneDetailContract.View, SceneDetailPresenter> implements SceneDetailContract.View{ ... /** * 删除场景确认弹窗 */ private void iniDelDialog(){ delDialog = CenterAlertDialog.newInstance(getResources().getString(R.string.scene_remove), null, true); delDialog.setConfirmListener(new CenterAlertDialog.OnConfirmListener() { @Override public void onConfirm(boolean del) { isDel = true; mPresenter.delScene(sceneId);
} }); } ... /**************************** 点击事件 ********************************/ @OnClick({R.id.ivBack, R.id.llCondition, R.id.llTask, R.id.tvConditionType, R.id.ivAdd, R.id.clTimePeriod, R.id.ivTaskAdd, R.id.llFinish, R.id.tvRemove}) void onClick(View view){ switch (view.getId()){ ...
case R.id.tvRemove: // 删除 if (delDialog!=null && !delDialog.isShowing()){ delDialog.show(this); } break;
} }}
#
2.8 业务功能:Web端专业版集成专业版通过JS代码注入WebView实现交互。
#
2.8.1 WebView的初始化 public static final String ZHITING_APP = "zhitingApp";//js交互别名 public static final String ZHITING_USER_AGENT = "zhitingua";//webview设置代理后缀 @SuppressLint("SetJavaScriptEnabled") public void initWebView(WebView webView) { webView.setHorizontalScrollBarEnabled(false);// 滚动条水平不显示 webView.setVerticalScrollBarEnabled(false); // 垂直不显示 webView.getSettings().setJavaScriptEnabled(true);// 允许 JS的使用 webView.getSettings().setAllowFileAccess(false); //修复file域漏洞 webView.getSettings().setAllowFileAccessFromFileURLs(false); webView.getSettings().setAllowUniversalAccessFromFileURLs(false); webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);// 可以使用JS打开新窗口 webView.getSettings().setUseWideViewPort(true); webView.getSettings().setSupportZoom(true); webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); webView.getSettings().setBuiltInZoomControls(true); webView.getSettings().setLoadWithOverviewMode(true); webView.getSettings().setLoadsImagesAutomatically(true); webView.getSettings().setDisplayZoomControls(false); webView.getSettings().setNeedInitialFocus(false); webView.getSettings().setDefaultTextEncodingName("UTF-8"); webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);// 解决缓存问题 webView.setWebViewClient(new MyWebViewClient()); webView.setWebChromeClient(new BankWebChromeClient()); webView.getSettings().setUserAgentString(ua + "; " + Constant.ZHITING_USER_AGENT); webView.addJavascriptInterface(new JsInterface(), Constant.ZHITING_APP); }
#
2.8.2 JS代码的注入在自定义MyWebViewClient回调onPageStarted注入以下代码:
/** * 专业版js代码 */ public static String professional_js = "var zhiting = {\n" + "\n" + " invoke: function (funcName, params, callback) {\n" + " var message;\n" + " var timeStamp = new Date().getTime();\n" + " var callbackID = funcName + '_' + timeStamp + '_' + 'callback';\n" + " \n" + " if (callback) {\n" + " if (!WKBridgeEvent._listeners[callbackID]) {\n" + " WKBridgeEvent.addEvent(callbackID, function (data) {\n" + "\n" + " callback(data);\n" + "\n" + " });\n" + " }\n" + " }\n" + "\n" + "\n" + "\n" + " if (callback) {\n" + " message = { 'func': funcName, 'params': params, 'callbackID': callbackID };\n" + "\n" + " } else {\n" + " message = { 'func': funcName, 'params': params };\n" + "\n" + " }\n" + " zhitingApp.entry(JSON.stringify(message));\n" + " },\n" + "\n" + " callBack: function (callBackID, data) {\n" + " WKBridgeEvent.fireEvent(callBackID, data);\n" + " WKBridgeEvent.removeEvent(callBackID);\n" + " },\n" + "\n" + " removeAllCallBacks: function (data) {\n" + " WKBridgeEvent._listeners = {};\n" + " }\n" + "\n" + " };\n" + "\n" + "\n" + "\n" + "\n" + " var WKBridgeEvent = {\n" + "\n" + " _listeners: {},\n" + "\n" + " addEvent: function (callBackID, fn) {\n" + " this._listeners[callBackID] = fn;\n" + " return this;\n" + " },\n" + "\n" + "\n" + " fireEvent: function (callBackID, param) {\n" + " var fn = this._listeners[callBackID];\n" + " if (typeof callBackID === \"string\" && typeof fn === \"function\") {\n" + " fn(JSON.parse(param));\n" + " } else {\n" + " delete this._listeners[callBackID];\n" + " }\n" + " return this;\n" + " },\n" + "\n" + " removeEvent: function (callBackID) {\n" + " delete this._listeners[callBackID];\n" + " return this;\n" + " }\n" + " };";}
#
2.8.3 WebView 打开App选择图片- js 给予一个点击事件,类型是文件;在WebView>WebChromeClient接收到onShowFileChooser;然后打开文件,代码如下:
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); startActivityForResult(intent, Constant.OPEN_FILE);
- 在onActivityResult回调得到数据,调用js方法发送网页
public static void seleteH5Phone(Intent data, ValueCallback valueCallback) { if (valueCallback == null) { // todo valueCallback 为空的逻辑 return; } try { Uri[] results = null; String dataString = data.getDataString(); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { ClipData clipData = data.getClipData(); if (clipData != null) { results = new Uri[clipData.getItemCount()]; for (int i = 0; i < clipData.getItemCount(); i++) { ClipData.Item item = clipData.getItemAt(i); results[i] = item.getUri(); } } }
if (dataString != null) { results = new Uri[]{Uri.parse(dataString)}; valueCallback.onReceiveValue(results); } } catch (Exception e) { e.printStackTrace(); } valueCallback = null; }
#
2.8.4 JS 调用 Android class JsInterface { @SuppressLint("JavascriptInterface") @JavascriptInterface public void entry(String json) { JsBean jsBean = new Gson().fromJson(json, JsBean.class); switch (jsBean.getFunc()) { case JsMethodConstant.NETWORK_TYPE: // 网络类型 networkType(jsBean); break;
case JsMethodConstant.GET_USER_INFO: // 用户信息 getUserInfo(jsBean); break;
case JsMethodConstant.SET_TITLE: // 标题属性 setTitle(jsBean); break;
case JsMethodConstant.IS_PROFESSION: // 是否专业版 isProfession(jsBean); break; } } }
#
2.8.5 Android 调用 js
/** * 网络类型 * * @param jsBean */ void networkType(JsBean jsBean) { String js = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{}')"; runOnMainUi(js); }
/** * 用户信息 * * @param jsBean */ void getUserInfo(JsBean jsBean) { String js = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{\"userId\":" + Constant.CurrentHome.getUser_id() + ",\"token\":\"" + Constant.CurrentHome.getSa_user_token() + "\"}')"; runOnMainUi(js); }
/** * 设置标题 * * @param jsBean */ void setTitle(JsBean jsBean) { String js = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{}')"; runOnMainUi(js); }
/** * 是否专业版 * * @param jsBean */ void isProfession(JsBean jsBean) { String js = "zhiting.callBack('" + jsBean.getCallbackID() + "'," + "'{\"result\":true}')"; runOnMainUi(js); }
private void runOnMainUi(String js) { UiUtil.runInMainThread(() -> webView.loadUrl("javascript:" + js)); }
#
2.9 业务扩展:活动情景篇#
2.9.1 活动情景分析从上图中,我们可以知道iOS终端的活动情景支持3大类:
- 终端存储
- 室内智慧中心(以下简称SA)请求
- 室外智汀云(以下简称SC)中转
本篇章的目的在于解决以下三大问题:
- 解决存储数据同步问题
- 明确在什么条件下请求哪个服务端
- 各种活动场景下的数据同步逻辑
#
2.9.2 如何解决存储数据同步问题?在解决这个问题之前,我们先回忆一下之前篇章介绍的,iOS终端的本地数据库采用的是realm数据库,表设计结构如下:
realm本地数据库
表名 | 描述 |
---|---|
zhiting_home_company | 家庭/公司表 |
zhiting_location | 房间/区域表 |
zhiting_devices | 设备表 |
zhiting_scene | 场景表 |
zhiting_user_info | 用户表 |
-附录: Android数据库设计文档
上述表设计中,体现以下几大功能数据的存储:
- 家庭/公司
- 房间/区域
- 设备
- 场景
- 个人信息
数据同步如下图示:
#
2.9.3 根据当前环境使用服务端属性名称 | 代码属性 | 描述 |
---|---|---|
家庭的Id | HomeCompanyBean.id | id=0 表示本地家庭 |
家庭的SA绑定状态 | HomeCompanyBean.is_bind_sa | 判断当前家庭是否绑定SA |
家庭的SA授权 | HomeCompanyBean.sa_user_token | 访问SA服务器的权限认证key |
家庭的SA的WiFi名称 | HomeCompanyBean.ssid | 访问SA服务器的WiFi名称 |
家庭的SA地址 | HomeCompanyBean.sa_lan_address | 访问SA服务器的Host地址;如果SC返回列表中该字段为空,即存在两种可能:1. SA未绑定 2. 未触发SA同步数据至SC |
家庭的MAC地址 | HomeCompanyBean.macAddr | 当前家庭已绑SA的网络环境,判断是否同个局域网的依据 |
家庭归属SC账号Id | HomeCompanyBean.cloud_user_id | 目的是判断当前家庭是否归属当前登录SC账号 |
是否SC登录 | UserUtils.isLogin() | 判断当前用户(已绑定SC)是否登录,触发APP端与SC数据同步的依据 |
#
2.9.4 终端存储(本地)条件:HomeCompanyBean.is_bind_sa = false && UserUtils.isLogin = false
以下操作本地数据库存储:
- 家庭/公司新增
- 家庭/公司修改、删除 (符合判断条件)
- 家庭/公司(符合判断条件)下房间区域新增、修改、删除
- 个人信息修改
#
2.9.5 室内SA请求条件:HomeCompanyBean.mac_address = 当前WiFi环境的bssid
API请求Header携带以下参数:
参数 | 值 |
---|---|
smart-assistant-token | HomeCompanyBean.sa_user_token |
#
2.9.6 室外SC中转条件:UserUtils.isLogin() && HomeUtil.getHomeId() > 0&&!HomeUtil.isSAEnvironment()
public class HomeUtil { private static HomeCompanyBean mHome = new HomeCompanyBean();
public static boolean tokenIsInvalid; // saToken是否失效
//获取家庭名字 public static String getHomeName() { mHome = Constant.CurrentHome; if (mHome != null && !TextUtils.isEmpty(mHome.getName())) { return mHome.getName(); } return ""; }
//获取家庭id public static long getHomeId() { mHome = Constant.CurrentHome; if (mHome != null) { return mHome.getId(); } return 0; }
//获取家庭SaToken public static String getSaToken() { mHome = Constant.CurrentHome; if (mHome != null && !TextUtils.isEmpty(mHome.getSa_user_token())) { return mHome.getSa_user_token(); } return ""; }
/** * 判断家庭是否有id,云端有虚拟sa * * @return */ public static boolean isHomeIdThanZero() { mHome = Constant.CurrentHome; if (mHome == null) return false; return (mHome.getId() > 0 && UserUtils.isLogin()) || mHome.isIs_bind_sa(); }
/** * 判断是否有SA * * @return */ public static boolean isBindSA() { mHome = Constant.CurrentHome; if (mHome == null) return false; return mHome.isIs_bind_sa(); }
/** * 用户user_id * * @return */ public static int getUserId() { mHome = Constant.CurrentHome; if (mHome != null) return mHome.getUser_id(); return -1; }
//判断当前的家庭是否SA环境 public static boolean isSAEnvironment() { return isSAEnvironment(Constant.CurrentHome); }
//判断当前的家庭是否SA环境 public static boolean isSAEnvironment(HomeCompanyBean home) { WifiInfo wifiInfo = Constant.wifiInfo; if (home != null && wifiInfo != null && home.getMac_address() != null && wifiInfo.getBSSID() != null && home.getMac_address().equalsIgnoreCase(wifiInfo.getBSSID())) { return true; } return false; }
public static boolean notLoginAndSAEnvironment() { if (UserUtils.isLogin()) { // 登录了 return false; } else { // 没登录 return !HomeUtil.isSAEnvironment(); // 是否在sa } }}
package com.yctc.zhiting.utils;
import android.text.TextUtils;
import com.app.main.framework.baseutil.SpConstant;import com.app.main.framework.baseutil.SpUtil;import com.app.main.framework.gsonutils.GsonConverter;import com.yctc.zhiting.config.Constant;import com.yctc.zhiting.entity.mine.MemberDetailBean;
/** * date : 2021/7/7 11:15 * desc : */public class UserUtils {
/** * 保存用户信息 * * @param user */ public static void saveUser(MemberDetailBean user) { if (user == null) { SpUtil.put(Constant.CLOUD_USER, ""); SpUtil.put(SpConstant.CLOUD_USER_ID, 0); } else { SpUtil.put(SpConstant.CLOUD_USER_ID, user.getUser_id()); SpUtil.put(Constant.CLOUD_USER, GsonConverter.getGson().toJson(user)); } }
/** * 获取云用户id * * @return */ public static int getCloudUserId() { MemberDetailBean user = getUser(); if (user != null) return user.getUser_id(); return 0; }
/** * 用户昵称 * * @return */ public static String getCloudUserName() { MemberDetailBean user = getUser(); if (user != null) return user.getNickname(); return ""; }
/** * 更新名字 * * @param name */ public static void setCloudUserName(String name) { MemberDetailBean user = getUser(); if (user != null) { user.setNickname(name); SpUtil.put(Constant.CLOUD_USER, GsonConverter.getGson().toJson(user)); } }
/** * 用户手机 * * @return */ public static String getPhone() { MemberDetailBean user = getUser(); if (user != null) return user.getPhone(); return ""; }
/** * 判断是否登陆状态 * * @return */ public static boolean isLogin() { return getCloudUserId() > 0; }
private static MemberDetailBean getUser() { String userJson = SpUtil.get(Constant.CLOUD_USER); if (TextUtils.isEmpty(userJson)) { return null; } else { MemberDetailBean user = GsonConverter.getGson().fromJson(userJson, MemberDetailBean.class); return user; } }}
API请求Header携带以下参数:
参数 | 值 |
---|---|
smart-assistant-token | Area.sa_user_token |
Area-ID | Area.id |
附:专属于智汀云的接口
环境域名:https://sc.zhitingtech.com
接口名称 | Path | Method | 描述 |
---|---|---|---|
登录 | /sessions/login | POST | 登录智汀云 |
绑定云 | /users | POST | 智汀云注册用户账号 |
获取验证码 | /captcha | GET | 获取绑定云中的验证码 |
获取家庭/公司列表 | /areas | GET | 获取当前账号在智汀云关联的家庭/公司列表 |
#
2.9.7 各种活动情景下的数据同步逻辑我们清晰了情景的3大类,3大类很好的概述了家庭/公司在上述条件中的处理逻辑。
那么对于家庭/公司添加SA或者扫码加入家庭的活动中,存在着更多小分类活动,我们先根据下列表格,了解一下:
#
2.9.8 小类活动情景名词解释:室内:与SA在同个局域网环境内 室外:与SA不在同个局域网环境内
序号 | 活动步骤 前置条件:家庭未绑定SA、未绑定SC | 绑定云 | 登录云 | 绑定SA | 扫码加入 | ||
---|---|---|---|---|---|---|---|
室内 | 室外 | 室内 | 室内 | 室外 | |||
1. | 家庭无关联SA、SC情况 | -- | -- | -- | -- | -- | -- |
2. | 家庭绑定SA且无关联SC 步骤1:家庭绑定SA | -- | -- | -- | step1 | -- | -- |
3. | 室内扫码加入SA家庭且无关联SC 步骤1:扫描加入SA家庭 | -- | -- | -- | -- | step1 | -- |
4. | 在登录SC后绑定SA 步骤1:绑定SC 步骤2:登录SC 步骤3:室内绑定SA | step1 | step2 | step3 | -- | -- | |
5. | 退出SC登录状态下绑定SA,室内再登录SC 步骤1:绑定SC 步骤2:室内绑定SA 步骤3:室内登录SC | step1 | step3 | -- | step2 | -- | -- |
6. | 退出SC登录状态下绑定SA,室外再登录SC 步骤1:绑定SC 步骤2:室内绑定SA 步骤3:室外登录SC | step1 | -- | step3 | step2 | -- | -- |
7. | 空SC用户状态下绑定SA, 然后绑定SC, 室内登录SC 步骤1:室内绑定SA 步骤2:绑定SC 步骤3:室内登录SC | step2 | step3 | -- | step1 | -- | -- |
8. | 空SC用户状态下绑定SA, 然后绑定SC, 室外登录SC 步骤1:室内绑定SA 步骤2:绑定SC 步骤3:室外登录SC | step2 | -- | step3 | step1 | -- | -- |
9. | 已登录SC状态下室内扫码加入SA家庭 步骤1:绑定SC 步骤2:登录SC 步骤3:室内扫码加入SA家庭 | step1 | step2 | -- | step3 | -- | |
10. | 已登录SC状态下室外扫码加入SA家庭 步骤1:绑定SC 步骤2:登录SC 步骤3:室外扫码加入SA家庭 | step1 | step2 | -- | -- | step3 | |
11. | 退出SC登录状态下室内扫码加入SA家庭,然后室内登录SC 步骤1:绑定SC 步骤2:室内扫码加入SA家庭 步骤3:室内登录SC | step1 | step3 | -- | -- | step2 | -- |
12. | 退出SC登录状态下室内扫码加入SA家庭,然后室外登录SC 步骤1:绑定SC 步骤2:室内扫码加入SA家庭 步骤3:室外登录SC | step1 | -- | step3 | -- | step2 | -- |
13. | 退出SC登录状态下室外扫码加入SA家庭,然后室内登录SC 步骤1:绑定SC 步骤2:室外扫码加入SA家庭 步骤3:室内登录SC | step1 | step3 | -- | -- | -- | step2 |
14. | 退出SC登录状态下室外扫码加入SA家庭,然后室外登录SC 步骤1:绑定SC 步骤2:室外扫码加入SA家庭 步骤3:室外登录SC | step1 | -- | step3 | -- | -- | step2 |
15. | 空SC用户状态下室内扫码加入SA家庭, 然后绑定SC, 室内登录SC 步骤1:室内扫码加入SA家庭 步骤2:绑定SC 步骤3:室内登录SC | step2 | step3 | -- | -- | step1 | -- |
16. | 空SC用户状态下室内扫码加入SA家庭, 然后绑定SC, 室外登录SC 步骤1:室内扫码加入SA家庭 步骤2:绑定SC 步骤3:室外登录SC | step2 | -- | step3 | -- | step1 | -- |
#
2.9.9 小类活动情景分析上述的各种活动情景,主要产生的问题在于Android终端、SA、SC的数据同步,体现在家庭数据上。
我们先回顾一下家庭表的设计,跟上述活动情景息息相关的字段如下:
名称 | 类型 | 是否null | 注释 |
---|---|---|---|
id | integer | 否 | id |
sa_user_id | integer | 用户Id | |
sa_user_token | string | 用户SA认证授权 | |
is_bind_sa | bool | 否 | 家庭/公司是否绑定SA |
ssid | string | sa的wifi名称 | |
sa_lan_address | string | sa的地址 | |
macAddr | string | sa的mac地址 | |
cloud_user_id | integer | 云端用户的user_id |
1)在数据同步至SC时,以下字段获得赋值:
HomeCompanyBean.id
HomeCompanyBean.cloud_user_id
2) 在绑定SA、扫描加入SA家庭(室内/室外)时,以下字段获得赋值:
HomeCompanyBean.sa_user_id
HomeCompanyBean.sa_user_token
HomeCompanyBean.is_bind_sa
HomeCompanyBean.sa_lan_address
3)在室内连接上SA时,以下字段获得赋值:
HomeCompanyBean.ssid
HomeCompanyBean.mac_address
综上,Android端的处理思路就可以清晰的梳理为:
- 登录事件处理
- 登录状态获取家庭列表事件处理(考虑多终端操作家庭数据同步)
- 绑定SA事件处理
- 扫码加入SA家庭事件处理
- 家庭列表切换家庭事件处理(需要对选中家庭的网络判断)
- 网络环境变化事件处理
#
2.9.10 16小类活动情景处理方法1)登录事件处理
请求服务器:SC服务器
处理逻辑:登录成功获取SC.user_id
(1). 清空非归属当前登录账号的家庭及相关信息
条件筛选:HomeCompanyBean.cloud_user_id > 0 && HomeCompanyBean.cloud_user_id != SC.user_id
(2). 获取SC的家庭列表信息,并同步到本地数据库存储
条件筛选:SC.HomeCompanyBean.id not in HomeCompanyBean.id
(3). 同步本地待同步的家庭信息至SC
情况1:请求SA将当前SA环境的家庭绑定到SC
条件筛选: HomeCompanyBean.id == 0 && HomeCompanyBean.is_bind_sa == true && HomeCompanyBean.mac_address = 当前WiFi环境的bssid
情况2:其余待同步数据直接同步至SC
条件筛选:HomeCompanyBean.id == 0
2)绑定SA事件处理
请求服务器:SA服务器
处理逻辑:
(1). 本地当前家庭/公司信息、房间/区域信息、用户信息同步至SA服务器
(2). 新增本地家庭信息(绑定SA相关信息、网络相关信息),清空同步前该家庭相关数据
(3). 已登录SC情况下,请求SA将当前SA环境的家庭绑定到SC
条件筛选:SC.HomCompanyBean.id not in HomCompanyBean.id
(2). 本地存储移除SC已删除的家庭信息
条件筛选:HomeCompanyBean.id not in SC.HomeCompanyBean.id
(3). 请求SA将当前SA环境的家庭绑定到SC
工具类代码如下:
package com.yctc.zhiting.utils;
import android.content.Context;import android.database.Cursor;import android.text.TextUtils;import android.util.Log;
import androidx.fragment.app.FragmentActivity;
import com.app.main.framework.baseutil.LibLoader;import com.app.main.framework.baseutil.LogUtil;import com.app.main.framework.baseutil.SpUtil;import com.app.main.framework.baseutil.UiUtil;import com.app.main.framework.baseutil.toast.ToastUtil;import com.app.main.framework.dialog.CertificateDialog;import com.app.main.framework.entity.ChannelEntity;import com.app.main.framework.gsonutils.GsonConverter;import com.app.main.framework.httputil.HTTPCaller;import com.app.main.framework.httputil.Header;import com.app.main.framework.httputil.HttpResponseHandler;import com.app.main.framework.httputil.NameValuePair;import com.app.main.framework.httputil.RequestDataCallback;import com.app.main.framework.httputil.SSLSocketClient;import com.app.main.framework.httputil.TempChannelUtil;import com.app.main.framework.httputil.comfig.HttpConfig;import com.app.main.framework.httputil.cookie.CookieJarImpl;import com.app.main.framework.httputil.cookie.PersistentCookieStore;import com.app.main.framework.httputil.log.LoggerInterceptor;import com.google.gson.Gson;import com.yctc.zhiting.R;import com.yctc.zhiting.application.Application;import com.yctc.zhiting.config.Constant;import com.yctc.zhiting.config.HttpUrlConfig;import com.yctc.zhiting.config.HttpUrlParams;import com.yctc.zhiting.db.DBManager;import com.yctc.zhiting.entity.AreaIdBean;import com.yctc.zhiting.entity.home.AccessTokenBean;import com.yctc.zhiting.entity.home.SynPost;import com.yctc.zhiting.entity.mine.HomeCompanyBean;import com.yctc.zhiting.entity.mine.HomeCompanyListBean;import com.yctc.zhiting.entity.mine.IdBean;import com.yctc.zhiting.entity.mine.LocationBean;import com.yctc.zhiting.entity.mine.UpdateUserPost;import com.yctc.zhiting.event.MineUserInfoEvent;import com.yctc.zhiting.event.RefreshHome;import com.yctc.zhiting.event.UpdateSaUserNameEvent;import com.yctc.zhiting.request.AddHCRequest;import com.yctc.zhiting.request.BindCloudRequest;import com.yctc.zhiting.request.BindCloudStrRequest;
import org.greenrobot.eventbus.EventBus;import org.json.JSONObject;
import java.io.UnsupportedEncodingException;import java.lang.ref.WeakReference;import java.nio.charset.StandardCharsets;import java.security.cert.Certificate;import java.security.cert.CertificateEncodingException;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.WeakHashMap;import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLPeerUnverifiedException;
import okhttp3.CacheControl;import okhttp3.OkHttpClient;
public class AllRequestUtil {
static WeakReference<Context> mContext = new WeakReference<>(Application.getContext()); static DBManager dbManager = DBManager.getInstance(mContext.get()); private static boolean hasDialog; public static String nickName = ""; /** * 获取云端家庭数据 */ public static void getCloudArea() { if (!UserUtils.isLogin()) return; HTTPCaller.getInstance().get(HomeCompanyListBean.class, HttpUrlConfig.getSCAreasUrl() + Constant.ONLY_SC, new RequestDataCallback<HomeCompanyListBean>() { @Override public void onSuccess(HomeCompanyListBean obj) { super.onSuccess(obj); if (obj != null) { List<HomeCompanyBean> areas = obj.getAreas(); if (CollectionUtil.isNotEmpty(areas)) {// 云端家庭数据不为空,则同步 getCloudAreaSuccess(areas); } else {// 否则创建一个云端家庭 UiUtil.starThread(() -> { List<HomeCompanyBean> homeList = dbManager.queryLocalHomeCompanyList(); if (CollectionUtil.isEmpty(homeList)) {//本地数据库是否有默认家庭,否则创建家庭 insertDefaultHome(); } sysAreaCloud(true); }); } } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); Log.e("getCloudArea", errorMessage); } }); }
/** * 插入默认 我的家庭 */ private static void insertDefaultHome() { HomeCompanyBean homeCompanyBean = new HomeCompanyBean(UiUtil.getString(R.string.main_my_home)); dbManager.insertHomeCompany(homeCompanyBean, null, false); }
/** * 成功获取云端数据 * * @param areas */ private static void getCloudAreaSuccess(List<HomeCompanyBean> areas) { UiUtil.starThread(() -> { int cloudUserId = UserUtils.getCloudUserId(); dbManager.removeFamilyNotPresentUserFamily(cloudUserId); for (HomeCompanyBean homeBean : areas) { homeBean.setCloud_user_id(cloudUserId); }// List<HomeCompanyBean> userHomeCompanyList = dbManager.queryHomeCompanyListByCloudUserId(cloudUserId); List<HomeCompanyBean> userHomeCompanyList = dbManager.queryHomeCompanyList();
List<Long> cloudIdList = new ArrayList<>(); List<Long> areaIdList = new ArrayList<>(); for (HomeCompanyBean hcb : userHomeCompanyList) { cloudIdList.add(hcb.getId()); areaIdList.add(hcb.getArea_id()); } for (HomeCompanyBean area : areas) { if (cloudIdList.contains(area.getId()) || areaIdList.contains(area.getId())) { // 如果家庭已存在,则更新 dbManager.updateHomeCompanyByCloudId(area); } else {//不存在,插入 if (area.isIs_bind_sa()){ area.setArea_id(area.getId()); } dbManager.insertCloudHomeCompany(area); } } sysAreaCloud(false); EventBus.getDefault().post(new RefreshHome()); }); }
/** * 本地数据同步到云端 */ public static void sysAreaCloud(boolean updateScName) { List<HomeCompanyBean> homeCompanyList = dbManager.queryLocalHomeCompanyList(); if (CollectionUtil.isNotEmpty(homeCompanyList)) { for (int i = 0; i < homeCompanyList.size(); i++) { HomeCompanyBean homeBean = homeCompanyList.get(i); if (homeBean.isIs_bind_sa() && homeBean.getId()!=homeBean.getArea_id()){ // 如果绑了sa// bindCloudWithoutCreateHome(homeBean, null); }else {
boolean isLast = (i == homeCompanyList.size() - 1); List<LocationBean> list = dbManager.queryLocations(homeBean.getLocalId()); List<String> locationNames = new ArrayList<>(); if (CollectionUtil.isNotEmpty(list)) { for (LocationBean locationBean : list) { locationNames.add(locationBean.getName()); } } AddHCRequest addHCRequest = new AddHCRequest(homeBean.getName(), homeBean.getArea_type(), locationNames); homeBean.setCloud_user_id(UserUtils.getCloudUserId()); HTTPCaller.getInstance().post(IdBean.class, HttpUrlConfig.getScAreas() + Constant.ONLY_SC, addHCRequest, new RequestDataCallback<IdBean>() { @Override public void onSuccess(IdBean obj) { super.onSuccess(obj); Log.e("sysAreaCloud-success", "success=homeId=" + obj.getId()); IdBean.CloudSaUserInfo cloudSaUserInfo = obj.getCloud_sa_user_info(); int userId = homeBean.getUser_id(); String saToken = homeBean.getSa_user_token(); if (cloudSaUserInfo != null && !homeBean.isIs_bind_sa()) { userId = cloudSaUserInfo.getId(); saToken = cloudSaUserInfo.getToken(); } updateArea(homeBean.getLocalId(), obj.getId(), userId, saToken, isLast); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); Log.e("sysAreaCloud-fail", errorMessage); } }); } if (i==homeCompanyList.size()-1){ if (updateScName){ updateSCUserName(); UserUtils.setCloudUserName(nickName); }else { EventBus.getDefault().post(new MineUserInfoEvent(true)); } } } } }
/** * 修改sc昵称 */ public static void updateSCUserName(){ UpdateUserPost updateUserPost = new UpdateUserPost(); updateUserPost.setNickname(nickName); String body = new Gson().toJson(updateUserPost); HTTPCaller.getInstance().put(Object.class, HttpUrlConfig.getSCUsersWithoutHeader() + "/" + UserUtils.getCloudUserId() + Constant.ONLY_SC, body, new RequestDataCallback<Object>() { @Override public void onSuccess(Object obj) { super.onSuccess(obj); LogUtil.e("updateSCUserName=====成功"); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); LogUtil.e("updateSCUserName=====失败"); } }); }
/** * 更新本地家庭cloud_id和cloud_user_id * * @param localId 本地家庭 * @param cloudId 云端id */ public static void updateArea(long localId, long cloudId, int saUserId, String saToken, boolean isLast) { dbManager.updateHomeCompanyCloudId(localId, cloudId, UserUtils.getCloudUserId(), saUserId, saToken); //数据全部同步完,检查是否有绑定sa数据 if (isLast) { List<HomeCompanyBean> homeCompanyList = dbManager.queryHomeCompanyList(); LogUtil.e("updateArea1=" + GsonConverter.getGson().toJson(homeCompanyList)); for (HomeCompanyBean homeBean : homeCompanyList) { System.out.println("绑定云:第三次"); bindCloudWithoutCreateHome(homeBean, null, homeBean.getSa_lan_address()); } } }
/** * 判断是否再SA环境 * * @param homeBean * @return */ public static boolean isSAEnvironment(HomeCompanyBean homeBean) { if (homeBean.isIs_bind_sa() && Constant.wifiInfo != null && homeBean.getMac_address() != null && homeBean.getMac_address().equalsIgnoreCase(Constant.wifiInfo.getBSSID())) { return true; } return false; }
/** * 创建并绑定家庭云 */ public static void createHomeBindSC(HomeCompanyBean homeBean, ChannelEntity channelEntity) { //云端id=0表示没有绑定云 if (UserUtils.isLogin()) { if (homeBean.getId()==0 && homeBean.isIs_bind_sa()) { SynPost.AreaBean areaBean = new SynPost.AreaBean(homeBean.getName(), new ArrayList<>());//家庭 HTTPCaller.getInstance().post(IdBean.class, HttpUrlConfig.getScAreas()+Constant.ONLY_SC, areaBean, new RequestDataCallback<IdBean>() { @Override public void onSuccess(IdBean data) { super.onSuccess(data); LogUtil.e("sysAreaCloud-success", "success"); homeBean.setId(data.getId()); Constant.CurrentHome = homeBean; int userId = Constant.CurrentHome.getUser_id(); String token = Constant.CurrentHome.getSa_user_token(); IdBean.CloudSaUserInfo cloudSaUserInfo = data.getCloud_sa_user_info(); if (cloudSaUserInfo!=null && ! Constant.CurrentHome.isIs_bind_sa()){ userId = cloudSaUserInfo.getId(); token = cloudSaUserInfo.getToken(); } dbManager.updateHomeCompanyCloudId(homeBean.getLocalId(), homeBean.getId(), UserUtils.getCloudUserId(), userId, token); bindCloud(homeBean, channelEntity); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); LogUtil.e("sysAreaCloud-fail", errorMessage); } }); } else { bindCloud(homeBean,channelEntity); } } }
/** * 绑定云家庭 * * @param homeBean 家庭对象 */ public static void bindCloud(HomeCompanyBean homeBean, ChannelEntity channelEntity) { //如果再sa环境,如果未绑定云端,则绑定云端;否则不绑定// if (isSAEnvironment(homeBean)) { if (homeBean.isIs_bind_sa() && homeBean.getId()>0) { LogUtil.e("updateArea2=" + GsonConverter.getGson().toJson(homeBean)); Constant.CurrentHome.setId(homeBean.getId()); BindCloudStrRequest request = new BindCloudStrRequest();// request.setCloud_area_id(String.valueOf(homeBean.getId())); request.setCloud_user_id(homeBean.getCloud_user_id());
HttpConfig.addHeader(homeBean.getSa_user_token()); HTTPCaller.getInstance().post(AreaIdBean.class, channelEntity!=null ? Constant.HTTPS_HEAD + channelEntity.getHost() + HttpUrlConfig.API + HttpUrlParams.cloud_bind : HttpUrlConfig.getBindCloud(), request, new RequestDataCallback<AreaIdBean>() { @Override public void onSuccess(AreaIdBean obj) { super.onSuccess(obj); if (obj!=null) { Constant.CurrentHome.setId(obj.getArea_id()); // 重新设置云id值 dbManager.updateHCAreaId(homeBean.getLocalId(), obj.getArea_id(), channelEntity==null); // 绑定云端成功之后,修改本地云id值 } EventBus.getDefault().post(new UpdateSaUserNameEvent()); Log.e("updateArea-bind", "success"); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); Log.e("updateArea-fail", errorMessage); } });
} }
/** * 绑定云端家庭(不用在云端创建家庭) * @param homeCompanyBean * @param channelEntity */ public static void bindCloudWithoutCreateHome(HomeCompanyBean homeCompanyBean, ChannelEntity channelEntity, String sa_lan_address){ if (homeCompanyBean.isIs_bind_sa() && homeCompanyBean.getId() != homeCompanyBean.getArea_id() && UserUtils.isLogin()){ checkUrl500(homeCompanyBean.getSa_lan_address(), new onCheckUrlListener() { @Override public void onSuccess() { getAccessToken(homeCompanyBean, null, sa_lan_address); }
@Override public void onError() { if (channelEntity!=null){ getAccessToken(homeCompanyBean, channelEntity, sa_lan_address); }else { List<Header> headers = new ArrayList<>(); headers.add(new Header(HttpConfig.SA_ID, homeCompanyBean.getSa_id())); List<NameValuePair> requestData = new ArrayList<>(); requestData.add(new NameValuePair("scheme", Constant.HTTPS)); String url = TempChannelUtil.baseSCUrl + "/datatunnel";
HTTPCaller.getInstance().getChannel(ChannelEntity.class, url, headers.toArray(new Header[headers.size()]), requestData, new RequestDataCallback<ChannelEntity>() { @Override public void onSuccess(ChannelEntity obj) { // 获取临时通道成功 super.onSuccess(obj); if (obj != null) { getAccessToken(homeCompanyBean, obj, sa_lan_address); } }
@Override public void onFailed(int errorCode, String errorMessage) { // 获取临时通道失败 super.onFailed(errorCode, errorMessage); Log.e("CaptureNewActivity=", "checkTemporaryUrl=onFailed"); } }, false); } } }); } }
/** * 获取设备access_token * @param homeCompanyBean * @param channelEntity */ public static void getAccessToken(HomeCompanyBean homeCompanyBean, ChannelEntity channelEntity, String sa_lan_address){ HTTPCaller.getInstance().post(AccessTokenBean.class, HttpUrlConfig.getDeviceAccessToken(), "", new RequestDataCallback<AccessTokenBean>() { @Override public void onSuccess(AccessTokenBean obj) { super.onSuccess(obj); if (obj!=null){ LogUtil.e("getAccessToken==============成功"); BindCloudStrRequest request = new BindCloudStrRequest(); request.setAccess_token(obj.getAccess_token()); request.setCloud_user_id(UserUtils.getCloudUserId()); HttpConfig.addHeader(HttpConfig.SA_ID, homeCompanyBean.getSa_id()); HttpConfig.addHeader(HttpConfig.TOKEN_KEY, homeCompanyBean.getSa_user_token()); HTTPCaller.getInstance().post(AreaIdBean.class, channelEntity!=null ? Constant.HTTPS_HEAD + channelEntity.getHost() + HttpUrlConfig.API + HttpUrlParams.cloud_bind : sa_lan_address+ HttpUrlConfig.API + HttpUrlParams.cloud_bind, request, new RequestDataCallback<AreaIdBean>() { @Override public void onSuccess(AreaIdBean obj) { super.onSuccess(obj); if (obj!=null) { Constant.CurrentHome.setId(obj.getArea_id()); // 重新设置云id值 dbManager.updateHCAreaId(homeCompanyBean.getLocalId(), obj.getArea_id(), channelEntity==null); // 绑定云端成功之后,修改本地云id值 } EventBus.getDefault().post(new UpdateSaUserNameEvent()); LogUtil.e("updateArea-bind====success"); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); LogUtil.e("updateArea-fail====="+errorMessage); } }); } }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); LogUtil.e("getAccessToken==============失败:"+errorMessage); } }); }
/** * 检测接口 * * @param url 家庭对象 */ public static void checkUrl(String url, onCheckUrlListener listener) { if (url != null && !TextUtils.isEmpty(url)) { if (!url.startsWith("http")) url = "http://" + url; url = url + "/api/check"; } HTTPCaller.getInstance().post(String.class, url, "", new RequestDataCallback<String>() { @Override public void onSuccess(String obj) { super.onSuccess(obj); if (listener != null) listener.onSuccess(); }
@Override public void onFailed(int errorCode, String errorMessage) { super.onFailed(errorCode, errorMessage); if (listener != null) listener.onError(); } }); }
public static void checkUrl500(String url, onCheckUrlListener listener){ if (url != null && !TextUtils.isEmpty(url)) { if (!url.startsWith("http")) url = "http://" + url; url = url + "/api/check"; } String finalUrl = url; HTTPCaller.getInstance().postBuilder(url, new Header[]{}, "", getClient(), new HttpResponseHandler() { @Override public void onSuccess(int status, Header[] headers, byte[] responseBody) { String str = null; try { str = new String(responseBody, StandardCharsets.UTF_8); LogUtil.e(finalUrl + " " + status + " " + str); String data; if (str.contains("\"data\"")) { JSONObject jsonObject = new JSONObject(str); data = jsonObject.getString("data"); LogUtil.e("HTTPCaller1=" + data); } if (listener != null) listener.onSuccess(); LogUtil.e("HTTPCaller2=" + str); } catch (Exception e) { e.printStackTrace(); if (listener != null) listener.onError(); } }
@Override public void onFailure(int status, byte[] data) { super.onFailure(status, data); String datas = null; try { datas = new String(data, StandardCharsets.UTF_8); LogUtil.e(finalUrl + " " + status + " " + datas); System.out.println("结果:" + datas); String dataStr = ""; String result=""; if (status != -1) { JSONObject jsonObject = new JSONObject(datas); dataStr = jsonObject.getString("data"); } if (listener != null) listener.onError(); LogUtil.e("HTTPCaller1=" + dataStr); } catch (Exception e) { e.printStackTrace(); if (listener != null) listener.onError(); }
} }); }
public static OkHttpClient getClient(){ OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(500, TimeUnit.MILLISECONDS) .writeTimeout(500, TimeUnit.MILLISECONDS) .readTimeout(500, TimeUnit.MILLISECONDS) .addInterceptor(new LoggerInterceptor("ZhiTing", true)) .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager()) .hostnameVerifier((hostname, session) -> { if (hostname.equals(HTTPCaller.CLOUD_HOST_NAME)) { // SC直接访问 return true; } else { if (session != null) { String cersCacheJson = SpUtil.get(hostname); // 根据主机名获取本地存储的数据 LogUtil.e("缓存证书:"+cersCacheJson); try { Certificate[] certificates = session.getPeerCertificates(); // 证书 String cersJson = HTTPCaller.byte2Base64String(certificates[0].getEncoded()); // 把证书转为base64存储 if (!TextUtils.isEmpty(cersCacheJson)) { // 如果之前存储过 LogUtil.e("服务证书:"+cersJson); String ccj = new String(cersCacheJson.getBytes(), "UTF-8"); String cj = new String(cersJson.getBytes(), "UTF-8"); boolean cer = cj.equals(ccj); if (cer) { // 之前存储过的证书和当前证书一样,直接访问 return true; } else {// 之前存储过的证书和当前证书不一样,重新授权 showAlertDialog(LibLoader.getCurrentActivity().getString(com.app.main.framework.R.string.whether_trust_this_certificate_again), hostname, cersJson); return false; } } else {// 之前没存储过,询问是否信任证书 showAlertDialog(LibLoader.getCurrentActivity().getString(com.app.main.framework.R.string.whether_trust_this_certificate), hostname, cersJson); return false; } } catch (SSLPeerUnverifiedException e) { e.printStackTrace(); } catch (CertificateEncodingException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return false; } }) .cookieJar(new CookieJarImpl(PersistentCookieStore.getInstance())) .build(); return client; }
private static void showAlertDialog(String tips, String hostName, String cerCache) { FragmentActivity activity = (FragmentActivity) LibLoader.getCurrentActivity(); if (activity != null) { if (!hasDialog) { hasDialog = true; CertificateDialog certificateDialog = CertificateDialog.newInstance(tips); certificateDialog.setConfirmListener(() -> { SpUtil.put(hostName, cerCache); certificateDialog.dismiss(); hasDialog = false; }); certificateDialog.show(activity); } } }
public interface onCheckUrlListener { void onSuccess();
void onError(); }}
3)登录状态获取家庭列表事件处理
请求服务器:SC服务器
处理逻辑:前置条件:UserUtils.isLogin = true
(1). 获取SC的家庭列表信息,并同步到本地数据库存储
条件筛选:SC.HomeCompanyBean.id not in HomeCompanyBean.id
(2). 本地存储移除SC已删除的家庭信息
条件筛选:HomeCompanyBean.id not in SC.HomeCompanyBean.id
(3). 请求SA将当前SA环境的家庭绑定到SC
条件筛选: HomeCompanyBean.is_bind_sa == true && SC.HomeCompanyBean.sa_lan_address == "" && HomeCompanyBean.mac_address = 当前WiFi环境的bssid
代码如下:
private synchronized void handleHomeList(List<HomeCompanyBean> homeList, boolean isRefreshData, boolean isFailed) { if (CollectionUtil.isNotEmpty(homeList)) { mHomeList.clear(); mHomeList.addAll(homeList);
//删除本地云端家庭数据 if (UserUtils.isLogin() && isRefreshData) { dbManager.removeFamilyNotPresentUserFamily(UserUtils.getCloudUserId()); int cloudUserId = UserUtils.getCloudUserId(); for (HomeCompanyBean homeBean : homeList) { homeBean.setCloud_user_id(cloudUserId); } List<HomeCompanyBean> userHomeCompanyList = dbManager.queryHomeCompanyList(); // 用于存储本地已绑云的数据 List<Long> cloudIdList = new ArrayList<>(); List<Long> areaIdList = new ArrayList<>(); for (HomeCompanyBean hcb : userHomeCompanyList) { cloudIdList.add(hcb.getId()); areaIdList.add(hcb.getArea_id()); }
// 用于存储从服务获取的数据 List<Long> serverIdList = new ArrayList<>(); // 遍历从云端获取的数据是否已存在本地 unBindHomes.clear(); for (HomeCompanyBean area : homeList) { long aId = area.getId(); serverIdList.add(aId); if (!area.isIs_bind_sa()) { unBindHomes.add(area); } // 已存在,更新 if (cloudIdList.contains(aId) || areaIdList.contains(aId)) { dbManager.updateHomeCompanyByCloudId(area); } else { // 不存在,插入 if (area.isIs_bind_sa()) { area.setArea_id(area.getId()); } dbManager.insertCloudHomeCompany(area); } }
// 移除sc已删除的数据 for (Long id : cloudIdList) { if (serverIdList.contains(id)) { // 如果云端数据还在,继续 continue; } else { // 如果云端数据不在,则删除本地数据 if (id > 0) dbManager.removeFamilyByCloudId(id); } }
mHomeList = dbManager.queryHomeCompanyList(); } }
4)扫码加入SA家庭事件处理
请求服务器:SA服务器 或 SC服务器
处理逻辑:
(1). 解析二维码信息
{ "qr_code":"xxxxxxxxxxx", "url":"http(s)://xxx:xxxx", "area_name":"xxxx"}
(2). 判断当前二维码适合哪种情况的请求? SA服务器请求 、SC服务器请求?
(3). 扫描加入SA家庭成功后,新增/更新该家庭至本地数据库(当家庭HomeCompanyBean.sa_lan_address 存在时为更新)
@Override public void invitationCheckSuccess(InvitationCheckBean invitationCheckBean) { UiUtil.starThread(() -> { homeCompanyBean = new HomeCompanyBean(mQRCodeBean.getArea_name()); homeCompanyBean.setIs_bind_sa(true); homeCompanyBean.setName(mQRCodeBean.getArea_name()); homeCompanyBean.setSa_lan_address(mQRCodeBean.getUrl()); homeCompanyBean.setCloud_user_id(UserUtils.getCloudUserId()); homeCompanyBean.setUser_id(invitationCheckBean.getUser_info().getUser_id()); homeCompanyBean.setSa_user_token(invitationCheckBean.getUser_info().getToken()); homeCompanyBean.setSa_id(saId); homeCompanyBean.setArea_type(area_type); IdBean idBean = invitationCheckBean.getArea_info(); long areaId = 0; if (idBean != null) { areaId = idBean.getId(); homeCompanyBean.setArea_id(areaId); } if (wifiInfo != null && channelEntity == null) { // wifi信息不为空且没有走临时通道时 homeCompanyBean.setSs_id(wifiInfo.getSSID()); homeCompanyBean.setMac_address(wifiInfo.getBSSID()); } HttpConfig.addAreaIdHeader(HttpConfig.AREA_ID, String.valueOf(homeCompanyBean.getArea_id())); HttpConfig.addAreaIdHeader(HttpConfig.TOKEN_KEY, homeCompanyBean.getSa_user_token()); Constant.CurrentHome = homeCompanyBean; if (channelEntity != null) { SpUtil.put(SpConstant.SA_TOKEN, homeCompanyBean.getSa_user_token()); TempChannelUtil.saveTempChannelUrl(channelEntity); } HomeCompanyBean checkHome = dbManager.queryHomeCompanyByAreaId(areaId); // 根据areaId查找家庭 LogUtil.e("CaptureNewActivity=" + GsonConverter.getGson().toJson(homeCompanyBean)); if (checkHome == null) { // 没有加入过,加入数据库 insertHome(homeCompanyBean); } else { // 加入过,更新数据库 dbManager.updateHomeCompanyByAreaId(homeCompanyBean); homeCompanyBean.setId(checkHome.getId()); UiUtil.runInMainThread(() -> toMain()); } }); }
5)家庭列表切换家庭事件处理
处理逻辑:获取选中家庭信息(本地)触发更新当前家庭信息
(1). 更新家庭网络情况(满足家庭已绑SA,但家庭的网络信息为空时更新)
条件筛选:HomeCompanyBean.is_bind_sa == true && HomeCompanyBean.sa_lan_address != "" && HomeCompanyBean.ssid == "" && HomeCompanyBean.mac_address == ""
检测网络:HomeCompanyBean.sa_lan_address 是否在当前网络环境下能够请求
(2). 获取相应用户操作权限
/** * 设置当前选中的家庭 * * @param home * @param isReconnect 是否重新连接 */ public void setCurrentHome(HomeCompanyBean home, boolean isReconnect, boolean isFailed) { hasLoadHomeList = true; if (home == null) return; HomeUtil.tokenIsInvalid = false; currentItem = 0; UiUtil.runInMainThread(() -> { CurrentHome = home; Constant.AREA_TYPE = CurrentHome.getArea_type(); if (!TextUtils.isEmpty(home.getSa_lan_address())) { System.out.println("sa的地址:" + home.getSa_lan_address()); HttpUrlConfig.baseSAUrl = home.getSa_lan_address(); HttpUrlConfig.apiSAUrl = HttpUrlConfig.baseSAUrl + HttpUrlConfig.API; TempChannelUtil.baseSAUrl = HttpUrlConfig.baseSAUrl + HttpUrlConfig.API; } else { if (home.isIs_bind_sa()) { EventBus.getDefault().post(new FourZeroFourEvent()); } else { HttpUrlConfig.baseSAUrl = ""; HttpUrlConfig.apiSAUrl = ""; TempChannelUtil.baseSAUrl = ""; } } homeLocalId = CurrentHome.getLocalId();
if (UserUtils.isLogin() && CurrentHome.getArea_id() > 0 && CurrentHome.getId() == 0) { System.out.println("绑定云:第一次"); AllRequestUtil.bindCloudWithoutCreateHome(CurrentHome, null, CurrentHome.getSa_lan_address()); }
HttpConfig.clearHeader(); HttpConfig.addHeader(CurrentHome.getSa_user_token()); SpUtil.put(SpConstant.SA_TOKEN, home.getSa_user_token()); SpUtil.put(SpConstant.AREA_ID, String.valueOf(home.getId())); SpUtil.put(SpConstant.IS_BIND_SA, home.isIs_bind_sa());
EventBus.getDefault().postSticky(new HomeSelectedEvent()); ChannelUtil.reSaveChannel();//重新获取临时通道
//先显示缓存数据 queryRooms(CurrentHome.getLocalId()); if (isFailed) return; String homeMacAddress = CurrentHome.getMac_address(); WSocketManager.getInstance().start(); if (wifiInfo != null) { handleTipStatus(home.isIs_bind_sa() && (TextUtils.isEmpty(homeMacAddress) ? true : !homeMacAddress.equals(wifiInfo.getBSSID()))); } else { handleTipStatus(home.isIs_bind_sa()); }
if (home.isIs_bind_sa() && CurrentHome.getArea_id() > 0 && CurrentHome.getId() == CurrentHome.getArea_id() && TextUtils.isEmpty(CurrentHome.getMac_address()) && isReconnect) {//需要绑定SA才检查接口 checkInterfaceEnabled(); } else {//如果没有 handleTipStatus1(); getRoomList(false); } }); }
6)网络环境变化事件处理
/** * Wifi 状态接收器 */ private final WifiReceiver mWifiReceiver = new WifiReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); EventBus.getDefault().post(new UpdateProfessionStatusEvent()); if (info.getState().equals(NetworkInfo.State.DISCONNECTED)) { wifiInfo = null; LogUtil.e(TAG, "网络=wifi断开"); if (CurrentHome != null) { handleTipStatus(CurrentHome.isIs_bind_sa()); } handleDisconnect("", false); } else if (info.getState().equals(NetworkInfo.State.CONNECTED)) { if (info.getType() == ConnectivityManager.TYPE_WIFI) { WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); wifiInfo = wifiManager.getConnectionInfo(); if (CurrentHome != null && !TextUtils.isEmpty(CurrentHome.getSa_lan_address())) { if (!TextUtils.isEmpty(CurrentHome.getMac_address())) { bindSaHomeHideTips(); } if (hasLoadHomeList) { checkInterfaceEnabled(); } } //获取当前wifi名称 LogUtil.e(TAG, "网络=连接到网络1 " + wifiInfo.getSSID()); LogUtil.e(TAG, "网络=连接到网络2 " + GsonConverter.getGson().toJson(wifiInfo)); LogUtil.e(TAG, "网络=连接到网络3 " + wifiInfo.getBSSID()); LogUtil.e(TAG, "网络=连接到网络4 " + wifiInfo.getMacAddress()); } } if (isFirstInit) { isFirstInit = false; loadData(); } } } };
/** * wifi断开就不是SA环境了 */ private void handleDisconnect(String address, boolean bindScToSa) { if (CurrentHome != null) { String macAddress = CurrentHome.getMac_address(); if (wifiInfo != null) { if (TextUtils.isEmpty(macAddress)) { if (!TextUtils.isEmpty(address)) { CurrentHome.setMac_address(address); dbManager.updateHomeMacAddress(CurrentHome.getLocalId(), address); } } else { if (!macAddress.equals(wifiInfo.getBSSID())) CurrentHome.setMac_address(""); } }
getRoomList(false); if (!TextUtils.isEmpty(address)) { handleTipStatus1(); } else { handleTipStatus(CurrentHome.isIs_bind_sa()); } EventBus.getDefault().post(new UpdateProfessionStatusEvent()); startConnectSocket(); } }
示例情景:活动13 退出SC登录状态下室外扫码加入SA家庭,然后室内登录SC
1)识别二维码,判断访问服务端为SC
2)调用SC服务器API接口:POST:/invitation/check
3)扫码加入成功后,新增/更新该家庭至本地服务器
4)触发当前家庭切换后更新状态事件
(1). 更新家庭网络情况(满足家庭已绑SA,但家庭的网络信息为空时更新)
条件筛选:HomeCompanyBean.is_bind_sa == true && HomeCompanyBean.sa_lan_address != "" && HomeCompanyBean.ssid == "" && HomeCompanyBean.macAddr == ""
检测网络:HomeCompanyBean.sa_lan_address 是否在当前网络环境下能够请求
(2). 获取相应用户操作权限
#
2.10 WebSocket模块#
2.10.1 websocket主要功能是发现设备、设备交互#
2.10.2 WebSocket 初始化 public void start() { Log.e(TAG, "start"); if (mOkHttpClient != null) { mOkHttpClient = null; } if (mRequest != null) { mRequest = null; } if (mWebSocket != null) { mWebSocket.close(1000, "close"); mWebSocket = null; } mOkHttpClient = new OkHttpClient.Builder() .retryOnConnectionFailure(false)//允许失败重试 .readTimeout(TIMEOUT, TimeUnit.SECONDS)//设置读取超时时间 .writeTimeout(TIMEOUT, TimeUnit.SECONDS)//设置写的超时时间 .connectTimeout(TIMEOUT, TimeUnit.SECONDS)//设置连接超时时间 .pingInterval(PING_TIME, TimeUnit.SECONDS)//心跳 .certificatePinner(CertificatePinner.DEFAULT) .hostnameVerifier(SSLSocketClient.getHostnameVerifier())//配置 .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager()) .cookieJar(new CookieJarImpl(PersistentCookieStore.getInstance())) .addInterceptor(new UserAgentInterceptor()) .build();
String url = getUrl(); mRequest = new Request.Builder() .url(url) .build();
mWebSocket = mOkHttpClient.newWebSocket(mRequest, mWebSocketListener); mOkHttpClient.dispatcher().executorService().shutdown();//内存不足时释 Log.e(TAG, "url=" + url); }
#
2.10.3 WebSocket 监听器 public WebSocketListener mWebSocketListener = new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); //连接成功 Log.e(TAG, "websocket连接成功"); isConnecting = true; if (mWebSocketListeners.size() > 0) { for (IWebSocketListener listener : mWebSocketListeners) { if (listener != null) { UiUtil.runInMainThread(() -> listener.onOpen(webSocket, response)); } } } }
@Override public void onClosing(WebSocket webSocket, int code, String reason) { super.onClosing(webSocket, code, reason); Log.e(TAG, "onClosing"); isConnecting = false; }
@Override public void onClosed(WebSocket webSocket, int code, String reason) { super.onClosed(webSocket, code, reason); Log.e(TAG, "onClosed"); isConnecting = false; }
@Override public void onMessage(WebSocket webSocket, String text) { super.onMessage(webSocket, text); //接收服务器消息 text Log.e(TAG, "onMessage1=" + text); isConnecting = true; UiUtil.runInMainThread(() -> { if (mWebSocketListeners.size() > 0) { for (IWebSocketListener listener : mWebSocketListeners) { if (listener != null) { listener.onMessage(webSocket, text); } } } }); }
@Override public void onMessage(WebSocket webSocket, ByteString bytes) { super.onMessage(webSocket, bytes); //如果服务器传递的是byte类型的 isConnecting = true; String msg = bytes.utf8(); }
@Override public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) { super.onFailure(webSocket, t, response); t.printStackTrace(); isConnecting = false; UiUtil.runInMainThread(() -> { if (mWebSocketListeners.size() > 0) { for (IWebSocketListener listener : mWebSocketListeners) { if (listener != null) { listener.onFailure(webSocket, t, response); } } } }); } };
#
2.10.4 WebSocket 使用WSocketManager.getInstance().addWebSocketListener(new IWebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); mSendId = 0; setDeviceStatus(); LogUtil.e("HomeItemFragment=123=open"); }
@Override public void onMessage(WebSocket webSocket, String text) { handleMessage(text); } });
#
2.10.5 WebSocket 发送消息 public WSocketManager sendMessage(String json) { if (isConnecting && mWebSocket != null && !TextUtils.isEmpty(json)) { boolean success = mWebSocket.send(json); } return this; }
#
3. 附录#
3.1 Android 数据库设计文档数据库: Sqlite
使用参考手册:https://developer.android.google.cn/training/data-storage/sqlite?hl=zh_cn
数据表设计:
zhiting_home_company 家庭/公司表
名称 | 类型 | 是否null | 注释 |
---|---|---|---|
h_id | string | 家庭id | |
name | string | 否 | 家庭/公司名称 |
sa_user_id | integer | 用户sa_Id | |
sa_user_token | string | 用户SA认证授权 | |
is_bind_sa | bool | 否 | 家庭/公司是否绑定SA |
ss_id | string | sa的wifi名称 | |
sa_lan_address | string | sa的地址 | |
mac_address | string | sa的mac地址 | |
user_id | integer | 否 | 用户id |
accountName | string | SA专业版账号名 | |
cloud_user_id | integer | 云端用户的user_id | |
cloud_id | integer | 否 | 云端家庭id |
area_id | integer | 否 | sa设备id |
area_type | integer | 否 | 区分家庭和公司, 1是家庭,2是公司 |
zhiting_location 房间/区域表
名称 | 类型 | 是否null | 注释 |
---|---|---|---|
r_id | integer | 否 | 主键 |
name | string | 否 | 房间/区域名称 |
area_id | stirng | 家庭/公司Id | |
sort | integer | 否 | 排序编号 |
l_id | integer | 否 | 房间、区域id |
zhiting_devices 设备表
名称 | 类型 | 是否null | 注释 |
---|---|---|---|
d_id | integer | 否 | 主键 |
name | string | 否 | 设备名称 |
sa_user_token | string | 是 | sa token |
brand_id | string | 品牌Id | |
logo_url | string | 设备Logo | |
identity | string | 设备属性 | |
plugin_id | string | 品牌插件Id | |
area_id | string | 家庭/公司Id | |
l_id | integer | 房间/区域Id | |
is_sa | bool | 否 | 是否SA设备 |
zhiting_scene 场景表
名称 | 类型 | 是否null | 注释 |
---|---|---|---|
sa_user_token | string | 否 | sa token |
scene | string | 否 | 场景内容 |
zhiting_user_info 用户表
名称 | 类型 | 是否null | 注释 |
---|---|---|---|
nickname | string | 否 | 用户昵称 |
phone | string | 手机号 | |
icon_url | string | 用户头像 | |
user_id | integer | SA用户Id |