├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 | }
--------------------------------------------------------------------------------