package com.salama.android.webcore;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import MetoXML.XmlDeserializer;
import MetoXML.XmlSerializer;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.webkit.WebView;
import com.salama.android.support.ServiceSupportApplication;
import com.salama.android.util.ResourceFileManager;
import com.salama.android.util.SSLog;
public class WebController {
public static final String WEB_RESOURCE_FILE_DIR_NAME = "res";
public static final String LOCAL_WEB_URL_PREFIX = "file://";
public static final String LOAD_URL_JAVASCRIPT = "javascript:";
private final static String INIT_CHECK_FILE_NAME = ".init_file_CHECK_just_for_check";
private final static String NOTIFICATION_FOR_JAVASCRIPT_USER_INFO_RESULT_NAME = "result";
private enum NativeServiceCmdPositionInBlock {
NativeServiceCmdPositionInBlockFirstCmd,
NativeServiceCmdPositionInBlockNotFirstAndLastCmd,
NativeServiceCmdPositionInBlockLastCmd
};
private String _webPackageName = null;
private String _webBaseDirPath = null;
private String _webRootDirPath = null;
private String _tempPath = null;
//private String _currentDirPath = null;
private ResourceFileManager _resourceFileManager = null;
private ConcurrentHashMap<String, String> _sessionContainer = new ConcurrentHashMap<String, String>();
private NativeService _nativeService;
private static boolean debugMode = true;
private ExecutorService _queueForWeb;
/**
* 取得本地页面压缩包文件名
* @return
*/
public String getWebPackageName() {
return _webPackageName;
}
/**
* 取得本地页面基本目录路径
* @return
*/
public String getWebBaseDirPath() {
return _webBaseDirPath;
}
/**
* 取得本地页面根目录路径
* @return
*/
public String getWebRootDirPath() {
return _webRootDirPath;
}
/**
* 取得临时目录路径
* @return
*/
public String getTempPath() {
return _tempPath;
}
public String toRealPath(String virtualPath) {
if(virtualPath == null) {
return null;
}
File file = new File(_webRootDirPath, virtualPath);
return file.getAbsolutePath();
}
/**
* 取得ResourceFileManager
* @return ResourceFileManager
*/
public ResourceFileManager getResourceFileManager() {
return _resourceFileManager;
}
/**
* 设置ResourceFileManager
* @return
*/
public void setResourceFileManager(ResourceFileManager resourceFileManager) {
_resourceFileManager = resourceFileManager;
}
/**
* 取得nativeService
* @return NativeService
*/
public NativeService getNativeService() {
return _nativeService;
}
/**
* 设置nativeService
* @return
*/
public void setNativeService(NativeService nativeService) {
_nativeService = nativeService;
}
/**
* 设置debug模式
* @param isDebug debug模式
*/
public static void setDebugMode(boolean isDebug) {
debugMode = isDebug;
}
/**
* 构造函数
* @param webPackageName 本地页面压缩包文件名
* @param htmlPackageStream 压缩包InputStream
* @throws IOException
*/
public WebController(String webPackageName, InputStream htmlPackageStream) throws IOException {
this(webPackageName, htmlPackageStream, new File(ServiceSupportApplication.singleton().getFilesDir().getAbsolutePath()));
}
/**
* 构造函数
* @param webPackageName 本地页面压缩包文件名
* @param htmlPackageStream 压缩包InputStream
* @param webBaseDir 本地页面基本目录路径
* @throws IOException
*/
public WebController(String webPackageName, InputStream htmlPackageStream, File webBaseDir) throws IOException {
//default base dir:files
_webBaseDirPath = webBaseDir.getAbsolutePath();
_webPackageName = webPackageName;
initObjs(htmlPackageStream);
}
/**
* 初始化(本地网页目录已存在,无需解压zip)
* @param existingWebRootDir 本地网页根路径
* @return WebController
*/
public WebController(File existingWebRootDir) {
switchToWebRootDirPath(existingWebRootDir.getAbsolutePath());
initServiceObjs();
}
/**
* 装载本地页面
* @param relativeUrl 本地页面URL
* @param webView LocalWebView
*/
public void loadLocalPage(String relativeUrl, LocalWebView webView) {
if(relativeUrl == null) {
return;
}
File absolutePath = new File(_webRootDirPath, relativeUrl);
String url = LOCAL_WEB_URL_PREFIX + absolutePath;
webView.loadUrl(url);
}
/**
* 设置session值
* @param name 名称
* @param value 值
*/
public void setSessionValueWithName(String name, String value) {
_sessionContainer.put(name, value);
}
/**
* 删除session值
* @param name 名称
*/
public void removeSessionValueWithName(String name) {
_sessionContainer.remove(name);
}
/**
* 取得session值
* @param name 名称
* @return session值
*/
public String getSessionValueWithName(String name) {
return _sessionContainer.get(name);
}
public boolean handleUrlLoadingEvent(String url, final WebView webView, final Object thisView) {
final Object msg = NativeService.parseNativeServiceCmd(url);
if(msg != null) {
invokeNativeService(msg, webView, thisView);
} else {
webView.loadUrl(url);
}
return true;
}
/**
* 调用本地Service
* @param msg 指令
* @param webView LocalWebView实例
* @param thisView 当前View实例
*/
public void invokeNativeService(final Object msg, final WebView webView, final Object thisView) {
_queueForWeb.execute(new Runnable() {
@Override
public void run() {
NativeServiceCmdPositionInBlock cmdPosition;
if(NativeService.isInstanceAssignableToClass(msg, List.class)) {
@SuppressWarnings("unchecked")
List<InvokeMsg> msgList = (List<InvokeMsg>)msg;
for(int i = 0; i < msgList.size(); i++) {
if(i == (msgList.size() - 1)) {
cmdPosition = NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockLastCmd;
} else if(i == 0) {
cmdPosition = NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockFirstCmd;
} else {
cmdPosition = NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockNotFirstAndLastCmd;
}
/* Android4.0开始,默认的StrictMode不允许在UIThread里进行网络操作,因而为了接口方法代码的简洁,所有接口调用都在后台Thread里进行。
* 所以,接口方法里如果有UI操作的话,必须放入UIThread。
if(msgList.get(i).getNotification() == null || msgList.get(i).getNotification().length() == 0) {
invokeNativeServiceSingleCmd(msgList.get(i),
webView, thisView, cmdPosition);
} else
{
final InvokeMsg msgTmp = msgList.get(i);
final NativeServiceCmdPositionInBlock cmdPositionTmp = cmdPosition;
_queueForWeb.execute(new Runnable() {
@Override
public void run() {
invokeNativeServiceSingleCmd(msgTmp,
webView, thisView, cmdPositionTmp);
}
});
}
*/
final InvokeMsg msgTmp = msgList.get(i);
final NativeServiceCmdPositionInBlock cmdPositionTmp = cmdPosition;
invokeNativeServiceSingleCmd(msgTmp,
webView, thisView, cmdPositionTmp);
}
} else {
/* Android4.0开始,默认的StrictMode不允许在UIThread里进行网络操作,因而为了接口方法代码的简洁,所有接口调用都在后台Thread里进行。
* 所以,接口方法里如果有UI操作的话,必须放入UIThread。
if(((InvokeMsg)msg).getNotification() == null || ((InvokeMsg)msg).getNotification().length() == 0) {
invokeNativeServiceSingleCmd((InvokeMsg)msg, webView, thisView,
NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockLastCmd);
} else
{
final InvokeMsg msgTmp = (InvokeMsg)msg;
_queueForWeb.execute(new Runnable() {
@Override
public void run() {
invokeNativeServiceSingleCmd(msgTmp, webView, thisView,
NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockLastCmd);
}
});
}
*/
final InvokeMsg msgTmp = (InvokeMsg)msg;
invokeNativeServiceSingleCmd(msgTmp, webView, thisView,
NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockLastCmd);
}
}
});
}
private void invokeNativeServiceSingleCmd(InvokeMsg invokeMsg, final WebView webView,
Object thisView, NativeServiceCmdPositionInBlock cmdPositionInBlock) {
try {
SSLog.d("WebController", "invokeNativeServiceSingleCmd() target:" + invokeMsg.getTarget()
+ " method:" + invokeMsg.getMethod());
//invoke the service method
Object returnVal = _nativeService.invoke(
invokeMsg.getTarget(), invokeMsg.getMethod(),
invokeMsg.getParams(), thisView);
//handle the variableStack
if(NativeService.isInstanceAssignableToClass(thisView, WebVariableStack.class)) {
if(invokeMsg.getReturnValueKeeper() != null
&& invokeMsg.getReturnValueKeeper().length() > 0) {
int scope;
if(invokeMsg.getKeeperScope() == null
|| invokeMsg.getKeeperScope().length() == 0) {
scope = WebVariableStack.WebVariableStackScopeTemp;
} else {
if(invokeMsg.getKeeperScope().equalsIgnoreCase("page")) {
scope = WebVariableStack.WebVariableStackScopePage;
} else if(invokeMsg.getKeeperScope().equalsIgnoreCase("temp")) {
scope = WebVariableStack.WebVariableStackScopeTemp;
} else {
scope = WebVariableStack.WebVariableStackScopeTemp;
}
}
if(returnVal == null) {
((WebVariableStack)thisView).removeVariable(invokeMsg.getReturnValueKeeper(), scope);
} else {
((WebVariableStack)thisView).setVariable(returnVal, invokeMsg.getReturnValueKeeper(), scope);
SSLog.d("WebController", "WebVariableStack setVariable() valueKeeper:" + invokeMsg.getReturnValueKeeper());
}
}
if(cmdPositionInBlock == NativeServiceCmdPositionInBlock.NativeServiceCmdPositionInBlockLastCmd) {
//clear temp scope
((WebVariableStack)thisView).clearVariablesOfScope(WebVariableStack.WebVariableStackScopeTemp);
}
}
//handle callback
final String script = scriptCallBackWhenSucceed(invokeMsg.getCallBackWhenSucceed(), returnVal);
if(invokeMsg.getNotification() == null || invokeMsg.getNotification().length() == 0) {
if(script != null && script.length() > 0) {
SSLog.d("nativeService() script:", script);
getActivityFromThisView(thisView).runOnUiThread(new Runnable() {
@Override
public void run() {
webView.loadUrl(LOAD_URL_JAVASCRIPT + script);
}
});
}
} else {
//post notification
String result = returnValueToResultString(returnVal);
ServiceSupportApplication.singleton().sendWrappedLocalBroadcast(
invokeMsg.getNotification(), result,
NOTIFICATION_FOR_JAVASCRIPT_USER_INFO_RESULT_NAME);
}
returnVal = null;
} catch(Exception e) {
try {
Log.e("WebController", "invokeNativeServiceSingleCmd()", e);
if(invokeMsg.getNotification() == null || invokeMsg.getNotification().length() == 0) {
final String script = scriptCallBackWhenError(invokeMsg.getCallBackWhenError(), e);
if(script != null) {
getActivityFromThisView(thisView).runOnUiThread(new Runnable() {
@Override
public void run() {
webView.loadUrl(LOAD_URL_JAVASCRIPT + script);
}
});
}
} else {
//post notification
String result = returnValueToResultString(errorMsgOfException(e));
ServiceSupportApplication.singleton().sendWrappedLocalBroadcast(
invokeMsg.getNotification(), result,
NOTIFICATION_FOR_JAVASCRIPT_USER_INFO_RESULT_NAME);
}
} catch(Exception e1) {
Log.e("WebController", "invokeNativeServiceSingleCmd()", e1);
}
}
}
private Activity getActivityFromThisView(Object thisViewObj) {
if(Activity.class.isAssignableFrom(thisViewObj.getClass())) {
return (Activity)thisViewObj;
} else if(Fragment.class.isAssignableFrom(thisViewObj.getClass())) {
return ((Fragment)thisViewObj).getActivity();
} else {
try {
return (Activity)thisViewObj.getClass()
.getMethod("getActivity", (Class[])null).invoke(thisViewObj, (Object[])null);
} catch (Exception e) {
Log.e("WebController", "getActivityFromThisView()", e);
return null;
}
}
}
private String returnValueToResultString(Object returnVal)
throws IOException, InvocationTargetException, IllegalAccessException {
if(returnVal == null) {
return "";
} else {
//SSLog.d("WebController", "returnValueToResultString() returnVal.getClass():" + returnVal.getClass().getName());
if(isPrimitiveType(returnVal.getClass())) {
//SSLog.d("WebController", "returnValueToResultString() isPrimitive");
return String.valueOf(returnVal);
} else if(returnVal.getClass().isAssignableFrom(String.class)) {
//SSLog.d("WebController", "returnValueToResultString() isString");
return (String) returnVal;
} else {
return XmlSerializer.objectToString(
returnVal, returnVal.getClass(), false, false);
}
}
}
private boolean isPrimitiveType(Class cls) {
if(cls.isPrimitive()) {
return true;
} else if(cls == Byte.class || cls == Short.class || cls == Integer.class || cls == Long.class
|| cls == Float.class || cls == Double.class) {
return true;
} else {
return false;
}
}
private String scriptCallBackWhenSucceed(String callBackFuncName, Object returnVal)
throws IOException, InvocationTargetException, IllegalAccessException {
if(callBackFuncName != null && callBackFuncName.length() > 0) {
String returnValStr = returnValueToResultString(returnVal);
String callBackFuncScript = callBackFuncName + "('" + encodeToScriptStringValue(returnValStr) + "')";
return callBackFuncScript;
} else {
return null;
}
}
private String errorMsgOfException(Throwable e) {
return e.getClass().getName() + " " + e.getMessage();
}
private String scriptCallBackWhenError(String callBackFuncName, Throwable e) {
if(callBackFuncName == null || callBackFuncName.length() == 0) {
return null;
} else {
String errorMsg = errorMsgOfException(e);
String callBackFuncScript = callBackFuncName + "('" + encodeToScriptStringValue(errorMsg) + "')";
return callBackFuncScript;
}
}
private void initObjs(InputStream htmlPackageStream) throws IOException {
initWebDirPaths(htmlPackageStream);
initServiceObjs();
}
private void initWebDirPaths(InputStream htmlPackageStream) throws IOException {
//
File webRootDir = new File(_webBaseDirPath, _webPackageName);
_webRootDirPath = webRootDir.getAbsolutePath();
SSLog.d("WebController", "_webRootDirPath:" + _webRootDirPath);
resetPathsAfterWebRootDirPathChanged();
//unzip the html.zip
extractWebSource(htmlPackageStream);
}
private void initServiceObjs() {
_queueForWeb = ServiceSupportApplication.singleton().createCachedThreadPool();
_nativeService = new NativeService();
}
/**
* 改变本地网页根路径
*/
public void switchToWebRootDirPath(String webRootDirPath) {
if(webRootDirPath.endsWith("/")) {
_webRootDirPath = webRootDirPath.substring(0, webRootDirPath.length() - 1);
} else {
_webRootDirPath = webRootDirPath;
}
int index = _webRootDirPath.lastIndexOf('/');
_webBaseDirPath = _webRootDirPath.substring(0, index);
_webPackageName = _webRootDirPath.substring(index + 1);
resetPathsAfterWebRootDirPathChanged();
}
private void resetPathsAfterWebRootDirPathChanged() {
File cacheDir = ServiceSupportApplication.singleton().getCacheDir();
File tempDir = new File(cacheDir, "tmp");
_tempPath = tempDir.getAbsolutePath();
if(!tempDir.exists()) {
tempDir.mkdirs();
}
File resStorageDir = new File(_webRootDirPath, WEB_RESOURCE_FILE_DIR_NAME);
_resourceFileManager = new ResourceFileManager(resStorageDir);
}
/**
*
* @return the rootDirName in the zip file
*/
private void extractWebSource(InputStream htmlPackageStream) throws IOException {
//source file is supposed in res/raw
ZipInputStream zipInputS = null;
File webBaseDir = new File(_webBaseDirPath);
try {
boolean isNeedUnzip = false;
if(debugMode) {
isNeedUnzip = true;
SSLog.d("WebController", "In debug mode, then it extracts html.zip every time.");
} else {
File webRootDir = new File(_webRootDirPath);
if(!webRootDir.exists()) {
isNeedUnzip = true;
} else {
//html root dir exists, then check the init_time_check_file
isNeedUnzip = isNeedExtractZipByCheckingInitFile();
}
}
if(!isNeedUnzip) {
return;
}
zipInputS = new ZipInputStream(htmlPackageStream);
ZipEntry entry;
File file = null;
String entryName = null;
byte[] tempBuf = new byte[1024];
FileOutputStream fos = null;
int readCnt;
int entryCount = 0;
while(true) {
entry = zipInputS.getNextEntry();
if(entry == null) {
break;
}
entryCount++;
entryName = entry.getName();
if(entry.isDirectory()) {
//create dir
file = new File(webBaseDir, entryName);
file.mkdir();
} else {
file = new File(webBaseDir, entryName);
//save the file
fos = new FileOutputStream(file);
try {
while(true) {
readCnt = zipInputS.read(tempBuf, 0, tempBuf.length);
if(readCnt <= 0) {
break;
}
fos.write(tempBuf, 0, readCnt);
fos.flush();
}
} finally {
try {
fos.close();
} catch(Exception e) {
}
}
}
zipInputS.closeEntry();
}
SSLog.d("WebController", "Extracted " + entryCount + " zip entries");
} finally {
try {
zipInputS.close();
} catch(Exception e) {
}
try {
htmlPackageStream.close();
} catch(Exception e) {
}
}
}
private boolean isNeedExtractZipByCheckingInitFile() {
File initCheckFile = new File(_webRootDirPath, INIT_CHECK_FILE_NAME);
String curVersion = getCurrentPackageVersion();
if(initCheckFile.exists()) {
//check time
try {
String lastVersion = readAllText(initCheckFile);
SSLog.d("WebController", "isNeedExtractZipByCheckingInitFile() lastVersion:" + lastVersion + " curVersion:" + curVersion);
if(curVersion != null && curVersion.equals(lastVersion)) {
return false;
} else {
return true;
}
} catch (IOException e) {
SSLog.d("WebController", "isNeedExtractZipByCheckingInitTimeCompareToZipFileTime()", e);
return true;
}
} else {
try {
writeTextToFile(initCheckFile, curVersion);
} catch (IOException e) {
SSLog.d("WebController", "isNeedExtractZipByCheckingInitTimeCompareToZipFileTime()", e);
}
return true;
}
}
private String getCurrentPackageVersion() {
try {
PackageInfo pkgInfo = ServiceSupportApplication.singleton().getPackageManager().getPackageInfo(
ServiceSupportApplication.singleton().getPackageName(), 0);
return pkgInfo.versionName;
} catch (NameNotFoundException e) {
SSLog.d("WebController", "getCurrentPackageVersion()", e);
return null;
}
}
private String readAllText(File file) throws IOException {
InputStreamReader reader = null;
FileInputStream fis = null;
StringBuilder sb = new StringBuilder();
char[] chBuff = new char[32];
int readCnt = 0;
try {
fis = new FileInputStream(file);
reader = new InputStreamReader(fis, XmlDeserializer.DefaultCharset);
while(true) {
readCnt = reader.read(chBuff, 0, chBuff.length);
if(readCnt < 0) {
break;
}
if(readCnt > 0) {
sb.append(chBuff, 0, readCnt);
}
}
return sb.toString();
} finally {
try {
fis.close();
} catch(Exception e) {
}
try {
reader.close();
} catch(Exception e) {
}
}
}
private void writeTextToFile(File file, String text) throws IOException {
FileOutputStream fos = null;
OutputStreamWriter writer = null;
try {
fos = new FileOutputStream(file);
writer = new OutputStreamWriter(fos, XmlDeserializer.DefaultCharset);
writer.write(text);
writer.flush();
} finally {
try {
fos.close();
} catch(Exception e) {
}
try {
writer.close();
} catch(Exception e) {
}
}
}
public static String encodeToScriptStringValue(String input) {
if(input == null) {
return null;
}
StringBuilder output = new StringBuilder();
int len = input.length();
char c;
for(int i = 0; i < len; i++) {
c = input.charAt(i);
if(c == '"') {
output.append("\\\"");
} else if (c == '\r') {
output.append("\\r");
} else if (c == '\n') {
output.append("\\n");
} else if (c == '\'') {
output.append("\\'");
} else {
output.append(c);
}
}
return output.toString();
}
}