├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── .project ├── .settings └── org.eclipse.buildship.core.prefs ├── LICENSE ├── README.md ├── app ├── .classpath ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── build.gradle ├── proguard-rules.pro ├── release │ ├── mwbpcontainer_v1.3.8_221107.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mcuking │ │ └── mwbpcontainer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── filedownloader.properties │ │ └── main.zip │ ├── java │ │ └── com │ │ │ └── mcuking │ │ │ └── mwbpcontainer │ │ │ ├── MainActivity.java │ │ │ ├── application │ │ │ └── MWBPApplication.java │ │ │ ├── network │ │ │ └── JsApi.java │ │ │ └── utils │ │ │ └── CalendarReminderUtils.java │ ├── jniLibs │ │ ├── armeabi-v7a │ │ │ └── libApkPatchLibrary.so │ │ └── armeabi │ │ │ └── libApkPatchLibrary.so │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── icon.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── mcuking │ └── mwbpcontainer │ └── ExampleUnitTest.java ├── assets ├── architecture.png └── principle.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── webpackagekit ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── hht │ └── webpackagekit │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── hht │ │ ├── lib │ │ └── bsdiff │ │ │ └── PatchUtils.java │ │ └── webpackagekit │ │ ├── OfflineWebViewClient.java │ │ ├── PackageConfig.java │ │ ├── PackageManager.java │ │ ├── PackageValidator.java │ │ ├── core │ │ ├── AssetResourceLoader.java │ │ ├── Constants.java │ │ ├── Downloader.java │ │ ├── PackageEntity.java │ │ ├── PackageInfo.java │ │ ├── PackageInstaller.java │ │ ├── PackageStatus.java │ │ ├── ResourceInfo.java │ │ ├── ResourceInfoEntity.java │ │ ├── ResourceKey.java │ │ ├── ResourceManager.java │ │ ├── ResoureceValidator.java │ │ └── util │ │ │ ├── FileUtils.java │ │ │ ├── GsonUtils.java │ │ │ ├── Logger.java │ │ │ ├── MD5Utils.java │ │ │ ├── MimeTypeUtils.java │ │ │ └── VersionUtils.java │ │ └── inner │ │ ├── AssetResourceLoaderImpl.java │ │ ├── DownloaderImpl.java │ │ ├── PackageInstallerImpl.java │ │ └── ResourceManagerImpl.java ├── jniLibs │ ├── armeabi-v7a │ │ └── libApkPatchLibrary.so │ └── armeabi │ │ └── libApkPatchLibrary.so └── res │ └── values │ └── strings.xml └── test └── java └── com └── hht └── webpackagekit └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | mobile-web-best-practice-container 4 | Project mobile-web-best-practice-container created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mcuking 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mobile-web-best-practice-container 2 | 3 | ## Hybrid App 架构: 4 | 5 | 6 | 7 | 包含以下几个方面: 8 | 9 | 1. JS 通信及 API 设计 10 | 11 | 2. 离线包设计 12 | 13 | #### 相关项目 14 | 15 | H5 项目: [mobile-web-best-practice](https://github.com/mcuking/mobile-web-best-practice) 16 | 17 | 离线包管理平台:[offline-package-admin](https://github.com/mcuking/offline-package-admin) 18 | 19 | 离线包 webpack 插件:[offline-package-webpack-plugin](https://github.com/mcuking/offline-package-webpack-plugin) 20 | 21 | ## 离线包方案 22 | 23 | 原理如下图所示: 24 | 25 | 26 | 27 | 整体方案说明文章: 28 | 29 | [Hybrid App 离线包方案实践](https://github.com/mcuking/blog/issues/63) 30 | 31 | ## 已有功能 32 | 33 | 1. 集成 [DSBridge-Android](https://github.com/wendux/DSBridge-Android) 34 | 35 | 2. 向 h5 提供同步到本地日历功能,API 如下: 36 | 37 | ```ts 38 | interface SyncCalendarParams { 39 | id: string; // 日程唯一标识符 40 | title: string; // 日程名称 41 | location: string; // 日程地址 42 | startTime: number; // 日程开始时间 43 | endTime: number; // 日程结束时间 44 | alarm: number[]; // 提前提醒时间,单位分钟 45 | } 46 | 47 | dsbridge.call('syncCalendar', params: SyncCalendarParams, cb); 48 | ``` 49 | -------------------------------------------------------------------------------- /app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 31 5 | defaultConfig { 6 | applicationId "com.mcuking.mwbpcontainer" 7 | minSdkVersion 23 8 | targetSdkVersion 31 9 | versionCode 138 10 | versionName "1.3.8" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | ndk { 13 | abiFilters "armeabi-v7a", "x86" 14 | } 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | namespace 'com.mcuking.mwbpcontainer' 23 | 24 | android.applicationVariants.all { 25 | variant -> 26 | variant.outputs.all { 27 | //这里修改apk文件名 28 | outputFileName = "mwbpcontainer_v${variant.versionName}_${releaseTime()}.apk" 29 | } 30 | } 31 | } 32 | 33 | static def releaseTime() { 34 | return new Date().format("yyMMdd", TimeZone.getTimeZone("UTC")) 35 | } 36 | 37 | dependencies { 38 | implementation project(':webpackagekit') 39 | implementation fileTree(dir: 'libs', include: ['*.jar']) 40 | implementation 'com.squareup.okhttp3:okhttp:4.2.1' 41 | implementation 'androidx.appcompat:appcompat:1.1.0' 42 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 43 | implementation 'com.github.wendux:DSBridge-Android:3.0-SNAPSHOT' 44 | implementation 'org.greenrobot:eventbus:3.1.1' 45 | testImplementation 'junit:junit:4.12' 46 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 48 | } 49 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/release/mwbpcontainer_v1.3.8_221107.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/release/mwbpcontainer_v1.3.8_221107.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.mcuking.mwbpcontainer", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 138, 15 | "versionName": "1.3.8", 16 | "outputFile": "mwbpcontainer_v1.3.8_221107.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mcuking/mwbpcontainer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.mcuking.mwbpcontainer; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.mcuking.mwbpcontainer", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/assets/filedownloader.properties: -------------------------------------------------------------------------------- 1 | # The FileDownloadService runs in the separate process ':filedownloader' as default, if you want to 2 | # run the FileDownloadService in the main process, just set true. default false. 3 | process.non-separate=true -------------------------------------------------------------------------------- /app/src/main/assets/main.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/assets/main.zip -------------------------------------------------------------------------------- /app/src/main/java/com/mcuking/mwbpcontainer/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mcuking.mwbpcontainer; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.content.pm.PackageInfo; 6 | import android.content.pm.PackageManager; 7 | import androidx.core.app.ActivityCompat; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import android.os.Bundle; 10 | import android.webkit.WebChromeClient; 11 | import android.webkit.WebSettings; 12 | import android.view.KeyEvent; 13 | import android.view.View; 14 | 15 | import com.mcuking.mwbpcontainer.network.JsApi; 16 | import com.hht.webpackagekit.OfflineWebViewClient; 17 | 18 | import wendu.dsbridge.DWebView; 19 | 20 | @SuppressLint("SetJavaScriptEnabled") 21 | public class MainActivity extends AppCompatActivity { 22 | private DWebView mWebview; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | 28 | setContentView(R.layout.activity_main); 29 | 30 | int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 31 | getWindow().getDecorView().setSystemUiVisibility(option); 32 | 33 | // 设置状态栏背景色和字体颜色 34 | getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); 35 | 36 | getWindow().setStatusBarColor(getResources().getColor(R.color.colorPrimary, getTheme())); 37 | 38 | // 请求获取日历权限 39 | requestPermission(); 40 | 41 | mWebview = findViewById(R.id.mWebview); 42 | 43 | // 向 js 环境注入 ds_bridge 44 | mWebview.addJavascriptObject(new JsApi(), null); 45 | 46 | // 复写 WebviewClient 类 47 | mWebview.setWebViewClient(new OfflineWebViewClient()); 48 | 49 | // 可复写 WebviewChromeClient 50 | mWebview.setWebChromeClient(new WebChromeClient()); 51 | 52 | WebSettings mWebSettings = mWebview.getSettings(); 53 | 54 | // 如果访问的页面中要与Javascript交互,则WebView必须设置支持Javascript 55 | mWebSettings.setJavaScriptEnabled(true); 56 | 57 | // 开启DOM缓存 58 | mWebSettings.setDomStorageEnabled(true); 59 | mWebSettings.setDatabaseEnabled(true); 60 | 61 | // 使用WebView中缓存 62 | mWebSettings.setCacheMode(WebSettings.LOAD_DEFAULT); 63 | 64 | // 支持 Chrome 调试 65 | DWebView.setWebContentsDebuggingEnabled(true); 66 | 67 | // 获取 app 版本 68 | PackageManager packageManager = getPackageManager(); 69 | PackageInfo packInfo = null; 70 | try { 71 | // getPackageName()是你当前类的包名,0代表是获取版本信息 72 | packInfo = packageManager.getPackageInfo(getPackageName(),0); 73 | } catch (PackageManager.NameNotFoundException e) { 74 | e.printStackTrace(); 75 | } 76 | assert packInfo != null; 77 | String appVersion = packInfo.versionName; 78 | 79 | // 获取系统版本 80 | String systemVersion = android.os.Build.VERSION.RELEASE; 81 | 82 | mWebSettings.setUserAgentString( 83 | mWebSettings.getUserAgentString() + " DSBRIDGE_" + appVersion + "_" + systemVersion + "_android" 84 | ); 85 | 86 | mWebview.loadUrl("https://mcuking.github.io/mobile-web-best-practice/"); 87 | } 88 | 89 | // 复写安卓返回事件 转为响应 h5 返回 90 | @Override 91 | public boolean onKeyDown(int keyCode, KeyEvent event) { 92 | if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebview.canGoBack()) { 93 | mWebview.goBack(); 94 | return true; 95 | } else { 96 | this.finish(); 97 | } 98 | return super.onKeyDown(keyCode, event); 99 | } 100 | 101 | // 请求获取日历权限 102 | private void requestPermission() { 103 | ActivityCompat.requestPermissions(this, new String[] { 104 | Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR 105 | }, 0); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcuking/mwbpcontainer/application/MWBPApplication.java: -------------------------------------------------------------------------------- 1 | package com.mcuking.mwbpcontainer.application; 2 | 3 | import android.content.Context; 4 | import android.app.Application; 5 | import android.util.Log; 6 | 7 | import com.hht.webpackagekit.PackageManager; 8 | import com.hht.webpackagekit.core.Constants; 9 | 10 | import java.io.IOException; 11 | 12 | import okhttp3.OkHttpClient; 13 | import okhttp3.Request; 14 | import okhttp3.Response; 15 | import org.greenrobot.eventbus.EventBus; 16 | import org.greenrobot.eventbus.Subscribe; 17 | import org.greenrobot.eventbus.ThreadMode; 18 | 19 | public class MWBPApplication extends Application { 20 | 21 | private static Context context; 22 | 23 | @Override 24 | public void onCreate() { 25 | super.onCreate(); 26 | context = getApplicationContext(); 27 | EventBus.getDefault().register(this); 28 | 29 | PackageManager.getInstance().init(context); 30 | getPackageIndex(Constants.BASE_PACKAGE_INDEX); 31 | } 32 | 33 | private void getPackageIndex(final String url){ 34 | new Thread(new Runnable() { 35 | @Override 36 | public void run() { 37 | try { 38 | OkHttpClient client = new OkHttpClient(); 39 | Request request = new Request.Builder().url(url).build(); 40 | try (Response response = client.newCall(request).execute()) { 41 | String data = response.body().string(); 42 | Log.d("getPackageIndex", "do post"); 43 | EventBus.getDefault().post(data); 44 | } 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | } 48 | } 49 | }).start(); 50 | } 51 | 52 | @Subscribe(threadMode = ThreadMode.MAIN) 53 | public void updatePackageManager(String res){ 54 | Log.d("getPackageIndex", "updatePackageManager"); 55 | PackageManager.getInstance().update(res); 56 | } 57 | 58 | public static Context getGlobalContext() { 59 | return context; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcuking/mwbpcontainer/network/JsApi.java: -------------------------------------------------------------------------------- 1 | package com.mcuking.mwbpcontainer.network; 2 | 3 | import android.webkit.JavascriptInterface; 4 | 5 | import com.mcuking.mwbpcontainer.utils.CalendarReminderUtils; 6 | 7 | import org.json.JSONArray; 8 | import org.json.JSONObject; 9 | 10 | import wendu.dsbridge.CompletionHandler; 11 | 12 | public class JsApi { 13 | /** 14 | * 同步日历接口 15 | * msg 格式如下: 16 | * { 17 | * "id": 日程唯一标识符 字符串 18 | * "title": 日程名称 字符串 19 | * "location": 日程地址 字符串 20 | * "startTime": 日程开始时间 13位时间戳 21 | * "endTime": 日程结束时间 13位时间戳 22 | * "alarm": [ 提前提醒时间,数组,单位分钟 23 | * 5 24 | * ] 25 | * } 26 | * 27 | */ 28 | @JavascriptInterface 29 | public void syncCalendar(Object msg, CompletionHandler handler) { 30 | try { 31 | JSONObject obj = new JSONObject(msg.toString()); 32 | String id = obj.getString("id"); 33 | String title = obj.getString("title"); 34 | long deadline = obj.getLong("deadline"); 35 | JSONArray earlyRemindTime = obj.getJSONArray("alarm"); 36 | String res = CalendarReminderUtils.addCalendarEvent(id, title, deadline, earlyRemindTime); 37 | handler.complete(Integer.valueOf(res)); 38 | } catch (Exception e) { 39 | e.printStackTrace(); 40 | handler.complete(6005); 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/mcuking/mwbpcontainer/utils/CalendarReminderUtils.java: -------------------------------------------------------------------------------- 1 | package com.mcuking.mwbpcontainer.utils; 2 | 3 | import android.content.ContentUris; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.database.Cursor; 7 | import android.graphics.Color; 8 | import android.net.Uri; 9 | import android.provider.CalendarContract; 10 | import android.text.TextUtils; 11 | import android.util.Log; 12 | 13 | 14 | import com.mcuking.mwbpcontainer.application.MWBPApplication; 15 | 16 | import org.json.JSONArray; 17 | import org.json.JSONException; 18 | 19 | import java.util.Calendar; 20 | import java.util.TimeZone; 21 | 22 | public class CalendarReminderUtils { 23 | private static final String TAG = "CalendarReminderUtils"; 24 | 25 | private static final String CALENDER_URL = "content://com.android.calendar/calendars"; 26 | private static final String CALENDER_EVENT_URL = "content://com.android.calendar/events"; 27 | private static final String CALENDER_REMINDER_URL = "content://com.android.calendar/reminders"; 28 | 29 | private static final String CALENDARS_NAME = "mwbp"; 30 | private static final String CALENDARS_ACCOUNT_NAME = "mcuking.tang@gmail.com"; 31 | private static final String CALENDARS_ACCOUNT_TYPE = "com.mcuking.mwbpContainer"; 32 | private static final String CALENDARS_DISPLAY_NAME = "mwbp"; 33 | 34 | private static final Context context = MWBPApplication.getGlobalContext(); 35 | 36 | 37 | /** 38 | * 检查是否已经添加了日历账户,如果没有添加先添加一个日历账户再查询 39 | * 获取账户成功返回账户id,否则返回-1 40 | */ 41 | private static int checkAndAddCalendarAccount(Context context) { 42 | int oldId = checkCalendarAccount(context); 43 | if( oldId >= 0 ){ 44 | return oldId; 45 | }else{ 46 | long addId = addCalendarAccount(context); 47 | if (addId >= 0) { 48 | return checkCalendarAccount(context); 49 | } else { 50 | return -1; 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * 检查是否存在现有账户,存在则返回账户id,否则返回-1 57 | */ 58 | private static int checkCalendarAccount(Context context) { 59 | Cursor userCursor = context.getContentResolver().query(Uri.parse(CALENDER_URL), null, null, null, null); 60 | try { 61 | if (userCursor == null) { //查询返回空值 62 | return -1; 63 | } 64 | int count = userCursor.getCount(); 65 | if (count > 0) { //存在现有账户,取第一个账户的id返回 66 | userCursor.moveToFirst(); 67 | return userCursor.getInt(userCursor.getColumnIndex(CalendarContract.Calendars._ID)); 68 | } else { 69 | return -1; 70 | } 71 | } finally { 72 | if (userCursor != null) { 73 | userCursor.close(); 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * 添加日历账户,账户创建成功则返回账户id,否则返回-1 80 | */ 81 | private static long addCalendarAccount(Context context) { 82 | TimeZone timeZone = TimeZone.getDefault(); 83 | ContentValues value = new ContentValues(); 84 | value.put(CalendarContract.Calendars.NAME, CALENDARS_NAME); 85 | value.put(CalendarContract.Calendars.ACCOUNT_NAME, CALENDARS_ACCOUNT_NAME); 86 | value.put(CalendarContract.Calendars.ACCOUNT_TYPE, CALENDARS_ACCOUNT_TYPE); 87 | value.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, CALENDARS_DISPLAY_NAME); 88 | value.put(CalendarContract.Calendars.VISIBLE, 1); 89 | value.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.BLUE); 90 | value.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER); 91 | value.put(CalendarContract.Calendars.SYNC_EVENTS, 1); 92 | value.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, timeZone.getID()); 93 | value.put(CalendarContract.Calendars.OWNER_ACCOUNT, CALENDARS_ACCOUNT_NAME); 94 | value.put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0); 95 | 96 | Uri calendarUri = Uri.parse(CALENDER_URL); 97 | calendarUri = calendarUri.buildUpon() 98 | .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 99 | .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, CALENDARS_ACCOUNT_NAME) 100 | .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CALENDARS_ACCOUNT_TYPE) 101 | .build(); 102 | 103 | Uri result = context.getContentResolver().insert(calendarUri, value); 104 | long id = result == null ? -1 : ContentUris.parseId(result); 105 | return id; 106 | } 107 | 108 | /** 109 | * 添加日历事件 110 | * 111 | * 6000: '会议已成功添加到手机日历中', 112 | * 6001: '系统版本太低,不支持设置日历', 113 | * 6002: '日历打开失败,请稍后重试', 114 | * 6003: '日程添加失败', 115 | * 6004: '日程提醒功能添加失败', 116 | */ 117 | public static String addCalendarEvent(String id, String title, long deadline, JSONArray earlyRemindTime) throws JSONException { 118 | 119 | if (context == null) { 120 | return "6001"; 121 | } 122 | int calId = checkAndAddCalendarAccount(context); //获取日历账户的id 123 | if (calId < 0) { //获取账户id失败直接返回,添加日历事件失败 124 | return "6002"; 125 | } 126 | 127 | // 如果id事件已存在,删除重写 128 | if (queryCalendarEvent(id)) { 129 | Log.d(TAG, "event exist"); 130 | deleteCalendarEvent(id); 131 | } 132 | 133 | //添加日历事件 134 | Calendar mCalendar = Calendar.getInstance(); 135 | 136 | mCalendar.setTimeInMillis(deadline); //设置截止时间 137 | long endTime = mCalendar.getTime().getTime(); 138 | 139 | ContentValues event = new ContentValues(); 140 | event.put("calendar_id", calId); //插入账户的id 141 | event.put("title", title); 142 | event.put("description", id); 143 | event.put(CalendarContract.Events.DTSTART, endTime); 144 | event.put(CalendarContract.Events.DTEND, endTime); 145 | event.put(CalendarContract.Events.HAS_ALARM, 1); //设置有闹钟提醒 146 | event.put(CalendarContract.Events.EVENT_TIMEZONE, "Asia/Shanghai"); //这个是时区,必须有 147 | Uri newEvent = context.getContentResolver().insert(Uri.parse(CALENDER_EVENT_URL), event); //添加事件 148 | Log.d(TAG, "event:" + newEvent); 149 | if (newEvent == null) { //添加日历事件失败直接返回 150 | return "6003"; 151 | } 152 | 153 | try { 154 | if (earlyRemindTime == null || earlyRemindTime.length() == 0) { 155 | Log.d(TAG, "earlyRemindTime is empty or invalid"); 156 | return "6000"; 157 | } 158 | //事件提醒的设定 159 | for (int i=0;i 0) { 192 | for (eventCursor.moveToFirst(); !eventCursor.isAfterLast(); eventCursor.moveToNext()) { 193 | String eventId = eventCursor.getString(eventCursor.getColumnIndex(CalendarContract.Events.DESCRIPTION)); 194 | if (eventId != null) { 195 | if (eventId.equals(id)) { 196 | Log.d(TAG, "id:" + eventId); 197 | return true; 198 | } 199 | } 200 | } 201 | } 202 | } finally { 203 | if (eventCursor != null) { 204 | eventCursor.close(); 205 | } 206 | } 207 | return false; 208 | 209 | } 210 | 211 | /** 212 | * 删除日历事件 213 | */ 214 | public static void deleteCalendarEvent(String description) { 215 | if (context == null) { 216 | return; 217 | } 218 | Cursor eventCursor = context.getContentResolver().query(Uri.parse(CALENDER_EVENT_URL), null, null, null, null); 219 | try { 220 | if (eventCursor == null) { //查询返回空值 221 | return; 222 | } 223 | if (eventCursor.getCount() > 0) { 224 | //遍历所有事件,找到title跟需要查询的title一样的项 225 | for (eventCursor.moveToFirst(); !eventCursor.isAfterLast(); eventCursor.moveToNext()) { 226 | String eventDescription = eventCursor.getString(eventCursor.getColumnIndex(CalendarContract.Events.DESCRIPTION)); 227 | if (!TextUtils.isEmpty(description) && description.equals(eventDescription)) { 228 | int id = eventCursor.getInt(eventCursor.getColumnIndex(CalendarContract.Calendars._ID));//取得id 229 | Uri deleteUri = ContentUris.withAppendedId(Uri.parse(CALENDER_EVENT_URL), id); 230 | int rows = context.getContentResolver().delete(deleteUri, null, null); 231 | if (rows == -1) { //事件删除失败 232 | return; 233 | } 234 | } 235 | } 236 | } 237 | } finally { 238 | if (eventCursor != null) { 239 | eventCursor.close(); 240 | } 241 | } 242 | } 243 | 244 | } -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libApkPatchLibrary.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/jniLibs/armeabi-v7a/libApkPatchLibrary.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi/libApkPatchLibrary.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/jniLibs/armeabi/libApkPatchLibrary.so -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/drawable/icon.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #F8F8F8 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MWBPContainer 3 | 4 | -------------------------------------------------------------------------------- /app/src/test/java/com/mcuking/mwbpcontainer/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.mcuking.mwbpcontainer; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/assets/architecture.png -------------------------------------------------------------------------------- /assets/principle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/assets/principle.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:7.3.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | maven { url 'https://jitpack.io' } 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | 17 | 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 27 15:48:51 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':webpackagekit' 2 | -------------------------------------------------------------------------------- /webpackagekit/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /webpackagekit/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | 7 | defaultConfig { 8 | minSdkVersion 23 9 | targetSdkVersion 28 10 | 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | consumerProguardFiles 'consumer-rules.pro' 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | namespace 'com.hht.webpackagekit' 22 | 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | 28 | testImplementation 'junit:junit:4.12' 29 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 30 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 31 | implementation 'com.liulishuo.filedownloader:library:1.7.5' 32 | implementation 'com.google.code.gson:gson:2.7' 33 | } 34 | 35 | -------------------------------------------------------------------------------- /webpackagekit/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/webpackagekit/consumer-rules.pro -------------------------------------------------------------------------------- /webpackagekit/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /webpackagekit/src/androidTest/java/com/hht/webpackagekit/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.yjj.webpackagekit.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webpackagekit/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/lib/bsdiff/PatchUtils.java: -------------------------------------------------------------------------------- 1 | package com.hht.lib.bsdiff; 2 | 3 | public class PatchUtils { 4 | 5 | static PatchUtils instance; 6 | 7 | public static PatchUtils getInstance() { 8 | if (instance == null) 9 | instance = new PatchUtils(); 10 | return instance; 11 | } 12 | 13 | static { 14 | System.loadLibrary("ApkPatchLibrary"); 15 | } 16 | 17 | /** 18 | * native方法 使用路径为oldPath的文件与路径为patchPath的补丁包,合成新的文件,并存储于newPath 19 | * 返回:0,说明操作成功 20 | * 21 | * @param oldPath 示例:/sdcard/old.apk 22 | * @param newPath 示例:/sdcard/new.apk 23 | * @param patchPath 示例:/sdcard/xx.patch 24 | */ 25 | public native int patch(String oldPath, String newPath, String patchPath); 26 | } -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/OfflineWebViewClient.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | import android.util.Log; 6 | import android.webkit.WebResourceRequest; 7 | import android.webkit.WebResourceResponse; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | 11 | public class OfflineWebViewClient extends WebViewClient { 12 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 13 | @Override 14 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 15 | final String url = request.getUrl().toString(); 16 | WebResourceResponse resourceResponse = getWebResourceResponse(url); 17 | if (resourceResponse == null) { 18 | Log.d("WebViewClient", "request from remote , " + url); 19 | return super.shouldInterceptRequest(view, request); 20 | } 21 | Log.d("WebViewClient", "request from local , " + url); 22 | return resourceResponse; 23 | } 24 | 25 | /** 26 | * 从本地命中并返回资源 27 | * @param url 资源地址 28 | */ 29 | private WebResourceResponse getWebResourceResponse(String url) { 30 | try { 31 | WebResourceResponse resourceResponse = PackageManager.getInstance().getResource(url); 32 | return resourceResponse; 33 | } catch (Exception e) { 34 | Log.e("Error", e.toString()); 35 | e.printStackTrace(); 36 | } 37 | return null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/PackageConfig.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit; 2 | 3 | /** 4 | * 配置信息 5 | */ 6 | public class PackageConfig { 7 | private boolean enableAssets = true; 8 | private boolean enableBsDiff; 9 | private String assetPath = "package.zip"; 10 | 11 | public boolean isEnableAssets() { 12 | return enableAssets; 13 | } 14 | 15 | public boolean isEnableBsDiff() { 16 | return enableBsDiff; 17 | } 18 | 19 | public String getAssetPath() { 20 | return assetPath; 21 | } 22 | 23 | public void setEnableAssets(boolean enableAssets) { 24 | this.enableAssets = enableAssets; 25 | } 26 | 27 | public void setEnableBsDiff(boolean enableBsDiff) { 28 | this.enableBsDiff = enableBsDiff; 29 | } 30 | 31 | public void setAssetPath(String assetPath) { 32 | this.assetPath = assetPath; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/PackageManager.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.HandlerThread; 6 | import android.os.Looper; 7 | import android.os.Message; 8 | import android.text.TextUtils; 9 | import android.util.Log; 10 | import android.webkit.WebResourceResponse; 11 | 12 | import com.google.gson.Gson; 13 | import com.hht.webpackagekit.core.AssetResourceLoader; 14 | import com.hht.webpackagekit.core.Constants; 15 | import com.hht.webpackagekit.core.Downloader; 16 | import com.hht.webpackagekit.core.PackageEntity; 17 | import com.hht.webpackagekit.core.PackageInfo; 18 | import com.hht.webpackagekit.core.PackageInstaller; 19 | import com.hht.webpackagekit.core.PackageStatus; 20 | import com.hht.webpackagekit.core.ResourceManager; 21 | import com.hht.webpackagekit.core.util.FileUtils; 22 | import com.hht.webpackagekit.core.util.GsonUtils; 23 | import com.hht.webpackagekit.core.util.Logger; 24 | import com.hht.webpackagekit.core.util.VersionUtils; 25 | import com.hht.webpackagekit.inner.AssetResourceLoaderImpl; 26 | import com.hht.webpackagekit.inner.DownloaderImpl; 27 | import com.hht.webpackagekit.inner.PackageInstallerImpl; 28 | import com.hht.webpackagekit.inner.ResourceManagerImpl; 29 | import com.liulishuo.filedownloader.FileDownloader; 30 | 31 | import java.io.File; 32 | import java.io.FileInputStream; 33 | import java.io.FileNotFoundException; 34 | import java.io.FileOutputStream; 35 | import java.io.IOException; 36 | import java.util.ArrayList; 37 | import java.util.HashMap; 38 | import java.util.List; 39 | import java.util.Map; 40 | import java.util.concurrent.locks.Lock; 41 | import java.util.concurrent.locks.ReentrantLock; 42 | 43 | /** 44 | * 离线包管理器 45 | */ 46 | public class PackageManager { 47 | private static final int WHAT_DOWNLOAD_SUCCESS = 1; 48 | private static final int WHAT_DOWNLOAD_FAILURE = 2; 49 | private static final int WHAT_START_UPDATE = 3; 50 | private static final int WHAT_INIT_ASSETS = 4; 51 | 52 | private static final int STATUS_PACKAGE_CANUSE = 1; 53 | 54 | private volatile static PackageManager instance; 55 | 56 | private Context context; 57 | private ResourceManager resourceManager; 58 | private PackageInstaller packageInstaller; 59 | private AssetResourceLoader assetResourceLoader; 60 | private volatile boolean isUpdating = false; 61 | private Handler packageHandler; 62 | private HandlerThread packageThread; 63 | private PackageEntity localPackageEntity; 64 | //即将下载的离线包资源PackageInfo的集合 65 | private List willDownloadPackageInfoList; 66 | //需要更新的离线包资源PackageInfo的集合 67 | private List onlyUpdatePackageInfoList; 68 | private final Lock resourceLock; 69 | private final Map packageStatusMap = new HashMap<>(); 70 | private final PackageConfig config = new PackageConfig(); 71 | 72 | 73 | public static synchronized PackageManager getInstance(){ 74 | if(instance == null){ 75 | instance = new PackageManager(); 76 | } 77 | return instance; 78 | } 79 | 80 | private PackageManager() { 81 | resourceLock = new ReentrantLock(); 82 | } 83 | 84 | public void init(Context context) { 85 | 86 | Log.d("PackageManager","init"); 87 | this.context = context; 88 | 89 | resourceManager = new ResourceManagerImpl(context); 90 | packageInstaller = new PackageInstallerImpl(context); 91 | FileDownloader.init(context); 92 | 93 | String packageIndexFileName = FileUtils.getPackageIndexFileName(context); 94 | File packageIndexFile = new File(packageIndexFileName); 95 | 96 | if (config.isEnableAssets() && !TextUtils.isEmpty(config.getAssetPath()) && !packageIndexFile.exists()) { 97 | assetResourceLoader = new AssetResourceLoaderImpl(context); 98 | ensurePackageThread(); 99 | packageHandler.sendEmptyMessage(WHAT_INIT_ASSETS); 100 | } 101 | } 102 | 103 | //从服务端获取最新的packageIndex.json 104 | public void update(String packageIndexStr) { 105 | if (isUpdating) { 106 | return; 107 | } 108 | if (packageIndexStr == null) { 109 | packageIndexStr = ""; 110 | } 111 | ensurePackageThread(); 112 | Message message = Message.obtain(); 113 | message.what = WHAT_START_UPDATE; 114 | message.obj = packageIndexStr; 115 | packageHandler.sendMessage(message); 116 | } 117 | 118 | private void ensurePackageThread() { 119 | if (packageThread == null) { 120 | packageThread = new HandlerThread("offline_package_thread"); 121 | packageThread.start(); 122 | packageHandler = new PackageHandler(packageThread.getLooper()); 123 | } 124 | } 125 | 126 | //获取预置在assets中的离线包并解压到相应目录 127 | private void performLoadAssets() { 128 | if (assetResourceLoader == null) { 129 | return; 130 | } 131 | 132 | for (int i = 0; i < Constants.LOCAL_ASSET_LIST.length ; i++) { 133 | Log.d("PKGM1", Constants.LOCAL_ASSET_LIST[i]); 134 | PackageInfo packageInfo = assetResourceLoader.load(Constants.LOCAL_ASSET_LIST[i]); 135 | if (packageInfo == null) { 136 | return; 137 | } 138 | Log.d("PKGM2", packageInfo.getPackageId()); 139 | Log.d("performLoadAssets", "installPackage"); 140 | installPackage(packageInfo.getPackageId(), packageInfo, true); 141 | } 142 | } 143 | 144 | //基于从服务端拉取的packageIndex.json,决定升级哪些离线包 145 | private void performUpdate(String packageIndexStr) { 146 | Log.d("download", "performupdate"); 147 | //读取本地packageIndex.json文件 148 | String localPackageIndexFileName = FileUtils.getPackageIndexFileName(context); 149 | File localPackageIndexFile = new File(localPackageIndexFileName); 150 | 151 | //是否是第一次加载离线包 152 | boolean isFirstLoadPackage = !localPackageIndexFile.exists(); 153 | 154 | //将从服务端拉取的packageIndex.json中离线包数组信息转化成willDownloadPackageInfoList数组 155 | PackageEntity packageEntity = null; 156 | packageEntity = GsonUtils.fromJsonIgnoreException(packageIndexStr, PackageEntity.class); 157 | willDownloadPackageInfoList = new ArrayList<>(2); 158 | if (packageEntity != null && packageEntity.getItems() != null) { 159 | willDownloadPackageInfoList.addAll(packageEntity.getItems()); 160 | } 161 | 162 | //不是第一次load package,则要比对本地和服务端拉取的packageIndex.json信息,决定哪些离线包需要加载 163 | if (!isFirstLoadPackage) { 164 | initLocalEntity(localPackageIndexFile); 165 | } 166 | List packageInfoList = new ArrayList<>(willDownloadPackageInfoList.size()); 167 | for (PackageInfo packageInfo : willDownloadPackageInfoList) { 168 | if (packageInfo.getStatus() == PackageStatus.offLine) { 169 | continue; 170 | } 171 | if(TextUtils.isEmpty(packageInfo.getDownloadUrl())){ 172 | packageInfo.setDownloadUrl(packageInfo.getOrigin_file_path()); 173 | } 174 | packageInfoList.add(packageInfo); 175 | } 176 | willDownloadPackageInfoList.clear(); 177 | willDownloadPackageInfoList.addAll(packageInfoList); 178 | 179 | //遍历处理好的willDownloadPackageInfoList,下载相应离线包,下载成功后调用PackageInstaller.install将包与之前的离线包合并或替换,并解压到指定目录 180 | for (PackageInfo packageInfo : willDownloadPackageInfoList) { 181 | Downloader downloader = new DownloaderImpl(context); 182 | downloader.download(packageInfo, new DownloadCallback(this)); 183 | } 184 | 185 | if (onlyUpdatePackageInfoList != null && onlyUpdatePackageInfoList.size() > 0) { 186 | for (PackageInfo packageInfo : onlyUpdatePackageInfoList) { 187 | Log.d("performUpdate", packageInfo.getVersion()); 188 | resourceManager.updateResource(packageInfo.getPackageId(), packageInfo.getVersion()); 189 | synchronized (packageStatusMap) { 190 | packageStatusMap.put(packageInfo.getPackageId(), STATUS_PACKAGE_CANUSE); 191 | } 192 | } 193 | } 194 | } 195 | 196 | //比对willDownloadPackageInfoList中离线包packageInfo和本地离线包localPackageInfo版本,决定是否下载 197 | private void initLocalEntity(File localPackageIndexFile) { 198 | FileInputStream localIndexFis = null; 199 | try { 200 | localIndexFis = new FileInputStream(localPackageIndexFile); 201 | } catch (FileNotFoundException e) { 202 | 203 | } 204 | if (localIndexFis == null) { 205 | return; 206 | } 207 | localPackageEntity = GsonUtils.fromJsonIgnoreException(localIndexFis, PackageEntity.class); 208 | if (localPackageEntity == null || localPackageEntity.getItems() == null) { 209 | return; 210 | } 211 | int index = 0; 212 | for (PackageInfo localPackageInfo : localPackageEntity.getItems()) { 213 | Log.d("localVersion", localPackageInfo.getVersion()); 214 | 215 | // 如果本地 packageIndex 的某个包 localPackageInfo 不在从服务器拉下来的 packageIndex 中,则跳出本次循环 216 | if ((index = willDownloadPackageInfoList.indexOf(localPackageInfo)) < 0) { 217 | continue; 218 | } 219 | //当本地离线包localPackageInfo版本大于等于从服务端拉取的最新的packageIndex.json的packageInfo版本 220 | //则从willDownloadPackageInfoList中移除离线包packageInfo,移入onlyUpdatePackageInfoList,即只需更新packageInfo,无需下载 221 | //否则更新本地离线包localPackageInfo的版本,然后等待下载 222 | PackageInfo packageInfo = willDownloadPackageInfoList.get(index); 223 | Log.d("PKGM","pkgId:"+packageInfo.getPackageId() + "|" +"remote version:" + packageInfo.getVersion() + "|" +"local version:" + localPackageInfo.getVersion()); 224 | if (VersionUtils.compareVersion(packageInfo.getVersion(), localPackageInfo.getVersion()) <= 0) { 225 | if (!checkResourceFileValid(packageInfo.getPackageId(), packageInfo.getVersion())) { 226 | return; 227 | } 228 | willDownloadPackageInfoList.remove(index); 229 | if (onlyUpdatePackageInfoList == null) { 230 | onlyUpdatePackageInfoList = new ArrayList<>(2); 231 | } 232 | if (packageInfo.getStatus() == PackageStatus.onLine) { 233 | onlyUpdatePackageInfoList.add(localPackageInfo); 234 | } 235 | localPackageInfo.setStatus(packageInfo.getStatus()); 236 | } else { 237 | if ((Integer.parseInt(packageInfo.getVersion()) - Integer.parseInt(localPackageInfo.getVersion()) > 1)) { 238 | // 如果远端离线包版本比本地离线包版本高出 1 个版本,则下载全量包 239 | packageInfo.setDownloadUrl(packageInfo.getOrigin_file_path()); 240 | packageInfo.setIsPatch(false); 241 | packageInfo.setMd5(packageInfo.getOrigin_file_md5()); 242 | } else { 243 | // 否则表示远端离线包版本和本地离线包版本仅差 1 各版本,则下载差量包 244 | // 注意:本项目的离线包平台仅对相邻的版本做差量包,读者可以根据自己的需求,设置远端和本地离线包版本相差多少以内,才会使用离线包, 245 | // 然后修改两处:1. 这里的版本比对逻辑 2. 离线包管理平台的计算差量包相关逻辑 246 | packageInfo.setDownloadUrl(packageInfo.getPatch_file_path()); 247 | packageInfo.setIsPatch(true); 248 | packageInfo.setMd5(packageInfo.getPatch_file_md5()); 249 | } 250 | 251 | localPackageInfo.setStatus(packageInfo.getStatus()); 252 | localPackageInfo.setVersion(packageInfo.getVersion()); 253 | } 254 | } 255 | } 256 | 257 | private boolean checkResourceFileValid(String packageId, String version) { 258 | File indexFile = FileUtils.getResourceIndexFile(context, packageId, version); 259 | return indexFile.exists() && indexFile.isFile(); 260 | } 261 | 262 | //更新packageIndex.json中packageId对应的离线包版本 263 | private void updatePackageIndexFile(String packageId, String version) { 264 | Log.d("updatePackageIndexFile1", packageId + "|"+version); 265 | String packageIndexFileName = FileUtils.getPackageIndexFileName(context); 266 | File packageIndexFile = new File(packageIndexFileName); 267 | //若不存在packageIndex.json,则创建一个packageIndex.json 268 | if (!packageIndexFile.exists()) { 269 | boolean isSuccess = true; 270 | try { 271 | isSuccess = packageIndexFile.createNewFile(); 272 | } catch (IOException e) { 273 | isSuccess = false; 274 | } 275 | if (!isSuccess) { 276 | return; 277 | } 278 | } 279 | if (localPackageEntity == null) { 280 | FileInputStream indexFis = null; 281 | try { 282 | indexFis = new FileInputStream(packageIndexFile); 283 | } catch (FileNotFoundException e) { 284 | 285 | } 286 | if (indexFis == null) { 287 | return; 288 | } 289 | localPackageEntity = GsonUtils.fromJsonIgnoreException(indexFis, PackageEntity.class); 290 | } 291 | if (localPackageEntity == null) { 292 | localPackageEntity = new PackageEntity(); 293 | } 294 | 295 | //获取localPackageEntity下所有的离线包packageInfo,如果存在packageId对应的包,则更新版本状态等,没有则加进去 296 | List packageInfoList = new ArrayList<>(2); 297 | if (localPackageEntity.getItems() != null) { 298 | packageInfoList.addAll(localPackageEntity.getItems()); 299 | } 300 | PackageInfo packageInfo = new PackageInfo(); 301 | packageInfo.setPackageId(packageId); 302 | int index = 0; 303 | if ((index = packageInfoList.indexOf(packageInfo)) >= 0) { 304 | packageInfoList.get(index).setVersion(version); 305 | } else { 306 | packageInfo.setStatus(PackageStatus.onLine); 307 | packageInfo.setVersion(version); 308 | packageInfoList.add(packageInfo); 309 | } 310 | localPackageEntity.setItems(packageInfoList); 311 | if (localPackageEntity == null || localPackageEntity.getItems() == null 312 | || localPackageEntity.getItems().size() == 0) { 313 | return; 314 | } 315 | 316 | //将最新的数据写入到packageIndex.json文件中 317 | String updateStr = new Gson().toJson(localPackageEntity); 318 | try { 319 | FileOutputStream outputStream = new FileOutputStream(packageIndexFile); 320 | try { 321 | outputStream.write(updateStr.getBytes()); 322 | } catch (IOException ignore) { 323 | Logger.e("write packageIndex file error"); 324 | } finally { 325 | if (outputStream != null) { 326 | try { 327 | outputStream.close(); 328 | } catch (IOException e) { 329 | e.printStackTrace(); 330 | } 331 | } 332 | } 333 | } catch (Exception ignore) { 334 | Logger.e("read packageIndex file error"); 335 | } 336 | } 337 | 338 | public WebResourceResponse getResource(String url) { 339 | synchronized (packageStatusMap) { 340 | 341 | String packageId = resourceManager.getPackageId(url); 342 | Integer status = packageStatusMap.get(packageId); 343 | Log.d("WebResourceResponse", status + " | " + url + " | " + packageId +"| packageStatusMap size:"+packageStatusMap.size()); 344 | if (status == null) { 345 | return null; 346 | } 347 | if (status != STATUS_PACKAGE_CANUSE) { 348 | return null; 349 | } 350 | } 351 | WebResourceResponse resourceResponse = null; 352 | 353 | synchronized (resourceManager) { 354 | resourceResponse = resourceManager.getResource(url); 355 | } 356 | return resourceResponse; 357 | } 358 | 359 | private void downloadSuccess(String packageId) { 360 | Log.d("download", "success"); 361 | if (packageHandler == null) { 362 | return; 363 | } 364 | Message message = Message.obtain(); 365 | message.what = WHAT_DOWNLOAD_SUCCESS; 366 | message.obj = packageId; 367 | packageHandler.sendMessage(message); 368 | } 369 | 370 | private void downloadFailure(String packageId) { 371 | Log.d("download", "failure"); 372 | if (packageHandler == null) { 373 | return; 374 | } 375 | Message message = Message.obtain(); 376 | message.what = WHAT_DOWNLOAD_FAILURE; 377 | message.obj = packageId; 378 | packageHandler.sendMessage(message); 379 | } 380 | 381 | //将预置在assets或刚下载的离线包解压到指定目录 382 | private void installPackage(String packageId, PackageInfo packageInfo, boolean isAssets) { 383 | if (packageInfo != null) { 384 | //暂时关闭对下载文件的MD5校验 385 | // boolean isValid = (isAssets && assetValidator.validate(packageInfo)) || validator.validate(packageInfo); 386 | // if (isValid) { 387 | resourceLock.lock(); 388 | boolean isSuccess = packageInstaller.install(packageInfo, isAssets); 389 | resourceLock.unlock(); 390 | //安装失败情况下,不做任何处理,因为资源既然资源需要最新资源,失败了,就没有必要再用缓存了 391 | if (isSuccess) { 392 | Log.d("installPackage", "version" + packageInfo.getVersion() + "| isAssets " + isAssets ); 393 | resourceManager.updateResource(packageInfo.getPackageId(), packageInfo.getVersion()); 394 | //更新安装成功的离线包版本到packageIndex.json 395 | updatePackageIndexFile(packageInfo.getPackageId(), packageInfo.getVersion()); 396 | synchronized (packageStatusMap) { 397 | packageStatusMap.put(packageId, STATUS_PACKAGE_CANUSE); 398 | } 399 | } 400 | // } 401 | } 402 | } 403 | 404 | //某个离线包下载成功后,则从willDownloadPackageInfoLis移除,并解压该离线包到相应文件 405 | private void performDownloadSuccess(String packageId) { 406 | if (willDownloadPackageInfoList == null) { 407 | return; 408 | } 409 | PackageInfo packageInfo = null; 410 | PackageInfo tmp = new PackageInfo(); 411 | tmp.setPackageId(packageId); 412 | int pos = willDownloadPackageInfoList.indexOf(tmp); 413 | if (pos >= 0) { 414 | packageInfo = willDownloadPackageInfoList.remove(pos); 415 | } 416 | allResouceUpdateFinished(); 417 | Log.d("performDownloadSuccess", "installPackage"); 418 | installPackage(packageId, packageInfo, false); 419 | } 420 | 421 | //某个离线包下载失败后,则从willDownloadPackageInfoLis移除 422 | private void performDownloadFailure(String packageId) { 423 | if (willDownloadPackageInfoList == null) { 424 | return; 425 | } 426 | PackageInfo packageInfo = null; 427 | PackageInfo tmp = new PackageInfo(); 428 | tmp.setPackageId(packageId); 429 | int pos = willDownloadPackageInfoList.indexOf(tmp); 430 | if (pos >= 0) { 431 | willDownloadPackageInfoList.remove(pos); 432 | } 433 | allResouceUpdateFinished(); 434 | } 435 | 436 | private void allResouceUpdateFinished() { 437 | if (willDownloadPackageInfoList.size() == 0) { 438 | isUpdating = false; 439 | } 440 | } 441 | 442 | //离线包handler处理器 443 | class PackageHandler extends Handler { 444 | public PackageHandler(Looper looper) { 445 | super(looper); 446 | } 447 | 448 | @Override 449 | public void handleMessage(Message msg) { 450 | switch (msg.what) { 451 | case WHAT_DOWNLOAD_SUCCESS: 452 | performDownloadSuccess((String) msg.obj); 453 | break; 454 | case WHAT_DOWNLOAD_FAILURE: 455 | performDownloadFailure((String) msg.obj); 456 | break; 457 | case WHAT_START_UPDATE: 458 | performUpdate((String) msg.obj); 459 | break; 460 | case WHAT_INIT_ASSETS: 461 | performLoadAssets(); 462 | break; 463 | default: 464 | break; 465 | } 466 | } 467 | } 468 | 469 | static class DownloadCallback implements Downloader.DownloadCallback { 470 | private final PackageManager packageManager; 471 | 472 | public DownloadCallback(PackageManager packageManager) { 473 | this.packageManager = packageManager; 474 | } 475 | 476 | @Override 477 | public void onSuccess(String packageId) { 478 | packageManager.downloadSuccess(packageId); 479 | } 480 | 481 | @Override 482 | public void onFailure(String packageId) { 483 | packageManager.downloadFailure(packageId); 484 | } 485 | } 486 | 487 | } 488 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/PackageValidator.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit; 2 | 3 | import com.hht.webpackagekit.core.PackageInfo; 4 | 5 | /** 6 | * 校验资源信息的有效性 7 | */ 8 | public interface PackageValidator { 9 | boolean validate(PackageInfo info); 10 | } 11 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/AssetResourceLoader.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | /** 4 | * asset资源加载器 5 | */ 6 | public interface AssetResourceLoader { 7 | /** 8 | * asset资源路径信息 9 | * @param path 10 | */ 11 | PackageInfo load(String path); 12 | } 13 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/Constants.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | /** 4 | * 所有的常量信息都放在此处 5 | */ 6 | public class Constants { 7 | public static String[] LOCAL_ASSET_LIST = {"main.zip"}; 8 | 9 | // 服务器地址 10 | public static final String BASE_URL = "http://122.51.132.117:5002"; 11 | 12 | // package.json请求地址 13 | public static final String BASE_PACKAGE_INDEX = BASE_URL + "/getPackageIndex?appName=mwbp"; 14 | 15 | /*** 16 | * 所有离线包的根目录 17 | * */ 18 | public static final String PACKAGE_FILE_ROOT_PATH = "offlinepackage"; 19 | 20 | /*** 21 | * 配置信息 22 | * */ 23 | public static final String PACKAGE_FILE_INDEX = "packageIndex.json"; 24 | /*** 25 | * 每个离线包的索引信息文件 26 | * */ 27 | public static final String RESOURCE_INDEX_NAME = "index.json"; 28 | 29 | /** 30 | * 工作目录 31 | */ 32 | public static final String PACKAGE_WORK = "work"; 33 | 34 | /*** 35 | * 36 | * 更新临时目录 37 | * */ 38 | public static final String PACKAGE_UPDATE_TEMP = "update_tmp.zip"; 39 | 40 | /*** 41 | * 42 | * 更新目录 43 | * */ 44 | public static final String PACKAGE_UPDATE = "update.zip"; 45 | 46 | /** 47 | * 下载文件名称 48 | */ 49 | public static final String PACKAGE_DOWNLOAD = "download.zip"; 50 | 51 | /** 52 | * merge路径 53 | */ 54 | public static final String PACKAGE_MERGE = "merge.zip"; 55 | 56 | /** 57 | * 中间路径 58 | */ 59 | public static final String RESOURCE_MIDDLE_PATH = "package"; 60 | 61 | /** 62 | * asstes文件名称 63 | */ 64 | public static final String PACKAGE_ASSETS= "package.zip"; 65 | } 66 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/Downloader.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | /** 4 | * 离线包下载器 5 | */ 6 | public interface Downloader { 7 | /*** 8 | * 离线包下载 9 | * */ 10 | void download(PackageInfo packageInfo, DownloadCallback callback); 11 | 12 | interface DownloadCallback { 13 | void onSuccess(String packageId); 14 | 15 | void onFailure(String packageID); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/PackageEntity.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * 离线包Index信息 7 | */ 8 | public class PackageEntity { 9 | private int errorCode; 10 | private List data; 11 | 12 | public void setItems(List data) { 13 | this.data = data; 14 | } 15 | 16 | public List getItems() { 17 | return data; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/PackageInfo.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | import android.text.TextUtils; 4 | 5 | /** 6 | * 离线包信息 7 | */ 8 | public class PackageInfo { 9 | //实际的md5 10 | private String md5; 11 | 12 | private String module_name; 13 | 14 | //离线包版本号 15 | private String version; 16 | 17 | //离线包的状态 {@link PackageStatus} 18 | private int status = PackageStatus.onLine; 19 | 20 | //是否是patch包 21 | private boolean is_patch; 22 | 23 | 24 | // 实际上的下载地址 25 | private String file_path; 26 | 27 | 28 | 29 | //离线包md值 由后端下发 30 | private String patch_file_md5; 31 | private String origin_file_md5; 32 | private String patch_file_path; 33 | private String origin_file_path; 34 | 35 | 36 | public void setPatch_file_path(String patch_file_path) { 37 | this.patch_file_path = patch_file_path; 38 | } 39 | 40 | public void setOrigin_file_path(String origin_file_path) { 41 | this.origin_file_path = origin_file_path; 42 | } 43 | 44 | public String getPackageId() { 45 | return module_name; 46 | } 47 | 48 | public String getOrigin_file_md5() { 49 | return origin_file_md5; 50 | } 51 | 52 | public String getPatch_file_md5() { 53 | return patch_file_md5; 54 | } 55 | 56 | public String getOrigin_file_path() { 57 | return origin_file_path; 58 | } 59 | 60 | public String getPatch_file_path() { 61 | return patch_file_path; 62 | } 63 | 64 | //设置离线包下载地址 65 | public void setDownloadUrl(String file_path) { 66 | this.file_path = file_path; 67 | } 68 | 69 | //获取离线包下载地址 70 | public String getDownloadUrl() { 71 | return file_path; 72 | } 73 | 74 | public String getVersion() { 75 | return version; 76 | } 77 | 78 | public int getStatus() { 79 | return status; 80 | } 81 | 82 | public void setIsPatch(boolean isPatch){ 83 | this.is_patch = isPatch; 84 | } 85 | 86 | public boolean isPatch() { 87 | return is_patch; 88 | } 89 | 90 | 91 | public void setMd5(String md5) { 92 | this.md5 = md5; 93 | } 94 | 95 | 96 | public String getMd5() { 97 | return md5; 98 | } 99 | 100 | public void setVersion(String version) { 101 | this.version = version; 102 | } 103 | 104 | public void setPackageId(String module_name) { 105 | this.module_name = module_name; 106 | } 107 | 108 | public void setStatus(int status) { 109 | this.status = status; 110 | } 111 | 112 | // public void setMd5(String md5) { 113 | // this.md5 = md5; 114 | // } 115 | 116 | @Override 117 | public boolean equals(Object obj) { 118 | if (!(obj instanceof PackageInfo)) { 119 | return false; 120 | } 121 | PackageInfo that = (PackageInfo) obj; 122 | return TextUtils.equals(module_name, that.module_name); 123 | } 124 | 125 | @Override 126 | public int hashCode() { 127 | int result = 17; 128 | result = result * 37 + module_name == null ? 0 : module_name.hashCode(); 129 | return result; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/PackageInstaller.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | /** 4 | * 离线包安装器 5 | * 1、解压离线包到update下,如果存在update包,重命名update为update_temp,然后解压到update,如果解压失败,再讲update_temp重新命名为update,最后删除update_temp 6 | * 2、更新current包 7 | * 3、提取index.json中的内容到ResourceManager中 8 | */ 9 | public interface PackageInstaller { 10 | boolean install(PackageInfo packageInfo, boolean isAssets); 11 | } 12 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/PackageStatus.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | /** 4 | * 离线包状态信息 5 | */ 6 | public class PackageStatus { 7 | public final static int onLine = 1; 8 | public final static int offLine = 0; 9 | } 10 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/ResourceInfo.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | import com.google.gson.annotations.Expose; 4 | 5 | /** 6 | * 资源信息,每个url资源请求对应的资源信息 7 | */ 8 | public class ResourceInfo { 9 | //所关联的package id 10 | //后续需要根据该id索引离线包信息 11 | @Expose(deserialize = false, serialize = false) 12 | private String packageId; 13 | 14 | //远端路径 15 | private String remoteUrl; 16 | 17 | //相对路径 18 | private String path; 19 | 20 | @Expose(deserialize = false, serialize = false) 21 | //本地绝对路径 22 | private String localPath; 23 | 24 | //类型 25 | private String mimeType; 26 | 27 | private String md5; 28 | 29 | public void setPackageId(String packageId) { 30 | this.packageId = packageId; 31 | } 32 | 33 | public void setRemoteUrl(String remoteUrl) { 34 | this.remoteUrl = remoteUrl; 35 | } 36 | 37 | public void setLocalPath(String localPath) { 38 | this.localPath = localPath; 39 | } 40 | 41 | public void setMimeType(String mimeType) { 42 | this.mimeType = mimeType; 43 | } 44 | 45 | public String getPackageId() { 46 | return packageId; 47 | } 48 | 49 | public String getRemoteUrl() { 50 | return remoteUrl; 51 | } 52 | 53 | public String getLocalPath() { 54 | return localPath; 55 | } 56 | 57 | 58 | public String getMimeType() { 59 | return mimeType; 60 | } 61 | 62 | public String getPath() { 63 | return path; 64 | } 65 | 66 | public void setPath(String path) { 67 | this.path = path; 68 | } 69 | 70 | public String getMd5() { 71 | return md5; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/ResourceInfoEntity.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | import com.google.gson.annotations.Expose; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * index entity 9 | */ 10 | public class ResourceInfoEntity { 11 | private String version; 12 | private String packageId; 13 | 14 | @Expose(deserialize = false, serialize = false) private String md5; 15 | private List items; 16 | 17 | public String getVersion() { 18 | return version; 19 | } 20 | 21 | public String getPackageId() { 22 | return packageId; 23 | } 24 | 25 | public List getItems() { 26 | return items; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/ResourceKey.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | import android.net.Uri; 4 | import android.text.TextUtils; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 资源请求键 10 | */ 11 | public class ResourceKey { 12 | private final String host; 13 | private final String schema; 14 | private final List pathList; 15 | 16 | public ResourceKey(String url) { 17 | Uri uri = Uri.parse(url); 18 | host = uri.getHost(); 19 | schema = uri.getScheme(); 20 | pathList = uri.getPathSegments(); 21 | } 22 | 23 | @Override 24 | public int hashCode() { 25 | int result = 17; 26 | result = result * 37 + hashNotNull(host); 27 | result = result * 37 + hashNotNull(schema); 28 | if (pathList != null) { 29 | for (String pathSeg : pathList) { 30 | result = result * 37 + hashNotNull(pathSeg); 31 | } 32 | } 33 | return result; 34 | } 35 | 36 | private int hashNotNull(Object o) { 37 | return o == null ? 0 : o.hashCode(); 38 | } 39 | 40 | @Override 41 | public boolean equals(Object obj) { 42 | if (!(obj instanceof ResourceKey)) { 43 | return false; 44 | } 45 | ResourceKey that = (ResourceKey) obj; 46 | if (!TextUtils.equals(host, that.host)) { 47 | return false; 48 | } 49 | if (!TextUtils.equals(schema, that.schema)) { 50 | return false; 51 | } 52 | if (this.pathList == that.pathList) { 53 | return true; 54 | } 55 | if (pathList == null && that.pathList != null) { 56 | return false; 57 | } 58 | if (pathList != null && that.pathList == null) { 59 | return false; 60 | } 61 | boolean isEqual = true; 62 | for (String pa : pathList) { 63 | if (!that.pathList.contains(pa)) { 64 | isEqual = false; 65 | break; 66 | } 67 | } 68 | 69 | return isEqual; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/ResourceManager.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | import android.webkit.WebResourceResponse; 4 | 5 | /** 6 | * 资源管理器 7 | */ 8 | public interface ResourceManager { 9 | WebResourceResponse getResource(String url); 10 | 11 | boolean updateResource(String packageId, String version); 12 | 13 | void setResourceValidator(ResoureceValidator validator); 14 | 15 | String getPackageId(String url); 16 | } 17 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/ResoureceValidator.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core; 2 | 3 | /** 4 | * 资源验证器 5 | * 在webview加载资源前,给使用者一次验证资源 有效性的机会 6 | */ 7 | public interface ResoureceValidator { 8 | boolean validate(ResourceInfo resourceInfo); 9 | } 10 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core.util; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | 8 | import com.hht.webpackagekit.core.Constants; 9 | 10 | import java.io.BufferedInputStream; 11 | import java.io.Closeable; 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.nio.channels.FileChannel; 19 | import java.util.zip.ZipEntry; 20 | import java.util.zip.ZipInputStream; 21 | 22 | /** 23 | * file 工具类 24 | */ 25 | public class FileUtils { 26 | 27 | private static final String TAG = "FileUtils"; 28 | 29 | /** 30 | * 根据fileName获取inputStream 31 | */ 32 | public static InputStream getInputStream(String fileName) { 33 | 34 | if (TextUtils.isEmpty(fileName)) { 35 | return null; 36 | } 37 | File file = new File(fileName); 38 | if (!file.exists()) { 39 | return null; 40 | } 41 | if (file.isDirectory()) { 42 | return null; 43 | } 44 | FileInputStream fileInputStream = null; 45 | try { 46 | fileInputStream = new FileInputStream(file); 47 | } catch (Exception e) { 48 | 49 | } 50 | if (fileInputStream == null) { 51 | return null; 52 | } 53 | return new BufferedInputStream(fileInputStream); 54 | } 55 | 56 | /** 57 | * 解压zip到指定的路径 58 | * 59 | * @param zipFileString ZIP的名称 60 | * @param outPathString 要解压缩路径 61 | */ 62 | public static boolean unZipFolder(String zipFileString, String outPathString) { 63 | ZipInputStream inZip = deleteOutUnZipFileIfNeed(zipFileString, outPathString); 64 | if (inZip == null) { 65 | return false; 66 | } 67 | boolean isSuccess = true; 68 | ZipEntry zipEntry = null; 69 | zipEntry = readZipNextZipEntry(inZip); 70 | if (zipEntry == null) { 71 | return false; 72 | } 73 | String szName; 74 | while (zipEntry != null) { 75 | szName = zipEntry.getName(); 76 | /** 77 | * 不是package开头,认为是无效数据 78 | */ 79 | if (!szName.startsWith(Constants.RESOURCE_MIDDLE_PATH)) { 80 | zipEntry = readZipNextZipEntry(inZip); 81 | continue; 82 | } 83 | if (zipEntry.isDirectory()) { 84 | szName = szName.substring(0, szName.length() - 1); 85 | File folder = new File(outPathString + File.separator + szName); 86 | isSuccess = folder.mkdirs(); 87 | if (!isSuccess) { 88 | break; 89 | } 90 | } else { 91 | File file = new File(outPathString + File.separator + szName); 92 | if (!file.exists()) { 93 | isSuccess = makeUnZipFile(outPathString, szName); 94 | } 95 | if (!isSuccess) { 96 | break; 97 | } 98 | isSuccess = writeUnZipFileToFile(inZip, file); 99 | } 100 | if (!isSuccess) { 101 | break; 102 | } 103 | zipEntry = readZipNextZipEntry(inZip); 104 | } 105 | isSuccess = safeCloseFile(inZip); 106 | return isSuccess; 107 | } 108 | 109 | private static ZipInputStream deleteOutUnZipFileIfNeed(String zipFileString, String outPathString) { 110 | boolean isSuccess = true; 111 | ZipInputStream inZip = null; 112 | try { 113 | inZip = new ZipInputStream(new FileInputStream(zipFileString)); 114 | } catch (FileNotFoundException e) { 115 | isSuccess = false; 116 | } 117 | if (!isSuccess) { 118 | return null; 119 | } 120 | File outPath = new File(outPathString); 121 | if (outPath.exists()) { 122 | isSuccess = deleteDir(outPath); 123 | } 124 | if (!isSuccess) { 125 | return null; 126 | } 127 | return inZip; 128 | } 129 | 130 | private static boolean makeUnZipFile(String outPathString, String szName) { 131 | boolean isSuccess = true; 132 | File file = new File(outPathString + File.separator + szName); 133 | if (file.getParentFile() != null && !file.getParentFile().exists()) { 134 | isSuccess = file.getParentFile().mkdirs(); 135 | } 136 | if (!isSuccess) { 137 | return false; 138 | } 139 | try { 140 | isSuccess = file.createNewFile(); 141 | } catch (IOException e) { 142 | isSuccess = false; 143 | } 144 | return isSuccess; 145 | } 146 | 147 | /** 148 | * 读取zip数据,如果抛出异常返回-2 149 | */ 150 | private static int readZipFile(ZipInputStream inZip, byte[] buffer) { 151 | int len = -1; 152 | try { 153 | len = inZip.read(buffer); 154 | } catch (IOException e) { 155 | len = -2; 156 | } 157 | return len; 158 | } 159 | 160 | private static ZipEntry readZipNextZipEntry(ZipInputStream inZip) { 161 | ZipEntry zipEntry = null; 162 | boolean isSuccess = true; 163 | try { 164 | zipEntry = inZip.getNextEntry(); 165 | } catch (IOException e) { 166 | isSuccess = false; 167 | } 168 | if (!isSuccess) { 169 | return null; 170 | } 171 | return zipEntry; 172 | } 173 | 174 | private static boolean writeUnZipFileToFile(ZipInputStream inZip, File file) { 175 | boolean isSuccess = true; 176 | // 获取文件的输出流 177 | FileOutputStream out = null; 178 | try { 179 | out = new FileOutputStream(file); 180 | } catch (FileNotFoundException e) { 181 | isSuccess = false; 182 | } 183 | int len = -1; 184 | byte[] buffer = new byte[1024]; 185 | len = readZipFile(inZip, buffer); 186 | if (len == -1 || len == -2) { 187 | isSuccess = false; 188 | } 189 | if (!isSuccess) { 190 | return false; 191 | } 192 | // 读取(字节)字节到缓冲区 193 | while (len != -1) { 194 | // 从缓冲区(0)位置写入(字节)字节 195 | try { 196 | out.write(buffer, 0, len); 197 | } catch (IOException e) { 198 | isSuccess = false; 199 | } 200 | if (!isSuccess) { 201 | break; 202 | } 203 | try { 204 | out.flush(); 205 | } catch (IOException e) { 206 | isSuccess = false; 207 | } 208 | if (!isSuccess) { 209 | break; 210 | } 211 | len = readZipFile(inZip, buffer); 212 | if (len == -2) { 213 | isSuccess = false; 214 | break; 215 | } 216 | } 217 | try { 218 | out.close(); 219 | } catch (IOException e) { 220 | 221 | } 222 | return isSuccess; 223 | } 224 | 225 | /** 226 | * 获取缓存目录 227 | */ 228 | public static File getFileDirectory(Context context, boolean preferExternal) { 229 | File appCacheDir = null; 230 | if (preferExternal && isExternalStorageMounted()) { 231 | appCacheDir = getExternalCacheDir(context); 232 | } 233 | if (appCacheDir == null) { 234 | appCacheDir = context.getFilesDir(); 235 | } 236 | if (appCacheDir == null) { 237 | String cacheDirPath = "/data/data/" + context.getPackageName() + "/file/"; 238 | appCacheDir = new File(cacheDirPath); 239 | } 240 | return appCacheDir; 241 | } 242 | 243 | public static File getResourceIndexFile(Context context, String packageId, String version) { 244 | String indexPath = 245 | getPackageWorkName(context, packageId, version) + File.separator + Constants.RESOURCE_MIDDLE_PATH 246 | + File.separator + Constants.RESOURCE_INDEX_NAME; 247 | return new File(indexPath); 248 | } 249 | 250 | private static File getExternalCacheDir(Context context) { 251 | File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"); 252 | File appCacheDir = new File(new File(dataDir, context.getPackageName()), "file"); 253 | if (!appCacheDir.exists()) { 254 | if (!appCacheDir.mkdirs()) { 255 | return null; 256 | } 257 | } 258 | return appCacheDir; 259 | } 260 | 261 | public static boolean isExternalStorageMounted() { 262 | return Environment.MEDIA_MOUNTED.equalsIgnoreCase(getExternalStorageState()); 263 | } 264 | 265 | public static String getExternalStorageState() { 266 | String externalStorageState; 267 | try { 268 | externalStorageState = Environment.getExternalStorageState(); 269 | } catch (NullPointerException e) { // (sh)it happens 270 | externalStorageState = ""; 271 | } 272 | return externalStorageState; 273 | } 274 | 275 | /** 276 | * 获取根容器的地址 277 | */ 278 | public static String getPackageRootPath(Context context) { 279 | File fileDir = getFileDirectory(context, false); 280 | if (fileDir == null) { 281 | return null; 282 | } 283 | 284 | String path = fileDir + File.separator + Constants.PACKAGE_FILE_ROOT_PATH; 285 | File file; 286 | if (!(file = new File(path)).exists()) { 287 | file.mkdirs(); 288 | } 289 | return path; 290 | } 291 | 292 | /** 293 | * 获取根容器的地址 294 | */ 295 | public static String getPackageLoadPath(Context context, String packageId) { 296 | String root = getPackageRootPath(context); 297 | if (TextUtils.isEmpty(root)) { 298 | return null; 299 | } 300 | return root + File.separator + packageId; 301 | } 302 | 303 | /*** 304 | * 根据packageId获取work地址 305 | * */ 306 | public static String getPackageWorkName(Context context, String packageId, String version) { 307 | String root = getPackageRootPath(context); 308 | if (TextUtils.isEmpty(root)) { 309 | return null; 310 | } 311 | return root + File.separator + packageId + File.separator + version + File.separator + Constants.PACKAGE_WORK; 312 | } 313 | 314 | /*** 315 | * 根据packageId获取work地址 316 | * */ 317 | public static String getPackageRootByPackageId(Context context, String packageId) { 318 | String root = getPackageRootPath(context); 319 | if (TextUtils.isEmpty(root)) { 320 | return null; 321 | } 322 | return root + File.separator + packageId; 323 | } 324 | 325 | //获取packageIndex.json地址 326 | public static String getPackageIndexFileName(Context context) { 327 | String root = getPackageRootPath(context); 328 | if (TextUtils.isEmpty(root)) { 329 | return null; 330 | } 331 | makeDir(root); 332 | return root + File.separator + Constants.PACKAGE_FILE_INDEX; 333 | } 334 | 335 | /*** 336 | * 根据packageId获取update地址 337 | * */ 338 | public static String getPackageUpdateName(Context context, String packageId, String version) { 339 | String root = getPackageRootPath(context); 340 | if (TextUtils.isEmpty(root)) { 341 | return null; 342 | } 343 | return root + File.separator + packageId + File.separator + version + File.separator + Constants.PACKAGE_UPDATE; 344 | } 345 | 346 | /*** 347 | * 根据packageId获取下载目录文件 348 | * */ 349 | public static String getPackageDownloadName(Context context, String packageId, String version) { 350 | String root = getPackageRootPath(context); 351 | if (TextUtils.isEmpty(root)) { 352 | return null; 353 | } 354 | return root + File.separator + packageId + File.separator + version + File.separator 355 | + Constants.PACKAGE_DOWNLOAD; 356 | } 357 | 358 | /*** 359 | * 获取预置的离线包被拷贝到指定目录的路径 360 | * */ 361 | public static String getPackageAssetsName(Context context, String packageId, String version) { 362 | String root = getPackageRootPath(context); 363 | if (TextUtils.isEmpty(root)) { 364 | return null; 365 | } 366 | return root + File.separator + "assets" + File.separator + packageId + File.separator + version + File.separator 367 | + Constants.PACKAGE_ASSETS; 368 | } 369 | 370 | /*** 371 | * 根据packageId获取下载目录文件 372 | * */ 373 | public static String getPackageMergePatch(Context context, String packageId, String version) { 374 | String root = getPackageRootPath(context); 375 | if (TextUtils.isEmpty(root)) { 376 | return null; 377 | } 378 | return root + File.separator + packageId + File.separator + version + File.separator 379 | + Constants.PACKAGE_MERGE; 380 | } 381 | 382 | /** 383 | * 复制单个文件 384 | * 385 | * @param srcFileName 待复制的文件名 386 | * @param descFileName 目标文件名 387 | * @return 如果复制成功,则返回true,否则返回false 388 | */ 389 | public static boolean copyFileCover(String srcFileName, String descFileName) { 390 | File srcFile = new File(srcFileName); 391 | if (!srcFile.exists()) { 392 | return false; 393 | } else if (!srcFile.isFile()) { 394 | return false; 395 | } 396 | File descFile = new File(descFileName); 397 | if (descFile.exists()) { 398 | if (!FileUtils.delFile(descFileName)) { 399 | return false; 400 | } 401 | } else if (descFile.getParentFile() != null) { 402 | if (!descFile.getParentFile().exists()) { 403 | if (!descFile.getParentFile().mkdirs()) { 404 | return false; 405 | } 406 | } 407 | } else { 408 | return false; 409 | } 410 | boolean isSuccess; 411 | try { 412 | isSuccess = copyFileByChannel(srcFile, descFile); 413 | return isSuccess; 414 | } catch (Exception e) { 415 | return false; 416 | } finally { 417 | } 418 | } 419 | 420 | /** 421 | * 删除文件,可以删除单个文件或文件夹 422 | * 423 | * @param fileName 被删除的文件名 424 | * @return 如果删除成功,则返回true,否是返回false 425 | */ 426 | public static boolean delFile(String fileName) { 427 | File file = new File(fileName); 428 | if (!file.exists()) { 429 | return true; 430 | } else { 431 | if (file.isFile()) { 432 | return FileUtils.deleteFile(fileName); 433 | } 434 | } 435 | return true; 436 | } 437 | 438 | public static boolean deleteFile(String fileName) { 439 | File file = new File(fileName); 440 | if (file.exists() && file.isFile()) { 441 | return file.delete(); 442 | } else { 443 | return true; 444 | } 445 | } 446 | 447 | public static boolean makeDir(String path) { 448 | File file = new File(path); 449 | if (!file.exists()) { 450 | return file.mkdirs(); 451 | } 452 | return true; 453 | } 454 | 455 | /** 456 | * 复制某个目录及目录下的所有子目录和文件到新文件夹 457 | * 458 | * @param oldPath 源文件夹路径 459 | * @param newPath 目标文件夹路径 460 | */ 461 | public static boolean copyFolder(String oldPath, String newPath) { 462 | boolean isSuccess = true; 463 | try { 464 | File newFile = new File(newPath); 465 | if (newFile.exists()) { 466 | isSuccess = deleteDir(newFile); 467 | } 468 | if (!isSuccess) { 469 | return false; 470 | } 471 | isSuccess = newFile.mkdirs(); 472 | if (!isSuccess) { 473 | return false; 474 | } 475 | File fileList = new File(oldPath); 476 | String[] file = fileList.list(); 477 | File tempFile; 478 | for (String itemFile : file) { 479 | // 如果oldPath以路径分隔符/或者\结尾,那么则oldPath/文件名就可以了 480 | // 否则要自己oldPath后面补个路径分隔符再加文件名 481 | if (oldPath.endsWith(File.separator)) { 482 | tempFile = new File(oldPath + itemFile); 483 | } else { 484 | tempFile = new File(oldPath + File.separator + itemFile); 485 | } 486 | 487 | if (tempFile.isFile()) { 488 | isSuccess = copyFileByChannel(tempFile, new File(newPath + File.separator + tempFile.getName())); 489 | } 490 | if (!isSuccess) { 491 | break; 492 | } 493 | if (tempFile.isDirectory()) { 494 | isSuccess = copyFolder(oldPath + File.separator + itemFile, newPath + File.separator + itemFile); 495 | } 496 | if (!isSuccess) { 497 | break; 498 | } 499 | } 500 | } catch (Exception e) { 501 | 502 | } 503 | return isSuccess; 504 | } 505 | 506 | public static boolean copyFileByChannel(File src, File dest) { 507 | FileInputStream fi = null; 508 | FileOutputStream fo = null; 509 | FileChannel in = null; 510 | FileChannel out = null; 511 | boolean isSuccess = true; 512 | try { 513 | fi = new FileInputStream(src); 514 | fo = new FileOutputStream(dest); 515 | in = fi.getChannel(); 516 | out = fo.getChannel(); 517 | in.transferTo(0, in.size(), out); 518 | } catch (IOException e) { 519 | isSuccess = false; 520 | } finally { 521 | try { 522 | fi.close(); 523 | in.close(); 524 | fo.close(); 525 | out.close(); 526 | } catch (IOException e) { 527 | e.printStackTrace(); 528 | } 529 | } 530 | return isSuccess; 531 | } 532 | 533 | /** 534 | * 删除某个目录及目录下的所有子目录和文件 535 | * 536 | * @param dir File path 537 | * @return boolean 538 | */ 539 | public static boolean deleteDir(File dir) { 540 | if (dir.isDirectory()) { 541 | String[] children = dir.list(); 542 | if (children != null) { 543 | for (String aChildren : children) { 544 | boolean isDelete = deleteDir(new File(dir, aChildren)); 545 | if (!isDelete) { 546 | return false; 547 | } 548 | } 549 | } 550 | } 551 | return dir.delete(); 552 | } 553 | 554 | public static String getStringForZip(InputStream zipFileString) { 555 | boolean isSuccess = true; 556 | ZipInputStream inZip = null; 557 | try { 558 | inZip = new ZipInputStream(zipFileString); 559 | } catch (Exception e) { 560 | isSuccess = false; 561 | } 562 | if (!isSuccess) { 563 | return null; 564 | } 565 | ZipEntry zipEntry = null; 566 | zipEntry = readZipNextZipEntry(inZip); 567 | if (zipEntry == null) { 568 | safeCloseFile(inZip); 569 | return ""; 570 | } 571 | String szName; 572 | String zipName = zipEntry.getName().split("\\/")[0]; 573 | Log.d(TAG,"zipName>>>"+zipName); 574 | while (zipEntry != null) { 575 | szName = zipEntry.getName(); 576 | Log.d(TAG, "RESOURCE_MIDDLE_PATH>>> " +szName); 577 | //index.json 578 | if (szName.equals(zipName + File.separator + Constants.RESOURCE_INDEX_NAME)) { 579 | break; 580 | } 581 | if (!isSuccess) { 582 | break; 583 | } 584 | zipEntry = readZipNextZipEntry(inZip); 585 | } 586 | if (zipEntry == null) { 587 | safeCloseFile(inZip); 588 | return ""; 589 | } 590 | StringBuilder sb = new StringBuilder(); 591 | int len = -1; 592 | byte[] buffer = new byte[2048]; 593 | len = readZipFile(inZip, buffer); 594 | while (len != -1) { 595 | if (len == -2) { 596 | isSuccess = false; 597 | break; 598 | } 599 | sb.append(new String(buffer, 0, len)); 600 | len = readZipFile(inZip, buffer); 601 | } 602 | isSuccess = safeCloseFile(inZip); 603 | if (isSuccess) { 604 | return sb.toString(); 605 | } 606 | return ""; 607 | } 608 | 609 | public static boolean safeCloseFile(Closeable file) { 610 | boolean isSuccess = true; 611 | try { 612 | file.close(); 613 | } catch (IOException e) { 614 | isSuccess = false; 615 | } 616 | return isSuccess; 617 | } 618 | 619 | public static boolean copyFile(InputStream inStream, String newPath) { 620 | boolean isSuccess = true; 621 | try { 622 | 623 | int byteread = 0; 624 | File file = new File(newPath); 625 | if (file.exists()) { 626 | isSuccess = file.delete(); 627 | } 628 | if (!isSuccess) { 629 | return false; 630 | } 631 | if (file.getParentFile() != null && !file.getParentFile().exists()) { 632 | isSuccess = file.getParentFile().mkdirs(); 633 | } 634 | if (!isSuccess) { 635 | return false; 636 | } 637 | isSuccess = file.createNewFile(); 638 | if (!isSuccess) { 639 | return false; 640 | } 641 | FileOutputStream fs = new FileOutputStream(file); 642 | byte[] buffer = new byte[1024 * 16]; 643 | while ((byteread = inStream.read(buffer)) != -1) { 644 | fs.write(buffer, 0, byteread); 645 | } 646 | try { 647 | fs.flush(); 648 | } catch (Exception e) { 649 | 650 | } 651 | safeCloseFile(inStream); 652 | safeCloseFile(fs); 653 | } catch (Exception e) { 654 | isSuccess = false; 655 | } 656 | return isSuccess; 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/util/GsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core.util; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.Reader; 10 | 11 | /** 12 | * gson 工具 13 | */ 14 | public class GsonUtils { 15 | 16 | private static final Gson gson = new Gson(); 17 | 18 | /** 19 | * 在格式错误时不抛异常, 返回null, 为了处理服务器500+的情况, 会返回一个普通字符串 20 | */ 21 | public static T fromJsonIgnoreException(String json, Class classOfT) { 22 | try { 23 | return gson.fromJson(json, classOfT); 24 | } catch (Throwable ignore) { 25 | return null; 26 | } 27 | } 28 | 29 | /** 30 | * 在格式错误时不抛异常, 返回null, 为了处理服务器500+的情况, 会返回一个普通字符串 31 | */ 32 | public static T fromJsonIgnoreException(InputStream json, Class classOfT) { 33 | Reader reader = null; 34 | BufferedReader bufferedReader = null; 35 | T entity = null; 36 | try { 37 | reader = new InputStreamReader(json); 38 | bufferedReader = new BufferedReader(reader); 39 | entity = gson.fromJson(bufferedReader, classOfT); 40 | } catch (Throwable ignore) { 41 | 42 | } finally { 43 | 44 | if (reader != null) { 45 | try { 46 | reader.close(); 47 | } catch (IOException e) { 48 | } 49 | } 50 | if (bufferedReader != null) { 51 | try { 52 | bufferedReader.close(); 53 | } catch (IOException e) { 54 | } 55 | } 56 | } 57 | return entity; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/util/Logger.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core.util; 2 | 3 | import android.util.Log; 4 | 5 | public class Logger { 6 | private static final String TAG = "webpackagekit"; 7 | public static boolean DEBUG = true; 8 | 9 | public static void d(String msg) { 10 | if (DEBUG) { 11 | Log.d(TAG, msg); 12 | } 13 | } 14 | 15 | public static void e(String msg) { 16 | if (DEBUG) { 17 | Log.e(TAG, msg); 18 | } 19 | } 20 | 21 | public static void w(String msg) { 22 | if (DEBUG) { 23 | Log.w(TAG, msg); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/util/MD5Utils.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core.util; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.FileNotFoundException; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.math.BigInteger; 12 | import java.security.MessageDigest; 13 | import java.security.NoSuchAlgorithmException; 14 | 15 | /** 16 | * md5工具类 17 | */ 18 | public class MD5Utils { 19 | private static final String TAG = "MD5"; 20 | private final static char[] HEX_CHARS = "0123456789abcdef".toCharArray(); 21 | 22 | public static boolean checkMD5(String md5, File updateFile) { 23 | if (TextUtils.isEmpty(md5) || updateFile == null) { 24 | Log.e(TAG, "MD5 string empty or updateFile null"); 25 | return false; 26 | } 27 | 28 | String calculatedDigest = calculateMD5(updateFile); 29 | if (calculatedDigest == null) { 30 | Log.e(TAG, "calculatedDigest null"); 31 | return false; 32 | } 33 | 34 | Log.v(TAG, "Calculated digest: " + calculatedDigest); 35 | Log.v(TAG, "Provided digest: " + md5); 36 | 37 | return calculatedDigest.equalsIgnoreCase(md5); 38 | } 39 | 40 | public static String calculateMD5(File updateFile) { 41 | try { 42 | 43 | InputStream is; 44 | try { 45 | is = new FileInputStream(updateFile); 46 | } catch (FileNotFoundException e) { 47 | Log.e(TAG, "Exception while getting FileInputStream", e); 48 | return ""; 49 | } 50 | 51 | return calculateMD5(is); 52 | } catch (Exception e) { 53 | return ""; 54 | } 55 | } 56 | 57 | public static String calculateMD5(InputStream is) { 58 | MessageDigest digest; 59 | try { 60 | digest = MessageDigest.getInstance("MD5"); 61 | } catch (NoSuchAlgorithmException e) { 62 | Log.e(TAG, "Exception while getting digest", e); 63 | return ""; 64 | } 65 | 66 | byte[] buffer = new byte[8192]; 67 | int read; 68 | try { 69 | while ((read = is.read(buffer)) > 0) { 70 | digest.update(buffer, 0, read); 71 | } 72 | byte[] md5sum = digest.digest(); 73 | BigInteger bigInt = new BigInteger(1, md5sum); 74 | String output = bigInt.toString(16); 75 | // Fill to 32 chars 76 | output = String.format("%32s", output).replace(' ', '0'); 77 | return output; 78 | } catch (IOException e) { 79 | Log.e(TAG, "Unable to process file for MD5", e); 80 | return ""; 81 | } finally { 82 | try { 83 | is.close(); 84 | } catch (IOException e) { 85 | Log.e(TAG, "Exception on closing MD5 input stream", e); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 计算一个字符串的MD5值 92 | */ 93 | public static String getMD5(String str) { 94 | try { 95 | // 生成一个MD5加密计算摘要 96 | MessageDigest md = MessageDigest.getInstance("MD5"); 97 | // 计算md5函数 98 | md.update(str.getBytes()); 99 | // digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符 100 | // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值 101 | return bytesToHex(md.digest()); 102 | } catch (Exception e) { 103 | e.printStackTrace(); 104 | return null; 105 | } 106 | } 107 | 108 | /** 109 | * 将bytes数组转化为16进制 110 | */ 111 | private static String bytesToHex(byte[] bytes) { 112 | char[] hexChars = new char[bytes.length * 2]; 113 | for (int j = 0; j < bytes.length; j++) { 114 | int v = bytes[j] & 0xFF; 115 | hexChars[j * 2] = HEX_CHARS[v >>> 4]; 116 | hexChars[j * 2 + 1] = HEX_CHARS[v & 0x0F]; 117 | } 118 | return new String(hexChars); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/util/MimeTypeUtils.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * mimeType工具类 8 | */ 9 | public class MimeTypeUtils { 10 | private static final List supportMineTypeList = new ArrayList<>(2); 11 | 12 | static { 13 | supportMineTypeList.add("application/x-javascript"); 14 | supportMineTypeList.add("image/jpeg"); 15 | supportMineTypeList.add("image/tiff"); 16 | supportMineTypeList.add("text/css"); 17 | supportMineTypeList.add("image/gif"); 18 | supportMineTypeList.add("image/png"); 19 | supportMineTypeList.add("application/javascript"); 20 | } 21 | 22 | public static boolean checkIsSupportMimeType(String mimeType) { 23 | return supportMineTypeList.contains(mimeType); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/core/util/VersionUtils.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.core.util; 2 | 3 | /** 4 | * 版本工具 5 | */ 6 | public class VersionUtils { 7 | 8 | public static int compareVersion(String version1, String version2) { 9 | 10 | if (version1 == null || version2 == null) { 11 | return 0; 12 | } 13 | //注意此处为正则匹配,不能用"."; 14 | String[] versionArray1 = version1.split("\\."); 15 | //如果位数只有一位则自动补零(防止出现一个是04,一个是5 直接以长度比较) 16 | for (int i = 0; i < versionArray1.length; i++) { 17 | if (versionArray1[i].length() == 1) { 18 | versionArray1[i] = "0" + versionArray1[i]; 19 | } 20 | } 21 | String[] versionArray2 = version2.split("\\."); 22 | //如果位数只有一位则自动补零 23 | for (int i = 0; i < versionArray2.length; i++) { 24 | if (versionArray2[i].length() == 1) { 25 | versionArray2[i] = "0" + versionArray2[i]; 26 | } 27 | } 28 | int idx = 0; 29 | //取最小长度值 30 | int minLength = Math.min(versionArray1.length, versionArray2.length); 31 | int diff = 0; 32 | //先比较长度再比较字符 33 | while (idx < minLength && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0 34 | && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) { 35 | ++idx; 36 | } 37 | //如果已经分出大小,则直接返回,如果未分出大小,则再比较位数,有子版本的为大; 38 | diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length; 39 | return diff; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/inner/AssetResourceLoaderImpl.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.inner; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | 6 | import com.hht.webpackagekit.core.AssetResourceLoader; 7 | import com.hht.webpackagekit.core.PackageInfo; 8 | import com.hht.webpackagekit.core.PackageStatus; 9 | import com.hht.webpackagekit.core.ResourceInfoEntity; 10 | import com.hht.webpackagekit.core.util.FileUtils; 11 | import com.hht.webpackagekit.core.util.GsonUtils; 12 | import com.hht.webpackagekit.core.util.MD5Utils; 13 | 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.FileNotFoundException; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | 20 | /** 21 | * asset 资源加载 22 | */ 23 | public class AssetResourceLoaderImpl implements AssetResourceLoader { 24 | private final Context context; 25 | 26 | public AssetResourceLoaderImpl(Context context) { 27 | this.context = context; 28 | } 29 | 30 | //基于预置的离线包生成PackageInfo 31 | @Override 32 | public PackageInfo load(String path) { 33 | InputStream inputStream = null; 34 | 35 | //读取assets目录下的离线包 36 | inputStream = openAssetInputStream(path); 37 | if (inputStream == null) { 38 | return null; 39 | } 40 | 41 | String indexInfo = FileUtils.getStringForZip(inputStream); 42 | 43 | if (TextUtils.isEmpty(indexInfo)) { 44 | return null; 45 | } 46 | 47 | //基于assets下某个离线包生成ResourceInfoEntity实例,可获取离线包的version、packageId和ResourceInfo(具体每个远程资源对应一个本地资源)数组 48 | ResourceInfoEntity assetEntity = GsonUtils.fromJsonIgnoreException(indexInfo, ResourceInfoEntity.class); 49 | if (assetEntity == null) { 50 | return null; 51 | } 52 | 53 | //读取update.zip文件流 54 | File file = 55 | new File(FileUtils.getPackageUpdateName(context, assetEntity.getPackageId(), assetEntity.getVersion())); 56 | ResourceInfoEntity localEntity = null; 57 | FileInputStream fileInputStream = null; 58 | if (file.exists()) { 59 | try { 60 | fileInputStream = new FileInputStream(file); 61 | } catch (FileNotFoundException e) { 62 | 63 | } 64 | } 65 | String local = null; 66 | if (fileInputStream != null) { 67 | local = FileUtils.getStringForZip(fileInputStream); 68 | } 69 | //基于update.zip生成ResourceInfoEntity实例 70 | if (!TextUtils.isEmpty(local)) { 71 | localEntity = GsonUtils.fromJsonIgnoreException(local, ResourceInfoEntity.class); 72 | } 73 | //比对update.zip和assets生成的ResourceInfoEntity实例的版本version,如果assets版本小于等于update.zip则返回null 74 | 75 | // if (localEntity != null 76 | // && VersionUtils.compareVersion(assetEntity.getVersion(), localEntity.getVersion()) <= 0) { 77 | // return null; 78 | // } 79 | 80 | //将assets目录下某个资源包拷贝到设定的目录下PackageAssetsName: ${root}/assets/${packageId}/${version}/package.zip 81 | String assetPath = 82 | FileUtils.getPackageAssetsName(context, assetEntity.getPackageId(), assetEntity.getVersion()); 83 | 84 | inputStream = openAssetInputStream(path); 85 | if (inputStream == null) { 86 | return null; 87 | } 88 | boolean isSuccess = FileUtils.copyFile(inputStream, assetPath); 89 | if (!isSuccess) { 90 | return null; 91 | } 92 | FileUtils.safeCloseFile(inputStream); 93 | 94 | String md5 = MD5Utils.calculateMD5(new File(assetPath)); 95 | if (TextUtils.isEmpty(md5)) { 96 | return null; 97 | } 98 | 99 | //将assets文件下某个压缩资源包的信息转化成PackageInfo实例的属性 100 | PackageInfo info = new PackageInfo(); 101 | info.setPackageId(assetEntity.getPackageId()); 102 | info.setStatus(PackageStatus.onLine); 103 | info.setVersion(assetEntity.getVersion()); 104 | // info.setMd5(md5); 105 | return info; 106 | } 107 | 108 | private InputStream openAssetInputStream(String path) { 109 | InputStream inputStream = null; 110 | try { 111 | inputStream = context.getAssets().open(path); 112 | } catch (IOException e) { 113 | } 114 | return inputStream; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/inner/DownloaderImpl.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.inner; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.hht.webpackagekit.core.Constants; 7 | import com.hht.webpackagekit.core.Downloader; 8 | import com.hht.webpackagekit.core.PackageInfo; 9 | import com.hht.webpackagekit.core.util.FileUtils; 10 | import com.hht.webpackagekit.core.util.Logger; 11 | import com.liulishuo.filedownloader.BaseDownloadTask; 12 | import com.liulishuo.filedownloader.FileDownloadSampleListener; 13 | import com.liulishuo.filedownloader.FileDownloader; 14 | import com.liulishuo.filedownloader.model.FileDownloadStatus; 15 | 16 | /** 17 | * 下载器实现类 18 | */ 19 | public class DownloaderImpl implements Downloader { 20 | private final Context context; 21 | private String downloadUrl; 22 | 23 | public DownloaderImpl(Context context) { 24 | this.context = context; 25 | } 26 | 27 | //根据PackageInfo的downloadUrl下载离线包到PackageDownloadName: ${root}/${packageId}/${version}/download.zip 28 | @Override 29 | public void download(PackageInfo packageInfo, final DownloadCallback callback) { 30 | downloadUrl = Constants.BASE_URL + packageInfo.getDownloadUrl(); 31 | Log.d("download", downloadUrl); 32 | 33 | BaseDownloadTask downloadTask = FileDownloader.getImpl() 34 | .create(downloadUrl) 35 | .setTag(packageInfo.getPackageId()) 36 | .setPath(FileUtils.getPackageDownloadName(context, packageInfo.getPackageId(), packageInfo.getVersion())) 37 | .setListener(new FileDownloadSampleListener() { 38 | @Override 39 | protected void completed(BaseDownloadTask task) { 40 | super.completed(task); 41 | if (callback != null && task.getStatus() == FileDownloadStatus.completed) { 42 | callback.onSuccess((String) task.getTag()); 43 | } else if (callback != null) { 44 | callback.onFailure((String) task.getTag()); 45 | } 46 | } 47 | 48 | @Override 49 | protected void error(BaseDownloadTask task, Throwable e) { 50 | super.error(task, e); 51 | Logger.e("packageResource download error: " + e.getMessage()); 52 | if (callback != null) { 53 | callback.onFailure((String) task.getTag()); 54 | } 55 | } 56 | }); 57 | downloadTask.start(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/inner/PackageInstallerImpl.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.inner; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import com.hht.lib.bsdiff.PatchUtils; 8 | import com.hht.webpackagekit.core.PackageEntity; 9 | import com.hht.webpackagekit.core.PackageInfo; 10 | import com.hht.webpackagekit.core.PackageInstaller; 11 | import com.hht.webpackagekit.core.util.FileUtils; 12 | import com.hht.webpackagekit.core.util.GsonUtils; 13 | import com.hht.webpackagekit.core.util.Logger; 14 | 15 | import java.io.File; 16 | import java.io.FileInputStream; 17 | import java.io.FileNotFoundException; 18 | import java.security.MessageDigest; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * 单个离线包安装器 24 | */ 25 | public class PackageInstallerImpl implements PackageInstaller { 26 | private final Context context; 27 | private final static String TAG = "PackageInstallerImpl"; 28 | 29 | public PackageInstallerImpl(Context context) { 30 | this.context = context; 31 | } 32 | 33 | /** 34 | * 下载的离线包download.zip或预置在assets的离线包package.zip 35 | * 如果是patch文件 merge.zip 36 | * 更新后的zip文件 update.zip 37 | */ 38 | @Override 39 | public boolean install(PackageInfo packageInfo, boolean isAssets) { 40 | //获取刚下载的离线包download.zip的路径,或者是预先加载到assets的离线包package.zip的路径 41 | String downloadFile = 42 | isAssets ? FileUtils.getPackageAssetsName(context, packageInfo.getPackageId(), packageInfo.getVersion()) 43 | : FileUtils.getPackageDownloadName(context, packageInfo.getPackageId(), packageInfo.getVersion()); 44 | String willCopyFile = downloadFile; 45 | 46 | //获取即将被更新的离线包update.zip的路径 47 | String updateFile = 48 | FileUtils.getPackageUpdateName(context, packageInfo.getPackageId(), packageInfo.getVersion()); 49 | 50 | boolean isSuccess = true; 51 | String lastVersion = getLastVersion(packageInfo.getPackageId()); 52 | if (packageInfo.isPatch() && TextUtils.isEmpty(lastVersion)) { 53 | Logger.e("资源为patch ,但是上个版本信息没有数据,无法patch!"); 54 | return false; 55 | } 56 | 57 | //如果是增量包,则合并增量包 58 | if (packageInfo.isPatch()) { 59 | //获取将被更新的离线包update.zip 60 | String baseFile = FileUtils.getPackageUpdateName(context, packageInfo.getPackageId(), lastVersion); 61 | //获取即将合并成的包 62 | String mergePatch = 63 | FileUtils.getPackageMergePatch(context, packageInfo.getPackageId(), packageInfo.getVersion()); 64 | //合并 已经即将被更新的包download.zip或res.zip 和 本地即将被更新的离线包update.zip,生成merge.zip 65 | //并删除刚下载的离线包download.zip或res.zip 66 | int status = -1; 67 | String downloadFileMD5 = getFileMD5(new File(downloadFile)); 68 | Log.d("mergepatch", "local file md5:"+ downloadFileMD5+"| packageInfo md5:"+ packageInfo.getMd5()); 69 | if (!downloadFileMD5.equals(packageInfo.getMd5())) { 70 | Log.d("mergepatch", "local file diff then remote"); 71 | return false; 72 | } 73 | 74 | try { 75 | status = PatchUtils.getInstance().patch(baseFile, mergePatch, downloadFile); 76 | } catch (Exception ignore) { 77 | Logger.e("patch error " + ignore.getMessage()); 78 | } 79 | if (status == 0) { 80 | willCopyFile = mergePatch; 81 | FileUtils.deleteFile(downloadFile); 82 | } else { 83 | isSuccess = false; 84 | } 85 | } 86 | if (!isSuccess) { 87 | Logger.e("资源patch merge 失败!"); 88 | return false; 89 | } 90 | 91 | //拷贝downloadFile(download.zip 或 res.zip)或合并增量包生成的merge.zip到update.zip,并删除刚被拷贝的文件 92 | isSuccess = FileUtils.copyFileCover(willCopyFile, updateFile); 93 | if (!isSuccess) { 94 | Logger.e("[" + packageInfo.getPackageId() + "] : " + "copy file error "); 95 | return false; 96 | } 97 | isSuccess = FileUtils.delFile(willCopyFile); 98 | if (!isSuccess) { 99 | Logger.e("[" + packageInfo.getPackageId() + "] : " + "delete will copy file error "); 100 | return false; 101 | } 102 | 103 | //解压已经更新过的update.zip资源包到work目录下 104 | String workPath = FileUtils.getPackageWorkName(context, packageInfo.getPackageId(), packageInfo.getVersion()); 105 | try { 106 | isSuccess = FileUtils.unZipFolder(updateFile, workPath); 107 | } catch (Exception e) { 108 | isSuccess = false; 109 | } 110 | if (!isSuccess) { 111 | Logger.e("[" + packageInfo.getPackageId() + "] : " + "unZipFolder error "); 112 | return false; 113 | } 114 | if (isSuccess) { 115 | FileUtils.deleteFile(willCopyFile); 116 | cleanOldFileIfNeed(packageInfo.getPackageId(), packageInfo.getVersion(), lastVersion); 117 | } 118 | return isSuccess; 119 | } 120 | 121 | 122 | public static String getFileMD5(File file) { 123 | if (!file.isFile()) { 124 | return null; 125 | } 126 | MessageDigest digest = null; 127 | FileInputStream in = null; 128 | byte[] buffer = new byte[1024]; 129 | int len; 130 | try { 131 | digest = MessageDigest.getInstance("MD5"); 132 | in = new FileInputStream(file); 133 | while ((len = in.read(buffer, 0, 1024)) != -1) { 134 | digest.update(buffer, 0, len); 135 | } 136 | in.close(); 137 | } catch (Exception e) { 138 | e.printStackTrace(); 139 | return null; 140 | } 141 | return bytesToHexString(digest.digest()); 142 | } 143 | 144 | public static String bytesToHexString(byte[] src) { 145 | StringBuilder stringBuilder = new StringBuilder(); 146 | if (src == null || src.length <= 0) { 147 | return null; 148 | } 149 | for (int i = 0; i < src.length; i++) { 150 | int v = src[i] & 0xFF; 151 | String hv = Integer.toHexString(v); 152 | if (hv.length() < 2) { 153 | stringBuilder.append(0); 154 | } 155 | stringBuilder.append(hv); 156 | } 157 | return stringBuilder.toString(); 158 | } 159 | 160 | private void cleanOldFileIfNeed(String packageId, String version, String lastVersion) { 161 | String path = FileUtils.getPackageRootByPackageId(context, packageId); 162 | File file = new File(path); 163 | if (!file.exists() || !file.isDirectory()) { 164 | return; 165 | } 166 | File[] versionList = file.listFiles(); 167 | if (versionList == null || versionList.length == 0) { 168 | return; 169 | } 170 | List deleteFiles = new ArrayList<>(); 171 | for (File item : versionList) { 172 | if (TextUtils.equals(version, item.getName()) || TextUtils.equals(lastVersion, item.getName())) { 173 | continue; 174 | } 175 | deleteFiles.add(item); 176 | } 177 | for (File file1 : deleteFiles) { 178 | FileUtils.deleteDir(file1); 179 | } 180 | } 181 | 182 | //根据packageId获取对应包的版本 183 | private String getLastVersion(String packageId) { 184 | //获取packageIndex.json文件(items数组中包含每个包的信息),并基于该文件生成PackageEntity实例localPackageEntity 185 | String packageIndexFile = FileUtils.getPackageIndexFileName(context); 186 | FileInputStream indexFis = null; 187 | try { 188 | indexFis = new FileInputStream(packageIndexFile); 189 | } catch (FileNotFoundException e) { 190 | 191 | } 192 | if (indexFis == null) { 193 | return ""; 194 | } 195 | PackageEntity localPackageEntity = GsonUtils.fromJsonIgnoreException(indexFis, PackageEntity.class); 196 | if (localPackageEntity == null || localPackageEntity.getItems() == null) { 197 | return ""; 198 | } 199 | 200 | //从localPackageEntity实例中获取items,PackageInfo的数组集合,一个PackageInfo对应一个离线资源包,根据packageId查找对应包的最新版本 201 | List list = localPackageEntity.getItems(); 202 | PackageInfo info = new PackageInfo(); 203 | info.setPackageId(packageId); 204 | int index = list.indexOf(info); 205 | if (index >= 0) { 206 | return list.get(index).getVersion(); 207 | } 208 | return ""; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /webpackagekit/src/main/java/com/hht/webpackagekit/inner/ResourceManagerImpl.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit.inner; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | import android.webkit.WebResourceResponse; 8 | 9 | import com.hht.webpackagekit.core.Constants; 10 | import com.hht.webpackagekit.core.ResourceInfo; 11 | import com.hht.webpackagekit.core.ResourceInfoEntity; 12 | import com.hht.webpackagekit.core.ResourceKey; 13 | import com.hht.webpackagekit.core.ResourceManager; 14 | import com.hht.webpackagekit.core.ResoureceValidator; 15 | import com.hht.webpackagekit.core.util.FileUtils; 16 | import com.hht.webpackagekit.core.util.GsonUtils; 17 | import com.hht.webpackagekit.core.util.Logger; 18 | import com.hht.webpackagekit.core.util.MD5Utils; 19 | import com.hht.webpackagekit.core.util.MimeTypeUtils; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | import java.util.concurrent.locks.Lock; 30 | import java.util.concurrent.locks.ReentrantLock; 31 | 32 | /** 33 | * 资源管理类实现 34 | */ 35 | public class ResourceManagerImpl implements ResourceManager { 36 | private final Map resourceInfoMap; 37 | private final Context context; 38 | private final Lock lock; 39 | private ResoureceValidator validator; 40 | 41 | public ResourceManagerImpl(Context context) { 42 | resourceInfoMap = new ConcurrentHashMap<>(16); 43 | this.context = context; 44 | lock = new ReentrantLock(); 45 | validator = new DefaultResourceValidator(); 46 | } 47 | 48 | /** 49 | * 获取资源信息 50 | * 51 | * @param url 请求地址 52 | */ 53 | @Override 54 | public WebResourceResponse getResource(String url) { 55 | //根据拦截的远程URL生成ResourceKey的实例,并据此在ResourceInfoMap中查找对应的本地资源,即ResourceInfo的实例 56 | ResourceKey key = new ResourceKey(url); 57 | 58 | // if (!lock.tryLock()) { 59 | Log.d("WebResourceResponse",url + "|" + "resourceInfoMap:"+resourceInfoMap.size()); 60 | // return null; 61 | // } 62 | ResourceInfo resourceInfo = resourceInfoMap.get(key); 63 | 64 | 65 | // lock.unlock(); 66 | if (resourceInfo == null) { 67 | return null; 68 | } 69 | //对于mimetype不在拦截范围的文件,则返回null并清除相应的key 70 | if (!MimeTypeUtils.checkIsSupportMimeType(resourceInfo.getMimeType())) { 71 | Logger.d("getResource [" + url + "]" + " is not support mime type,"+resourceInfo.getMimeType()); 72 | safeRemoveResource(key); 73 | return null; 74 | } 75 | //如果对应的本地文件不存在,则返回null并清除相应的key 76 | InputStream inputStream = FileUtils.getInputStream(resourceInfo.getLocalPath()); 77 | if (inputStream == null) { 78 | Logger.d("getResource [" + url + "]" + " inputStream is null"); 79 | safeRemoveResource(key); 80 | return null; 81 | } 82 | //对资源文件进行md5校验 83 | if (validator != null && !validator.validate(resourceInfo)) { 84 | safeRemoveResource(key); 85 | return null; 86 | } 87 | Log.d("qingqiu4", url); 88 | //高版本安卓返回资源的同时需要在响应头设置Access-Control相关字段 89 | WebResourceResponse response; 90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 91 | Map header = new HashMap<>(2); 92 | header.put("Access-Control-Allow-Origin", "*"); 93 | header.put("Access-Control-Allow-Headers", "Content-Type"); 94 | response = new WebResourceResponse(resourceInfo.getMimeType(), "UTF-8", 200, "ok", header, inputStream); 95 | } else { 96 | response = new WebResourceResponse(resourceInfo.getMimeType(), "UTF-8", inputStream); 97 | } 98 | return response; 99 | } 100 | 101 | private void safeRemoveResource(ResourceKey key) { 102 | if (lock.tryLock()) { 103 | resourceInfoMap.remove(key); 104 | lock.unlock(); 105 | } 106 | } 107 | 108 | @Override 109 | public boolean updateResource(String packageId, String version) { 110 | boolean isSuccess = false; 111 | //获取离线包的index.json 112 | String indexFileName = 113 | FileUtils.getPackageWorkName(context, packageId, version) + File.separator + Constants.RESOURCE_MIDDLE_PATH 114 | + File.separator + Constants.RESOURCE_INDEX_NAME; 115 | Logger.d("updateResource indexFileName: " + indexFileName); 116 | File indexFile = new File(indexFileName); 117 | if (!indexFile.exists()) { 118 | Logger.e("updateResource indexFile is not exists ,update Resource error "); 119 | return isSuccess; 120 | } 121 | if (!indexFile.isFile()) { 122 | Logger.e("updateResource indexFile is not file ,update Resource error "); 123 | return isSuccess; 124 | } 125 | FileInputStream indexFis = null; 126 | try { 127 | indexFis = new FileInputStream(indexFile); 128 | } catch (Exception e) { 129 | 130 | } 131 | 132 | if (indexFis == null) { 133 | Logger.e("updateResource indexStream is error, update Resource error "); 134 | return isSuccess; 135 | } 136 | //基于index.json生成对应的ResourceInfoEntity实例entity 137 | ResourceInfoEntity entity = GsonUtils.fromJsonIgnoreException(indexFis, ResourceInfoEntity.class); 138 | if (indexFis != null) { 139 | try { 140 | indexFis.close(); 141 | } catch (IOException e) { 142 | 143 | } 144 | } 145 | if (entity == null) { 146 | return isSuccess; 147 | } 148 | List resourceInfos = entity.getItems(); 149 | isSuccess = true; 150 | if (resourceInfos == null) { 151 | return isSuccess; 152 | } 153 | String workPath = FileUtils.getPackageWorkName(context, packageId, version); 154 | //遍历entity的items,即resourceInfo集合,更新每个resourceInfo中packageId、localPath等属性,并更新到ResourceInfoMap 155 | for (ResourceInfo resourceInfo : resourceInfos) { 156 | if (TextUtils.isEmpty(resourceInfo.getPath())) { 157 | continue; 158 | } 159 | resourceInfo.setPackageId(packageId); 160 | String path = resourceInfo.getPath(); 161 | path = path.startsWith(File.separator) ? path.substring(1) : path; 162 | resourceInfo.setLocalPath( 163 | workPath + File.separator + Constants.RESOURCE_MIDDLE_PATH + File.separator + path); 164 | lock.lock(); 165 | resourceInfoMap.put(new ResourceKey(resourceInfo.getRemoteUrl()), resourceInfo); 166 | lock.unlock(); 167 | } 168 | return isSuccess; 169 | } 170 | 171 | @Override 172 | public void setResourceValidator(ResoureceValidator validator) { 173 | this.validator = validator; 174 | } 175 | 176 | @Override 177 | public String getPackageId(String url) { 178 | if (!lock.tryLock()) { 179 | return null; 180 | } 181 | ResourceInfo resourceInfo = resourceInfoMap.get(new ResourceKey(url)); 182 | lock.unlock(); 183 | if (resourceInfo != null) { 184 | return resourceInfo.getPackageId(); 185 | } 186 | return null; 187 | } 188 | 189 | static class DefaultResourceValidator implements ResoureceValidator { 190 | @Override 191 | public boolean validate(ResourceInfo resourceInfo) { 192 | String rMd5 = resourceInfo.getMd5(); 193 | if (!TextUtils.isEmpty(rMd5) && !MD5Utils.checkMD5(rMd5, new File(resourceInfo.getLocalPath()))) { 194 | return false; 195 | } 196 | int size = 0; 197 | try { 198 | InputStream inputStream = FileUtils.getInputStream(resourceInfo.getLocalPath()); 199 | size = inputStream.available(); 200 | } catch (IOException e) { 201 | Logger.e("resource file is error " + e.getMessage()); 202 | } 203 | if (size == 0) { 204 | Logger.e("resource file is error "); 205 | return false; 206 | } 207 | return true; 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /webpackagekit/src/main/jniLibs/armeabi-v7a/libApkPatchLibrary.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/webpackagekit/src/main/jniLibs/armeabi-v7a/libApkPatchLibrary.so -------------------------------------------------------------------------------- /webpackagekit/src/main/jniLibs/armeabi/libApkPatchLibrary.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuking/mobile-web-best-practice-container/2bee1b81917340a93514080389f1a0af88fd5f4f/webpackagekit/src/main/jniLibs/armeabi/libApkPatchLibrary.so -------------------------------------------------------------------------------- /webpackagekit/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WebPackageKit 3 | 4 | -------------------------------------------------------------------------------- /webpackagekit/src/test/java/com/hht/webpackagekit/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.hht.webpackagekit; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } --------------------------------------------------------------------------------