376 lines
12 KiB
Java
376 lines
12 KiB
Java
/*
|
||
* 自然写互动课堂应用开发SDK软件 V1.0
|
||
* WritechSDK - SDK初始化与鉴权入口
|
||
*
|
||
* 功能说明:
|
||
* 1. SDK全局初始化(配置加载、模块注册)
|
||
* 2. 应用鉴权(AppKey/AppSecret验证)
|
||
* 3. 各子模块生命周期管理
|
||
* 4. 全局配置管理(服务器地址、超时、日志级别)
|
||
* 5. SDK版本信息与功能授权查询
|
||
*/
|
||
|
||
package com.writech.sdk.android;
|
||
|
||
import android.content.Context;
|
||
import android.content.SharedPreferences;
|
||
import android.util.Log;
|
||
|
||
import java.io.IOException;
|
||
import java.util.concurrent.atomic.AtomicBoolean;
|
||
|
||
/**
|
||
* 自然写SDK主入口类
|
||
* 使用前必须先调用 init() 方法进行初始化和鉴权
|
||
*
|
||
* 典型使用流程:
|
||
* 1. WritechSDK.init(context, config)
|
||
* 2. WritechSDK.getInstance().getPenManager().startScan()
|
||
* 3. WritechSDK.getInstance().getOCREngine().recognizeHandwriting(...)
|
||
*/
|
||
public class WritechSDK {
|
||
|
||
private static final String TAG = "WritechSDK";
|
||
|
||
/* SDK版本号 */
|
||
public static final String SDK_VERSION = "1.0.0";
|
||
|
||
/* SDK构建号 */
|
||
public static final int SDK_BUILD = 100;
|
||
|
||
/* 单例实例 */
|
||
private static volatile WritechSDK sInstance;
|
||
|
||
/* 是否已初始化 */
|
||
private static final AtomicBoolean sInitialized = new AtomicBoolean(false);
|
||
|
||
/* ========== 配置类 ========== */
|
||
|
||
/** SDK初始化配置 */
|
||
public static class Config {
|
||
/** 云平台API地址 */
|
||
public String cloudBaseUrl = "https://api.writech.com";
|
||
|
||
/** SDK应用标识(从自然写开放平台获取) */
|
||
public String appKey;
|
||
|
||
/** SDK应用密钥 */
|
||
public String appSecret;
|
||
|
||
/** 离线OCR模型文件路径(可选) */
|
||
public String offlineModelPath;
|
||
|
||
/** 是否启用调试日志 */
|
||
public boolean debugMode = false;
|
||
|
||
/** 笔迹数据本地缓存目录 */
|
||
public String cacheDir;
|
||
|
||
/** BLE扫描超时时间(毫秒) */
|
||
public int bleScanTimeout = 30000;
|
||
|
||
/** 网关自动发现 */
|
||
public boolean autoDiscoverGateway = true;
|
||
|
||
/** 最大同时连接笔数 */
|
||
public int maxPenConnections = 60;
|
||
}
|
||
|
||
/* ========== 成员变量 ========== */
|
||
|
||
private Context mContext;
|
||
private Config mConfig;
|
||
|
||
/* 各子模块实例 */
|
||
private PenManager mPenManager;
|
||
private StrokeCanvas mDefaultCanvas;
|
||
private OCREngine mOCREngine;
|
||
private GatewaySDK mGatewaySDK;
|
||
private CloudClient mCloudClient;
|
||
|
||
/* 鉴权状态 */
|
||
private boolean mIsAuthenticated = false;
|
||
private String mLicenseType; /* 授权类型: trial/standard/enterprise */
|
||
private long mLicenseExpireTime; /* 授权到期时间 */
|
||
|
||
/* 本地存储 */
|
||
private SharedPreferences mPrefs;
|
||
|
||
/* ========== 初始化入口 ========== */
|
||
|
||
/**
|
||
* 初始化SDK(必须在使用任何功能前调用)
|
||
*
|
||
* @param context Android上下文(Application级别)
|
||
* @param config SDK配置
|
||
* @return 初始化结果:true成功,false失败
|
||
*/
|
||
public static boolean init(Context context, Config config) {
|
||
if (sInitialized.getAndSet(true)) {
|
||
Log.w(TAG, "SDK已初始化,忽略重复调用");
|
||
return true;
|
||
}
|
||
|
||
if (context == null || config == null) {
|
||
Log.e(TAG, "初始化失败:context或config为null");
|
||
sInitialized.set(false);
|
||
return false;
|
||
}
|
||
|
||
if (config.appKey == null || config.appSecret == null) {
|
||
Log.e(TAG, "初始化失败:appKey或appSecret未配置");
|
||
sInitialized.set(false);
|
||
return false;
|
||
}
|
||
|
||
sInstance = new WritechSDK();
|
||
boolean success = sInstance.doInit(context, config);
|
||
|
||
if (!success) {
|
||
sInstance = null;
|
||
sInitialized.set(false);
|
||
}
|
||
|
||
return success;
|
||
}
|
||
|
||
/** 获取SDK单例 */
|
||
public static WritechSDK getInstance() {
|
||
if (sInstance == null) {
|
||
throw new IllegalStateException("WritechSDK未初始化,请先调用 WritechSDK.init()");
|
||
}
|
||
return sInstance;
|
||
}
|
||
|
||
/** 检查SDK是否已初始化 */
|
||
public static boolean isInitialized() {
|
||
return sInitialized.get();
|
||
}
|
||
|
||
/* ========== 内部初始化流程 ========== */
|
||
|
||
/** 执行具体的初始化逻辑 */
|
||
private boolean doInit(Context context, Config config) {
|
||
mContext = context.getApplicationContext();
|
||
mConfig = config;
|
||
mPrefs = mContext.getSharedPreferences("writech_sdk", Context.MODE_PRIVATE);
|
||
|
||
Log.i(TAG, "=== 自然写SDK V" + SDK_VERSION + " 初始化开始 ===");
|
||
Log.i(TAG, "云平台地址: " + config.cloudBaseUrl);
|
||
Log.i(TAG, "AppKey: " + config.appKey.substring(0, 8) + "****");
|
||
Log.i(TAG, "调试模式: " + config.debugMode);
|
||
|
||
/* 步骤1:应用鉴权(验证AppKey和AppSecret) */
|
||
if (!authenticate(config.appKey, config.appSecret)) {
|
||
Log.e(TAG, "SDK鉴权失败,请检查AppKey和AppSecret");
|
||
return false;
|
||
}
|
||
|
||
/* 步骤2:初始化云平台客户端 */
|
||
mCloudClient = new CloudClient(config.cloudBaseUrl, config.appKey, config.appSecret);
|
||
|
||
/* 恢复本地缓存的令牌 */
|
||
restoreTokens();
|
||
|
||
/* 步骤3:初始化蓝牙笔管理器 */
|
||
mPenManager = new PenManager(mContext);
|
||
|
||
/* 步骤4:初始化OCR引擎 */
|
||
mOCREngine = new OCREngine(mContext, config.cloudBaseUrl, null);
|
||
if (config.offlineModelPath != null) {
|
||
mOCREngine.loadOfflineModel(config.offlineModelPath);
|
||
}
|
||
|
||
/* 步骤5:初始化网关SDK */
|
||
mGatewaySDK = new GatewaySDK(mContext);
|
||
if (config.autoDiscoverGateway) {
|
||
mGatewaySDK.startDiscovery();
|
||
}
|
||
|
||
Log.i(TAG, "=== 自然写SDK初始化完成 ===");
|
||
return true;
|
||
}
|
||
|
||
/* ========== 应用鉴权 ========== */
|
||
|
||
/**
|
||
* 验证AppKey和AppSecret的有效性
|
||
* 首次验证需要联网,之后缓存鉴权结果
|
||
*/
|
||
private boolean authenticate(String appKey, String appSecret) {
|
||
/* 检查本地缓存的鉴权结果 */
|
||
String cachedLicense = mPrefs.getString("license_type", null);
|
||
long cachedExpire = mPrefs.getLong("license_expire", 0);
|
||
|
||
if (cachedLicense != null && cachedExpire > System.currentTimeMillis()) {
|
||
mIsAuthenticated = true;
|
||
mLicenseType = cachedLicense;
|
||
mLicenseExpireTime = cachedExpire;
|
||
Log.i(TAG, "使用缓存鉴权结果: " + mLicenseType
|
||
+ ",到期: " + new java.util.Date(mLicenseExpireTime));
|
||
return true;
|
||
}
|
||
|
||
/* 在线鉴权 */
|
||
try {
|
||
String authUrl = mConfig.cloudBaseUrl + "/api/v1/sdk/authenticate";
|
||
String body = "{\"appKey\":\"" + appKey
|
||
+ "\",\"appSecret\":\"" + appSecret
|
||
+ "\",\"sdkVersion\":\"" + SDK_VERSION + "\"}";
|
||
|
||
/* 使用CloudClient的静态方法发送无认证请求 */
|
||
java.net.HttpURLConnection conn =
|
||
(java.net.HttpURLConnection) new java.net.URL(authUrl).openConnection();
|
||
conn.setRequestMethod("POST");
|
||
conn.setRequestProperty("Content-Type", "application/json");
|
||
conn.setDoOutput(true);
|
||
conn.setConnectTimeout(10000);
|
||
conn.getOutputStream().write(body.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||
|
||
int responseCode = conn.getResponseCode();
|
||
if (responseCode == 200) {
|
||
java.io.InputStream is = conn.getInputStream();
|
||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||
byte[] buf = new byte[1024];
|
||
int len;
|
||
while ((len = is.read(buf)) != -1) {
|
||
baos.write(buf, 0, len);
|
||
}
|
||
String response = baos.toString("UTF-8");
|
||
is.close();
|
||
conn.disconnect();
|
||
|
||
/* 解析鉴权结果 */
|
||
mLicenseType = extractJsonField(response, "licenseType");
|
||
String expireStr = extractJsonField(response, "expireTime");
|
||
if (mLicenseType != null) {
|
||
mLicenseExpireTime = expireStr != null ? Long.parseLong(expireStr)
|
||
: System.currentTimeMillis() + 365L * 24 * 3600 * 1000;
|
||
mIsAuthenticated = true;
|
||
|
||
/* 缓存鉴权结果 */
|
||
mPrefs.edit()
|
||
.putString("license_type", mLicenseType)
|
||
.putLong("license_expire", mLicenseExpireTime)
|
||
.apply();
|
||
|
||
Log.i(TAG, "在线鉴权成功: " + mLicenseType);
|
||
return true;
|
||
}
|
||
}
|
||
conn.disconnect();
|
||
|
||
} catch (Exception e) {
|
||
Log.w(TAG, "在线鉴权异常: " + e.getMessage());
|
||
/* 联网失败时允许离线试用(7天) */
|
||
mLicenseType = "trial";
|
||
mLicenseExpireTime = System.currentTimeMillis() + 7L * 24 * 3600 * 1000;
|
||
mIsAuthenticated = true;
|
||
Log.i(TAG, "离线模式,试用授权7天");
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/** 恢复本地缓存的认证令牌 */
|
||
private void restoreTokens() {
|
||
String accessToken = mPrefs.getString("access_token", null);
|
||
String refreshToken = mPrefs.getString("refresh_token", null);
|
||
long expireTime = mPrefs.getLong("token_expire", 0);
|
||
|
||
if (accessToken != null && refreshToken != null) {
|
||
mCloudClient.setTokens(accessToken, refreshToken, expireTime);
|
||
Log.d(TAG, "已恢复缓存的认证令牌");
|
||
}
|
||
}
|
||
|
||
/* ========== 对外接口 ========== */
|
||
|
||
/** 获取笔管理器 */
|
||
public PenManager getPenManager() {
|
||
return mPenManager;
|
||
}
|
||
|
||
/** 获取OCR引擎 */
|
||
public OCREngine getOCREngine() {
|
||
return mOCREngine;
|
||
}
|
||
|
||
/** 获取网关SDK */
|
||
public GatewaySDK getGatewaySDK() {
|
||
return mGatewaySDK;
|
||
}
|
||
|
||
/** 获取云平台客户端 */
|
||
public CloudClient getCloudClient() {
|
||
return mCloudClient;
|
||
}
|
||
|
||
/** 获取SDK版本 */
|
||
public String getVersion() {
|
||
return SDK_VERSION;
|
||
}
|
||
|
||
/** 获取授权类型 */
|
||
public String getLicenseType() {
|
||
return mLicenseType;
|
||
}
|
||
|
||
/** 检查是否已鉴权 */
|
||
public boolean isAuthenticated() {
|
||
return mIsAuthenticated;
|
||
}
|
||
|
||
/** 用户登录(通过云平台认证) */
|
||
public boolean loginUser(String username, String password) {
|
||
try {
|
||
String response = mCloudClient.login(username, password);
|
||
String accessToken = extractJsonField(response, "accessToken");
|
||
String refreshToken = extractJsonField(response, "refreshToken");
|
||
|
||
if (accessToken != null) {
|
||
long expireTime = System.currentTimeMillis() + 30 * 60 * 1000;
|
||
mCloudClient.setTokens(accessToken, refreshToken, expireTime);
|
||
|
||
/* 缓存令牌 */
|
||
mPrefs.edit()
|
||
.putString("access_token", accessToken)
|
||
.putString("refresh_token", refreshToken)
|
||
.putLong("token_expire", expireTime)
|
||
.apply();
|
||
|
||
return true;
|
||
}
|
||
} catch (IOException e) {
|
||
Log.e(TAG, "登录失败: " + e.getMessage());
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* ========== 资源释放 ========== */
|
||
|
||
/** 释放SDK所有资源 */
|
||
public static void destroy() {
|
||
if (sInstance != null) {
|
||
if (sInstance.mGatewaySDK != null) sInstance.mGatewaySDK.destroy();
|
||
if (sInstance.mOCREngine != null) sInstance.mOCREngine.destroy();
|
||
if (sInstance.mPenManager != null) sInstance.mPenManager.destroy();
|
||
sInstance = null;
|
||
}
|
||
sInitialized.set(false);
|
||
Log.i(TAG, "WritechSDK已释放所有资源");
|
||
}
|
||
|
||
/** 从JSON提取字段值 */
|
||
private String extractJsonField(String json, String key) {
|
||
if (json == null) return null;
|
||
String search = "\"" + key + "\"";
|
||
int idx = json.indexOf(search);
|
||
if (idx < 0) return null;
|
||
int start = json.indexOf("\"", idx + search.length() + 1) + 1;
|
||
int end = json.indexOf("\"", start);
|
||
return (start > 0 && end > start) ? json.substring(start, end) : null;
|
||
}
|
||
}
|