├── demo
├── .gitignore
├── src
│ ├── main
│ │ ├── assets
│ │ │ ├── disk.zip
│ │ │ ├── index.html
│ │ │ └── js
│ │ │ │ └── DeviceBridge.js
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── drawable
│ │ │ │ └── ic_launcher.png
│ │ │ ├── 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
│ │ │ └── layout
│ │ │ │ ├── splash_layout.xml
│ │ │ │ └── activity_main.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── freelib
│ │ │ │ └── hybrid
│ │ │ │ ├── SplashFragment.java
│ │ │ │ ├── JsApi.java
│ │ │ │ └── MainActivity.java
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── freelib
│ │ │ └── hybrid
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── freelib
│ │ └── hybrid
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── library
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── freelib
│ │ │ │ └── hybrid
│ │ │ │ ├── bridge
│ │ │ │ ├── CallBack.java
│ │ │ │ ├── MessageHandler.java
│ │ │ │ ├── Const.java
│ │ │ │ ├── JsMethod.java
│ │ │ │ ├── Message.java
│ │ │ │ └── JsBridge.java
│ │ │ │ ├── file
│ │ │ │ ├── CloseUtils.java
│ │ │ │ ├── ZipUtils.java
│ │ │ │ ├── FileIOUtils.java
│ │ │ │ └── FileUtils.java
│ │ │ │ ├── splash
│ │ │ │ └── SplashHelper.java
│ │ │ │ └── webview
│ │ │ │ └── HybridWebView.java
│ │ ├── AndroidManifest.xml
│ │ └── assets
│ │ │ └── DeviceBridge.js
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── freelib
│ │ │ └── hybrid
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── freelib
│ │ └── hybrid
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── .gitignore
├── gradlew.bat
├── README.md
└── gradlew
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':demo', ':library'
2 |
--------------------------------------------------------------------------------
/demo/src/main/assets/disk.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/assets/disk.zip
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Library
3 |
4 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | HybridFoundation
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/drawable/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/free46000/HybridFoundation/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/bridge/CallBack.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.bridge;
2 |
3 | /**
4 | * @author free46000 2017/06/02
5 | * @version v1.0
6 | */
7 | public interface CallBack {
8 |
9 | void onMessageCallBack(Message message);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jun 08 15:16:08 CST 2017
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-3.3-all.zip
7 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/bridge/MessageHandler.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.bridge;
2 |
3 | /**
4 | * @author free46000 2017/06/06
5 | * @version v1.0
6 | */
7 | public interface MessageHandler {
8 |
9 | void handler(Message message, CallBack responseCallBack);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/freelib/hybrid/SplashFragment.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
2 |
3 | import android.app.Fragment;
4 |
5 | import org.androidannotations.annotations.EFragment;
6 |
7 | /**
8 | * @author free46000 2017/06/09
9 | * @version v1.0
10 | */
11 | @EFragment(R.layout.splash_layout)
12 | public class SplashFragment extends Fragment {
13 | }
14 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/bridge/Const.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.bridge;
2 |
3 | /**
4 | * @author free46000 2017/06/02
5 | * @version v1.0
6 | */
7 | public class Const {
8 | public static final String LOG_TAG = "JS_BRIDGE";
9 |
10 | public static final String RECEIVE_MESSAGE_METHOD = "receiveMessage";
11 | public static final String PROTOCOL_SCHEME_NAME = "JsBridge";
12 | public static final String PARAMETER_NAME = "msg";
13 | }
14 |
--------------------------------------------------------------------------------
/demo/src/test/java/com/freelib/hybrid/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
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() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/library/src/test/java/com/freelib/hybrid/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
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() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/freelib/hybrid/JsApi.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
2 |
3 | import com.freelib.hybrid.bridge.JsBridge;
4 | import com.freelib.hybrid.bridge.MessageHandler;
5 |
6 | /**
7 | * @author free46000 2017/06/08
8 | * @version v1.0
9 | */
10 | public class JsApi {
11 | private JsBridge jsBridge;
12 |
13 | public JsApi(JsBridge jsBridge) {
14 | this.jsBridge = jsBridge;
15 | }
16 |
17 | public void registConnectBluetoothHandler(MessageHandler messageHandler) {
18 | jsBridge.registMessageHandler("connectBluetooth", messageHandler);
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/
38 |
39 | # Keystore files
40 | *.jks
41 |
42 | # External native build folder generated in Android Studio 2.2 and later
43 | .externalNativeBuild
44 |
45 | # Google Services (e.g. APIs or Firebase)
46 | google-services.json
47 |
48 | # Freeline
49 | freeline.py
50 | freeline/
51 | freeline_project_description.json
--------------------------------------------------------------------------------
/demo/src/androidTest/java/com/freelib/hybrid/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation 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() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.freelib.hybrid", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/library/src/androidTest/java/com/freelib/hybrid/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation 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() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.freelib.library.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in E:\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in E:\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/demo/src/main/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
33 |
34 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/bridge/JsMethod.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.bridge;
2 |
3 | import android.os.Build;
4 | import android.os.Looper;
5 | import android.webkit.WebView;
6 |
7 | import com.google.gson.Gson;
8 |
9 | /**
10 | * @author free46000 2017/06/02
11 | * @version v1.0
12 | */
13 | public class JsMethod {
14 | private static final String EXEC_JS_FORMAT_VALUE = "javascript:%s('%s');";
15 |
16 |
17 | public static boolean execJs(WebView webView, Message message) {
18 | String messageStr = new Gson().toJson(message);
19 | String jsCode = String.format(EXEC_JS_FORMAT_VALUE, Const.RECEIVE_MESSAGE_METHOD, messageStr);
20 |
21 | execJs(webView, jsCode);
22 | return true;
23 | }
24 |
25 |
26 | private static void execJs(WebView webView, String jsCode) {
27 | if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
28 | webView.post(() -> execJs(webView, jsCode));
29 | }
30 |
31 |
32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
33 | webView.evaluateJavascript(jsCode, null);
34 | } else {
35 | webView.loadUrl(jsCode);
36 | }
37 |
38 | }
39 |
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/bridge/Message.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.bridge;
2 |
3 | import android.text.TextUtils;
4 |
5 | /**
6 | * @author free46000 2017/06/02
7 | * @version v1.0
8 | */
9 | public class Message {
10 | private String callBackId;
11 | private String responseId;
12 | private String handlerName;
13 | private String data;
14 |
15 | public boolean isResponse() {
16 | return !TextUtils.isEmpty(responseId);
17 | }
18 |
19 | public boolean isNeedCallBack() {
20 | return !TextUtils.isEmpty(callBackId);
21 | }
22 |
23 | public String getCallBackId() {
24 | return callBackId;
25 | }
26 |
27 | public void setCallBackId(String callBackId) {
28 | this.callBackId = callBackId;
29 | }
30 |
31 | public String getResponseId() {
32 | return responseId;
33 | }
34 |
35 | public void setResponseId(String responseId) {
36 | this.responseId = responseId;
37 | }
38 |
39 | public String getHandlerName() {
40 | return handlerName;
41 | }
42 |
43 | public void setHandlerName(String handlerName) {
44 | this.handlerName = handlerName;
45 | }
46 |
47 | public String getData() {
48 | return data;
49 | }
50 |
51 | public void setData(String data) {
52 | this.data = data;
53 | }
54 |
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/file/CloseUtils.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.file;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 |
6 | /**
7 | * @author free46000 2017/05/26
8 | * @version v1.0
9 | */
10 | public class CloseUtils {
11 | private CloseUtils() {
12 | throw new UnsupportedOperationException("u can't instantiate me...");
13 | }
14 |
15 | /**
16 | * 关闭IO
17 | *
18 | * @param closeables closeables
19 | */
20 | public static void closeIO(Closeable... closeables) {
21 | if (closeables == null) return;
22 | for (Closeable closeable : closeables) {
23 | if (closeable != null) {
24 | try {
25 | closeable.close();
26 | } catch (IOException e) {
27 | e.printStackTrace();
28 | }
29 | }
30 | }
31 | }
32 |
33 | /**
34 | * 安静关闭IO
35 | *
36 | * @param closeables closeables
37 | */
38 | public static void closeIOQuietly(Closeable... closeables) {
39 | if (closeables == null) return;
40 | for (Closeable closeable : closeables) {
41 | if (closeable != null) {
42 | try {
43 | closeable.close();
44 | } catch (IOException ignored) {
45 | }
46 | }
47 | }
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/splash_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/demo/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'me.tatarka.retrolambda'
3 |
4 | android {
5 | compileSdkVersion 25
6 | buildToolsVersion "25.0.2"
7 | defaultConfig {
8 | applicationId "com.freelib.hybrid"
9 | minSdkVersion 15
10 | targetSdkVersion 25
11 | versionCode 1
12 | versionName "1.0"
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 |
22 |
23 | compileOptions {
24 | targetCompatibility 1.8
25 | sourceCompatibility 1.8
26 | }
27 | }
28 |
29 | dependencies {
30 | compile fileTree(dir: 'libs', include: ['*.jar'])
31 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
32 | exclude group: 'com.android.support', module: 'support-annotations'
33 | })
34 | testCompile 'junit:junit:4.12'
35 |
36 | compile "com.android.support:appcompat-v7:$ANDROID_SUPPORT_VERSION"
37 | compile "org.androidannotations:androidannotations-api:$ANDROID_ANNOTATION_VERSION"
38 | annotationProcessor "org.androidannotations:androidannotations:$rootProject.ext.ANDROID_ANNOTATION_VERSION"
39 | compile "com.android.support.constraint:constraint-layout:$rootProject.ext.CONSTRAINT_VERSION"
40 |
41 | compile project(':library')
42 | }
43 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'me.tatarka.retrolambda'
3 |
4 | android {
5 | compileSdkVersion 25
6 | buildToolsVersion "25.0.2"
7 |
8 | defaultConfig {
9 | minSdkVersion 15
10 | targetSdkVersion 25
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | compileOptions {
25 | targetCompatibility 1.8
26 | sourceCompatibility 1.8
27 | }
28 | }
29 |
30 | dependencies {
31 | compile fileTree(include: ['*.jar'], dir: 'libs')
32 | androidTestCompile("com.android.support.test.espresso:espresso-core:$rootProject.ext.ESPRESSO_VERSION", {
33 | exclude group: 'com.android.support', module: 'support-annotations'
34 | })
35 | testCompile "junit:junit:$rootProject.ext.JUNIT_VERSION"
36 |
37 | compile "com.android.support:appcompat-v7:$rootProject.ext.ANDROID_SUPPORT_VERSION"
38 |
39 | compile "com.google.code.gson:gson:$rootProject.ext.GSON_VERSION"
40 | compile "com.squareup.retrofit2:retrofit:$rootProject.ext.RETROFIT_VERSION"
41 | compile "com.squareup.retrofit2:adapter-rxjava:$rootProject.ext.RETROFIT_VERSION"
42 | compile "io.reactivex:rxandroid:$rootProject.ext.REACTIVEX_VERSION"
43 | }
44 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
26 |
34 |
35 |
36 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
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 Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/demo/src/main/assets/js/DeviceBridge.js:
--------------------------------------------------------------------------------
1 | const CALLBACK_PROPERTY_NAME = 'callBackId'
2 | const CALLBACK_RESPONSE_PROPERTY_NAME = 'responseId'
3 | const HANDLER_PROPERTY_NAME = 'handlerName'
4 | const PROTOCOL_NAME = 'JsBridge'
5 | const MESSAGE_HOST_NAME = 'message'
6 | const MESSAGE_PARAM_NAME = 'msg'
7 | var uniqueId = 1
8 | const callBacks = {}
9 | const messageHandlers = {}
10 |
11 | function sendMessage(message, callBack) {
12 | if (!message || !message[HANDLER_PROPERTY_NAME]) {
13 | return
14 | }
15 |
16 | if (callBack) {
17 | const callBackId = 'callBack_' + uniqueId++ + '_' + message[HANDLER_PROPERTY_NAME]
18 | callBacks[callBackId] = callBack
19 | message[CALLBACK_PROPERTY_NAME] = callBackId
20 | }
21 |
22 | const result = _execDeviceMethod(message)
23 |
24 | return result
25 | }
26 |
27 | function onMessageCallBack(responseMessage) {
28 | const responseId = responseMessage[CALLBACK_RESPONSE_PROPERTY_NAME]
29 | const responseCallBack = callBacks[responseId]
30 |
31 | if (!responseCallBack) {
32 | return
33 | }
34 |
35 | responseCallBack(responseMessage)
36 | delete callBacks[responseId]
37 | }
38 |
39 | function receiveMessage(messageJson) {
40 | const message = JSON.parse(messageJson)
41 | if (!message) {
42 | return
43 | }
44 |
45 | if(message[CALLBACK_RESPONSE_PROPERTY_NAME]){
46 | onMessageCallBack(message)
47 | return
48 | }
49 |
50 |
51 | var responseCallback
52 | if (message[CALLBACK_PROPERTY_NAME]) {
53 | var callbackId = message[CALLBACK_PROPERTY_NAME]
54 | responseCallback = function (responseMessage) {
55 | responseMessage[CALLBACK_RESPONSE_PROPERTY_NAME] = callbackId
56 | sendMessage(responseMessage)
57 | }
58 | }
59 |
60 | var handler
61 | if (message[HANDLER_PROPERTY_NAME]) {
62 | handler = messageHandlers[message[HANDLER_PROPERTY_NAME]]
63 | }
64 |
65 | if (handler) {
66 | handler(message, responseCallback)
67 | } else {
68 | console.log('未找到对应的js处理' + messageJson)
69 | }
70 | }
71 |
72 | function registMessageHandler(handlerName, handler) {
73 | if (!handlerName || !handler) {
74 | return false
75 | }
76 | messageHandlers[handlerName] = handler
77 | return true
78 | }
79 | function unRegistMessageHandler(handlerName, handler) {
80 | if (!handlerName || !handler) {
81 | return false
82 | }
83 | if (!messageHandlers[handlerName]) {
84 | delete messageHandlers[handlerName]
85 | return true
86 | }
87 |
88 | return false
89 | }
90 |
91 | function _execDeviceMethod(message) {
92 | return prompt(PROTOCOL_NAME + '://' + MESSAGE_HOST_NAME + '?' + MESSAGE_PARAM_NAME + '=' + JSON.stringify(message))
93 | }
94 |
95 |
96 | const DeviceJsBridge = window.DeviceJsBridge = {
97 | sendMessage: sendMessage,
98 | onMessageCallBack: onMessageCallBack,
99 | receiveMessage: receiveMessage,
100 | registMessageHandler: registMessageHandler,
101 | unRegistMessageHandler: unRegistMessageHandler
102 | }
103 |
--------------------------------------------------------------------------------
/library/src/main/assets/DeviceBridge.js:
--------------------------------------------------------------------------------
1 | const CALLBACK_PROPERTY_NAME = 'callBackId'
2 | const CALLBACK_RESPONSE_PROPERTY_NAME = 'responseId'
3 | const HANDLER_PROPERTY_NAME = 'handlerName'
4 | const PROTOCOL_NAME = 'JsBridge'
5 | const MESSAGE_HOST_NAME = 'message'
6 | const MESSAGE_PARAM_NAME = 'msg'
7 | var uniqueId = 1
8 | const callBacks = {}
9 | const messageHandlers = {}
10 |
11 | function sendMessage(message, callBack) {
12 | if (!message || !message[HANDLER_PROPERTY_NAME]) {
13 | return
14 | }
15 |
16 | if (callBack) {
17 | const callBackId = 'callBack_' + uniqueId++ + '_' + message[HANDLER_PROPERTY_NAME]
18 | callBacks[callBackId] = callBack
19 | message[CALLBACK_PROPERTY_NAME] = callBackId
20 | }
21 |
22 | const result = _execDeviceMethod(message)
23 |
24 | return result
25 | }
26 |
27 | function onMessageCallBack(responseMessage) {
28 | const responseId = responseMessage[CALLBACK_RESPONSE_PROPERTY_NAME]
29 | const responseCallBack = callBacks[responseId]
30 |
31 | if (!responseCallBack) {
32 | return
33 | }
34 |
35 | responseCallBack(responseMessage)
36 | delete callBacks[responseId]
37 | }
38 |
39 | function receiveMessage(messageJson) {
40 | const message = JSON.parse(messageJson)
41 | if (!message) {
42 | return
43 | }
44 |
45 | if(message[CALLBACK_RESPONSE_PROPERTY_NAME]){
46 | onMessageCallBack(message)
47 | return
48 | }
49 |
50 |
51 | var responseCallback
52 | if (message[CALLBACK_PROPERTY_NAME]) {
53 | var callbackId = message[CALLBACK_PROPERTY_NAME]
54 | responseCallback = function (responseMessage) {
55 | responseMessage[CALLBACK_RESPONSE_PROPERTY_NAME] = callbackId
56 | sendMessage(responseMessage)
57 | }
58 | }
59 |
60 | var handler
61 | if (message[HANDLER_PROPERTY_NAME]) {
62 | handler = messageHandlers[message[HANDLER_PROPERTY_NAME]]
63 | }
64 |
65 | if (handler) {
66 | handler(message, responseCallback)
67 | } else {
68 | console.log('未找到对应的js处理' + messageJson)
69 | }
70 | }
71 |
72 | function registMessageHandler(handlerName, handler) {
73 | if (!handlerName || !handler) {
74 | return false
75 | }
76 | messageHandlers[handlerName] = handler
77 | return true
78 | }
79 | function unRegistMessageHandler(handlerName, handler) {
80 | if (!handlerName || !handler) {
81 | return false
82 | }
83 | if (!messageHandlers[handlerName]) {
84 | delete messageHandlers[handlerName]
85 | return true
86 | }
87 |
88 | return false
89 | }
90 |
91 | function _execDeviceMethod(message) {
92 | return prompt(PROTOCOL_NAME + '://' + MESSAGE_HOST_NAME + '?' + MESSAGE_PARAM_NAME + '=' + JSON.stringify(message))
93 | }
94 |
95 |
96 | const DeviceJsBridge = window.DeviceJsBridge = {
97 | sendMessage: sendMessage,
98 | onMessageCallBack: onMessageCallBack,
99 | receiveMessage: receiveMessage,
100 | registMessageHandler: registMessageHandler,
101 | unRegistMessageHandler: unRegistMessageHandler
102 | }
103 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/splash/SplashHelper.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.splash;
2 |
3 | import android.app.Activity;
4 | import android.app.Fragment;
5 | import android.app.FragmentTransaction;
6 | import android.view.View;
7 |
8 | import java.util.concurrent.TimeUnit;
9 |
10 | import rx.Observable;
11 | import rx.Subscriber;
12 | import rx.Subscription;
13 | import rx.android.schedulers.AndroidSchedulers;
14 | import rx.schedulers.Schedulers;
15 |
16 | /**
17 | * @author free46000 2017/06/09
18 | * @version v1.0
19 | */
20 | public class SplashHelper {
21 | private Activity activity;
22 | private Fragment splashFragment;
23 | private long delayTime = 1500; //延时时间,单位ms
24 | private Observable> delayObservable;
25 | private Subscription subscription;
26 | private SplashHideListener splashHideListener;
27 | private View container;
28 |
29 | public SplashHelper(Activity activity) {
30 | this.activity = activity;
31 | }
32 |
33 | /**
34 | * 设置闪屏等待时间,最终等待时间和setDelayObservable共同决定
35 | *
36 | * @param delayTime
37 | * @see #setDelayObservable(Observable)
38 | */
39 | public void setDelayTime(long delayTime) {
40 | this.delayTime = delayTime;
41 | }
42 |
43 | /**
44 | * 设置闪屏等待Observable 最终等待时间和setDelayTime共同决定
45 | *
46 | * @param delayObservable
47 | * @see #setDelayTime(long)
48 | */
49 | public void setDelayObservable(Observable> delayObservable) {
50 | this.delayObservable = delayObservable;
51 | }
52 |
53 | /**
54 | * 设置闪屏隐藏监听
55 | *
56 | * @param splashHideListener SplashHideListener
57 | */
58 | public void setSplashHideListener(SplashHideListener splashHideListener) {
59 | this.splashHideListener = splashHideListener;
60 | }
61 |
62 | /**
63 | * 展示闪屏
64 | *
65 | * @param viewId 包含Fragment的view id
66 | * @param splashFragment 闪屏Fragment
67 | */
68 | public void showSplash(int viewId, Fragment splashFragment) {
69 | container = activity.findViewById(viewId);
70 | container.setVisibility(View.VISIBLE);
71 | this.splashFragment = splashFragment;
72 |
73 | FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
74 | transaction.replace(viewId, splashFragment);
75 | transaction.commit();
76 |
77 | delayHideSplash();
78 | }
79 |
80 | /**
81 | * 隐藏闪屏 一般不需要手动调用
82 | */
83 | public void hideSplash() {
84 | if (splashFragment == null) {
85 | return;
86 | }
87 |
88 | FragmentTransaction transaction = activity.getFragmentManager().beginTransaction();
89 | transaction.remove(splashFragment);
90 | transaction.commit();
91 |
92 | splashFragment = null;
93 |
94 | try {
95 | container.setVisibility(View.GONE);
96 | container = null;
97 | subscription.unsubscribe();
98 | subscription = null;
99 | } catch (Throwable ignore) {
100 | }
101 | }
102 |
103 | private void delayHideSplash() {
104 | Observable delayTimeObservable = Observable.timer(delayTime, TimeUnit.MILLISECONDS);
105 | if (delayObservable == null) {
106 | delayObservable = Observable.just("");
107 | }
108 | subscription = Observable.zip(delayObservable, delayTimeObservable, (o, aLong) -> aLong)
109 | .subscribeOn(Schedulers.io())
110 | .observeOn(AndroidSchedulers.mainThread())
111 | .subscribe(new Subscriber() {
112 | @Override
113 | public void onCompleted() {
114 | hideSplash();
115 | }
116 |
117 | @Override
118 | public void onError(Throwable e) {
119 | hideSplash();
120 | }
121 |
122 | @Override
123 | public void onNext(Long aLong) {
124 | }
125 | });
126 |
127 | }
128 |
129 | /**
130 | * 闪屏消失监听
131 | */
132 | public interface SplashHideListener {
133 | void onSplashHide();
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 前言
2 | 这里我整理了`Android`端会用到代码,包含`JS`通信,文件处理工具类,闪屏辅助类和`WebView`的封装。由于源码并没有完善,所以暂时没有发布到`Maven`仓库
3 |
4 | ## 用法
5 |
6 | #### `JS`通信
7 | - 需要把`library`中`assets`文件夹下的`DeviceBridge.js`放入并加载到你的前端项目中
8 | - `Android`调用`JS`
9 | 首先在`JS`中注册对应`Handler`,以注册的字符串为`key`对应
10 | ``` javascript
11 | window.DeviceJsBridge.registMessageHandler("testCallJs", function (message, responseCallback) {
12 | alert("android调用js方法,消息内容:" + JSON.stringify(message))
13 | message.data = '我是在js中赋值的数据'
14 | responseCallback(message);
15 | });
16 | ```
17 | 然后就可以在`Android`代码中调用了
18 | ``` java
19 | Message message = new Message();
20 | message.setHandlerName("testCallJs");
21 | message.setData("数据内容(onPageFinished执行android调用js,js返回的数据会打印在logcat)");
22 | webView.getJsBridge().sendMessage(message, responseMessage ->
23 | System.out.println("js异步response消息:" + responseMessage));
24 | ```
25 |
26 | - `JS`调用`Android`
27 | 同样首先需要在`Android`中注册对应`Handler`,以注册的字符串为`key`对应
28 | ``` java
29 | jsBridge.registMessageHandler("connectBluetooth", messageHandler);
30 | ```
31 | 然后就可以在`JS`中调用了
32 | ``` javascript
33 | var message = {handlerName: 'testAndroid'}
34 | var result = window.DeviceJsBridge.sendMessage(message, function (message) {
35 | alert("js调用android异步方法Response结果:" + JSON.stringify(message))
36 | })
37 | ```
38 |
39 | #### 文件处理工具
40 | 相关用法详见代码注释,包含对压缩文件的处理
41 |
42 | #### 闪屏辅助类
43 | 下面列一下常用的方法
44 | ``` java
45 | /**
46 | * 设置闪屏等待时间,最终等待时间和setDelayObservable共同决定
47 | */
48 | public void setDelayTime(long delayTime)
49 |
50 | /**
51 | * 设置闪屏等待Observable 最终等待时间和setDelayTime共同决定
52 | */
53 | public void setDelayObservable(Observable> delayObservable)
54 |
55 | /**
56 | * 设置闪屏隐藏监听
57 | */
58 | public void setSplashHideListener(SplashHideListener splashHideListener)
59 |
60 | /**
61 | * 展示闪屏
62 | *
63 | * @param viewId 包含Fragment的view id
64 | * @param splashFragment 闪屏Fragment
65 | */
66 | public void showSplash(int viewId, Fragment splashFragment)
67 | ```
68 |
69 | #### `HybridWebView`的封装
70 | 主要封装了通用的方法,对外提供了必要的监听回调,还包括了`JSBridge`的使用,让你在使用`WebView`的时候更方便,具体使用详见`Demo`
71 |
72 |
73 | ## 概述
74 | 移动开发的跨平台与快速发布一直是开发者的追求,也是技术的一个发展趋势,现在各大厂开始有了自己的大前端团队,所以我们也开始了自己的探索,目前来说主要有两种思路:
75 | - `Hybrid App` 代表:`Cordova`
76 | 通过`Webview`加载`Web`页面,在`Native`和`Web`页面之间建立双向通信
77 | - `H5`代码`Native`化 代表:`ReactNative`,`Weex`等
78 | 使用各平台`Api`,把`H5`代码编译成二进制代码直接运行
79 |
80 | 其实关于这两种思路对比,网上有很多大牛分析的很全面了,总结来说各有利弊很难完美,本篇文章我们主要讲一下Hybrid App实践,采用前后端分离以及单页应用技术开发`Web`页面,使用`WebView`加载`Web`页面,并通过`JS`通信提供一些`Native`层的支持,通过接口获取更新后的差异化页面资源文件,在本地覆盖,就可以达到热更新的需求。在我看来此方案更适用于需要快速发布、多端兼容、对性能要求稍低的业务,正好符合我们的需求。
81 |
82 | ## 方案详解
83 | 既然确定了方向,那么就应该确定具体的方案了,通过自己的经验和网上资料整理,画了时序图:
84 | 
85 |
86 | 按照图上的时序,接下来说一下每一步中的实践,以及碰到的坑。下面讲解
87 |
88 | #### 初次安装
89 | - **打包**
90 | 在打包程序时这一步主要是把`Html`相关资源文件压缩后放在`assets`文件夹下即可
91 | - **安装**
92 | 用户安装完应用程序打开后,检测是否为初次使用,如果是则通过程序直接解压包内资源到手机存储上即可,不局限于SD卡。
93 |
94 | #### 展示页面
95 | - **闪屏页展示**
96 | 由于上面的解压资源,还有`Webview`初始化、`JS`的加载执行、`html`的渲染都是耗时操作,并且都是发生在`Html`展示之前,所以我们选择把闪屏页用`Android`原生代码来编写,采用覆盖`WebView`所在`Activity`的方案,这样在闪屏页隐藏的时候,用户就可以看到业务界面,可以提升用户体验。
97 | 注:另外提供两种闪屏优化的小技巧,使用透明主题或者设置主题背景图片
98 |
99 | - **加载本地Html页面**
100 | 直接使用`WebView#loadUrl()`加载本地资源文件即可。由于`WebView`加载不同页面会出现闪屏的问题,所以我们采用`Vue + Vue Router`构建单页应用即可。
101 | 这里`Vue Router`会有一个小坑,提醒大家注意一下:`Vue Router`默认采用`hash`模式,会有一个丑陋的`#`符号,作为一个有追求的程序员怎么能允许这种很丑的`hash`,一种更优雅的方式使用`HTML5 History`模式,但是不幸的是,加载本地资源文件的方式并不能正常解析`HTML5 History`模式的`url`,所以只能采用`hash`模式。
102 |
103 | - **数据请求**
104 | 为了节省用户的流量和时间,需要把`Html`资源文件存储在本地,这样数据的请求必须在客户端完成。有两种方案供选择:
105 | 一是`Native`层拦截并请求数据再返回给`Html`层去展示,有我们采用前后端分离直接通过`JS`请求接口获取数据即可,这样会增加工作量,也不利于职责的分离,所以放弃。
106 | 二是直接使用`JS`请求数据,这样会出现跨域访问的问题,相比较来说还是这个比较容易解决的,采用`CORS`即可
107 |
108 | - **Native调用JS**
109 | `Native`层调用`JS`比较简单,执行一段`JS`代码即可,如:`javascript:callJS()`
110 |
111 | - **JS调用Native**
112 | `JS`层调用`Native`主要分为三种:
113 | 一:通过`WebView#addJavascriptInterface()`进行映射,使用起来简单,但是有安全风险,弃用
114 | 二:自定义协议然后由`Native`层拦截并解析请求,使用起来复杂,容易和业务耦合,也不是最优选,弃用
115 | 三:拦截`JS#prompt()`方法并解析,使用起来复杂,但是比第一种更安全,比第二种灵活一些,所以使用此方案
116 |
117 | #### 资源文件获取
118 | 资源文件采取差异化更新方案,本地存储一个标识,可以为版本号或者更新时间,这个可以和后端同学一起商量确定,资源文件下载还有推送之类的由于`Html`的局限性,所以还是直接由`Native`层做比较合适,下面简单讲解下应用中的两种更新方式:
119 | - **服务端推送下发**
120 | 可以通过集成第三方的推送服务,在客户端收到更新推送后主动去请求下载差异化文件
121 | - **主动请求**
122 | 可以在选择合适的时机,如在应用启动时去请求差异化文件
123 |
124 | #### 资源文件更新
125 | 根据差异化清单对资源文件进行整合,存放在临时目录中,然后在第二次打开应用时更换,并展示更新后的界面,达到热更新的效果。
126 |
127 | ## 总结
128 | 只是概括的讲了结构的内容,可能会遗漏一些要点,如果大家有什么问题欢迎提交`issue`
129 |
130 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/bridge/JsBridge.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.bridge;
2 |
3 | import android.net.Uri;
4 | import android.support.annotation.NonNull;
5 | import android.text.TextUtils;
6 | import android.util.Log;
7 | import android.webkit.WebView;
8 |
9 | import com.google.gson.Gson;
10 |
11 | import java.lang.ref.WeakReference;
12 | import java.util.concurrent.ConcurrentHashMap;
13 |
14 | /**
15 | * @author free46000 2017/06/02
16 | * @version v1.0
17 | */
18 | public class JsBridge {
19 | private long uniqueId = 1;
20 | private ConcurrentHashMap> callBackMap = new ConcurrentHashMap<>();
21 | private ConcurrentHashMap> handlerMap = new ConcurrentHashMap<>();
22 |
23 | protected WebView webView;
24 |
25 | public JsBridge(WebView webView) {
26 | this.webView = webView;
27 | }
28 |
29 | public void registMessageHandler(@NonNull String handlerName, @NonNull MessageHandler messageHandler) {
30 | handlerMap.put(handlerName, new WeakReference<>(messageHandler));
31 | }
32 |
33 | public void unRegistMessageHandler(@NonNull String handlerName) {
34 | handlerMap.remove(handlerName);
35 | }
36 |
37 |
38 | public void sendMessage(@NonNull Message message) {
39 | sendMessage(message, null);
40 | }
41 |
42 | public void sendMessage(@NonNull Message message, CallBack callBack) {
43 | if (callBack != null) {
44 | message.setCallBackId("callBack_" + uniqueId++ + '_' + message.getHandlerName());
45 | callBackMap.put(message.getCallBackId(), new WeakReference<>(callBack));
46 | }
47 |
48 | JsMethod.execJs(webView, message);
49 | }
50 |
51 | /**
52 | * @param urlStr url字符串
53 | * @return 是否处理此消息
54 | */
55 | public boolean receiveUrl(@NonNull String urlStr) {
56 |
57 | Uri uri = Uri.parse(urlStr);
58 | if (uri == null) {
59 | return false;
60 | }
61 |
62 | if (!Const.PROTOCOL_SCHEME_NAME.equals(uri.getScheme())) {
63 | return false;
64 | }
65 |
66 | String param = uri.getQueryParameter(Const.PARAMETER_NAME);
67 | Message message = new Gson().fromJson(param, Message.class);
68 |
69 | if (message == null) {
70 | return true;
71 | }
72 |
73 | if (message.isResponse()) {
74 | onMessageCallBack(message);
75 | return true;
76 | }
77 |
78 | handlerMessage(message);
79 | return true;
80 | }
81 |
82 | private void handlerMessage(@NonNull Message message) {
83 | String handlerName = message.getHandlerName();
84 |
85 | if (TextUtils.isEmpty(handlerName)) {
86 | return;
87 | }
88 |
89 | WeakReference handlerReference = handlerMap.get(handlerName);
90 |
91 | if (handlerReference == null) {
92 | return;
93 | }
94 |
95 | MessageHandler messageHandler = handlerReference.get();
96 |
97 | if (messageHandler == null) {
98 | return;
99 | }
100 |
101 | CallBack callBack;
102 | if (message.isNeedCallBack()) {
103 | callBack = buildResponseCallBack(message.getCallBackId());
104 | } else {
105 | //不需要callback的创建空的callback,防止在使用callBack时抛出空指针
106 | callBack = buildEmptyResponseCallBack();
107 | }
108 |
109 | messageHandler.handler(message, callBack);
110 | }
111 |
112 | public CallBack buildResponseCallBack(@NonNull final String callBackId) {
113 | if (TextUtils.isEmpty(callBackId)) {
114 | return null;
115 | }
116 |
117 | return responseMessage -> {
118 | responseMessage.setResponseId(callBackId);
119 | sendMessage(responseMessage);
120 | };
121 | }
122 |
123 | private CallBack buildEmptyResponseCallBack() {
124 | return responseMessage -> Log.e(Const.LOG_TAG, "本消息不需要给js回调结果");
125 | }
126 |
127 |
128 | public void onMessageCallBack(@NonNull Message message) {
129 | String responseId = message.getResponseId();
130 |
131 | if (TextUtils.isEmpty(responseId)) {
132 | return;
133 | }
134 |
135 | WeakReference callBackReference = callBackMap.get(responseId);
136 |
137 | if (callBackReference == null) {
138 | return;
139 | }
140 |
141 | callBackMap.remove(responseId);
142 | CallBack callBack = callBackReference.get();
143 |
144 | if (callBack == null) {
145 | return;
146 | }
147 |
148 | callBack.onMessageCallBack(message);
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/freelib/hybrid/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid;
2 |
3 | import android.graphics.Bitmap;
4 | import android.os.Handler;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.util.Log;
7 |
8 | import com.freelib.hybrid.bridge.Message;
9 | import com.freelib.hybrid.file.FileUtils;
10 | import com.freelib.hybrid.file.ZipUtils;
11 | import com.freelib.hybrid.splash.SplashHelper;
12 | import com.freelib.hybrid.webview.HybridWebView;
13 | import com.google.gson.Gson;
14 |
15 | import org.androidannotations.annotations.AfterViews;
16 | import org.androidannotations.annotations.EActivity;
17 | import org.androidannotations.annotations.ViewById;
18 |
19 | import rx.Observable;
20 | import rx.android.schedulers.AndroidSchedulers;
21 |
22 | @EActivity(R.layout.activity_main)
23 | public class MainActivity extends AppCompatActivity {
24 | public static final long DELAY_TIME = 2000;
25 |
26 | @ViewById(R.id.webView)
27 | protected HybridWebView webView;
28 |
29 | private SplashHelper splashHelper;
30 |
31 |
32 | @AfterViews
33 | public void initView() {
34 | splashHelper = new SplashHelper(this);
35 | splashHelper.setDelayObservable(createH5ResourceObservable());
36 | splashHelper.setDelayTime(DELAY_TIME);
37 | splashHelper.showSplash(R.id.container, SplashFragment_.builder().build());
38 |
39 | JsApi jsApi = new JsApi(webView.getJsBridge());
40 |
41 | jsApi.registConnectBluetoothHandler((message, responseCallBack) -> {
42 | System.out.println("testAndroid:::" + new Gson().toJson(message));
43 |
44 | message.setData("android 异步 response 结果");
45 | responseCallBack.onMessageCallBack(message);
46 | });
47 |
48 |
49 | webView.getJsBridge().registMessageHandler("testAndroid", (message, responseCallBack) -> {
50 | System.out.println("testAndroid:::" + new Gson().toJson(message));
51 |
52 | message.setData("android 异步 response 结果");
53 | responseCallBack.onMessageCallBack(message);
54 | });
55 | webView.setListener(new HybridWebView.Listener() {
56 | @Override
57 | public void onPageStarted(String url, Bitmap favicon) {
58 | System.out.println("page started:" + url);
59 | }
60 |
61 | @Override
62 | public void onPageProgressed(int newProgress) {
63 | System.out.println("page progress:" + newProgress);
64 | }
65 |
66 | @Override
67 | public void onPageFinished(String url) {
68 | new Handler().postDelayed(() -> {
69 | Message message = new Message();
70 | message.setHandlerName("testCallJs");
71 | message.setData("数据内容(onPageFinished执行android调用js,js返回的数据会打印在logcat)");
72 | webView.getJsBridge().sendMessage(message, responseMessage ->
73 | System.out.println("js异步response消息:" + responseMessage));
74 | }, DELAY_TIME);
75 | System.out.println("page finished:" + url);
76 |
77 | }
78 |
79 | @Override
80 | public void onPageError(int errorCode, String description, String failingUrl) {
81 | System.out.println("page error:" + errorCode + "=" + description + "=" + failingUrl);
82 | }
83 |
84 | @Override
85 | public void onReceiveTitle(String title) {
86 | System.out.println("title:" + title);
87 | }
88 | });
89 |
90 | }
91 |
92 | public void showWebViewUrl() {
93 | Log.e("MainActivity", "loadUrl:" + "file:///android_asset/test.html");
94 | webView.loadUrl("file:///android_asset/test.html");
95 | }
96 |
97 | private Observable createH5ResourceObservable() {
98 | return Observable.just(getH5SavePath() + "/disk/index.html").map(url -> {
99 | // return Observable.just("file:///android_asset/index.html").map(url -> {
100 | unZip();
101 | return url;
102 | }).observeOn(AndroidSchedulers.mainThread())
103 | .doOnNext(url -> webView.loadUrl("file://" + url));
104 | }
105 |
106 | private void unZip() {
107 | boolean isSuccess = ZipUtils.unZipFile(this, getH5SavePath() + "/disk_temp/", "disk.zip");
108 | Log.e("MainActivity", "temp unZipFile:" + isSuccess);
109 | isSuccess = FileUtils.deleteDir(getH5SavePath() + "/disk/");
110 | Log.e("MainActivity", "deleteDir:" + isSuccess);
111 | isSuccess = FileUtils.copyDir(getH5SavePath() + "/disk_temp/", getH5SavePath() + "/disk/");
112 | Log.e("MainActivity", "copyDir:" + isSuccess);
113 | isSuccess = FileUtils.deleteDir(getH5SavePath() + "/disk_temp/");
114 | Log.e("MainActivity", "temp deleteDir:" + isSuccess);
115 |
116 |
117 | }
118 |
119 | /**
120 | * 获取H5保存路径
121 | *
122 | * @return
123 | */
124 | public String getH5SavePath() {
125 | return getDir("h5", MODE_PRIVATE).getPath();
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/webview/HybridWebView.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.webview;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.annotation.TargetApi;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.graphics.Bitmap;
8 | import android.net.Uri;
9 | import android.os.Build;
10 | import android.os.Message;
11 | import android.support.annotation.NonNull;
12 | import android.util.AttributeSet;
13 | import android.util.Log;
14 | import android.view.ViewGroup;
15 | import android.webkit.ConsoleMessage;
16 | import android.webkit.CookieManager;
17 | import android.webkit.JsPromptResult;
18 | import android.webkit.WebChromeClient;
19 | import android.webkit.WebResourceResponse;
20 | import android.webkit.WebSettings;
21 | import android.webkit.WebView;
22 | import android.webkit.WebViewClient;
23 |
24 | import com.freelib.hybrid.bridge.JsBridge;
25 |
26 | import java.io.File;
27 | import java.util.HashMap;
28 | import java.util.Map;
29 |
30 | /**
31 | * @author free46000 2017/05/12
32 | * @version v1.0
33 | */
34 | public class HybridWebView extends WebView {
35 | protected Listener listener;
36 | protected JsBridge jsBridge = new JsBridge(this);
37 | protected long lastErrorTime;
38 | protected final Map httpHeaders = new HashMap<>();
39 |
40 | public HybridWebView(Context context, AttributeSet attrs) {
41 | super(context, attrs);
42 | init();
43 | }
44 |
45 | public HybridWebView(Context context, AttributeSet attrs, int defStyleAttr) {
46 | super(context, attrs, defStyleAttr);
47 | init();
48 | }
49 |
50 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
51 | public HybridWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
52 | super(context, attrs, defStyleAttr, defStyleRes);
53 | init();
54 | }
55 |
56 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
57 | public HybridWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) {
58 | super(context, attrs, defStyleAttr, privateBrowsing);
59 | init();
60 | }
61 |
62 | public JsBridge getJsBridge() {
63 | return jsBridge;
64 | }
65 |
66 | protected void init() {
67 | setFocusable(true);
68 | setFocusableInTouchMode(true);
69 | initSettings();
70 | }
71 |
72 |
73 | @SuppressLint({"SetJavaScriptEnabled"})
74 | protected void initSettings() {
75 | WebSettings settings = getSettings();
76 | settings.setJavaScriptEnabled(true); //支持js
77 | // settings.setPluginState(); //支持插件
78 |
79 | settings.setUseWideViewPort(true); //将图片调整到适合webview的大小
80 | settings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小
81 | settings.setSupportZoom(false); //支持缩放,默认为true。是下面那个的前提。
82 | settings.setBuiltInZoomControls(true); //设置内置的缩放控件。
83 | settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); //支持内容重新布局
84 | settings.setAllowFileAccess(true); //设置可以访问文件
85 | settings.setNeedInitialFocus(true); //当webview调用requestFocus时为webview设置节点
86 | settings.setLoadsImagesAutomatically(true); //支持自动加载图片
87 | settings.setDefaultTextEncodingName("utf-8");//设置编码格式
88 | settings.setSupportMultipleWindows(true);
89 | settings.setJavaScriptCanOpenWindowsAutomatically(true); //自动开启窗口 js:window.open()
90 |
91 | setMixedContent(settings);
92 | setCache(settings);
93 | setCookiesEnabled(true);
94 |
95 | setWebViewClient(new BaseWebViewClient(this));
96 | setWebChromeClient(new BaseWebChromeClient(this));
97 | }
98 |
99 | private void setMixedContent(WebSettings settings) {
100 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // 允许http https 混合图片加载
101 | settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
102 | }
103 | }
104 |
105 | private void setCache(WebSettings settings) {
106 | settings.setCacheMode(WebSettings.LOAD_DEFAULT);//默认的缓存使用模式。在进行页面前进或后退的操作时,如果缓存可用并未过期就优先加载缓存,否则从网络上加载数据。这样可以减少页面的网络请求次数
107 | File cacheDir = getContext().getCacheDir();
108 | settings.setDomStorageEnabled(true);
109 | if (cacheDir != null) {
110 | String appCachePath = cacheDir.getAbsolutePath();
111 | settings.setDatabaseEnabled(true);
112 | settings.setAppCacheEnabled(true);
113 | settings.setDatabasePath(appCachePath);
114 | settings.setAppCachePath(appCachePath);
115 | }
116 | }
117 |
118 | @SuppressLint("NewApi")
119 | public void setCookiesEnabled(boolean enabled) {
120 | if (Build.VERSION.SDK_INT >= 21) {
121 | CookieManager.getInstance().setAcceptThirdPartyCookies(this, enabled);
122 | }
123 | CookieManager.getInstance().setAcceptCookie(enabled);
124 | }
125 |
126 |
127 | public void setDesktopMode(final boolean enabled) {
128 | final WebSettings webSettings = getSettings();
129 |
130 | final String newUserAgent;
131 | if (enabled) {
132 | newUserAgent = webSettings.getUserAgentString().replace("Mobile", "eliboM").replace("Android", "diordnA");
133 | } else {
134 | newUserAgent = webSettings.getUserAgentString().replace("eliboM", "Mobile").replace("diordnA", "Android");
135 | }
136 |
137 | webSettings.setUserAgentString(newUserAgent);
138 | webSettings.setUseWideViewPort(enabled);
139 | webSettings.setLoadWithOverviewMode(enabled);
140 | webSettings.setSupportZoom(enabled);
141 | webSettings.setBuiltInZoomControls(enabled);
142 | }
143 |
144 |
145 | /**
146 | * 设置监听
147 | *
148 | * @param listener
149 | */
150 | public void setListener(@NonNull Listener listener) {
151 | this.listener = listener;
152 | }
153 |
154 | @Override
155 | public void destroy() {
156 | try {
157 | ((ViewGroup) getParent()).removeView(this);
158 | removeAllViews();
159 | } catch (Exception ignored) {
160 | } finally {
161 | super.destroy();
162 | }
163 | }
164 |
165 |
166 | /**
167 | * 添加header,load url的时候默认携带
168 | *
169 | * @param name header 名
170 | * @param value header 值
171 | * @see #loadUrl(String, Map)
172 | */
173 | public void addHeader(@NonNull String name, @NonNull String value) {
174 | httpHeaders.put(name, value);
175 | }
176 |
177 | /**
178 | * 移除header中Name对应的一项
179 | *
180 | * @param name header 名
181 | */
182 | public void removeHeader(@NonNull String name) {
183 | httpHeaders.remove(name);
184 | }
185 |
186 | public void clearHeader() {
187 | httpHeaders.clear();
188 | }
189 |
190 | /**
191 | * 点击返回键处理
192 | *
193 | * @return false:证明web view中页面回退
194 | */
195 | public boolean onBackPressed() {
196 | if (canGoBack()) {
197 | goBack();
198 | return false;
199 | } else {
200 | return true;
201 | }
202 | }
203 |
204 | @Override
205 | public void loadUrl(@NonNull String url, Map additionalHttpHeaders) {
206 | if (additionalHttpHeaders == null) {
207 | additionalHttpHeaders = httpHeaders;
208 | } else if (httpHeaders.size() > 0) {
209 | additionalHttpHeaders.putAll(httpHeaders);
210 | }
211 |
212 | super.loadUrl(url, additionalHttpHeaders);
213 | }
214 |
215 | @Override
216 | public void loadUrl(@NonNull String url) {
217 | if (httpHeaders.size() > 0) {
218 | super.loadUrl(url, httpHeaders);
219 | } else {
220 | super.loadUrl(url);
221 | }
222 | }
223 |
224 | public void loadUrl(@NonNull String url, boolean preventCaching) {
225 | if (preventCaching) {
226 | url = makeUrlUnique(url);
227 | }
228 | loadUrl(url);
229 | }
230 |
231 | public void loadUrl(@NonNull String url, boolean preventCaching, Map additionalHttpHeaders) {
232 | if (preventCaching) {
233 | url = makeUrlUnique(url);
234 | }
235 | loadUrl(url, additionalHttpHeaders);
236 | }
237 |
238 | protected String makeUrlUnique(@NonNull String url) {
239 | StringBuilder unique = new StringBuilder();
240 | unique.append(url);
241 |
242 | if (url.contains("?")) {
243 | unique.append('&');
244 | } else {
245 | if (url.lastIndexOf('/') <= 7) {
246 | unique.append('/');
247 | }
248 | unique.append('?');
249 | }
250 |
251 | unique.append(System.currentTimeMillis());
252 | unique.append('=');
253 | unique.append(1);
254 |
255 | return unique.toString();
256 | }
257 |
258 |
259 | protected void setLastError() {
260 | lastErrorTime = System.currentTimeMillis();
261 | }
262 |
263 | protected boolean hasError() {
264 | return (lastErrorTime + 500) >= System.currentTimeMillis();
265 | }
266 |
267 | protected Listener getListener() {
268 | if (listener == null) {
269 | listener = new EmptyListener();
270 | }
271 | return listener;
272 | }
273 |
274 | public static class BaseWebChromeClient extends WebChromeClient {
275 | private HybridWebView hybridWebView;
276 |
277 | public BaseWebChromeClient(@NonNull HybridWebView hybridWebView) {
278 | this.hybridWebView = hybridWebView;
279 | }
280 |
281 | @Override
282 | public void onReceivedTitle(WebView view, String title) {
283 | hybridWebView.getListener().onReceiveTitle(title);
284 | super.onReceivedTitle(view, title);
285 | }
286 |
287 | @Override
288 | public void onProgressChanged(WebView view, int newProgress) {
289 | hybridWebView.getListener().onPageProgressed(newProgress);
290 | super.onProgressChanged(view, newProgress);
291 | }
292 |
293 | @Override
294 | public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
295 | WebView newWebView = new WebView(view.getContext());
296 | WebViewTransport transport = (WebViewTransport) resultMsg.obj;
297 | transport.setWebView(newWebView);
298 | resultMsg.sendToTarget();
299 |
300 | newWebView.setWebViewClient(new WebViewClient() {
301 | @SuppressWarnings("deprecation")
302 | @Override
303 | public boolean shouldOverrideUrlLoading(WebView view, String url) {
304 | Intent browserIntent = new Intent(Intent.ACTION_VIEW);
305 | browserIntent.setData(Uri.parse(url));
306 | view.getContext().startActivity(browserIntent);
307 | return true;
308 | }
309 | });
310 | return true;
311 | }
312 |
313 | // @Override
314 | // public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
315 | // Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();
316 | // result.cancel();
317 | // return true;
318 | // }
319 |
320 | @Override
321 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
322 | if (hybridWebView.getJsBridge().receiveUrl(message)) {
323 | result.confirm("js调用了Android的方法成功啦");
324 | return true;
325 | }
326 |
327 | return super.onJsPrompt(view, url, message, defaultValue, result);
328 | }
329 |
330 |
331 | public boolean onConsoleMessage(ConsoleMessage cm) {
332 | Log.e("BaseWebView", cm.message() + " -- From line "
333 | + cm.lineNumber() + " of "
334 | + cm.sourceId());
335 | return true;
336 | }
337 | }
338 |
339 | @SuppressWarnings("deprecation")
340 | public static class BaseWebViewClient extends WebViewClient {
341 | private HybridWebView hybridWebView;
342 |
343 | public BaseWebViewClient(@NonNull HybridWebView hybridWebView) {
344 | this.hybridWebView = hybridWebView;
345 | }
346 |
347 | @Override
348 | public void onPageStarted(WebView view, String url, Bitmap favicon) {
349 | if (!hybridWebView.hasError()) {
350 | hybridWebView.getListener().onPageStarted(url, favicon);
351 | }
352 | }
353 |
354 | @Override
355 | public void onPageFinished(WebView view, String url) {
356 | if (!hybridWebView.hasError()) {
357 | hybridWebView.getListener().onPageFinished(url);
358 | }
359 |
360 | }
361 |
362 | @Override
363 | public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
364 | hybridWebView.setLastError();
365 | hybridWebView.getListener().onPageError(errorCode, description, failingUrl);
366 | }
367 |
368 | @Override
369 | public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
370 | Log.e("BaseWebView", "shouldOverrideUrlLoading:" + url);
371 | view.loadUrl(url);
372 | return true;
373 | }
374 |
375 | @Override
376 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
377 | Log.e("BaseWebView", "shouldInterceptRequest:" + url);
378 | return super.shouldInterceptRequest(view, url);
379 | }
380 | }
381 |
382 | public interface Listener {
383 |
384 | void onPageStarted(String url, Bitmap favicon);
385 |
386 | void onPageProgressed(int newProgress);
387 |
388 | void onPageFinished(String url);
389 |
390 | void onPageError(int errorCode, String description, String failingUrl);
391 |
392 | void onReceiveTitle(String title);
393 |
394 | }
395 |
396 | private class EmptyListener implements Listener {
397 |
398 | @Override
399 | public void onPageStarted(String url, Bitmap favicon) {
400 | }
401 |
402 | @Override
403 | public void onPageProgressed(int newProgress) {
404 | }
405 |
406 | @Override
407 | public void onPageFinished(String url) {
408 | }
409 |
410 | @Override
411 | public void onPageError(int errorCode, String description, String failingUrl) {
412 | }
413 |
414 | @Override
415 | public void onReceiveTitle(String title) {
416 | }
417 | }
418 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/file/ZipUtils.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.file;
2 |
3 | import android.content.Context;
4 | import android.text.TextUtils;
5 |
6 | import java.io.BufferedInputStream;
7 | import java.io.BufferedOutputStream;
8 | import java.io.File;
9 | import java.io.FileInputStream;
10 | import java.io.FileOutputStream;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.io.OutputStream;
14 | import java.util.ArrayList;
15 | import java.util.Collection;
16 | import java.util.Enumeration;
17 | import java.util.List;
18 | import java.util.zip.ZipEntry;
19 | import java.util.zip.ZipFile;
20 | import java.util.zip.ZipInputStream;
21 | import java.util.zip.ZipOutputStream;
22 |
23 | /**
24 | * 压缩相关工具类
25 | *
26 | * @author free46000 2017/05/18
27 | * @version v1.0
28 | */
29 | public class ZipUtils {
30 |
31 | private static final int KB = 1024;
32 |
33 | private ZipUtils() {
34 | throw new UnsupportedOperationException("u can't instantiate me...");
35 | }
36 |
37 | /**
38 | * 批量压缩文件
39 | *
40 | * @param resFiles 待压缩文件集合
41 | * @param zipFilePath 压缩文件路径
42 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
43 | * @throws IOException IO错误时抛出
44 | */
45 | public static boolean zipFiles(Collection resFiles, String zipFilePath)
46 | throws IOException {
47 | return zipFiles(resFiles, zipFilePath, null);
48 | }
49 |
50 | /**
51 | * 批量压缩文件
52 | *
53 | * @param resFiles 待压缩文件集合
54 | * @param zipFilePath 压缩文件路径
55 | * @param comment 压缩文件的注释
56 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
57 | * @throws IOException IO错误时抛出
58 | */
59 | public static boolean zipFiles(Collection resFiles, String zipFilePath, String comment)
60 | throws IOException {
61 | return zipFiles(resFiles, FileUtils.getFileByPath(zipFilePath), comment);
62 | }
63 |
64 | /**
65 | * 批量压缩文件
66 | *
67 | * @param resFiles 待压缩文件集合
68 | * @param zipFile 压缩文件
69 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
70 | * @throws IOException IO错误时抛出
71 | */
72 | public static boolean zipFiles(Collection resFiles, File zipFile)
73 | throws IOException {
74 | return zipFiles(resFiles, zipFile, null);
75 | }
76 |
77 | /**
78 | * 批量压缩文件
79 | *
80 | * @param resFiles 待压缩文件集合
81 | * @param zipFile 压缩文件
82 | * @param comment 压缩文件的注释
83 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
84 | * @throws IOException IO错误时抛出
85 | */
86 | public static boolean zipFiles(Collection resFiles, File zipFile, String comment)
87 | throws IOException {
88 | if (resFiles == null || zipFile == null) return false;
89 | ZipOutputStream zos = null;
90 | try {
91 | zos = new ZipOutputStream(new FileOutputStream(zipFile));
92 | for (File resFile : resFiles) {
93 | if (!zipFile(resFile, "", zos, comment)) return false;
94 | }
95 | return true;
96 | } finally {
97 | if (zos != null) {
98 | zos.finish();
99 | CloseUtils.closeIO(zos);
100 | }
101 | }
102 | }
103 |
104 | /**
105 | * 压缩文件
106 | *
107 | * @param resFilePath 待压缩文件路径
108 | * @param zipFilePath 压缩文件路径
109 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
110 | * @throws IOException IO错误时抛出
111 | */
112 | public static boolean zipFile(String resFilePath, String zipFilePath)
113 | throws IOException {
114 | return zipFile(resFilePath, zipFilePath, null);
115 | }
116 |
117 | /**
118 | * 压缩文件
119 | *
120 | * @param resFilePath 待压缩文件路径
121 | * @param zipFilePath 压缩文件路径
122 | * @param comment 压缩文件的注释
123 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
124 | * @throws IOException IO错误时抛出
125 | */
126 | public static boolean zipFile(String resFilePath, String zipFilePath, String comment)
127 | throws IOException {
128 | return zipFile(FileUtils.getFileByPath(resFilePath), FileUtils.getFileByPath(zipFilePath), comment);
129 | }
130 |
131 | /**
132 | * 压缩文件
133 | *
134 | * @param resFile 待压缩文件
135 | * @param zipFile 压缩文件
136 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
137 | * @throws IOException IO错误时抛出
138 | */
139 | public static boolean zipFile(File resFile, File zipFile)
140 | throws IOException {
141 | return zipFile(resFile, zipFile, null);
142 | }
143 |
144 | /**
145 | * 压缩文件
146 | *
147 | * @param resFile 待压缩文件
148 | * @param zipFile 压缩文件
149 | * @param comment 压缩文件的注释
150 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
151 | * @throws IOException IO错误时抛出
152 | */
153 | public static boolean zipFile(File resFile, File zipFile, String comment)
154 | throws IOException {
155 | if (resFile == null || zipFile == null) return false;
156 | ZipOutputStream zos = null;
157 | try {
158 | zos = new ZipOutputStream(new FileOutputStream(zipFile));
159 | return zipFile(resFile, "", zos, comment);
160 | } finally {
161 | if (zos != null) {
162 | CloseUtils.closeIO(zos);
163 | }
164 | }
165 | }
166 |
167 | /**
168 | * 压缩文件
169 | *
170 | * @param resFile 待压缩文件
171 | * @param rootPath 相对于压缩文件的路径
172 | * @param zos 压缩文件输出流
173 | * @param comment 压缩文件的注释
174 | * @return {@code true}: 压缩成功
{@code false}: 压缩失败
175 | * @throws IOException IO错误时抛出
176 | */
177 | private static boolean zipFile(File resFile, String rootPath, ZipOutputStream zos, String comment)
178 | throws IOException {
179 | rootPath = rootPath + (isSpace(rootPath) ? "" : File.separator) + resFile.getName();
180 | if (resFile.isDirectory()) {
181 | File[] fileList = resFile.listFiles();
182 | // 如果是空文件夹那么创建它,我把'/'换为File.separator测试就不成功,eggPain
183 | if (fileList == null || fileList.length <= 0) {
184 | ZipEntry entry = new ZipEntry(rootPath + '/');
185 | if (!TextUtils.isEmpty(comment)) entry.setComment(comment);
186 | zos.putNextEntry(entry);
187 | zos.closeEntry();
188 | } else {
189 | for (File file : fileList) {
190 | // 如果递归返回false则返回false
191 | if (!zipFile(file, rootPath, zos, comment)) return false;
192 | }
193 | }
194 | } else {
195 | InputStream is = null;
196 | try {
197 | is = new BufferedInputStream(new FileInputStream(resFile));
198 | ZipEntry entry = new ZipEntry(rootPath);
199 | if (!TextUtils.isEmpty(comment)) entry.setComment(comment);
200 | zos.putNextEntry(entry);
201 | byte buffer[] = new byte[KB];
202 | int len;
203 | while ((len = is.read(buffer, 0, KB)) != -1) {
204 | zos.write(buffer, 0, len);
205 | }
206 | zos.closeEntry();
207 | } finally {
208 | CloseUtils.closeIO(is);
209 | }
210 | }
211 | return true;
212 | }
213 |
214 | /**
215 | * 批量解压文件
216 | *
217 | * @param zipFiles 压缩文件集合
218 | * @param destDirPath 目标目录路径
219 | * @return {@code true}: 解压成功
{@code false}: 解压失败
220 | * @throws IOException IO错误时抛出
221 | */
222 | public static boolean unzipFiles(Collection zipFiles, String destDirPath)
223 | throws IOException {
224 | return unzipFiles(zipFiles, FileUtils.getFileByPath(destDirPath));
225 | }
226 |
227 | /**
228 | * 批量解压文件
229 | *
230 | * @param zipFiles 压缩文件集合
231 | * @param destDir 目标目录
232 | * @return {@code true}: 解压成功
{@code false}: 解压失败
233 | * @throws IOException IO错误时抛出
234 | */
235 | public static boolean unzipFiles(Collection zipFiles, File destDir)
236 | throws IOException {
237 | if (zipFiles == null || destDir == null) return false;
238 | for (File zipFile : zipFiles) {
239 | if (!unzipFile(zipFile, destDir)) return false;
240 | }
241 | return true;
242 | }
243 |
244 | /**
245 | * 解压文件
246 | *
247 | * @param zipFilePath 待解压文件路径
248 | * @param destDirPath 目标目录路径
249 | * @return {@code true}: 解压成功
{@code false}: 解压失败
250 | * @throws IOException IO错误时抛出
251 | */
252 | public static boolean unzipFile(String zipFilePath, String destDirPath)
253 | throws IOException {
254 | return unzipFile(FileUtils.getFileByPath(zipFilePath), FileUtils.getFileByPath(destDirPath));
255 | }
256 |
257 | /**
258 | * 解压文件
259 | *
260 | * @param zipFile 待解压文件
261 | * @param destDir 目标目录
262 | * @return {@code true}: 解压成功
{@code false}: 解压失败
263 | * @throws IOException IO错误时抛出
264 | */
265 | public static boolean unzipFile(File zipFile, File destDir)
266 | throws IOException {
267 | return unzipFileByKeyword(zipFile, destDir, null) != null;
268 | }
269 |
270 | /**
271 | * 解压带有关键字的文件
272 | *
273 | * @param zipFilePath 待解压文件路径
274 | * @param destDirPath 目标目录路径
275 | * @param keyword 关键字
276 | * @return 返回带有关键字的文件链表
277 | * @throws IOException IO错误时抛出
278 | */
279 | public static List unzipFileByKeyword(String zipFilePath, String destDirPath, String keyword)
280 | throws IOException {
281 | return unzipFileByKeyword(FileUtils.getFileByPath(zipFilePath),
282 | FileUtils.getFileByPath(destDirPath), keyword);
283 | }
284 |
285 | /**
286 | * 解压带有关键字的文件
287 | *
288 | * @param zipFile 待解压文件
289 | * @param destDir 目标目录
290 | * @param keyword 关键字
291 | * @return 返回带有关键字的文件链表
292 | * @throws IOException IO错误时抛出
293 | */
294 | public static List unzipFileByKeyword(File zipFile, File destDir, String keyword)
295 | throws IOException {
296 | if (zipFile == null || destDir == null) return null;
297 | List files = new ArrayList<>();
298 | ZipFile zf = new ZipFile(zipFile);
299 | Enumeration> entries = zf.entries();
300 | while (entries.hasMoreElements()) {
301 | ZipEntry entry = ((ZipEntry) entries.nextElement());
302 | String entryName = entry.getName();
303 | if (TextUtils.isEmpty(keyword) || FileUtils.getFileName(entryName).toLowerCase().contains(keyword.toLowerCase())) {
304 | String filePath = destDir + File.separator + entryName;
305 | File file = new File(filePath);
306 | files.add(file);
307 | if (entry.isDirectory()) {
308 | if (!FileUtils.createOrExistsDir(file)) return null;
309 | } else {
310 | if (!FileUtils.createOrExistsFile(file)) return null;
311 | InputStream in = null;
312 | OutputStream out = null;
313 | try {
314 | in = new BufferedInputStream(zf.getInputStream(entry));
315 | out = new BufferedOutputStream(new FileOutputStream(file));
316 | byte buffer[] = new byte[KB];
317 | int len;
318 | while ((len = in.read(buffer)) != -1) {
319 | out.write(buffer, 0, len);
320 | }
321 | } finally {
322 | CloseUtils.closeIO(in, out);
323 | }
324 | }
325 | }
326 | }
327 | return files;
328 | }
329 |
330 | /**
331 | * 解压目标文件
332 | *
333 | * @param context
334 | * @param destDirName
335 | * @param fileName assets目标文件
336 | */
337 | public static boolean unZipFile(Context context, String destDirName, String fileName) {
338 | File zipFileDir = FileUtils.getFileByPath(destDirName);
339 | FileUtils.createOrExistsDir(zipFileDir);
340 |
341 | InputStream stream;
342 | ZipInputStream inZip = null;
343 | try {
344 | //将Assets文件夹下面的压缩包,转换成字节读取流
345 | stream = context.getAssets().open(fileName);
346 | //将字节读取流转成zip读取流
347 | inZip = new ZipInputStream(stream);
348 | //压缩文件实体
349 | ZipEntry zipEntry;
350 | //压缩文件实体中的文件名称
351 | String szName;
352 | while ((zipEntry = inZip.getNextEntry()) != null) {
353 | szName = zipEntry.getName();
354 | if (zipEntry.isDirectory()) {
355 | //zipEntry是目录,则创建目录
356 | szName = szName.substring(0, szName.length() - 1);
357 | File folder = new File(zipFileDir, szName);
358 | FileUtils.createOrExistsDir(folder);
359 | } else {
360 | //否则创建文件,并输出文件的内容
361 | File file = new File(zipFileDir, szName);
362 | FileUtils.createOrExistsFile(file);
363 | FileOutputStream out = new FileOutputStream(file);
364 | int len;
365 | byte[] buffer = new byte[1024];
366 | while ((len = inZip.read(buffer)) != -1) {
367 | out.write(buffer, 0, len);
368 | out.flush();
369 | }
370 | out.close();
371 | }
372 | }
373 |
374 | } catch (Exception e) {
375 | e.printStackTrace();
376 | return false;
377 | } finally {
378 | CloseUtils.closeIO(inZip);
379 | }
380 | return true;
381 | }
382 |
383 | /**
384 | * 获取压缩文件中的文件路径链表
385 | *
386 | * @param zipFilePath 压缩文件路径
387 | * @return 压缩文件中的文件路径链表
388 | * @throws IOException IO错误时抛出
389 | */
390 | public static List getFilesPath(String zipFilePath)
391 | throws IOException {
392 | return getFilesPath(FileUtils.getFileByPath(zipFilePath));
393 | }
394 |
395 | /**
396 | * 获取压缩文件中的文件路径链表
397 | *
398 | * @param zipFile 压缩文件
399 | * @return 压缩文件中的文件路径链表
400 | * @throws IOException IO错误时抛出
401 | */
402 | public static List getFilesPath(File zipFile)
403 | throws IOException {
404 | if (zipFile == null) return null;
405 | List paths = new ArrayList<>();
406 | Enumeration> entries = getEntries(zipFile);
407 | while (entries.hasMoreElements()) {
408 | paths.add(((ZipEntry) entries.nextElement()).getName());
409 | }
410 | return paths;
411 | }
412 |
413 | /**
414 | * 获取压缩文件中的注释链表
415 | *
416 | * @param zipFilePath 压缩文件路径
417 | * @return 压缩文件中的注释链表
418 | * @throws IOException IO错误时抛出
419 | */
420 | public static List getComments(String zipFilePath)
421 | throws IOException {
422 | return getComments(FileUtils.getFileByPath(zipFilePath));
423 | }
424 |
425 | /**
426 | * 获取压缩文件中的注释链表
427 | *
428 | * @param zipFile 压缩文件
429 | * @return 压缩文件中的注释链表
430 | * @throws IOException IO错误时抛出
431 | */
432 | public static List getComments(File zipFile)
433 | throws IOException {
434 | if (zipFile == null) return null;
435 | List comments = new ArrayList<>();
436 | Enumeration> entries = getEntries(zipFile);
437 | while (entries.hasMoreElements()) {
438 | ZipEntry entry = ((ZipEntry) entries.nextElement());
439 | comments.add(entry.getComment());
440 | }
441 | return comments;
442 | }
443 |
444 | /**
445 | * 获取压缩文件中的文件对象
446 | *
447 | * @param zipFilePath 压缩文件路径
448 | * @return 压缩文件中的文件对象
449 | * @throws IOException IO错误时抛出
450 | */
451 | public static Enumeration> getEntries(String zipFilePath)
452 | throws IOException {
453 | return getEntries(FileUtils.getFileByPath(zipFilePath));
454 | }
455 |
456 | /**
457 | * 获取压缩文件中的文件对象
458 | *
459 | * @param zipFile 压缩文件
460 | * @return 压缩文件中的文件对象
461 | * @throws IOException IO错误时抛出
462 | */
463 | public static Enumeration> getEntries(File zipFile)
464 | throws IOException {
465 | if (zipFile == null) return null;
466 | return new ZipFile(zipFile).entries();
467 | }
468 |
469 | private static boolean isSpace(String s) {
470 | if (s == null) return true;
471 | for (int i = 0, len = s.length(); i < len; ++i) {
472 | if (!Character.isWhitespace(s.charAt(i))) {
473 | return false;
474 | }
475 | }
476 | return true;
477 | }
478 | }
479 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/file/FileIOUtils.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.file;
2 |
3 | import java.io.BufferedOutputStream;
4 | import java.io.BufferedReader;
5 | import java.io.BufferedWriter;
6 | import java.io.ByteArrayOutputStream;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.FileOutputStream;
10 | import java.io.FileWriter;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.io.InputStreamReader;
14 | import java.io.OutputStream;
15 | import java.io.RandomAccessFile;
16 | import java.nio.ByteBuffer;
17 | import java.nio.MappedByteBuffer;
18 | import java.nio.channels.FileChannel;
19 | import java.util.ArrayList;
20 | import java.util.List;
21 |
22 | /**
23 | * @author free46000 2017/05/26
24 | * @version v1.0
25 | */
26 | public class FileIOUtils {
27 | private FileIOUtils() {
28 | throw new UnsupportedOperationException("u can't instantiate me...");
29 | }
30 |
31 | private static final String LINE_SEP = System.getProperty("line.separator");
32 |
33 | /**
34 | * 将输入流写入文件
35 | *
36 | * @param filePath 路径
37 | * @param is 输入流
38 | * @return {@code true}: 写入成功
{@code false}: 写入失败
39 | */
40 | public static boolean writeFileFromIS(String filePath, final InputStream is) {
41 | return writeFileFromIS(FileUtils.getFileByPath(filePath), is, false);
42 | }
43 |
44 | /**
45 | * 将输入流写入文件
46 | *
47 | * @param filePath 路径
48 | * @param is 输入流
49 | * @param append 是否追加在文件末
50 | * @return {@code true}: 写入成功
{@code false}: 写入失败
51 | */
52 | public static boolean writeFileFromIS(String filePath, final InputStream is, boolean append) {
53 | return writeFileFromIS(FileUtils.getFileByPath(filePath), is, append);
54 | }
55 |
56 | /**
57 | * 将输入流写入文件
58 | *
59 | * @param file 文件
60 | * @param is 输入流
61 | * @return {@code true}: 写入成功
{@code false}: 写入失败
62 | */
63 | public static boolean writeFileFromIS(File file, final InputStream is) {
64 | return writeFileFromIS(file, is, false);
65 | }
66 |
67 | /**
68 | * 将输入流写入文件
69 | *
70 | * @param file 文件
71 | * @param is 输入流
72 | * @param append 是否追加在文件末
73 | * @return {@code true}: 写入成功
{@code false}: 写入失败
74 | */
75 | public static boolean writeFileFromIS(File file, final InputStream is, boolean append) {
76 | if (!FileUtils.createOrExistsFile(file) || is == null) return false;
77 | OutputStream os = null;
78 | try {
79 | os = new BufferedOutputStream(new FileOutputStream(file, append));
80 | byte data[] = new byte[1024];
81 | int len;
82 | while ((len = is.read(data, 0, 1024)) != -1) {
83 | os.write(data, 0, len);
84 | }
85 | return true;
86 | } catch (IOException e) {
87 | e.printStackTrace();
88 | return false;
89 | } finally {
90 | CloseUtils.closeIO(is, os);
91 | }
92 | }
93 |
94 | /**
95 | * 将字节数组写入文件
96 | *
97 | * @param filePath 文件路径
98 | * @param bytes 字节数组
99 | * @return {@code true}: 写入成功
{@code false}: 写入失败
100 | */
101 | public static boolean writeFileFromBytesByStream(String filePath, final byte[] bytes) {
102 | return writeFileFromBytesByStream(FileUtils.getFileByPath(filePath), bytes, false);
103 | }
104 |
105 | /**
106 | * 将字节数组写入文件
107 | *
108 | * @param filePath 文件路径
109 | * @param bytes 字节数组
110 | * @param append 是否追加在文件末
111 | * @return {@code true}: 写入成功
{@code false}: 写入失败
112 | */
113 | public static boolean writeFileFromBytesByStream(String filePath, final byte[] bytes, boolean append) {
114 | return writeFileFromBytesByStream(FileUtils.getFileByPath(filePath), bytes, append);
115 | }
116 |
117 | /**
118 | * 将字节数组写入文件
119 | *
120 | * @param file 文件
121 | * @param bytes 字节数组
122 | * @return {@code true}: 写入成功
{@code false}: 写入失败
123 | */
124 | public static boolean writeFileFromBytesByStream(File file, final byte[] bytes) {
125 | return writeFileFromBytesByStream(file, bytes, false);
126 | }
127 |
128 | /**
129 | * 将字节数组写入文件
130 | *
131 | * @param file 文件
132 | * @param bytes 字节数组
133 | * @param append 是否追加在文件末
134 | * @return {@code true}: 写入成功
{@code false}: 写入失败
135 | */
136 | public static boolean writeFileFromBytesByStream(File file, final byte[] bytes, boolean append) {
137 | if (bytes == null || !FileUtils.createOrExistsFile(file)) return false;
138 | BufferedOutputStream bos = null;
139 | try {
140 | bos = new BufferedOutputStream(new FileOutputStream(file, append));
141 | bos.write(bytes);
142 | return true;
143 | } catch (IOException e) {
144 | e.printStackTrace();
145 | return false;
146 | } finally {
147 | CloseUtils.closeIO(bos);
148 | }
149 | }
150 |
151 | /**
152 | * 将字节数组写入文件
153 | *
154 | * @param filePath 文件路径
155 | * @param bytes 字节数组
156 | * @param isForce 是否写入文件
157 | * @return {@code true}: 写入成功
{@code false}: 写入失败
158 | */
159 | public static boolean writeFileFromBytesByChannel(String filePath, final byte[] bytes, boolean isForce) {
160 | return writeFileFromBytesByChannel(FileUtils.getFileByPath(filePath), bytes, false, isForce);
161 | }
162 |
163 | /**
164 | * 将字节数组写入文件
165 | *
166 | * @param filePath 文件路径
167 | * @param bytes 字节数组
168 | * @param append 是否追加在文件末
169 | * @param isForce 是否写入文件
170 | * @return {@code true}: 写入成功
{@code false}: 写入失败
171 | */
172 | public static boolean writeFileFromBytesByChannel(String filePath, final byte[] bytes, boolean append, boolean isForce) {
173 | return writeFileFromBytesByChannel(FileUtils.getFileByPath(filePath), bytes, append, isForce);
174 | }
175 |
176 | /**
177 | * 将字节数组写入文件
178 | *
179 | * @param file 文件
180 | * @param bytes 字节数组
181 | * @param isForce 是否写入文件
182 | * @return {@code true}: 写入成功
{@code false}: 写入失败
183 | */
184 | public static boolean writeFileFromBytesByChannel(File file, final byte[] bytes, boolean isForce) {
185 | return writeFileFromBytesByChannel(file, bytes, false, isForce);
186 | }
187 |
188 | /**
189 | * 将字节数组写入文件
190 | *
191 | * @param file 文件
192 | * @param bytes 字节数组
193 | * @param append 是否追加在文件末
194 | * @param isForce 是否写入文件
195 | * @return {@code true}: 写入成功
{@code false}: 写入失败
196 | */
197 | public static boolean writeFileFromBytesByChannel(File file, final byte[] bytes, boolean append, boolean isForce) {
198 | if (bytes == null) return false;
199 | if (!append && !FileUtils.createFileByDeleteOldFile(file)) return false;
200 | FileChannel fc = null;
201 | try {
202 | fc = new RandomAccessFile(file, "rw").getChannel();
203 | fc.position(fc.size());
204 | fc.write(ByteBuffer.wrap(bytes));
205 | if (isForce) fc.force(true);
206 | return true;
207 | } catch (IOException e) {
208 | e.printStackTrace();
209 | return false;
210 | } finally {
211 | CloseUtils.closeIO(fc);
212 | }
213 | }
214 |
215 | /**
216 | * 将字节数组写入文件
217 | *
218 | * @param filePath 文件路径
219 | * @param bytes 字节数组
220 | * @param isForce 是否写入文件
221 | * @return {@code true}: 写入成功
{@code false}: 写入失败
222 | */
223 | public static boolean writeFileFromBytesByMap(String filePath, final byte[] bytes, boolean isForce) {
224 | return writeFileFromBytesByMap(filePath, bytes, false, isForce);
225 | }
226 |
227 | /**
228 | * 将字节数组写入文件
229 | *
230 | * @param filePath 文件路径
231 | * @param bytes 字节数组
232 | * @param append 是否追加在文件末
233 | * @param isForce 是否写入文件
234 | * @return {@code true}: 写入成功
{@code false}: 写入失败
235 | */
236 | public static boolean writeFileFromBytesByMap(String filePath, final byte[] bytes, boolean append, boolean isForce) {
237 | return writeFileFromBytesByMap(FileUtils.getFileByPath(filePath), bytes, append, isForce);
238 | }
239 |
240 | /**
241 | * 将字节数组写入文件
242 | *
243 | * @param file 文件
244 | * @param bytes 字节数组
245 | * @param isForce 是否写入文件
246 | * @return {@code true}: 写入成功
{@code false}: 写入失败
247 | */
248 | public static boolean writeFileFromBytesByMap(File file, final byte[] bytes, boolean isForce) {
249 | return writeFileFromBytesByMap(file, bytes, false, isForce);
250 | }
251 |
252 | /**
253 | * 将字节数组写入文件
254 | *
255 | * @param file 文件
256 | * @param bytes 字节数组
257 | * @param append 是否追加在文件末
258 | * @param isForce 是否写入文件
259 | * @return {@code true}: 写入成功
{@code false}: 写入失败
260 | */
261 | public static boolean writeFileFromBytesByMap(File file, final byte[] bytes, boolean append, boolean isForce) {
262 | if (bytes == null || !FileUtils.createOrExistsFile(file)) return false;
263 | if (!append && !FileUtils.createFileByDeleteOldFile(file)) return false;
264 | FileChannel fc = null;
265 | try {
266 | fc = new RandomAccessFile(file, "rw").getChannel();
267 | MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, fc.size(), bytes.length);
268 | mbb.put(bytes);
269 | if (isForce) mbb.force();
270 | return true;
271 | } catch (IOException e) {
272 | e.printStackTrace();
273 | return false;
274 | } finally {
275 | CloseUtils.closeIO(fc);
276 | }
277 | }
278 |
279 | /**
280 | * 将字符串写入文件
281 | *
282 | * @param filePath 文件路径
283 | * @param content 写入内容
284 | * @return {@code true}: 写入成功
{@code false}: 写入失败
285 | */
286 | public static boolean writeFileFromString(String filePath, String content) {
287 | return writeFileFromString(FileUtils.getFileByPath(filePath), content, false);
288 | }
289 |
290 | /**
291 | * 将字符串写入文件
292 | *
293 | * @param filePath 文件路径
294 | * @param content 写入内容
295 | * @param append 是否追加在文件末
296 | * @return {@code true}: 写入成功
{@code false}: 写入失败
297 | */
298 | public static boolean writeFileFromString(String filePath, String content, boolean append) {
299 | return writeFileFromString(FileUtils.getFileByPath(filePath), content, append);
300 | }
301 |
302 | /**
303 | * 将字符串写入文件
304 | *
305 | * @param file 文件
306 | * @param content 写入内容
307 | * @return {@code true}: 写入成功
{@code false}: 写入失败
308 | */
309 | public static boolean writeFileFromString(File file, String content) {
310 | return writeFileFromString(file, content, false);
311 | }
312 |
313 | /**
314 | * 将字符串写入文件
315 | *
316 | * @param file 文件
317 | * @param content 写入内容
318 | * @param append 是否追加在文件末
319 | * @return {@code true}: 写入成功
{@code false}: 写入失败
320 | */
321 | public static boolean writeFileFromString(File file, String content, boolean append) {
322 | if (file == null || content == null) return false;
323 | if (!FileUtils.createOrExistsFile(file)) return false;
324 | BufferedWriter bw = null;
325 | try {
326 | bw = new BufferedWriter(new FileWriter(file, append));
327 | bw.write(content);
328 | return true;
329 | } catch (IOException e) {
330 | e.printStackTrace();
331 | return false;
332 | } finally {
333 | CloseUtils.closeIO(bw);
334 | }
335 | }
336 |
337 | ///////////////////////////////////////////////////////////////////////////
338 | // the divide line of write and read
339 | ///////////////////////////////////////////////////////////////////////////
340 |
341 | /**
342 | * 读取文件到字符串链表中
343 | *
344 | * @param filePath 文件路径
345 | * @return 字符串链表中
346 | */
347 | public static List readFile2List(String filePath) {
348 | return readFile2List(FileUtils.getFileByPath(filePath), null);
349 | }
350 |
351 | /**
352 | * 读取文件到字符串链表中
353 | *
354 | * @param filePath 文件路径
355 | * @param charsetName 编码格式
356 | * @return 字符串链表中
357 | */
358 | public static List readFile2List(String filePath, String charsetName) {
359 | return readFile2List(FileUtils.getFileByPath(filePath), charsetName);
360 | }
361 |
362 | /**
363 | * 读取文件到字符串链表中
364 | *
365 | * @param file 文件
366 | * @return 字符串链表中
367 | */
368 | public static List readFile2List(File file) {
369 | return readFile2List(file, 0, 0x7FFFFFFF, null);
370 | }
371 |
372 | /**
373 | * 读取文件到字符串链表中
374 | *
375 | * @param file 文件
376 | * @param charsetName 编码格式
377 | * @return 字符串链表中
378 | */
379 | public static List readFile2List(File file, String charsetName) {
380 | return readFile2List(file, 0, 0x7FFFFFFF, charsetName);
381 | }
382 |
383 | /**
384 | * 读取文件到字符串链表中
385 | *
386 | * @param filePath 文件路径
387 | * @param st 需要读取的开始行数
388 | * @param end 需要读取的结束行数
389 | * @return 字符串链表中
390 | */
391 | public static List readFile2List(String filePath, int st, int end) {
392 | return readFile2List(FileUtils.getFileByPath(filePath), st, end, null);
393 | }
394 |
395 | /**
396 | * 读取文件到字符串链表中
397 | *
398 | * @param filePath 文件路径
399 | * @param st 需要读取的开始行数
400 | * @param end 需要读取的结束行数
401 | * @param charsetName 编码格式
402 | * @return 字符串链表中
403 | */
404 | public static List readFile2List(String filePath, int st, int end, String charsetName) {
405 | return readFile2List(FileUtils.getFileByPath(filePath), st, end, charsetName);
406 | }
407 |
408 | /**
409 | * 读取文件到字符串链表中
410 | *
411 | * @param file 文件
412 | * @param st 需要读取的开始行数
413 | * @param end 需要读取的结束行数
414 | * @return 字符串链表中
415 | */
416 | public static List readFile2List(File file, int st, int end) {
417 | return readFile2List(file, st, end, null);
418 | }
419 |
420 | /**
421 | * 读取文件到字符串链表中
422 | *
423 | * @param file 文件
424 | * @param st 需要读取的开始行数
425 | * @param end 需要读取的结束行数
426 | * @param charsetName 编码格式
427 | * @return 字符串链表中
428 | */
429 | public static List readFile2List(File file, int st, int end, String charsetName) {
430 | if (!FileUtils.isFileExists(file)) return null;
431 | if (st > end) return null;
432 | BufferedReader reader = null;
433 | try {
434 | String line;
435 | int curLine = 1;
436 | List list = new ArrayList<>();
437 | if (isSpace(charsetName)) {
438 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
439 | } else {
440 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), charsetName));
441 | }
442 | while ((line = reader.readLine()) != null) {
443 | if (curLine > end) break;
444 | if (st <= curLine && curLine <= end) list.add(line);
445 | ++curLine;
446 | }
447 | return list;
448 | } catch (IOException e) {
449 | e.printStackTrace();
450 | return null;
451 | } finally {
452 | CloseUtils.closeIO(reader);
453 | }
454 | }
455 |
456 | /**
457 | * 读取文件到字符串中
458 | *
459 | * @param filePath 文件路径
460 | * @return 字符串
461 | */
462 | public static String readFile2String(String filePath) {
463 | return readFile2String(FileUtils.getFileByPath(filePath), null);
464 | }
465 |
466 | /**
467 | * 读取文件到字符串中
468 | *
469 | * @param filePath 文件路径
470 | * @param charsetName 编码格式
471 | * @return 字符串
472 | */
473 | public static String readFile2String(String filePath, String charsetName) {
474 | return readFile2String(FileUtils.getFileByPath(filePath), charsetName);
475 | }
476 |
477 | /**
478 | * 读取文件到字符串中
479 | *
480 | * @param file 文件
481 | * @return 字符串
482 | */
483 | public static String readFile2String(File file) {
484 | return readFile2String(file, null);
485 | }
486 |
487 | /**
488 | * 读取文件到字符串中
489 | *
490 | * @param file 文件
491 | * @param charsetName 编码格式
492 | * @return 字符串
493 | */
494 | public static String readFile2String(File file, String charsetName) {
495 | if (!FileUtils.isFileExists(file)) return null;
496 | BufferedReader reader = null;
497 | try {
498 | StringBuilder sb = new StringBuilder();
499 | if (isSpace(charsetName)) {
500 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
501 | } else {
502 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), charsetName));
503 | }
504 | String line;
505 | while ((line = reader.readLine()) != null) {
506 | sb.append(line).append(LINE_SEP);
507 | }
508 | // delete the last line separator
509 | return sb.delete(sb.length() - LINE_SEP.length(), sb.length()).toString();
510 | } catch (IOException e) {
511 | e.printStackTrace();
512 | return null;
513 | } finally {
514 | CloseUtils.closeIO(reader);
515 | }
516 | }
517 |
518 | /**
519 | * 读取文件到字节数组中
520 | *
521 | * @param filePath 文件路径
522 | * @return 字符数组
523 | */
524 | public static byte[] readFile2BytesByStream(String filePath) {
525 | return readFile2BytesByStream(FileUtils.getFileByPath(filePath));
526 | }
527 |
528 | /**
529 | * 读取文件到字节数组中
530 | *
531 | * @param file 文件
532 | * @return 字符数组
533 | */
534 | public static byte[] readFile2BytesByStream(File file) {
535 | if (!FileUtils.isFileExists(file)) return null;
536 | FileInputStream fis = null;
537 | ByteArrayOutputStream os = null;
538 | try {
539 | fis = new FileInputStream(file);
540 | os = new ByteArrayOutputStream();
541 | byte[] b = new byte[1024];
542 | int len;
543 | while ((len = fis.read(b, 0, 1024)) != -1) {
544 | os.write(b, 0, len);
545 | }
546 | return os.toByteArray();
547 | } catch (IOException e) {
548 | e.printStackTrace();
549 | return null;
550 | } finally {
551 | CloseUtils.closeIO(fis, os);
552 | }
553 | }
554 |
555 | /**
556 | * 读取文件到字节数组中
557 | *
558 | * @param filePath 文件路径
559 | * @return 字符数组
560 | */
561 | public static byte[] readFile2BytesByChannel(String filePath) {
562 | return readFile2BytesByChannel(FileUtils.getFileByPath(filePath));
563 | }
564 |
565 | /**
566 | * 读取文件到字节数组中
567 | *
568 | * @param file 文件
569 | * @return 字符数组
570 | */
571 | public static byte[] readFile2BytesByChannel(File file) {
572 | if (!FileUtils.isFileExists(file)) return null;
573 | FileChannel fc = null;
574 | try {
575 | fc = new RandomAccessFile(file, "r").getChannel();
576 | ByteBuffer byteBuffer = ByteBuffer.allocate((int) fc.size());
577 | while (true) {
578 | if (!((fc.read(byteBuffer)) > 0)) break;
579 | }
580 | return byteBuffer.array();
581 | } catch (IOException e) {
582 | e.printStackTrace();
583 | return null;
584 | } finally {
585 | CloseUtils.closeIO(fc);
586 | }
587 | }
588 |
589 | /**
590 | * 读取文件到字节数组中
591 | *
592 | * @param filePath 文件路径
593 | * @return 字符数组
594 | */
595 | public static byte[] readFile2BytesByMap(String filePath) {
596 | return readFile2BytesByMap(FileUtils.getFileByPath(filePath));
597 | }
598 |
599 | /**
600 | * 读取文件到字节数组中
601 | *
602 | * @param file 文件
603 | * @return 字符数组
604 | */
605 | public static byte[] readFile2BytesByMap(File file) {
606 | if (!FileUtils.isFileExists(file)) return null;
607 | FileChannel fc = null;
608 | try {
609 | fc = new RandomAccessFile(file, "r").getChannel();
610 | int size = (int) fc.size();
611 | MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, size).load();
612 | byte[] result = new byte[size];
613 | mbb.get(result, 0, size);
614 | return result;
615 | } catch (IOException e) {
616 | e.printStackTrace();
617 | return null;
618 | } finally {
619 | CloseUtils.closeIO(fc);
620 | }
621 | }
622 |
623 | private static boolean isSpace(String s) {
624 | if (s == null) return true;
625 | for (int i = 0, len = s.length(); i < len; ++i) {
626 | if (!Character.isWhitespace(s.charAt(i))) {
627 | return false;
628 | }
629 | }
630 | return true;
631 | }
632 | }
633 |
--------------------------------------------------------------------------------
/library/src/main/java/com/freelib/hybrid/file/FileUtils.java:
--------------------------------------------------------------------------------
1 | package com.freelib.hybrid.file;
2 |
3 |
4 | import android.annotation.SuppressLint;
5 |
6 | import java.io.BufferedInputStream;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.FileNotFoundException;
10 | import java.io.FilenameFilter;
11 | import java.io.IOException;
12 | import java.io.InputStream;
13 | import java.security.DigestInputStream;
14 | import java.security.MessageDigest;
15 | import java.security.NoSuchAlgorithmException;
16 | import java.util.ArrayList;
17 | import java.util.Collections;
18 | import java.util.List;
19 |
20 | /**
21 | * @author free46000 2017/05/26
22 | * @version v1.0
23 | */
24 | public final class FileUtils {
25 |
26 | private FileUtils() {
27 | throw new UnsupportedOperationException("u can't instantiate me...");
28 | }
29 |
30 | private static final String LINE_SEP = System.getProperty("line.separator");
31 |
32 | /**
33 | * 根据文件路径获取文件
34 | *
35 | * @param filePath 文件路径
36 | * @return 文件
37 | */
38 | public static File getFileByPath(String filePath) {
39 | return isSpace(filePath) ? null : new File(filePath);
40 | }
41 |
42 | /**
43 | * 判断文件是否存在
44 | *
45 | * @param filePath 文件路径
46 | * @return {@code true}: 存在
{@code false}: 不存在
47 | */
48 | public static boolean isFileExists(String filePath) {
49 | return isFileExists(getFileByPath(filePath));
50 | }
51 |
52 | /**
53 | * 判断文件是否存在
54 | *
55 | * @param file 文件
56 | * @return {@code true}: 存在
{@code false}: 不存在
57 | */
58 | public static boolean isFileExists(File file) {
59 | return file != null && file.exists();
60 | }
61 |
62 | /**
63 | * 重命名文件
64 | *
65 | * @param filePath 文件路径
66 | * @param newName 新名称
67 | * @return {@code true}: 重命名成功
{@code false}: 重命名失败
68 | */
69 | public static boolean rename(String filePath, String newName) {
70 | return rename(getFileByPath(filePath), newName);
71 | }
72 |
73 | /**
74 | * 重命名文件
75 | *
76 | * @param file 文件
77 | * @param newName 新名称
78 | * @return {@code true}: 重命名成功
{@code false}: 重命名失败
79 | */
80 | public static boolean rename(File file, String newName) {
81 | // 文件为空返回false
82 | if (file == null) return false;
83 | // 文件不存在返回false
84 | if (!file.exists()) return false;
85 | // 新的文件名为空返回false
86 | if (isSpace(newName)) return false;
87 | // 如果文件名没有改变返回true
88 | if (newName.equals(file.getName())) return true;
89 | File newFile = new File(file.getParent() + File.separator + newName);
90 | // 如果重命名的文件已存在返回false
91 | return !newFile.exists()
92 | && file.renameTo(newFile);
93 | }
94 |
95 | /**
96 | * 判断是否是目录
97 | *
98 | * @param dirPath 目录路径
99 | * @return {@code true}: 是
{@code false}: 否
100 | */
101 | public static boolean isDir(String dirPath) {
102 | return isDir(getFileByPath(dirPath));
103 | }
104 |
105 | /**
106 | * 判断是否是目录
107 | *
108 | * @param file 文件
109 | * @return {@code true}: 是
{@code false}: 否
110 | */
111 | public static boolean isDir(File file) {
112 | return isFileExists(file) && file.isDirectory();
113 | }
114 |
115 | /**
116 | * 判断是否是文件
117 | *
118 | * @param filePath 文件路径
119 | * @return {@code true}: 是
{@code false}: 否
120 | */
121 | public static boolean isFile(String filePath) {
122 | return isFile(getFileByPath(filePath));
123 | }
124 |
125 | /**
126 | * 判断是否是文件
127 | *
128 | * @param file 文件
129 | * @return {@code true}: 是
{@code false}: 否
130 | */
131 | public static boolean isFile(File file) {
132 | return isFileExists(file) && file.isFile();
133 | }
134 |
135 | /**
136 | * 判断目录是否存在,不存在则判断是否创建成功
137 | *
138 | * @param dirPath 目录路径
139 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败
140 | */
141 | public static boolean createOrExistsDir(String dirPath) {
142 | return createOrExistsDir(getFileByPath(dirPath));
143 | }
144 |
145 | /**
146 | * 判断目录是否存在,不存在则判断是否创建成功
147 | *
148 | * @param file 文件
149 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败
150 | */
151 | public static boolean createOrExistsDir(File file) {
152 | // 如果存在,是目录则返回true,是文件则返回false,不存在则返回是否创建成功
153 | return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
154 | }
155 |
156 | /**
157 | * 判断文件是否存在,不存在则判断是否创建成功
158 | *
159 | * @param filePath 文件路径
160 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败
161 | */
162 | public static boolean createOrExistsFile(String filePath) {
163 | return createOrExistsFile(getFileByPath(filePath));
164 | }
165 |
166 | /**
167 | * 判断文件是否存在,不存在则判断是否创建成功
168 | *
169 | * @param file 文件
170 | * @return {@code true}: 存在或创建成功
{@code false}: 不存在或创建失败
171 | */
172 | public static boolean createOrExistsFile(File file) {
173 | if (file == null) return false;
174 | // 如果存在,是文件则返回true,是目录则返回false
175 | if (file.exists()) return file.isFile();
176 | if (!createOrExistsDir(file.getParentFile())) return false;
177 | try {
178 | return file.createNewFile();
179 | } catch (IOException e) {
180 | e.printStackTrace();
181 | return false;
182 | }
183 | }
184 |
185 | /**
186 | * 判断文件是否存在,存在则在创建之前删除
187 | *
188 | * @param file 文件
189 | * @return {@code true}: 创建成功
{@code false}: 创建失败
190 | */
191 | public static boolean createFileByDeleteOldFile(File file) {
192 | if (file == null) return false;
193 | // 文件存在并且删除失败返回false
194 | if (file.exists() && file.isFile() && !file.delete()) return false;
195 | // 创建目录失败返回false
196 | if (!createOrExistsDir(file.getParentFile())) return false;
197 | try {
198 | return file.createNewFile();
199 | } catch (IOException e) {
200 | e.printStackTrace();
201 | return false;
202 | }
203 | }
204 |
205 | /**
206 | * 复制或移动目录
207 | *
208 | * @param srcDirPath 源目录路径
209 | * @param destDirPath 目标目录路径
210 | * @param isMove 是否移动
211 | * @return {@code true}: 复制或移动成功
{@code false}: 复制或移动失败
212 | */
213 | private static boolean copyOrMoveDir(String srcDirPath, String destDirPath, boolean isMove) {
214 | return copyOrMoveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), isMove);
215 | }
216 |
217 | /**
218 | * 复制或移动目录
219 | *
220 | * @param srcDir 源目录
221 | * @param destDir 目标目录
222 | * @param isMove 是否移动
223 | * @return {@code true}: 复制或移动成功
{@code false}: 复制或移动失败
224 | */
225 | private static boolean copyOrMoveDir(File srcDir, File destDir, boolean isMove) {
226 | if (srcDir == null || destDir == null) return false;
227 | // 如果目标目录在源目录中则返回false,看不懂的话好好想想递归怎么结束
228 | // srcPath : F:\\MyGithub\\AndroidUtilCode\\utilcode\\src\\test\\res
229 | // destPath: F:\\MyGithub\\AndroidUtilCode\\utilcode\\src\\test\\res1
230 | // 为防止以上这种情况出现出现误判,须分别在后面加个路径分隔符
231 | String srcPath = srcDir.getPath() + File.separator;
232 | String destPath = destDir.getPath() + File.separator;
233 | if (destPath.contains(srcPath)) return false;
234 | // 源文件不存在或者不是目录则返回false
235 | if (!srcDir.exists() || !srcDir.isDirectory()) return false;
236 | // 目标目录不存在返回false
237 | if (!createOrExistsDir(destDir)) return false;
238 | File[] files = srcDir.listFiles();
239 | for (File file : files) {
240 | File oneDestFile = new File(destPath + file.getName());
241 | if (file.isFile()) {
242 | // 如果操作失败返回false
243 | if (!copyOrMoveFile(file, oneDestFile, isMove)) return false;
244 | } else if (file.isDirectory()) {
245 | // 如果操作失败返回false
246 | if (!copyOrMoveDir(file, oneDestFile, isMove)) return false;
247 | }
248 | }
249 | return !isMove || deleteDir(srcDir);
250 | }
251 |
252 | /**
253 | * 复制或移动文件
254 | *
255 | * @param srcFilePath 源文件路径
256 | * @param destFilePath 目标文件路径
257 | * @param isMove 是否移动
258 | * @return {@code true}: 复制或移动成功
{@code false}: 复制或移动失败
259 | */
260 | private static boolean copyOrMoveFile(String srcFilePath, String destFilePath, boolean isMove) {
261 | return copyOrMoveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), isMove);
262 | }
263 |
264 | /**
265 | * 复制或移动文件
266 | *
267 | * @param srcFile 源文件
268 | * @param destFile 目标文件
269 | * @param isMove 是否移动
270 | * @return {@code true}: 复制或移动成功
{@code false}: 复制或移动失败
271 | */
272 | private static boolean copyOrMoveFile(File srcFile, File destFile, boolean isMove) {
273 | if (srcFile == null || destFile == null) return false;
274 | // 源文件不存在或者不是文件则返回false
275 | if (!srcFile.exists() || !srcFile.isFile()) return false;
276 | // 目标文件存在且是文件则返回false
277 | if (destFile.exists() && destFile.isFile()) return false;
278 | // 目标目录不存在返回false
279 | if (!createOrExistsDir(destFile.getParentFile())) return false;
280 | try {
281 | return FileIOUtils.writeFileFromIS(destFile, new FileInputStream(srcFile), false)
282 | && !(isMove && !deleteFile(srcFile));
283 | } catch (FileNotFoundException e) {
284 | e.printStackTrace();
285 | return false;
286 | }
287 | }
288 |
289 | /**
290 | * 复制目录
291 | *
292 | * @param srcDirPath 源目录路径
293 | * @param destDirPath 目标目录路径
294 | * @return {@code true}: 复制成功
{@code false}: 复制失败
295 | */
296 | public static boolean copyDir(String srcDirPath, String destDirPath) {
297 | return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath));
298 | }
299 |
300 | /**
301 | * 复制目录
302 | *
303 | * @param srcDir 源目录
304 | * @param destDir 目标目录
305 | * @return {@code true}: 复制成功
{@code false}: 复制失败
306 | */
307 | public static boolean copyDir(File srcDir, File destDir) {
308 | return copyOrMoveDir(srcDir, destDir, false);
309 | }
310 |
311 | /**
312 | * 复制文件
313 | *
314 | * @param srcFilePath 源文件路径
315 | * @param destFilePath 目标文件路径
316 | * @return {@code true}: 复制成功
{@code false}: 复制失败
317 | */
318 | public static boolean copyFile(String srcFilePath, String destFilePath) {
319 | return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath));
320 | }
321 |
322 | /**
323 | * 复制文件
324 | *
325 | * @param srcFile 源文件
326 | * @param destFile 目标文件
327 | * @return {@code true}: 复制成功
{@code false}: 复制失败
328 | */
329 | public static boolean copyFile(File srcFile, File destFile) {
330 | return copyOrMoveFile(srcFile, destFile, false);
331 | }
332 |
333 | /**
334 | * 移动目录
335 | *
336 | * @param srcDirPath 源目录路径
337 | * @param destDirPath 目标目录路径
338 | * @return {@code true}: 移动成功
{@code false}: 移动失败
339 | */
340 | public static boolean moveDir(String srcDirPath, String destDirPath) {
341 | return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath));
342 | }
343 |
344 | /**
345 | * 移动目录
346 | *
347 | * @param srcDir 源目录
348 | * @param destDir 目标目录
349 | * @return {@code true}: 移动成功
{@code false}: 移动失败
350 | */
351 | public static boolean moveDir(File srcDir, File destDir) {
352 | return copyOrMoveDir(srcDir, destDir, true);
353 | }
354 |
355 | /**
356 | * 移动文件
357 | *
358 | * @param srcFilePath 源文件路径
359 | * @param destFilePath 目标文件路径
360 | * @return {@code true}: 移动成功
{@code false}: 移动失败
361 | */
362 | public static boolean moveFile(String srcFilePath, String destFilePath) {
363 | return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath));
364 | }
365 |
366 | /**
367 | * 移动文件
368 | *
369 | * @param srcFile 源文件
370 | * @param destFile 目标文件
371 | * @return {@code true}: 移动成功
{@code false}: 移动失败
372 | */
373 | public static boolean moveFile(File srcFile, File destFile) {
374 | return copyOrMoveFile(srcFile, destFile, true);
375 | }
376 |
377 | /**
378 | * 删除目录
379 | *
380 | * @param dirPath 目录路径
381 | * @return {@code true}: 删除成功
{@code false}: 删除失败
382 | */
383 | public static boolean deleteDir(String dirPath) {
384 | return deleteDir(getFileByPath(dirPath));
385 | }
386 |
387 | /**
388 | * 删除目录
389 | *
390 | * @param dir 目录
391 | * @return {@code true}: 删除成功
{@code false}: 删除失败
392 | */
393 | public static boolean deleteDir(File dir) {
394 | if (dir == null) return false;
395 | // 目录不存在返回true
396 | if (!dir.exists()) return true;
397 | // 不是目录返回false
398 | if (!dir.isDirectory()) return false;
399 | // 现在文件存在且是文件夹
400 | File[] files = dir.listFiles();
401 | if (files != null && files.length != 0) {
402 | for (File file : files) {
403 | if (file.isFile()) {
404 | if (!deleteFile(file)) return false;
405 | } else if (file.isDirectory()) {
406 | if (!deleteDir(file)) return false;
407 | }
408 | }
409 | }
410 | return dir.delete();
411 | }
412 |
413 | /**
414 | * 删除文件
415 | *
416 | * @param srcFilePath 文件路径
417 | * @return {@code true}: 删除成功
{@code false}: 删除失败
418 | */
419 | public static boolean deleteFile(String srcFilePath) {
420 | return deleteFile(getFileByPath(srcFilePath));
421 | }
422 |
423 | /**
424 | * 删除文件
425 | *
426 | * @param file 文件
427 | * @return {@code true}: 删除成功
{@code false}: 删除失败
428 | */
429 | public static boolean deleteFile(File file) {
430 | return file != null && (!file.exists() || file.isFile() && file.delete());
431 | }
432 |
433 | /**
434 | * 删除目录下的所有文件
435 | *
436 | * @param dirPath 目录路径
437 | * @return {@code true}: 删除成功
{@code false}: 删除失败
438 | */
439 | public static boolean deleteFilesInDir(String dirPath) {
440 | return deleteFilesInDir(getFileByPath(dirPath));
441 | }
442 |
443 | /**
444 | * 删除目录下的所有文件
445 | *
446 | * @param dir 目录
447 | * @return {@code true}: 删除成功
{@code false}: 删除失败
448 | */
449 | public static boolean deleteFilesInDir(File dir) {
450 | if (dir == null) return false;
451 | // 目录不存在返回true
452 | if (!dir.exists()) return true;
453 | // 不是目录返回false
454 | if (!dir.isDirectory()) return false;
455 | // 现在文件存在且是文件夹
456 | File[] files = dir.listFiles();
457 | if (files != null && files.length != 0) {
458 | for (File file : files) {
459 | if (file.isFile()) {
460 | if (!deleteFile(file)) return false;
461 | } else if (file.isDirectory()) {
462 | if (!deleteDir(file)) return false;
463 | }
464 | }
465 | }
466 | return true;
467 | }
468 |
469 | /**
470 | * 获取目录下所有文件
471 | *
472 | * @param dirPath 目录路径
473 | * @param isRecursive 是否递归进子目录
474 | * @return 文件链表
475 | */
476 | public static List listFilesInDir(String dirPath, boolean isRecursive) {
477 | return listFilesInDir(getFileByPath(dirPath), isRecursive);
478 | }
479 |
480 | /**
481 | * 获取目录下所有文件
482 | *
483 | * @param dir 目录
484 | * @param isRecursive 是否递归进子目录
485 | * @return 文件链表
486 | */
487 | public static List listFilesInDir(File dir, boolean isRecursive) {
488 | if (!isDir(dir)) return null;
489 | if (isRecursive) return listFilesInDir(dir);
490 | List list = new ArrayList<>();
491 | File[] files = dir.listFiles();
492 | if (files != null && files.length != 0) {
493 | Collections.addAll(list, files);
494 | }
495 | return list;
496 | }
497 |
498 | /**
499 | * 获取目录下所有文件包括子目录
500 | *
501 | * @param dirPath 目录路径
502 | * @return 文件链表
503 | */
504 | public static List listFilesInDir(String dirPath) {
505 | return listFilesInDir(getFileByPath(dirPath));
506 | }
507 |
508 | /**
509 | * 获取目录下所有文件包括子目录
510 | *
511 | * @param dir 目录
512 | * @return 文件链表
513 | */
514 | public static List listFilesInDir(File dir) {
515 | if (!isDir(dir)) return null;
516 | List list = new ArrayList<>();
517 | File[] files = dir.listFiles();
518 | if (files != null && files.length != 0) {
519 | for (File file : files) {
520 | list.add(file);
521 | if (file.isDirectory()) {
522 | List fileList = listFilesInDir(file);
523 | if (fileList != null) {
524 | list.addAll(fileList);
525 | }
526 | }
527 | }
528 | }
529 | return list;
530 | }
531 |
532 | /**
533 | * 获取目录下所有后缀名为suffix的文件
534 | * 大小写忽略
535 | *
536 | * @param dirPath 目录路径
537 | * @param suffix 后缀名
538 | * @param isRecursive 是否递归进子目录
539 | * @return 文件链表
540 | */
541 | public static List listFilesInDirWithFilter(String dirPath, String suffix, boolean isRecursive) {
542 | return listFilesInDirWithFilter(getFileByPath(dirPath), suffix, isRecursive);
543 | }
544 |
545 | /**
546 | * 获取目录下所有后缀名为suffix的文件
547 | * 大小写忽略
548 | *
549 | * @param dir 目录
550 | * @param suffix 后缀名
551 | * @param isRecursive 是否递归进子目录
552 | * @return 文件链表
553 | */
554 | public static List listFilesInDirWithFilter(File dir, String suffix, boolean isRecursive) {
555 | if (isRecursive) return listFilesInDirWithFilter(dir, suffix);
556 | if (dir == null || !isDir(dir)) return null;
557 | List list = new ArrayList<>();
558 | File[] files = dir.listFiles();
559 | if (files != null && files.length != 0) {
560 | for (File file : files) {
561 | if (file.getName().toUpperCase().endsWith(suffix.toUpperCase())) {
562 | list.add(file);
563 | }
564 | }
565 | }
566 | return list;
567 | }
568 |
569 | /**
570 | * 获取目录下所有后缀名为suffix的文件包括子目录
571 | * 大小写忽略
572 | *
573 | * @param dirPath 目录路径
574 | * @param suffix 后缀名
575 | * @return 文件链表
576 | */
577 | public static List listFilesInDirWithFilter(String dirPath, String suffix) {
578 | return listFilesInDirWithFilter(getFileByPath(dirPath), suffix);
579 | }
580 |
581 | /**
582 | * 获取目录下所有后缀名为suffix的文件包括子目录
583 | * 大小写忽略
584 | *
585 | * @param dir 目录
586 | * @param suffix 后缀名
587 | * @return 文件链表
588 | */
589 | public static List listFilesInDirWithFilter(File dir, String suffix) {
590 | if (dir == null || !isDir(dir)) return null;
591 | List list = new ArrayList<>();
592 | File[] files = dir.listFiles();
593 | if (files != null && files.length != 0) {
594 | for (File file : files) {
595 | if (file.getName().toUpperCase().endsWith(suffix.toUpperCase())) {
596 | list.add(file);
597 | }
598 | if (file.isDirectory()) {
599 | list.addAll(listFilesInDirWithFilter(file, suffix));
600 | }
601 | }
602 | }
603 | return list;
604 | }
605 |
606 | /**
607 | * 获取目录下所有符合filter的文件
608 | *
609 | * @param dirPath 目录路径
610 | * @param filter 过滤器
611 | * @param isRecursive 是否递归进子目录
612 | * @return 文件链表
613 | */
614 | public static List listFilesInDirWithFilter(String dirPath, FilenameFilter filter, boolean isRecursive) {
615 | return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive);
616 | }
617 |
618 | /**
619 | * 获取目录下所有符合filter的文件
620 | *
621 | * @param dir 目录
622 | * @param filter 过滤器
623 | * @param isRecursive 是否递归进子目录
624 | * @return 文件链表
625 | */
626 | public static List listFilesInDirWithFilter(File dir, FilenameFilter filter, boolean isRecursive) {
627 | if (isRecursive) return listFilesInDirWithFilter(dir, filter);
628 | if (dir == null || !isDir(dir)) return null;
629 | List list = new ArrayList<>();
630 | File[] files = dir.listFiles();
631 | if (files != null && files.length != 0) {
632 | for (File file : files) {
633 | if (filter.accept(file.getParentFile(), file.getName())) {
634 | list.add(file);
635 | }
636 | }
637 | }
638 | return list;
639 | }
640 |
641 | /**
642 | * 获取目录下所有符合filter的文件包括子目录
643 | *
644 | * @param dirPath 目录路径
645 | * @param filter 过滤器
646 | * @return 文件链表
647 | */
648 | public static List listFilesInDirWithFilter(String dirPath, FilenameFilter filter) {
649 | return listFilesInDirWithFilter(getFileByPath(dirPath), filter);
650 | }
651 |
652 | /**
653 | * 获取目录下所有符合filter的文件包括子目录
654 | *
655 | * @param dir 目录
656 | * @param filter 过滤器
657 | * @return 文件链表
658 | */
659 | public static List listFilesInDirWithFilter(File dir, FilenameFilter filter) {
660 | if (dir == null || !isDir(dir)) return null;
661 | List list = new ArrayList<>();
662 | File[] files = dir.listFiles();
663 | if (files != null && files.length != 0) {
664 | for (File file : files) {
665 | if (filter.accept(file.getParentFile(), file.getName())) {
666 | list.add(file);
667 | }
668 | if (file.isDirectory()) {
669 | list.addAll(listFilesInDirWithFilter(file, filter));
670 | }
671 | }
672 | }
673 | return list;
674 | }
675 |
676 | /**
677 | * 获取目录下指定文件名的文件包括子目录
678 | * 大小写忽略
679 | *
680 | * @param dirPath 目录路径
681 | * @param fileName 文件名
682 | * @return 文件链表
683 | */
684 | public static List searchFileInDir(String dirPath, String fileName) {
685 | return searchFileInDir(getFileByPath(dirPath), fileName);
686 | }
687 |
688 | /**
689 | * 获取目录下指定文件名的文件包括子目录
690 | * 大小写忽略
691 | *
692 | * @param dir 目录
693 | * @param fileName 文件名
694 | * @return 文件链表
695 | */
696 | public static List searchFileInDir(File dir, String fileName) {
697 | if (dir == null || !isDir(dir)) return null;
698 | List list = new ArrayList<>();
699 | File[] files = dir.listFiles();
700 | if (files != null && files.length != 0) {
701 | for (File file : files) {
702 | if (file.getName().toUpperCase().equals(fileName.toUpperCase())) {
703 | list.add(file);
704 | }
705 | if (file.isDirectory()) {
706 | list.addAll(searchFileInDir(file, fileName));
707 | }
708 | }
709 | }
710 | return list;
711 | }
712 |
713 | /**
714 | * 获取文件最后修改的毫秒时间戳
715 | *
716 | * @param filePath 文件路径
717 | * @return 文件最后修改的毫秒时间戳
718 | */
719 | public static long getFileLastModified(String filePath) {
720 | return getFileLastModified(getFileByPath(filePath));
721 | }
722 |
723 | /**
724 | * 获取文件最后修改的毫秒时间戳
725 | *
726 | * @param file 文件
727 | * @return 文件最后修改的毫秒时间戳
728 | */
729 | public static long getFileLastModified(File file) {
730 | if (file == null) return -1;
731 | return file.lastModified();
732 | }
733 |
734 | /**
735 | * 简单获取文件编码格式
736 | *
737 | * @param filePath 文件路径
738 | * @return 文件编码
739 | */
740 | public static String getFileCharsetSimple(String filePath) {
741 | return getFileCharsetSimple(getFileByPath(filePath));
742 | }
743 |
744 | /**
745 | * 简单获取文件编码格式
746 | *
747 | * @param file 文件
748 | * @return 文件编码
749 | */
750 | public static String getFileCharsetSimple(File file) {
751 | int p = 0;
752 | InputStream is = null;
753 | try {
754 | is = new BufferedInputStream(new FileInputStream(file));
755 | p = (is.read() << 8) + is.read();
756 | } catch (IOException e) {
757 | e.printStackTrace();
758 | } finally {
759 | CloseUtils.closeIO(is);
760 | }
761 | switch (p) {
762 | case 0xefbb:
763 | return "UTF-8";
764 | case 0xfffe:
765 | return "Unicode";
766 | case 0xfeff:
767 | return "UTF-16BE";
768 | default:
769 | return "GBK";
770 | }
771 | }
772 |
773 | /**
774 | * 获取文件行数
775 | *
776 | * @param filePath 文件路径
777 | * @return 文件行数
778 | */
779 | public static int getFileLines(String filePath) {
780 | return getFileLines(getFileByPath(filePath));
781 | }
782 |
783 | /**
784 | * 获取文件行数
785 | * 比readLine要快很多
786 | *
787 | * @param file 文件
788 | * @return 文件行数
789 | */
790 | public static int getFileLines(File file) {
791 | int count = 1;
792 | InputStream is = null;
793 | try {
794 | is = new BufferedInputStream(new FileInputStream(file));
795 | byte[] buffer = new byte[1024];
796 | int readChars;
797 | if (LINE_SEP.endsWith("\n")) {
798 | while ((readChars = is.read(buffer, 0, 1024)) != -1) {
799 | for (int i = 0; i < readChars; ++i) {
800 | if (buffer[i] == '\n') ++count;
801 | }
802 | }
803 | } else {
804 | while ((readChars = is.read(buffer, 0, 1024)) != -1) {
805 | for (int i = 0; i < readChars; ++i) {
806 | if (buffer[i] == '\r') ++count;
807 | }
808 | }
809 | }
810 | } catch (IOException e) {
811 | e.printStackTrace();
812 | } finally {
813 | CloseUtils.closeIO(is);
814 | }
815 | return count;
816 | }
817 |
818 | /**
819 | * 获取目录大小
820 | *
821 | * @param dirPath 目录路径
822 | * @return 文件大小
823 | */
824 | public static String getDirSize(String dirPath) {
825 | return getDirSize(getFileByPath(dirPath));
826 | }
827 |
828 | /**
829 | * 获取目录大小
830 | *
831 | * @param dir 目录
832 | * @return 文件大小
833 | */
834 | public static String getDirSize(File dir) {
835 | long len = getDirLength(dir);
836 | return len == -1 ? "" : byte2FitMemorySize(len);
837 | }
838 |
839 | /**
840 | * 获取文件大小
841 | *
842 | * @param filePath 文件路径
843 | * @return 文件大小
844 | */
845 | public static String getFileSize(String filePath) {
846 | return getFileSize(getFileByPath(filePath));
847 | }
848 |
849 | /**
850 | * 获取文件大小
851 | *
852 | * @param file 文件
853 | * @return 文件大小
854 | */
855 | public static String getFileSize(File file) {
856 | long len = getFileLength(file);
857 | return len == -1 ? "" : byte2FitMemorySize(len);
858 | }
859 |
860 | /**
861 | * 获取目录长度
862 | *
863 | * @param dirPath 目录路径
864 | * @return 目录长度
865 | */
866 | public static long getDirLength(String dirPath) {
867 | return getDirLength(getFileByPath(dirPath));
868 | }
869 |
870 | /**
871 | * 获取目录长度
872 | *
873 | * @param dir 目录
874 | * @return 目录长度
875 | */
876 | public static long getDirLength(File dir) {
877 | if (!isDir(dir)) return -1;
878 | long len = 0;
879 | File[] files = dir.listFiles();
880 | if (files != null && files.length != 0) {
881 | for (File file : files) {
882 | if (file.isDirectory()) {
883 | len += getDirLength(file);
884 | } else {
885 | len += file.length();
886 | }
887 | }
888 | }
889 | return len;
890 | }
891 |
892 | /**
893 | * 获取文件长度
894 | *
895 | * @param filePath 文件路径
896 | * @return 文件长度
897 | */
898 | public static long getFileLength(String filePath) {
899 | return getFileLength(getFileByPath(filePath));
900 | }
901 |
902 | /**
903 | * 获取文件长度
904 | *
905 | * @param file 文件
906 | * @return 文件长度
907 | */
908 | public static long getFileLength(File file) {
909 | if (!isFile(file)) return -1;
910 | return file.length();
911 | }
912 |
913 | /**
914 | * 获取文件的MD5校验码
915 | *
916 | * @param filePath 文件路径
917 | * @return 文件的MD5校验码
918 | */
919 | public static String getFileMD5ToString(String filePath) {
920 | File file = isSpace(filePath) ? null : new File(filePath);
921 | return getFileMD5ToString(file);
922 | }
923 |
924 | /**
925 | * 获取文件的MD5校验码
926 | *
927 | * @param filePath 文件路径
928 | * @return 文件的MD5校验码
929 | */
930 | public static byte[] getFileMD5(String filePath) {
931 | File file = isSpace(filePath) ? null : new File(filePath);
932 | return getFileMD5(file);
933 | }
934 |
935 | /**
936 | * 获取文件的MD5校验码
937 | *
938 | * @param file 文件
939 | * @return 文件的MD5校验码
940 | */
941 | public static String getFileMD5ToString(File file) {
942 | return bytes2HexString(getFileMD5(file));
943 | }
944 |
945 | /**
946 | * 获取文件的MD5校验码
947 | *
948 | * @param file 文件
949 | * @return 文件的MD5校验码
950 | */
951 | public static byte[] getFileMD5(File file) {
952 | if (file == null) return null;
953 | DigestInputStream dis = null;
954 | try {
955 | FileInputStream fis = new FileInputStream(file);
956 | MessageDigest md = MessageDigest.getInstance("MD5");
957 | dis = new DigestInputStream(fis, md);
958 | byte[] buffer = new byte[1024 * 256];
959 | while (true) {
960 | if (!(dis.read(buffer) > 0)) break;
961 | }
962 | md = dis.getMessageDigest();
963 | return md.digest();
964 | } catch (NoSuchAlgorithmException | IOException e) {
965 | e.printStackTrace();
966 | } finally {
967 | CloseUtils.closeIO(dis);
968 | }
969 | return null;
970 | }
971 |
972 | /**
973 | * 获取全路径中的最长目录
974 | *
975 | * @param file 文件
976 | * @return filePath最长目录
977 | */
978 | public static String getDirName(File file) {
979 | if (file == null) return null;
980 | return getDirName(file.getPath());
981 | }
982 |
983 | /**
984 | * 获取全路径中的最长目录
985 | *
986 | * @param filePath 文件路径
987 | * @return filePath最长目录
988 | */
989 | public static String getDirName(String filePath) {
990 | if (isSpace(filePath)) return filePath;
991 | int lastSep = filePath.lastIndexOf(File.separator);
992 | return lastSep == -1 ? "" : filePath.substring(0, lastSep + 1);
993 | }
994 |
995 | /**
996 | * 获取全路径中的文件名
997 | *
998 | * @param file 文件
999 | * @return 文件名
1000 | */
1001 | public static String getFileName(File file) {
1002 | if (file == null) return null;
1003 | return getFileName(file.getPath());
1004 | }
1005 |
1006 | /**
1007 | * 获取全路径中的文件名
1008 | *
1009 | * @param filePath 文件路径
1010 | * @return 文件名
1011 | */
1012 | public static String getFileName(String filePath) {
1013 | if (isSpace(filePath)) return filePath;
1014 | int lastSep = filePath.lastIndexOf(File.separator);
1015 | return lastSep == -1 ? filePath : filePath.substring(lastSep + 1);
1016 | }
1017 |
1018 | /**
1019 | * 获取全路径中的不带拓展名的文件名
1020 | *
1021 | * @param file 文件
1022 | * @return 不带拓展名的文件名
1023 | */
1024 | public static String getFileNameNoExtension(File file) {
1025 | if (file == null) return null;
1026 | return getFileNameNoExtension(file.getPath());
1027 | }
1028 |
1029 | /**
1030 | * 获取全路径中的不带拓展名的文件名
1031 | *
1032 | * @param filePath 文件路径
1033 | * @return 不带拓展名的文件名
1034 | */
1035 | public static String getFileNameNoExtension(String filePath) {
1036 | if (isSpace(filePath)) return filePath;
1037 | int lastPoi = filePath.lastIndexOf('.');
1038 | int lastSep = filePath.lastIndexOf(File.separator);
1039 | if (lastSep == -1) {
1040 | return (lastPoi == -1 ? filePath : filePath.substring(0, lastPoi));
1041 | }
1042 | if (lastPoi == -1 || lastSep > lastPoi) {
1043 | return filePath.substring(lastSep + 1);
1044 | }
1045 | return filePath.substring(lastSep + 1, lastPoi);
1046 | }
1047 |
1048 | /**
1049 | * 获取全路径中的文件拓展名
1050 | *
1051 | * @param file 文件
1052 | * @return 文件拓展名
1053 | */
1054 | public static String getFileExtension(File file) {
1055 | if (file == null) return null;
1056 | return getFileExtension(file.getPath());
1057 | }
1058 |
1059 | /**
1060 | * 获取全路径中的文件拓展名
1061 | *
1062 | * @param filePath 文件路径
1063 | * @return 文件拓展名
1064 | */
1065 | public static String getFileExtension(String filePath) {
1066 | if (isSpace(filePath)) return filePath;
1067 | int lastPoi = filePath.lastIndexOf('.');
1068 | int lastSep = filePath.lastIndexOf(File.separator);
1069 | if (lastPoi == -1 || lastSep >= lastPoi) return "";
1070 | return filePath.substring(lastPoi + 1);
1071 | }
1072 |
1073 | ///////////////////////////////////////////////////////////////////////////
1074 | // copy from ConvertUtils
1075 | ///////////////////////////////////////////////////////////////////////////
1076 |
1077 | private static final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
1078 |
1079 | /**
1080 | * byteArr转hexString
1081 | * 例如:
1082 | * bytes2HexString(new byte[] { 0, (byte) 0xa8 }) returns 00A8
1083 | *
1084 | * @param bytes 字节数组
1085 | * @return 16进制大写字符串
1086 | */
1087 | private static String bytes2HexString(byte[] bytes) {
1088 | if (bytes == null) return null;
1089 | int len = bytes.length;
1090 | if (len <= 0) return null;
1091 | char[] ret = new char[len << 1];
1092 | for (int i = 0, j = 0; i < len; i++) {
1093 | ret[j++] = hexDigits[bytes[i] >>> 4 & 0x0f];
1094 | ret[j++] = hexDigits[bytes[i] & 0x0f];
1095 | }
1096 | return new String(ret);
1097 | }
1098 |
1099 | /**
1100 | * 字节数转合适内存大小
1101 | * 保留3位小数
1102 | *
1103 | * @param byteNum 字节数
1104 | * @return 合适内存大小
1105 | */
1106 | @SuppressLint("DefaultLocale")
1107 | private static String byte2FitMemorySize(long byteNum) {
1108 | if (byteNum < 0) {
1109 | return "shouldn't be less than zero!";
1110 | } else if (byteNum < 1024) {
1111 | return String.format("%.3fB", (double) byteNum + 0.0005);
1112 | } else if (byteNum < 1024) {
1113 | return String.format("%.3fKB", (double) byteNum / 1024 + 0.0005);
1114 | } else if (byteNum < 1073741824) {
1115 | return String.format("%.3fMB", (double) byteNum / 1048576 + 0.0005);
1116 | } else {
1117 | return String.format("%.3fGB", (double) byteNum / 1073741824 + 0.0005);
1118 | }
1119 | }
1120 |
1121 | private static boolean isSpace(String s) {
1122 | if (s == null) return true;
1123 | for (int i = 0, len = s.length(); i < len; ++i) {
1124 | if (!Character.isWhitespace(s.charAt(i))) {
1125 | return false;
1126 | }
1127 | }
1128 | return true;
1129 | }
1130 | }
1131 |
--------------------------------------------------------------------------------