Android中为了支持H5插件更新的特性,增加了插件包增量下载的功能。在获取插件列表的时候,从平台获取插件对应的插件包,然后跟本地插件包进行对比,如果本地没有插件包则下载,如果有插件包了,则通过平台返回的patch包下载,然后再跟本地的插件包合成新包完成插件的增量更新。
文件下载器
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class DevPluginDownloader {
private static final String TAG = DevPluginDownloader.class.getSimpleName();
private OnProgressCallback mOnProgressCallback;
private Long fileSizeFromHttp = -1L;
private PluginDownloadError urlDownloadError = null; //连接连接,下载过程中的错误
// private static final OkHttpClient sDownloadOkHttpClient = new OkHttpClient.Builder()
// .connectTimeout(30, TimeUnit.SECONDS)
// .readTimeout(60, TimeUnit.SECONDS)
// .writeTimeout(60, TimeUnit.SECONDS)
// .connectionPool(new ConnectionPool(1, 5, TimeUnit.SECONDS))
// .addInterceptor(new RetryInterceptor())
// .build();
public DevPluginDownloader() {
}
private synchronized OkHttpClient getNewOkHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
//.addInterceptor(new RetryInterceptor())
.build();
}
public void startDownload(URL url, File tmpFile) throws Exception {
urlDownloadError = null;
Exception error = null;
InputStream input;
RandomAccessFile output = null;
long downloadLength = 0;
if (tmpFile.exists()) {
downloadLength = tmpFile.length();
}
Request request = new Request.Builder().addHeader("Range", "bytes=" + downloadLength + "-")
.url(url).build();
OkHttpClient okHttpClient = getNewOkHttpClient();
Response response = null;
try {
response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
urlDownloadError = PluginDownloadError.CONNECT_ERROR;
urlDownloadError.setCode(String.valueOf(response.code()));
}
ResponseBody body = response.body();
if (body == null) {
throw new IOException("body is null!");
}
input = body.byteStream();
fileSizeFromHttp = body.contentLength() + downloadLength;
output = new RandomAccessFile(tmpFile, "rw");
output.seek(downloadLength);
byte[] buffer = new byte[8192];
int inputSize;
do {
inputSize = input.read(buffer);
if (inputSize == -1) {
break;
}
output.write(buffer, 0, inputSize);
downloadLength += inputSize;
int progress = (int) (downloadLength * 100 / fileSizeFromHttp);
if (null != mOnProgressCallback) {
mOnProgressCallback.onDownloadProgress(progress);
}
} while (true);
} catch (Exception e) {
e.printStackTrace();
error = e;
} finally {
if (null != response) {
try {
response.close();
} catch (Exception e) {
}
}
if (null != output) {
try {
output.close();
} catch (Exception e) {
}
}
if (null != okHttpClient) {
try {
okHttpClient.connectionPool().evictAll();
} catch (Exception e) {
}
}
if (null != error) {
throw error;
}
}
}
public long getFileLength() {
return fileSizeFromHttp;
}
public PluginDownloadError getUrlDownloadError() {
return urlDownloadError;
}
public void setOnProgressCallback(OnProgressCallback callback) {
this.mOnProgressCallback = callback;
}
public static interface OnProgressCallback {
void onDownloadProgress(int progress);
}
}
文件下载任务
import android.os.AsyncTask;
import android.text.TextUtils;
import com.blankj.utilcode.util.NetworkUtils;
import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DevPluginAsyncTask extends AsyncTask<String, Integer, Boolean> implements ITask<DevTaskInfo> {
private int progress = -1;
private Long lastUpdateTime = 0L; //降低刷新频率,在liveDta 模式下是否还需要?
private final IDownloadTaskCallback mIDownloadTaskCallback;
private String localMd5 = "primary null";
private PluginDownloadError afterDownloadError = null; //文件下载完毕后,md5校验,解压等业务的错误
private Exception error = null;
private DevTaskInfo mDevTaskInfo;
private final DevPluginDownloader mDevPluginDownloader;
private static final Map<String, DownloadCacheResult> sCacheResultMap = new HashMap<>();
private static final Set<String> sErrorCodeSet = new HashSet<>();
public DevPluginAsyncTask(IDownloadTaskCallback callback) {
this.mIDownloadTaskCallback = callback;
this.mDevPluginDownloader = new DevPluginDownloader();
}
@Override
protected Boolean doInBackground(String... params) {
long tempFileSize = -1L;
long zipFileSize = -1L;
long startTime = System.currentTimeMillis();
boolean isComplete = false;
boolean isStartUnzip = false;
//检查sd卡的状态
if (!FileUtil.getSDCardStatus()) {
return false;
}
try {
//建立下载的临时文件,拼接最后的插件地址
FileData fileData = PluginFileUtils.INSTANCE.createTmpFile(mDevTaskInfo.curPath, mDevTaskInfo.fileName);
//下载的实际操作
if (!executePatchDownloadPlugin(mDevTaskInfo, fileData.getTmpFile(), getPluginZipPath(mDevTaskInfo))) {
executeDownloadPlugin(new URL(mDevTaskInfo.url), fileData.getTmpFile(), mDevTaskInfo.downloadInfo.getFileMD5());
}
afterDownloadError = PluginDownloadError.MD5_CHECK_ERROR;
tempFileSize = fileData.getTmpFile().length();
//校验下载的插件包的md5
if (!checkMd5(fileData.getTmpFile(), mDevTaskInfo.downloadInfo.getFileMD5())) {
fileData.getTmpFile().delete();
throw new Exception(" md5 check failed!");
}
File destZipFile = new File(fileData.getDestFilePath());
afterDownloadError = PluginDownloadError.SILENCE_MOVE_STOP_ERROR;
//临时压缩包转正式之前,需要判断和终止当前的移动文件的板块
IFileOperationService fileOperation = getFileOperationService();
if (!TextUtils.isEmpty(mDevTaskInfo.moveDesPath) && fileOperation != null) {
fileOperation.cancelFileMoveOperationWithLock();
}
afterDownloadError = PluginDownloadError.TEMP_FILE_TURN_ERROR;
FileUtil.isExistsFileWithDel(mDevTaskInfo.curPath, mDevTaskInfo.fileName, mDevTaskInfo.pluginFolder); //删除旧的插件包和压缩包
PluginFileUtils.INSTANCE.coverZipFile(fileData.getTmpFile(), destZipFile); //下载的临时文件格式,改为正常的格式
zipFileSize = destZipFile.length();
isStartUnzip = true;
afterDownloadError = PluginDownloadError.UNZIP_PROGRESS_ERROR;
//执行插件解压的逻辑,解压到一个临时路径下
publishProgress(0, 0); //传递开始解压
executeUnzip(mDevTaskInfo.pluginFolder, fileData.getDestFilePath(), mDevTaskInfo.curPath); //解压下载完毕的插件
//创建解压完成的标志文件
File pluginFile = new File(mDevTaskInfo.curPath, mDevTaskInfo.pluginFolder);
new File(pluginFile, DownloadConstant.completeFile).createNewFile();
afterDownloadError = PluginDownloadError.SILENCE_MOVE_EXECUTE_ERROR;
//判断是否需要搬移插件包
movePlugin(mDevTaskInfo, fileOperation, destZipFile, pluginFile);
afterDownloadError = null; //执行到这里,说明md5 解压什么鬼的都完成了,这个报错可以清空了
isComplete = true;
} catch (Exception e) {
error = e;
}
return isComplete;
}
private String getPluginZipPath(DevTaskInfo info) {
return FileUtils.CUSPATH + info.fileName;;
}
private void movePlugin(DevTaskInfo info, IFileOperationService fileOperation, File destZipFile, File pluginFile) throws Exception {
if (info.moveDesPath != null && fileOperation != null && fileOperation.isFileCanMove()) {
//这里加锁是为了处理业务的操作
ReentrantLock lock = fileOperation.getFileLock(info.pluginFolder);
if (!lock.isLocked()) { //如果被锁,就说明打开插件在处理,优先让步,就不搬移了
boolean locRet = lock.tryLock(3, TimeUnit.SECONDS);
if (locRet) {
//因为解压之前就已经调用了cancelFileMoveOperationWithLock 的逻辑,所以这里就可以直接搬移处理
File destFile = new File(info.moveDesPath);
PluginMoveRunnable mover = new PluginMoveRunnable(new File(info.curPath), destFile);
mover.moveZip(destZipFile, destFile);
mover.moveFile(pluginFile, destFile);
unlock(lock);
}
}
}
}
private IFileOperationService getFileOperationService() {
IFileOperationService fileOperation = ServiceLoaderHelper.getService(IFileOperationService.class);
return fileOperation;
}
private void unlock(Lock lock) {
try {
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onProgressUpdate(Integer... progress) {
super.onProgressUpdate(progress);
if (null == mIDownloadTaskCallback) return;
if (progress[0] == 0) {
mIDownloadTaskCallback.onUnZip();
} else if (progress[0] == 1) {
mIDownloadTaskCallback.onDownload(progress[1]);
}
}
@Override
protected void onPostExecute(Boolean success) {
super.onPostExecute(success);
if (null == mIDownloadTaskCallback) return;
if (success) {
mIDownloadTaskCallback.onFinish();
} else {
mIDownloadTaskCallback.onFail();
}
}
@Override
public synchronized void startTask(DevTaskInfo taskInfo) {
if (getStatus() != Status.PENDING || null == taskInfo) {
return;
}
mDevTaskInfo = taskInfo;
if (taskInfo.isPluginLiteCard) {
executeOnExecutor(DevPluginExecutor.LITE_DEV_POOL_EXECUTOR);
} else {
executeOnExecutor(DevPluginExecutor.DEV_POOL_EXECUTOR);
}
}
private boolean executePatchDownloadPlugin(DevTaskInfo info, File tmpFile, String oldFilePath) {
String diffFileUrl = info.downloadInfo.getDiffFileUrl();
String tempPatchPath = oldFilePath + ".patch";
if (TextUtils.isEmpty(diffFileUrl)) {
return false;
}
if (!(new File(oldFilePath).exists())) {
return false;
}
try {
File tempPatchFile = new File(tempPatchPath);
executeDownloadPlugin(new URL(diffFileUrl), tempPatchFile, info.downloadInfo.getDiffFileMd5());
if (!checkMd5(tempPatchFile, info.downloadInfo.getDiffFileMd5())) {
tempPatchFile.delete();
return false;
}
String tempPath = tmpFile.getAbsolutePath();
if (tmpFile.exists()) tmpFile.delete();
// 将下载的插件与本地插件合并
BS.INSTANCE.patch(oldFilePath, tempPath, tempPatchPath);
tempPatchFile.delete();
if (!checkMd5(tmpFile, info.downloadInfo.getFileMD5())) {
tmpFile.delete();
tmpFile.createNewFile();
return false;
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private void executeDownloadPlugin(URL url,
File tmpFile, String fileMd5) throws Exception {
DevPluginAsyncTask.this.progress = 0;
mDevPluginDownloader.setOnProgressCallback(progress -> {
if (DevPluginAsyncTask.this.progress != progress) {
long updateTime = System.currentTimeMillis();
long interval = updateTime - DevPluginAsyncTask.this.lastUpdateTime;
if (interval < 500) {
return;
}
DevPluginAsyncTask.this.lastUpdateTime = updateTime;
publishProgress(1, progress); //传递进度
DevPluginAsyncTask.this.progress = progress;
}
});
int retryNum = 3;
do {
try {
mDevPluginDownloader.startDownload(url, tmpFile);
if (!checkMd5(tmpFile, fileMd5)) {
retryNum--;
if (retryNum > 0 && tmpFile.exists()) {
tmpFile.delete();
tmpFile.createNewFile();
}
} else {
retryNum = 0;
}
} catch (Exception e) {
String eCode = PluginCustomExceptionHandler.INSTANCE.transformException(e).getErrorCode();
if (TextUtils.equals("-101", eCode) && tmpFile.exists()) {
tmpFile.delete();
tmpFile.createNewFile();
}
retryNum--;
if (retryNum <= 0) {
throw e;
} else {
if (TextUtils.equals("-101", eCode)) {
try {
Thread.sleep(10000);
} catch (Exception exception) {
}
}
}
}
} while (retryNum > 0);
}
//进行md5的校验
private boolean checkMd5(File destFile, String netMd5) {
if (!TextUtils.isEmpty(netMd5)) { //文件检验
localMd5 = MD5util.getFileMD5(destFile);
if (!netMd5.equalsIgnoreCase(localMd5)) {
return false;
}
}
return true;
}
/**
* 执行插件解压
*/
private void executeUnzip(
String pluginFolder, //设备类型
String zipFileStr,
String curPath
) throws Exception {
boolean notUnzip = false;
String deleType = null;
if (PluginPackageManager.NOT_UNZIP_TYPE != null && PluginPackageManager.NOT_UNZIP_TYPE.length > 0) {
for (String type : PluginPackageManager.NOT_UNZIP_TYPE) {
if (pluginFolder.toLowerCase().endsWith(type.toLowerCase())) {
notUnzip = true;
deleType = type;
break;
}
}
}
if (notUnzip) {
if (!TextUtils.isEmpty(deleType)) {
if (mDevTaskInfo.isIrType) {
FileUtils.deleteAllFiles(new File(FileUtils.IR_CUSPATH, deleType));
} else if (mDevTaskInfo.isPluginLiteCard) {
FileUtils.deleteAllFiles(new File(FileUtils.LITE_CUSPATH, "T0x" + deleType));
} else {
FileUtils.deleteAllFiles(new File(FileUtils.CUSPATH, "T0x" + deleType));
}
}
} else {
ZipUtil.unZipFilesAdmix(new File(zipFileStr), new File(curPath), null);
}
}
}
任务接口
public interface ITask<T> {
void startTask(T t);
}
文件工具类
package com.midea.business.plugin.util;
import android.os.Environment;
import java.io.File;
public class FileUtil {
static String TAG = "FileUtil";
/**
* 判断文件是否存在
*
* @param path 文件路径
* @return
*/
public static boolean isExistFile(String path) {
boolean exist = new File(path).exists();
return exist;
}
/**
* 获取sd卡状态
*
* @return
*/
public static Boolean getSDCardStatus() {
String status = Environment.getExternalStorageState();
return status.equals(Environment.MEDIA_MOUNTED);
}
/**
* 判断是否存在相同文件
*
* @param
*/
public static void isExistsFileWithDel(String CUSPATH, String fileName, String appType) {
String pathSuf = CUSPATH + fileName;
String path = CUSPATH + appType;
File fileSuf = new File(pathSuf);
File file = new File(path);
if (fileSuf.exists()) {
fileSuf.delete();
}
if (file.exists()) {
file.delete();
}
}
}
插件工具类
package com.midea.business.plugin.util
import java.io.File
object PluginFileUtils {
fun createTmpFile(curPath: String?, fileName: String): FileData {
val curFile = File(curPath)
if (!curFile.exists()) {
curFile.mkdirs()
}
val isZip = true;
//保存文件的地址
val destfilePath = curPath + fileName
//下载的临时文件
val tmpfilePath = destfilePath + if (isZip) ".cache" else ""
var tmpFile = File(tmpfilePath)
try {
if (tmpFile.exists() && !isTempFileValid(tmpFile)) {//临时文件如果存在30分钟以上就删掉
tmpFile.delete()
tmpFile = File(tmpfilePath)
}
if (!tmpFile.exists()) {
tmpFile.createNewFile()
}
} catch (e: Exception) {
e.printStackTrace()
}
return FileData(destfilePath, tmpFile)
}
//把插件下载的临时文件覆盖成正式文件的命名
fun coverZipFile(
tmpFile: File,
destFile: File
) {
PluginPackageManager.getInstance().closeZip(destFile.name)
try {
destFile.delete()
} catch (e: Exception) {
e.printStackTrace()
}
tmpFile.renameTo(destFile)
}
private fun isTempFileValid(file: File): Boolean {
if (!file.exists()) {
return false
}
val lastTime = file.lastModified()
val nowTime = System.currentTimeMillis()
return nowTime - lastTime >= 0 && nowTime - lastTime < 30 * 60 * 1000
}
}
data class FileData(val destFilePath: String, val tmpFile: File)
插件包管理类
import android.text.TextUtils;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import com.file.zip.ZipEntry;
import com.file.zip.ZipFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
public class PluginPackageManager {
public static String NOT_UNZIP_TYPE[]={};
static final String TAG="PluginPackageManager";
static PluginPackageManager instance;
HashMap<String,ZipFile> zipFiles=new HashMap<>();
public static PluginPackageManager getInstance(){
if(instance==null){
instance=new PluginPackageManager();
}
return instance;
}
public void closeZip(String zipName){
synchronized (zipFiles) {
if(zipFiles.containsKey(zipName)){
ZipFile file=zipFiles.get(zipName);
zipFiles.remove(zipName);
try {
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public InputStream getInputStreamCheckEnterprise(String path){
String relativeString = path.substring(FileUtils.CUSPATH.length());
//0000_T0xAC
if (relativeString.length()<10){
return null;
}
String zipName = null;
if (relativeString.substring(4).startsWith("_T0x")){
zipName = relativeString.replaceFirst("_T0x","_0x");
} else {
return null;
}
String relativeString2 = relativeString.substring(5);
if (zipName.contains(File.separator)){
zipName = zipName.substring(0,relativeString.indexOf(File.separator)-1);
}
zipName += ".zip";
ZipFile zipFile = null;
synchronized (zipFiles) {
zipFile = zipFiles.get(zipName);
}
if (zipFile == null) {
try {
zipFile = new ZipFile(FileUtils.CUSPATH+zipName, "GBK");
} catch (IOException e) {
e.printStackTrace();
try {
zipFile = new ZipFile(FileUtils.CUSPATH+zipName, "UTF-8");
} catch (IOException ex) {
ex.printStackTrace();
}
}
if (zipFile!=null) {
synchronized (zipFiles) {
zipFiles.put(zipName, zipFile);
}
}
}
if (zipFile == null){
return null;
}
ZipEntry entry = zipFile.getEntry(relativeString);
if (entry == null){
entry = zipFile.getEntry(relativeString2);
}
try {
return zipFile.getInputStream(entry);
} catch (IOException e) {
return null;
}
}
public InputStream getInputStream(String path){
InputStream in=null;
if(TextUtils.isEmpty(path)) return null;
String preKey= FileUtils.CUSPATH+"T";
if(path.startsWith(preKey)){
//企业码扩展之后,这里的逻辑理论上是执行不了的
String filePath="";
if (path.length() >= FileUtils.CUSPATH.length()) {
filePath = path.substring(FileUtils.CUSPATH.length());
}
if(filePath.startsWith("T0x")) {
//个别产品问题 空气能热水器电控返回的类型码为小写,这里统一转为大写
filePath = "T0x" + filePath.substring(3, 5).toUpperCase() + filePath.substring(5);
}
String zipName= "";
if (filePath.length()>=1){
zipName = filePath.substring(1);
}
zipName=zipName.substring(0,zipName.indexOf('/'))+".zip";
ZipFile zipFile=null;
synchronized (zipFiles) {
zipFile = zipFiles.get(zipName);
}
if(zipFile==null){
try {
zipFile=new ZipFile(FileUtils.CUSPATH+zipName,"GBK");
synchronized (zipFiles) {
zipFiles.put(zipName, zipFile);
}
} catch (IOException e) {
e.printStackTrace();
}
if(zipFile==null){
try {
zipFile=new ZipFile(FileUtils.CUSPATH+zipName,"UTF-8");
synchronized (zipFiles) {
zipFiles.put(zipName, zipFile);
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
ZipEntry entry=zipFile.getEntry(filePath);
/*if(entry==null) {
Enumeration<ZipEntry> entrys = zipFile.getEntries();
while (entrys.hasMoreElements()) {
ZipEntry entry1 = entrys.nextElement();
DOFLogUtil.i(TAG, "pluginUpdate getINputstream searchEntry:" + filePath + " extry=" + entry1.getName());
if (entry1.getName().equals(filePath)) {
entry = entry1;
}
}
}*/
if(entry==null) {
return null;
}
try {
return zipFile.getInputStream(entry);
} catch (IOException e) {
return null;
}
} else if(path.startsWith(FileUtils.CUSPATH)){
InputStream inputStream = getInputStreamCheckEnterprise(path);
if (inputStream!=null){
return inputStream;
}
String filePath= "";
if (path.length() >= FileUtils.CUSPATH.length()) {
filePath = path.substring(FileUtils.CUSPATH.length());
}
String zipName="card_base.zip";
ZipFile zipFile=null;
synchronized (zipFiles) {
zipFile=zipFiles.get(zipName);
}
if(zipFile==null){
try {
zipFile=new ZipFile(FileUtils.CUSPATH+zipName);
synchronized (zipFiles) {
zipFiles.put(zipName, zipFile);
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
ZipEntry entry=zipFile.getEntry(filePath);
if(entry==null) return null;
try {
return zipFile.getInputStream(entry);
} catch (IOException e) {
return null;
}
}else if(path.startsWith(FileUtils.IR_CUSPATH)){
String filePath="";
if (path.length() >= FileUtils.IR_CUSPATH.length()) {
filePath = path.substring(FileUtils.IR_CUSPATH.length());
}
String zipName=filePath;
zipName=zipName.substring(0,zipName.indexOf('/'))+".zip";
ZipFile zipFile=null;
synchronized (zipFiles) {
zipFile = zipFiles.get(zipName);
}
if(zipFile==null){
try {
zipFile=new ZipFile(FileUtils.IR_CUSPATH+zipName,"GBK");
synchronized (zipFiles) {
zipFiles.put(zipName, zipFile);
}
} catch (IOException e) {
e.printStackTrace();
}
if(zipFile==null){
try {
zipFile=new ZipFile(FileUtils.IR_CUSPATH+zipName,"UTF-8");
synchronized (zipFiles) {
zipFiles.put(zipName, zipFile);
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
ZipEntry entry=zipFile.getEntry(filePath);
if(entry==null) {
return null;
}
try {
return zipFile.getInputStream(entry);
} catch (IOException e) {
return null;
}
}
return in;
}
public static WebResourceResponse getWebResourceResponse(WebView view, String url) {
try {
url= URLDecoder.decode(url,"utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
WebResourceResponse response = null;
String key="/MideaHome/T";
if(!url.contains(key)) return null;
int idx=url.indexOf(key);
String path=url.substring(idx+"/MideaHome/".length());
path= FileUtils.CUSPATH+path;
if(path.contains("?"))
path=path.substring(0,path.indexOf("?"));
if(path.contains("#"))
path=path.substring(0,path.indexOf("#"));
InputStream in=PluginPackageManager.getInstance().getInputStream(path);
if(in==null) return null;
int extIdx=path.lastIndexOf('.');
// TODO: 2019-12-16 代码恢复,这里不能随意改异常捕获的情况,若是要修改,要自测保证H5设备插件打开不受影响
String extName="*";
if(extIdx>0)
extName=path.substring(extIdx);
String mime= PluginPackageManager.mimeMap.get(extName);
DOFLogUtil.i("shouldInterceptRequest","return WebResourceResponse("+(mime==null?"text/html":mime)+",UTF-8,in");
return new WebResourceResponse(mime==null?"text/html":mime,"UTF-8",in);
}
}
差分工具类
object BS {
init {
System.loadLibrary("common_bs")
}
external fun patch(oldFile: String, newFile: String, patchFile: String): Boolean
external fun diff(diffFile1: String, diffFile2: String, resultFile: String): Boolean
}
文件操作服务
import java.util.concurrent.locks.ReentrantLock
interface IFileOperationService {
/**
* 业务层标志是否可以搬移
*/
fun setFileCanMove(canMove: Boolean)
/**
* 当前环境是否可以执行文件移动操作
*/
fun isFileCanMove(): Boolean
/**
* 触发插件的批量下载,但是要先调用setFileCanMove,将其置为true
*/
fun startFileMoveOperation()
// /**
// * 关闭"全量文件操作",并且监听结果,这个操作不会影响 setFileCanMove 标志位
// */
// fun cancelFileMoveOperation(fileName: String?,fileOperationListener: FileOperationListener)
/**
* 关闭"全量文件操作",这个方法会有一定的阻塞等待,仅适合后台线程使用
*/
fun cancelFileMoveOperationWithLock()
/***
* 这是根据插件名字的分段锁,
*/
fun getFileLock(fileName:String): ReentrantLock
}
import java.io.File
import java.util.concurrent.locks.ReentrantLock
class FileOperationService : IFileOperationService {
private val TAG = javaClass.simpleName
var thread: Thread? = null
private var pluginMoveRunnable = PluginMoveRunnable(File(FileUtils.CUSPATH, "downLoadTempPlugins"), File(FileUtils.CUSPATH))
var isCanMove = false
override fun setFileCanMove(canMove: Boolean) {
isCanMove = canMove
if (!isCanMove){
//直接停止插件文件的搬移
pluginMoveRunnable.isCancel = true
}
}
override fun isFileCanMove(): Boolean {
return isCanMove
}
//主动触发插件的搬移
override fun startFileMoveOperation() {
if (!isCanMove) {
return
}
if (isMoveThreadAlive()) {
return
}
thread = Thread(pluginMoveRunnable)
pluginMoveRunnable.isCancel = false
thread?.let {
it.start()
return
}
}
override fun cancelFileMoveOperationWithLock() {
if (isMoveThreadAlive()) {
pluginMoveRunnable.cancelFileMoveOperationWithLock()
}
}
private fun isMoveThreadAlive(): Boolean {
thread?.let {
if (it.isAlive) {
return true
}
}
return false
}
val fileLockMap = hashMapOf<String, ReentrantLock>()
override fun getFileLock(fileName: String): ReentrantLock {
var lock = fileLockMap.get(fileName)
if (lock == null) {
lock = ReentrantLock()
fileLockMap.put(fileName, lock)
}
return lock
}
}
import java.io.File
import java.lang.Exception
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class PluginMoveRunnable(val rootFile: File, val desFile: File) : Runnable {
val TAG = javaClass.simpleName
val lock = ReentrantLock()
var isCancel = false
fun moveZip(from: File, des: File): Boolean {
val desZip = File(des, from.name)
cmdReMove(desZip)
var ret = cmdMove(from, des)
if (!ret) {
ret = codeMove(from, des)
}
return ret
}
fun moveFile(from: File, des: File): Boolean {
//step1 判断当前文件夹是否是在解压中
if (!File(from, DownloadConstant.completeFile).exists()) {
//因为插件解压到一半可能会移除文件夹,所以忽略本次操作
return false
}
var ret = false
//step2 判断锁文件是否存在
val desFolder = File(des, from.name)
val fromLock = File(from, DownloadConstant.moveLock)
val desLock = File(desFolder, DownloadConstant.moveLock)
PluginDOFLog.i(PLUGIN_LOG, "$TAG ${desLock.absolutePath} judest ")
if (fromLock.exists() || desLock.exists()) {
//step 2.5 如果存在,就只是覆盖迁移,直接返回结果
ret = cmdMove(from, des)
desLock.delete()
fromLock.delete()
return ret
}
//step2 建立 "移动锁"文件,以确保文件移动的时候,用户退出等异常情况
fromLock.createNewFile()
//step3 清空旧插件
cmdReMove(desFolder)
//step4 使用mv指令搬除当前的文件
ret = cmdMove(from, des)
//step4.5 如果命令移动出现问题了。。 可能原因:已经有目标文件了
if (!ret) {
codeMove(from, des)
}
//step5 删掉对应的移动文件锁
desLock.delete()
fromLock.delete()
return ret
}
fun cancelFileMoveOperationWithLock(): Boolean {
var ret = false
try {
ret = lock.tryLock(3, TimeUnit.SECONDS)
isCancel = true
if (ret) {
lock.unlock()
}
} catch (e: Exception) {
e.printStackTrace()
}
return ret
}
/**
* 用命令行的方式删除文件,快
*/
private fun cmdReMove(file: File): Boolean {
val cmd = "rm -rf ${file.absolutePath}"
return cmdExec(cmd)
}
/**
* 用命令行的移动覆盖方式,很快,但是无法覆盖操作
*/
private fun cmdMove(src: File, des: File): Boolean {
val cmd = "mv ${src.absolutePath} ${des.absolutePath}"
return cmdExec(cmd)
}
private fun cmdExec(cmd: String): Boolean {
val startTime = System.currentTimeMillis()
val process = Runtime.getRuntime().exec(cmd)
val ret = process?.waitFor() == 1
val endTime = System.currentTimeMillis() - startTime
return ret
}
/**
* 用代码执行的移动覆盖方式,慢,但是能够覆盖操作
*/
private fun codeMove(src: File, des: File): Boolean {
val startTime = System.currentTimeMillis()
val ret = FileUtils.moveDir(src, des, null)
val endTime = System.currentTimeMillis() - startTime
return ret
}
//遍历所有的文件,将符合格式,可一串的
override fun run() {
val files = rootFile.listFiles() ?: return
for (subFile in files) {
try {
val isLock = lock.tryLock(3, TimeUnit.SECONDS)
if (isCancel) {
return
}
if (subFile.name.endsWith("zip")) {
moveZip(subFile, desFile)
} else {
subFile.listFiles()?.size?.let {
if (it > 0) {
moveFile(subFile, desFile)
}
}
}
if (isLock) {
lock.unlock()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
MD5工具
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
public class MD5util {
public static String getFileMD5(File file) {
if (!file.isFile()) {
return "local file is null";
}
MessageDigest digest = null;
FileInputStream in = null;
byte buffer[] = new byte[1024];
int len;
boolean isFailed = false;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer, 0, 1024)) != -1) {
digest.update(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
isFailed = true;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (isFailed) {
return "failed read file for checking md5";
}
}
// BigInteger bigInt = new BigInteger(1, digest.digest());
// return bigInt.toString(16);
return bytesToHexString(digest.digest());
}
public static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return "md5 byte to hex failed";
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
}