Android实现文件增量更新

233 阅读7分钟

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();
    }

}