Cordova生态中有很多插件,插件是通过Web页面中的js脚本调用App实现两者的数据交互。如果有一些定制化的数据交互,我们可以通过实现自定义插件的方式来实现,对于一些轻量的数据交互,这里提供一种非插件方式的轮子:通过在App中开启一个本地的HttpServer来响应html页面的请求
。下面记录一下实现步骤。
1 添加依赖
添加nanohttpd
依赖包,用于实现轻量级http服务。另外,为了便于操作,添加阿里巴巴的fastjson
包。如下:
注意:添加外部引用包,不能添加到
build.gradle
文件中,因为该文件在Cordova框架构建过程中会重新生成。这里需要按照以下格式将外部依赖添加到安卓平台项目根目录下的project.properties
文件中,并标定依赖包的顺序编号
cordova.system.library.1=org.nanohttpd:nanohttpd:2.3.1
cordova.system.library.2=com.alibaba:fastjson:1.2.68
2 构造相关类
这里我们实现一个通过Html页面的JS发送Post请求获取本机网络、存储信息的功能。
2.1 创建全局上下文类
为便于在全局使用,创建上下文类CordovaContext
,使当前上下文在全局可用,代码如下:
public final class CordovaContext {
private static Activity context ;
public static Activity getContext() {
return context;
}
public static void setContext(Activity context) {
CordovaContext.context = context;
}
}
2.2 创建网络工具类
需要在
AndroidManifest.xml
文件中manifest
节点下指定获取网络链接和网络状态的权限如下
<!--允许程序访问网络连接,可能产生GPRS流量-->
<uses-permission android:name="android.permission.INTERNET" />
<!--允许程序获取网络信息状态,如当前的网络连接是否有效-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
创建类NetworkUtil
用于获取网络相关信息,代码如下:
public class NetworkUtil {
//没有网络连接
public static final int NETWORK_NONE = 0;
//wifi连接
public static final int NETWORK_WIFI = 1;
//手机网络数据连接类型
public static final int NETWORK_2G = 2;
public static final int NETWORK_3G = 3;
public static final int NETWORK_4G = 4;
public static final int NETWORK_MOBILE = 5;
public static String getNetworkType(Context context) {
int type = getNetworkState(context);
switch (type) {
case 0:
return "无网络连接";
case 1:
return "Wifi网络";
default:
return "移动网络";
}
}
/**
* 获取当前网络连接类型
*
* @param context
* @return
*/
private static int getNetworkState(Context context) {
//获取系统的网络服务
ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
//如果当前没有网络
if (null == connManager)
return NETWORK_NONE;
//获取当前网络类型,如果为空,返回无网络
NetworkInfo activeNetInfo = connManager.getActiveNetworkInfo();
if (activeNetInfo == null || !activeNetInfo.isAvailable()) {
return NETWORK_NONE;
}
// 判断是不是连接的是不是wifi
NetworkInfo wifiInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (null != wifiInfo) {
NetworkInfo.State state = wifiInfo.getState();
if (null != state)
if (state == NetworkInfo.State.CONNECTED || state == NetworkInfo.State.CONNECTING) {
return NETWORK_WIFI;
}
}
// 如果不是wifi,则判断当前连接的是运营商的哪种网络2g、3g、4g等
NetworkInfo networkInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (null != networkInfo) {
NetworkInfo.State state = networkInfo.getState();
String strSubTypeName = networkInfo.getSubtypeName();
if (null != state)
if (state == NetworkInfo.State.CONNECTED || state == NetworkInfo.State.CONNECTING) {
switch (activeNetInfo.getSubtype()) {
//如果是2g类型
case TelephonyManager.NETWORK_TYPE_GPRS: // 联通2g
case TelephonyManager.NETWORK_TYPE_CDMA: // 电信2g
case TelephonyManager.NETWORK_TYPE_EDGE: // 移动2g
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_IDEN:
return NETWORK_2G;
//如果是3g类型
case TelephonyManager.NETWORK_TYPE_EVDO_A: // 电信3g
case TelephonyManager.NETWORK_TYPE_UMTS:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
case TelephonyManager.NETWORK_TYPE_EHRPD:
case TelephonyManager.NETWORK_TYPE_HSPAP:
return NETWORK_3G;
//如果是4g类型
case TelephonyManager.NETWORK_TYPE_LTE:
return NETWORK_4G;
default:
//中国移动 联通 电信 三种3G制式
if (strSubTypeName.equalsIgnoreCase("TD-SCDMA") || strSubTypeName.equalsIgnoreCase("WCDMA") || strSubTypeName.equalsIgnoreCase("CDMA2000")) {
return NETWORK_3G;
} else {
return NETWORK_MOBILE;
}
}
}
}
return NETWORK_NONE;
}
}
2.3 创建存储工具类
需要在
AndroidManifest.xml
文件中manifest
节点下指定获取读写存储的权限,如下
<!--允许程序写入外部存储,如SD卡上读写文件-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
创建类StorageUtil
用户获取Android机存储信息,代码如下。
public final class StorageUtil {
/**
* 根据路径获取内存状态
*
* @param path
* @return
*/
public static Map<String, String> getMemoryInfo(File path) {
// 获得一个磁盘状态对象
StatFs stat = new StatFs(path.getPath());
long blockSize = stat.getBlockSizeLong(); // 获得一个扇区的大小
long totalBlocks = stat.getBlockCountLong(); // 获得扇区的总数
long availableBlocks = stat.getAvailableBlocksLong(); // 获得可用的扇区数量
// 总空间
String totalMemory = Formatter.formatFileSize(CordovaContext.getContext(), totalBlocks * blockSize);
// 可用空间
String availableMemory = Formatter.formatFileSize(CordovaContext.getContext(), availableBlocks * blockSize);
Map<String, String> mInfo = new HashMap<>();
mInfo.put("total", totalMemory);
mInfo.put("available", availableMemory);
return mInfo;
}
public static Map<String, String> getRamInfo() {
ActivityManager manager = (ActivityManager) CordovaContext.getContext().getSystemService(CordovaContext.getContext().ACTIVITY_SERVICE);
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
manager.getMemoryInfo(info);
Map<String, String> mInfo = new HashMap<>();
mInfo.put("total", Formatter.formatFileSize(CordovaContext.getContext(), info.totalMem));
mInfo.put("available", Formatter.formatFileSize(CordovaContext.getContext(), info.availMem));
return mInfo;
}
public static Map<String, String> getRomInfo() {
return getMemoryInfo(Environment.getDataDirectory());
}
}
2.4 创建服务请求处理类
构造类LocalHttpServer
开启安卓机本机Server,实现nanohttpd
包中的NanoHTTPD
类,用于响应Html页面的请求。默认使用端口9090
,绑定本机所有地址(可通过父类的另外一个构造函数,限定绑定的IP,例如仅绑定本机回送地址127.0.0.1
,就不能响应其它IP发来的请求)。
在下面例子中,响应请求功能主要在serve
方法中,这里通过请求中op
参数的值判断需要做什么类型的操作,例如sysInfo
表示获取系统信息。
完整代码如下:
public final class LocalHttpServer extends NanoHTTPD {
private static final String TAG = "LocalHttpServer";
private static boolean started = false;
private static final int port = 9090;
private static LocalHttpServer instance;
static {
try {
instance = new LocalHttpServer();
} catch (Exception e) {
org.apache.cordova.LOG.e(TAG, "初始化本地服务异常!", e);
}
}
private LocalHttpServer() {
super(port);//绑定所有地址
}
public static LocalHttpServer getInstance() {
return instance;
}
public void startServer() {
if (started) {
org.apache.cordova.LOG.w(TAG, "本地服务已启动," + port);
return;
}
if (instance == null) {
try {
instance = new LocalHttpServer();
} catch (Exception e) {
org.apache.cordova.LOG.e(TAG, "初始化启动本地服务异常!", e);
return;
}
}
try {
//super("127.0.0.1", 9090);//仅绑定本地回送地址 只能响应本机发的请求
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
started = true;
} catch (Exception e) {
started = false;
}
}
@Override
public Response serve(IHTTPSession session) {
if (session.getMethod().name().equals("OPTIONS")) {
Response response = newFixedLengthResponse("");
setDefaultHeader(response);
return response;
}
if (session.getMethod().equals(Method.POST)) {
try {
session.parseBody(new HashMap<>());
} catch (IOException | ResponseException e) {
org.apache.cordova.LOG.e(TAG, "post 参数转化错误", e);
}
}
Map<String, List<String>> params = session.getParameters();
String opType = this.getOneParam("op", params);
Response response;
if (TextUtils.isEmpty(opType)) {
response = newFixedLengthResponse(new JSONObject().fluentPut("success", false).fluentPut("data", "").fluentPut("message", "parameter op is required").toString());
response.setStatus(Response.Status.BAD_REQUEST);
setDefaultHeader(response);
return response;
}
switch (opType) {
case "sysInfo":
// 获得sd卡的内存状态
File sdcardFileDir = Environment.getExternalStorageDirectory();
// 获得手机内部存储控件的状态
File dataFileDir = Environment.getDataDirectory();
JSONObject res = new JSONObject().fluentPut("success", true)
.fluentPut("data",
new JSONObject()
.fluentPut("Ram", StorageUtil.getRamInfo())
.fluentPut("Rom", StorageUtil.getRomInfo())
.fluentPut("Sdcard", StorageUtil.getMemoryInfo(sdcardFileDir))
.fluentPut("Internal", StorageUtil.getMemoryInfo(dataFileDir))
.fluentPut("Network", NetworkUtil.getNetworkType(CordovaContext.getContext()))
.fluentPut("Release", Build.VERSION.RELEASE)
.fluentPut("BaseOs", Build.VERSION.BASE_OS)
.fluentPut("Version", Build.ID + "_" + Build.VERSION.INCREMENTAL)
.fluentPut("Model", Build.MODEL)
.fluentPut("Serial", Build.SERIAL)
).fluentPut("message", "指令执行完成");
response = newFixedLengthResponse(res.toString());
response.setStatus(Response.Status.OK);
setDefaultHeader(response);
return response;
default:
response = newFixedLengthResponse(new JSONObject().fluentPut("success", false).fluentPut("data", "").fluentPut("message", "invalid operation").toString());
response.setStatus(Response.Status.BAD_REQUEST);
setDefaultHeader(response);
break;
}
return response;
}
private void setDefaultHeader(Response response) {
response.addHeader("Content-Type", "application/json");
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Headers", "Hlh-Code, Authorization");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
}
/**
* 获取参数
*
* @param key 参数名
* @param params 参数列表
* @return
*/
private String getOneParam(String key, Map<String, List<String>> params) {
if (TextUtils.isEmpty(key)) {
return "";
}
if (params == null || params.size() <= 0) {
return "";
}
List<String> tmps = params.get(key);
if (tmps == null || tmps.size() <= 0) {
return "";
}
String p = tmps.get(0);
return TextUtils.isEmpty(p) ? "" : p;
}
}
3 使用
在入口类MainActivity
的onCreate
方法中,启用本地Server,如下:
LocalHttpServer.getInstance().startServer();
4 模拟请求
完成上面步骤的操作后,生成新的Apk安装包并安装启动,然后使用连接在同一个网络的电脑用Postman发送请求模拟效果如下,可正常响应请求。如果有其他类型的请求,在LocalHttpServer
类中的serve
方法中通过op
参数判断实现即可。
注意,如果是在不同机器发送请求,
LocalHttpServer
类中不能只绑定本机回送地址,否则通过IP无法访问。