diff --git a/app/build.gradle b/app/build.gradle index 14165a485..97ffcd3eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,9 +89,9 @@ android { dimension "mode" applicationId "io.legado.cronet" manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"] - ndk { - abiFilters 'arm64-v8a' - } +// ndk { +// abiFilters 'arm64-v8a','armeabi-v7a' +// } } } compileOptions { @@ -123,7 +123,6 @@ kapt { dependencies { coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5') - implementation(fileTree(dir: 'libs', include: ['*.jar', '*.aar'])) testImplementation('junit:junit:4.13.2') androidTestImplementation('androidx.test:runner:1.4.0') androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0') @@ -188,10 +187,14 @@ dependencies { //网络 implementation('com.squareup.okhttp3:okhttp:4.9.1') def cronet_version = '92.0.4509.1' - compileOnly("org.microg:cronet-api:$cronet_version") - cronetImplementation("org.microg:cronet-native:$cronet_version") - cronetImplementation("org.microg:cronet-api:$cronet_version") - cronetImplementation("org.microg:cronet-common:$cronet_version") + //compileOnly("org.microg:cronet-api:$cronet_version") + compileOnly(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar'])) + cronetImplementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar'])) + //cronetImplementation("org.microg:cronet-native:$cronet_version") + //cronetImplementation("org.microg:cronet-api:$cronet_version") + //cronetImplementation("org.microg:cronet-common:$cronet_version") + //cronetImplementation('com.huawei.hms:hquic-provider:5.0.1.300') {exclude( group :"org.chromium.net")} + //cronetImplementation('com.google.android.gms:play-services-cronet:17.0.1') //Glide implementation('com.github.bumptech.glide:glide:4.12.0') diff --git a/app/cronetlib/cronet_api.jar b/app/cronetlib/cronet_api.jar new file mode 100644 index 000000000..d62e993bb Binary files /dev/null and b/app/cronetlib/cronet_api.jar differ diff --git a/app/cronetlib/cronet_impl_common_java.jar b/app/cronetlib/cronet_impl_common_java.jar new file mode 100644 index 000000000..f851e7120 Binary files /dev/null and b/app/cronetlib/cronet_impl_common_java.jar differ diff --git a/app/cronetlib/cronet_impl_native_java.jar b/app/cronetlib/cronet_impl_native_java.jar new file mode 100644 index 000000000..1a29039ad Binary files /dev/null and b/app/cronetlib/cronet_impl_native_java.jar differ diff --git a/app/cronetlib/cronet_impl_platform_java.jar b/app/cronetlib/cronet_impl_platform_java.jar new file mode 100644 index 000000000..6adb23853 Binary files /dev/null and b/app/cronetlib/cronet_impl_platform_java.jar differ diff --git a/app/cronetlib/src/cronet_api-src.jar b/app/cronetlib/src/cronet_api-src.jar new file mode 100644 index 000000000..78bf99a0e Binary files /dev/null and b/app/cronetlib/src/cronet_api-src.jar differ diff --git a/app/cronetlib/src/cronet_impl_common_java-src.jar b/app/cronetlib/src/cronet_impl_common_java-src.jar new file mode 100644 index 000000000..7d7da6f33 Binary files /dev/null and b/app/cronetlib/src/cronet_impl_common_java-src.jar differ diff --git a/app/cronetlib/src/cronet_impl_native_java-src.jar b/app/cronetlib/src/cronet_impl_native_java-src.jar new file mode 100644 index 000000000..83a357a91 Binary files /dev/null and b/app/cronetlib/src/cronet_impl_native_java-src.jar differ diff --git a/app/cronetlib/src/cronet_impl_platform_java-src.jar b/app/cronetlib/src/cronet_impl_platform_java-src.jar new file mode 100644 index 000000000..48c544ef7 Binary files /dev/null and b/app/cronetlib/src/cronet_impl_platform_java-src.jar differ diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index 6908c5715..0f277f010 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -14,6 +14,7 @@ import io.legado.app.help.AppConfig import io.legado.app.help.CrashHandler import io.legado.app.help.LifecycleHelp import io.legado.app.help.ThemeConfig.applyDayNight +import io.legado.app.help.http.cronet.CronetLoader import io.legado.app.utils.LanguageUtils import io.legado.app.utils.defaultSharedPreferences @@ -22,6 +23,11 @@ class App : MultiDexApplication() { override fun onCreate() { super.onCreate() CrashHandler(this) + if (AppConfig.isCronet) { + //预下载Cronet so + CronetLoader.getInstance(this).preDownload() + } + LanguageUtils.setConfiguration(this) createNotificationChannels() applyDayNight(this) diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index 0521136fd..4e1864b02 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -2,11 +2,13 @@ package io.legado.app.help.http import io.legado.app.help.AppConfig import io.legado.app.help.http.cronet.CronetInterceptor +import io.legado.app.help.http.cronet.CronetLoader import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.ConnectionSpec import okhttp3.Credentials import okhttp3.Interceptor import okhttp3.OkHttpClient +import splitties.init.appCtx import java.net.InetSocketAddress import java.net.Proxy import java.util.concurrent.ConcurrentHashMap @@ -43,7 +45,7 @@ val okHttpClient: OkHttpClient by lazy { .build() chain.proceed(request) }) - if (AppConfig.isCronet) { + if (AppConfig.isCronet&&CronetLoader.getInstance(appCtx).install()) { builder.addInterceptor(CronetInterceptor()) } diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt index e47a4abfa..60994332f 100644 --- a/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt @@ -9,7 +9,9 @@ import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK import org.chromium.net.ExperimentalCronetEngine import org.chromium.net.UploadDataProviders import org.chromium.net.UrlRequest +import org.chromium.net.urlconnection.CronetURLStreamHandlerFactory import splitties.init.appCtx +import java.net.URL import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -17,16 +19,58 @@ import java.util.concurrent.Executors val executor: Executor by lazy { Executors.newSingleThreadExecutor() } val cronetEngine: ExperimentalCronetEngine by lazy { + CronetLoader.getInstance(appCtx).preDownload() val builder = ExperimentalCronetEngine.Builder(appCtx) + //设置自定义so库加载 + .setLibraryLoader(CronetLoader.getInstance(appCtx)) + //设置缓存路径 .setStoragePath(appCtx.externalCacheDir?.absolutePath) + //设置缓存模式 .enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50)) + //设置支持http/3 .enableQuic(true) - .enablePublicKeyPinningBypassForLocalTrustAnchors(true) + //设置支持http/2 .enableHttp2(true) + .enablePublicKeyPinningBypassForLocalTrustAnchors(true) + //.enableNetworkQualityEstimator(true) + //Brotli压缩 builder.enableBrotli(true) - return@lazy builder.build() + //builder.setExperimentalOptions("{\"quic_version\": \"h3-29\"}") + val engine = builder.build() + URL.setURLStreamHandlerFactory(CronetURLStreamHandlerFactory(engine)) +// engine.addRequestFinishedListener(object : RequestFinishedInfo.Listener(executor) { +// override fun onRequestFinished(requestFinishedInfo: RequestFinishedInfo?) { +// val sb = StringBuilder(requestFinishedInfo!!.url).append("\r\n") +// +// try { +// if (requestFinishedInfo.responseInfo != null) { +// val responseInfo = requestFinishedInfo.responseInfo +// if (responseInfo != null) { +// sb.append("[Cached:").append(responseInfo.wasCached()) +// .append("][StatusCode:") +// .append( +// responseInfo.httpStatusCode +// ).append("][StatusText:").append(responseInfo.httpStatusText) +// .append("][Protocol:").append(responseInfo.negotiatedProtocol) +// .append("][ByteCount:").append( +// responseInfo.receivedByteCount +// ).append("]\r\n") +// } +// val httpHeaders = requestFinishedInfo.responseInfo!! +// .allHeadersAsList +// for ((key, value) in httpHeaders) { +// sb.append("[").append(key).append("]").append(value).append("\r\n") +// } +// Log.e("Cronet", sb.toString()) +// } +// } catch (e: URISyntaxException) { +// e.printStackTrace() +// } +// } +// }) + return@lazy engine } diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.java b/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.java new file mode 100644 index 000000000..77d8ffc5f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.java @@ -0,0 +1,350 @@ +package io.legado.app.help.http.cronet; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import org.chromium.net.CronetEngine; +import org.chromium.net.impl.ImplVersion; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class CronetLoader extends CronetEngine.Builder.LibraryLoader { + //https://storage.googleapis.com/chromium-cronet/android/92.0.4515.127/Release/cronet/libs/arm64-v8a/libcronet.92.0.4515.127.so + //https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@92.0.4515.127/cronet/92.0.4515.127/arm64-v8a/libcronet.92.0.4515.127.so.js + private final String soName = "libcronet." + ImplVersion.getCronetVersion() + ".so"; + private final String soUrl = "https://storage.googleapis.com/chromium-cronet/android/" + ImplVersion.getCronetVersion() + "/Release/cronet/libs/" + getCpuAbi() + "/" + soName; + private final String md5Url = "https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@" + ImplVersion.getCronetVersion() + "/cronet/" + ImplVersion.getCronetVersion() + "/" + getCpuAbi() + "/" + soName + ".js"; + private final File soFile; + private final File downloadFile; + private static final String TAG = "CronetLoader"; + + private static CronetLoader instance; + + public static CronetLoader getInstance(Context context) { + if (mContext == null) { + mContext = context; + } + if (instance == null) { + synchronized (CronetLoader.class) { + if (instance == null) { + instance = new CronetLoader(mContext); + } + } + } + return instance; + } + + private static Context mContext; + + CronetLoader(Context context) { + mContext = context.getApplicationContext(); + File dir = mContext.getDir("lib", Context.MODE_PRIVATE); + soFile = new File(dir + "/" + getCpuAbi(), soName); + downloadFile = new File(mContext.getCacheDir() + "/so_download", soName); + + + } + + public boolean install() { + return soFile.exists(); + } + + public void preDownload() { + new Thread(() -> { + String md5 = getUrlMd5(md5Url); + download(soUrl, md5, downloadFile, soFile); + Log.e(TAG, soName); + }).start(); + + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + @Override + public void loadLibrary(String libName) { + Log.e(TAG, "libName:" + libName); + long start = System.currentTimeMillis(); + try { + //非cronet的so调用系统方法加载 + if (!libName.contains("cronet")) { + System.loadLibrary(libName); + return; + } + //以下逻辑为cronet加载,优先加载本地,否则从远程加载 + //首先调用系统行为进行加载 + System.loadLibrary(libName); + Log.e(TAG, "load from system"); + + } catch (Throwable e) { + //如果找不到,则从远程下载 + + + //删除历史文件 + deleteHistoryFile(Objects.requireNonNull(soFile.getParentFile()), soFile); + + Log.e(TAG, "soUrl:" + soUrl); + String md5 = getUrlMd5(md5Url); + Log.e(TAG, "soMD5:" + md5); + Log.e(TAG, "soName+:" + soName); + Log.e(TAG, "destSuccessFile:" + soFile); + Log.e(TAG, "tempFile:" + downloadFile); + + + if (md5 == null || md5.length() != 32 || soUrl.length() == 0) { + //如果md5或下载的url为空,则调用系统行为进行加载 + System.loadLibrary(libName); + return; + } + + + if (!soFile.exists() || !soFile.isFile()) { + //noinspection ResultOfMethodCallIgnored + soFile.delete(); + download(soUrl, md5, downloadFile, soFile); + //如果文件不存在或不是文件,则调用系统行为进行加载 + System.loadLibrary(libName); + return; + } + + if (soFile.exists()) { + //如果文件存在,则校验md5值 + String fileMD5 = getFileMD5(soFile); + if (fileMD5 != null && fileMD5.equalsIgnoreCase(md5)) { + //md5值一样,则加载 + System.load(soFile.getAbsolutePath()); + Log.e(TAG, "load from:" + soFile); + return; + } + //md5不一样则删除 + //noinspection ResultOfMethodCallIgnored + soFile.delete(); + + } + //不存在则下载 + download(soUrl, md5, downloadFile, soFile); + //使用系统加载方法 + System.loadLibrary(libName); + } finally { + Log.e(TAG, "time:" + (System.currentTimeMillis() - start)); + } + } + + + public static String getCpuAbi() { + //貌似只有这个过时了的API能获取当前APP使用的ABI + return Build.CPU_ABI; + } + + + private static String getUrlMd5(String url) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + outputStream = new ByteArrayOutputStream(); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + inputStream = connection.getInputStream(); + byte[] buffer = new byte[32768]; + int read; + while ((read = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + outputStream.flush(); + } + return outputStream.toString(); + + } catch (IOException e) { + return null; + } + } + + /** + * 删除历史文件 + */ + private static void deleteHistoryFile(File dir, File currentFile) { + File[] files = dir.listFiles(); + if (files != null && files.length > 0) { + for (File f : files) { + if (f.exists() && (currentFile == null || !f.getAbsolutePath().equals(currentFile.getAbsolutePath()))) { + boolean delete = f.delete(); + Log.e(TAG, "delete file: " + f + " result: " + delete); + if (!delete) { + f.deleteOnExit(); + } + } + } + } + } + + /** + * 下载文件 + */ + private static boolean downloadFileIfNotExist(String url, File destFile) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + inputStream = connection.getInputStream(); + if (destFile.exists()) { + return true; + } + destFile.getParentFile().mkdirs(); + destFile.createNewFile(); + outputStream = new FileOutputStream(destFile); + byte[] buffer = new byte[32768]; + int read; + while ((read = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + outputStream.flush(); + } + return true; + } catch (Throwable e) { + e.printStackTrace(); + if (destFile.exists() && !destFile.delete()) { + destFile.deleteOnExit(); + } + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return false; + } + + static boolean download = false; + static Executor executor = Executors.newSingleThreadExecutor(); + + /** + * 下载并拷贝文件 + */ + private static synchronized void download(final String url, final String md5, final File downloadTempFile, final File destSuccessFile) { + if (download) { + return; + } + download = true; + executor.execute(() -> { + boolean result = downloadFileIfNotExist(url, downloadTempFile); + Log.e(TAG, "download result:" + result); + //文件md5再次校验 + String fileMD5 = getFileMD5(downloadTempFile); + if (md5 != null && !md5.equalsIgnoreCase(fileMD5)) { + boolean delete = downloadTempFile.delete(); + if (!delete) { + downloadTempFile.deleteOnExit(); + } + download = false; + return; + } + Log.e(TAG, "download success, copy to " + destSuccessFile); + //下载成功拷贝文件 + copyFile(downloadTempFile, destSuccessFile); + File parentFile = downloadTempFile.getParentFile(); + deleteHistoryFile(parentFile, null); + }); + + } + + + /** + * 拷贝文件 + */ + private static boolean copyFile(File source, File dest) { + if (source == null || !source.exists() || !source.isFile() || dest == null) { + return false; + } + if (source.getAbsolutePath().equals(dest.getAbsolutePath())) { + return true; + } + FileInputStream is = null; + FileOutputStream os = null; + File parent = dest.getParentFile(); + if (parent != null && (!parent.exists())) { + boolean mkdirs = parent.mkdirs(); + if (!mkdirs) { + mkdirs = parent.mkdirs(); + } + } + try { + is = new FileInputStream(source); + os = new FileOutputStream(dest, false); + + byte[] buffer = new byte[1024 * 512]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + return true; + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (is != null) { + try { + is.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + if (os != null) { + try { + os.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + return false; + } + + /** + * 获得文件md5 + */ + private static String getFileMD5(File file) { + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(file); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[1024]; + int numRead = 0; + while ((numRead = fileInputStream.read(buffer)) > 0) { + md5.update(buffer, 0, numRead); + } + return String.format("%032x", new BigInteger(1, md5.digest())).toLowerCase(); + } catch (Exception e) { + e.printStackTrace(); + } catch (OutOfMemoryError e) { + e.printStackTrace(); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + return null; + } +} diff --git a/build.gradle b/build.gradle index 2e71dab2c..28a0006f8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.5.30-M1' repositories { google() mavenCentral()