├── .gitignore ├── README.md ├── app ├── .gitignore ├── baseline.apk ├── build.gradle ├── demo.keystore ├── libs │ └── flutter.jar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── isolate_snapshot_data │ ├── java │ └── io │ │ └── github │ │ └── lizhangqu │ │ ├── app │ │ ├── App.java │ │ └── MainActivity.java │ │ └── flutter │ │ ├── FlutterUpdate.java │ │ └── PatchVerify.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── buildSrc ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── groovy │ └── io │ │ └── github │ │ └── lizhangqu │ │ └── flutter │ │ ├── BSDiff.java │ │ ├── Consumer.java │ │ ├── CustomClassTransform.java │ │ ├── DynamicPatchRedirectTransform.groovy │ │ ├── FlutterPatchPlugin.groovy │ │ ├── FlutterPatchTask.groovy │ │ ├── FlutterTransformExtension.groovy │ │ ├── FlutterTransformPlugin.groovy │ │ ├── TaskConfiguration.groovy │ │ └── TransformHelper.groovy │ └── resources │ └── META-INF │ └── gradle-plugins │ ├── flutter.patch.properties │ └── flutter.transform.properties ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.idea 3 | /.gradle 4 | *.iml 5 | local.properties 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### flutter patch gradle plugin 2 | 3 | 文章链接 4 | 5 | [Flutter 动态化探索](http://lizhangqu.github.io/2019/03/22/Flutter-%E5%8A%A8%E6%80%81%E5%8C%96%E6%8E%A2%E7%B4%A2/) 6 | 7 | ``` 8 | buildscript { 9 | repositories { 10 | jcenter() 11 | } 12 | dependencies { 13 | classpath "io.github.lizhangqu:plugin-flutter-patch:1.0.7" 14 | } 15 | } 16 | 17 | apply plugin: 'flutter.patch' 18 | 19 | ``` 20 | 21 | 22 | 生成patch 23 | 24 | ``` 25 | gradlew assembleReleaseFlutterPatch -PbaselineApk=/path/to/baseline.apk 26 | ``` 27 | 28 | 或者传递maven坐标 29 | 30 | ``` 31 | gradlew assembleReleaseFlutterPatch -PbaselineApk=x:y:z 32 | ``` 33 | 34 | 其中x:y:z为基线包maven坐标 35 | 36 | 37 | 38 | 自定义patch下载url,下载模式,安装模式 39 | 40 | ``` 41 | apply plugin: 'flutter.transform' 42 | flutterTransform { 43 | patchClass = "io.github.lizhangqu.flutter.FlutterUpdate" 44 | downloadUrlMethod = "getDownloadURL" 45 | installModeMethod = "getInstallMode" 46 | downloadModeMethod = "getDownloadMode" 47 | } 48 | ``` 49 | 50 | 51 | ``` 52 | @Keep 53 | public class FlutterUpdate { 54 | 55 | @Keep 56 | public static String getDownloadURL(Context context) { 57 | return null; 58 | } 59 | 60 | @Keep 61 | public static String getDownloadMode(Context context) { 62 | return null; 63 | } 64 | 65 | @Keep 66 | public static String getInstallMode(Context context) { 67 | return null; 68 | } 69 | } 70 | 71 | ``` -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/baseline.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/baseline.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'flutter.patch' 3 | apply plugin: 'flutter.transform' 4 | 5 | android { 6 | compileSdkVersion 27 7 | buildToolsVersion "27.0.3" 8 | 9 | defaultConfig { 10 | applicationId "io.github.lizhangqu.app" 11 | minSdkVersion 14 12 | targetSdkVersion 27 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | signingConfigs { 18 | release { 19 | keyAlias 'demo' 20 | keyPassword '123456' 21 | storeFile project.file('demo.keystore') 22 | storePassword '123456' 23 | } 24 | } 25 | buildTypes { 26 | debug { 27 | debuggable true 28 | signingConfig signingConfigs.release 29 | minifyEnabled false 30 | } 31 | release { 32 | debuggable false 33 | signingConfig signingConfigs.release 34 | minifyEnabled true 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | } 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | } 44 | 45 | dependencies { 46 | compile fileTree(dir: 'libs', include: ['*.jar']) 47 | compile 'com.android.support:appcompat-v7:27.1.1' 48 | compile 'com.android.support.constraint:constraint-layout:1.1.3' 49 | } 50 | 51 | -------------------------------------------------------------------------------- /app/demo.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/demo.keystore -------------------------------------------------------------------------------- /app/libs/flutter.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/libs/flutter.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/assets/isolate_snapshot_data: -------------------------------------------------------------------------------- 1 | just mock patch -------------------------------------------------------------------------------- /app/src/main/java/io/github/lizhangqu/app/App.java: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.app; 2 | 3 | import android.app.Application; 4 | 5 | /** 6 | * @author lizhangqu 7 | * @version V1.0 8 | * @since 2018-11-08 17:08 9 | */ 10 | public class App extends Application { 11 | @Override 12 | public void onCreate() { 13 | super.onCreate(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/lizhangqu/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.app; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | /** 7 | * @author lizhangqu 8 | * @version V1.0 9 | * @since 2018-11-08 17:08 10 | */ 11 | public class MainActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/lizhangqu/flutter/FlutterUpdate.java: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Keep; 5 | import android.util.Log; 6 | 7 | import java.io.File; 8 | import java.io.FileInputStream; 9 | import java.io.FilenameFilter; 10 | import java.math.BigInteger; 11 | import java.security.MessageDigest; 12 | 13 | /** 14 | * @author lizhangqu 15 | * @version V1.0 16 | * @since 2019-03-22 13:51 17 | */ 18 | @Keep 19 | public class FlutterUpdate { 20 | 21 | @Keep 22 | public static class FlutterPatch { 23 | public String url; 24 | public String md5; 25 | public String downloadMode; 26 | public String installMode; 27 | 28 | @Override 29 | public String toString() { 30 | return "FlutterPatch{" + 31 | "url='" + url + '\'' + 32 | ", md5='" + md5 + '\'' + 33 | ", downloadMode='" + downloadMode + '\'' + 34 | ", installMode='" + installMode + '\'' + 35 | '}'; 36 | } 37 | } 38 | 39 | private static FlutterPatch flutterPatch = null; 40 | 41 | 42 | private static String getFileMD5(File file) { 43 | if (!file.exists()) { 44 | return null; 45 | } 46 | FileInputStream fileInputStream = null; 47 | try { 48 | fileInputStream = new FileInputStream(file); 49 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 50 | byte[] buffer = new byte[1024 * 1024]; 51 | int numRead = 0; 52 | while ((numRead = fileInputStream.read(buffer)) > 0) { 53 | md5.update(buffer, 0, numRead); 54 | } 55 | return String.format("%032x", new BigInteger(1, md5.digest())).toLowerCase(); 56 | } catch (Exception e) { 57 | e.printStackTrace(); 58 | } finally { 59 | if (fileInputStream != null) { 60 | try { 61 | fileInputStream.close(); 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | } 65 | } 66 | } 67 | return null; 68 | } 69 | 70 | 71 | private static String getInstalledPatchMd5(Context context) { 72 | File file = new File(context.getFilesDir().toString() + "/patch.zip"); 73 | return getFileMD5(file); 74 | } 75 | 76 | private static void checkSign(Context context) { 77 | File file = new File(context.getFilesDir().toString() + "/patch.zip"); 78 | if (!file.exists()) { 79 | return; 80 | } 81 | boolean success = PatchVerify.verifySign(context, file); 82 | if (!success) { 83 | 84 | //删除patch文件 85 | file.delete(); 86 | 87 | //删除时间戳文件重新释放 88 | deleteFiles(context); 89 | } 90 | 91 | } 92 | 93 | private static String[] getExistingTimestamps(File dataDir) { 94 | return dataDir.list(new FilenameFilter() { 95 | @Override 96 | public boolean accept(File dir, String name) { 97 | return name.startsWith("res_timestamp-"); 98 | } 99 | }); 100 | } 101 | 102 | private static void deleteFiles(Context context) { 103 | final File dataDir = new File(context.getDir("flutter", Context.MODE_PRIVATE).getPath()); 104 | final String[] existingTimestamps = getExistingTimestamps(dataDir); 105 | if (existingTimestamps == null) { 106 | return; 107 | } 108 | for (String timestamp : existingTimestamps) { 109 | new File(dataDir, timestamp).delete(); 110 | } 111 | } 112 | 113 | 114 | private static void ensureConfig(Context context) { 115 | if (flutterPatch != null) { 116 | return; 117 | } 118 | //TODO 此处是自定义逻辑,根据本地配置持久化的远程配置,反序列化创建flutterPatch即可,这里mock一个 119 | FlutterPatch patch = new FlutterPatch(); 120 | patch.url = "下载patch的url"; 121 | patch.md5 = "下载patch的md5"; 122 | patch.downloadMode = "patch的下载模式"; 123 | patch.installMode = "patch的安装模式"; 124 | 125 | flutterPatch = patch; 126 | 127 | Log.e("FlutterUpdate", "flutterPatch:" + flutterPatch); 128 | 129 | } 130 | 131 | @Keep 132 | public static String getDownloadURL(Context context) { 133 | ensureConfig(context); 134 | //校验签名,保障文件来源安全 135 | checkSign(context); 136 | if (flutterPatch == null) { 137 | Log.e("FlutterUpdate", "flutterPatch == null"); 138 | return null; 139 | } 140 | //校验文件md5,防止patch文件重复下载 141 | String installedPatchMd5 = getInstalledPatchMd5(context); 142 | if (installedPatchMd5 != null && installedPatchMd5.equalsIgnoreCase(flutterPatch.md5)) { 143 | Log.e("FlutterUpdate", "md5 equals:" + flutterPatch.md5); 144 | return null; 145 | } 146 | 147 | return flutterPatch.url; 148 | } 149 | 150 | @Keep 151 | public static String getDownloadMode(Context context) { 152 | ensureConfig(context); 153 | //校验签名,保障文件来源安全 154 | checkSign(context); 155 | if (flutterPatch == null) { 156 | Log.e("FlutterUpdate", "flutterPatch == null"); 157 | return null; 158 | } 159 | 160 | return flutterPatch.downloadMode; 161 | } 162 | 163 | @Keep 164 | public static String getInstallMode(Context context) { 165 | ensureConfig(context); 166 | //校验签名,保障文件来源安全 167 | checkSign(context); 168 | if (flutterPatch == null) { 169 | Log.e("FlutterUpdate", "flutterPatch == null"); 170 | return null; 171 | } 172 | 173 | return flutterPatch.installMode; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/lizhangqu/flutter/PatchVerify.java: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.security.PublicKey; 12 | import java.security.cert.Certificate; 13 | import java.security.cert.CertificateFactory; 14 | import java.security.cert.X509Certificate; 15 | import java.util.jar.JarEntry; 16 | import java.util.jar.JarFile; 17 | 18 | /** 19 | * @author lizhangqu 20 | * @version V1.0 21 | * @since 2019-03-22 14:04 22 | */ 23 | public class PatchVerify { 24 | /** 25 | * 验证签名 26 | */ 27 | public static boolean verifySign(Context context, File path) { 28 | if (context == null || path == null || !path.exists()) { 29 | return false; 30 | } 31 | JarFile jarFile = null; 32 | try { 33 | jarFile = new JarFile(path); 34 | JarEntry jarEntry = jarFile.getJarEntry("manifest.json"); 35 | if (null == jarEntry) {// no code 36 | return false; 37 | } 38 | loadDigestes(jarFile, jarEntry); 39 | Certificate[] certs = jarEntry.getCertificates(); 40 | if (certs == null) { 41 | return false; 42 | } 43 | return check(context, certs); 44 | } catch (Throwable e) { 45 | return false; 46 | } finally { 47 | try { 48 | if (jarFile != null) { 49 | jarFile.close(); 50 | } 51 | } catch (Throwable e) { 52 | } 53 | } 54 | } 55 | 56 | private static void loadDigestes(JarFile jarFile, JarEntry je) throws IOException { 57 | InputStream is = null; 58 | try { 59 | is = jarFile.getInputStream(je); 60 | byte[] bytes = new byte[8192]; 61 | while (is.read(bytes) > 0) { 62 | } 63 | } finally { 64 | if (is != null) { 65 | try { 66 | is.close(); 67 | } catch (Exception e) { 68 | } 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * 用apk公钥去验证patch 75 | * 76 | * @param context 77 | * @param certs 78 | * @return 79 | * @throws Exception 80 | */ 81 | private static boolean check(Context context, Certificate[] certs) throws Exception { 82 | PublicKey apkPublicKey = getApkPublicKey(context); 83 | if (apkPublicKey == null) { 84 | return false; 85 | } 86 | boolean result = true; 87 | if (certs != null && certs.length > 0) { 88 | for (int i = certs.length - 1; i >= 0; i--) { 89 | try { 90 | certs[i].verify(apkPublicKey); 91 | } catch (Exception e) { 92 | result = false; 93 | e.printStackTrace(); 94 | } 95 | } 96 | } 97 | return result; 98 | } 99 | 100 | /** 101 | * 获得当前安装apk的公钥 102 | * 103 | * @param context 104 | * @return 105 | */ 106 | private static PublicKey getApkPublicKey(Context context) { 107 | if (context == null) { 108 | return null; 109 | } 110 | try { 111 | PackageManager pm = context.getPackageManager(); 112 | String packageName = context.getPackageName(); 113 | PackageInfo packageInfo = pm.getPackageInfo(packageName, 114 | PackageManager.GET_SIGNATURES); 115 | CertificateFactory certFactory = CertificateFactory 116 | .getInstance("X.509"); 117 | ByteArrayInputStream stream = new ByteArrayInputStream( 118 | packageInfo.signatures[0].toByteArray()); 119 | X509Certificate cert = (X509Certificate) certFactory 120 | .generateCertificate(stream); 121 | PublicKey publicKey = cert.getPublicKey(); 122 | return publicKey; 123 | } catch (Throwable e) { 124 | e.printStackTrace(); 125 | } 126 | return null; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | App 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { 4 | url 'https://maven.aliyun.com/repository/public' 5 | } 6 | maven { 7 | url 'https://maven.aliyun.com/repository/central' 8 | } 9 | maven { 10 | url 'https://maven.aliyun.com/repository/gradle-plugin' 11 | } 12 | maven { 13 | url 'https://maven.aliyun.com/repository/google' 14 | } 15 | maven { 16 | url 'https://maven.aliyun.com/repository/jcenter' 17 | } 18 | maven { 19 | url 'https://dl.google.com/dl/android/maven2/' 20 | } 21 | jcenter() 22 | mavenCentral() 23 | mavenLocal() 24 | } 25 | dependencies { 26 | File localFile = project.rootProject.file('local.properties') 27 | Properties extProperties = new Properties() 28 | if (localFile.exists()) { 29 | extProperties.load(localFile.newDataInputStream()) 30 | } 31 | def androidGradlePluginVersion = "3.2.1" 32 | if (extProperties.containsKey('gradleVersion')) { 33 | androidGradlePluginVersion = extProperties.get("gradleVersion") as String 34 | } 35 | if (project.hasProperty('gradleVersion')) { 36 | androidGradlePluginVersion = project.getProperties().get("gradleVersion") as String 37 | } 38 | project.logger.error "root build.gradle androidGradlePluginVersion ${androidGradlePluginVersion}" 39 | 40 | classpath "com.android.tools.build:gradle:${androidGradlePluginVersion}" 41 | } 42 | } 43 | 44 | allprojects { 45 | repositories { 46 | maven { 47 | url 'https://maven.aliyun.com/repository/public' 48 | } 49 | maven { 50 | url 'https://maven.aliyun.com/repository/central' 51 | } 52 | maven { 53 | url 'https://maven.aliyun.com/repository/gradle-plugin' 54 | } 55 | maven { 56 | url 'https://maven.aliyun.com/repository/google' 57 | } 58 | maven { 59 | url 'https://maven.aliyun.com/repository/jcenter' 60 | } 61 | maven { 62 | url 'https://dl.google.com/dl/android/maven2/' 63 | } 64 | jcenter() 65 | mavenCentral() 66 | mavenLocal() 67 | } 68 | } 69 | 70 | task clean(type: Delete) { 71 | delete rootProject.buildDir 72 | } 73 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'java' 3 | 4 | repositories { 5 | maven { 6 | url 'https://maven.aliyun.com/repository/public' 7 | } 8 | maven { 9 | url 'https://maven.aliyun.com/repository/central' 10 | } 11 | maven { 12 | url 'https://maven.aliyun.com/repository/gradle-plugin' 13 | } 14 | maven { 15 | url 'https://maven.aliyun.com/repository/google' 16 | } 17 | maven { 18 | url 'https://maven.aliyun.com/repository/jcenter' 19 | } 20 | maven { 21 | url 'https://dl.google.com/dl/android/maven2/' 22 | } 23 | jcenter() 24 | mavenCentral() 25 | mavenLocal() 26 | } 27 | 28 | dependencies { 29 | compile localGroovy() 30 | compile gradleApi() 31 | 32 | File localFile = project.rootProject.file('../local.properties') 33 | Properties extProperties = new Properties() 34 | if (!localFile.exists()) { 35 | localFile = project.rootProject.file('local.properties') 36 | } 37 | if (localFile.exists()) { 38 | extProperties.load(localFile.newDataInputStream()) 39 | } 40 | def androidGradlePluginVersion = "3.2.1" 41 | if (extProperties.containsKey('gradleVersion')) { 42 | androidGradlePluginVersion = extProperties.get("gradleVersion") as String 43 | } 44 | if (project.hasProperty('gradleVersion')) { 45 | androidGradlePluginVersion = project.getProperties().get("gradleVersion") as String 46 | } 47 | project.logger.error "buildSrc build.gradle androidGradlePluginVersion ${androidGradlePluginVersion}" 48 | 49 | compile "com.android.tools.build:gradle:${androidGradlePluginVersion}" 50 | 51 | compile 'org.javassist:javassist:3.20.0-GA' 52 | compile 'org.zeroturnaround:zt-zip:1.12' 53 | } 54 | 55 | buildscript { 56 | repositories { 57 | maven { 58 | url 'https://maven.aliyun.com/repository/public' 59 | } 60 | maven { 61 | url 'https://maven.aliyun.com/repository/central' 62 | } 63 | maven { 64 | url 'https://maven.aliyun.com/repository/gradle-plugin' 65 | } 66 | maven { 67 | url 'https://maven.aliyun.com/repository/google' 68 | } 69 | maven { 70 | url 'https://maven.aliyun.com/repository/jcenter' 71 | } 72 | maven { 73 | url 'https://dl.google.com/dl/android/maven2/' 74 | } 75 | jcenter() 76 | mavenCentral() 77 | mavenLocal() 78 | } 79 | dependencies { 80 | classpath 'io.github.lizhangqu:core-publish:1.4.0' 81 | } 82 | configurations.all { 83 | it.resolutionStrategy.cacheDynamicVersionsFor(5, 'minutes') 84 | it.resolutionStrategy.cacheChangingModulesFor(0, 'seconds') 85 | } 86 | } 87 | 88 | group = "io.github.lizhangqu" 89 | archivesBaseName = "plugin-flutter-patch" 90 | apply plugin: 'android.publish' 91 | 92 | pom { 93 | exclude "com.android.tools.build:gradle" 94 | } 95 | -------------------------------------------------------------------------------- /buildSrc/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_DESCRIPTION=Flutter Patch Plugin 2 | POM_LICENSE=Apache-2.0 3 | POM_LICENSE_URL=https://opensource.org/licenses/Apache-2.0 4 | POM_WEBSITE_URL=https://github.com/lizhangqu/plugin-flutter-patch 5 | POM_VCS_URL=https://github.com/lizhangqu/plugin-flutter-patch.git 6 | POM_ISSUE_URL=https://github.com/lizhangqu/plugin-flutter-patch/issues 7 | POM_DEVELOPER_ID=lizhangqu 8 | POM_DEVELOPER_NAME=lizhangqu 9 | POM_DEVELOPER_EMAIL=li330324@gmail.com 10 | release.bintray=true 11 | 12 | version=1.0.8-SNAPSHOT 13 | ###latest release version=1.0.7 14 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/BSDiff.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 THL A29 Limited, a Tencent company. 3 | * Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com) 4 | * Copyright 2003-2005 Colin Percival 5 | * All rights reserved 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted providing that the following conditions 9 | * are met: 10 | * 1. Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 17 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 24 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | * POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | package io.github.lizhangqu.flutter; 29 | 30 | import java.io.BufferedInputStream; 31 | import java.io.ByteArrayInputStream; 32 | import java.io.ByteArrayOutputStream; 33 | import java.io.DataInputStream; 34 | import java.io.DataOutputStream; 35 | import java.io.File; 36 | import java.io.FileInputStream; 37 | import java.io.FileOutputStream; 38 | import java.io.IOException; 39 | import java.io.InputStream; 40 | import java.io.OutputStream; 41 | import java.util.zip.GZIPInputStream; 42 | import java.util.zip.GZIPOutputStream; 43 | 44 | /** 45 | * Java Binary Diff utility. Based on 46 | * bsdiff (v4.2) by Colin Percival 47 | * (see http://www.daemonology.net/bsdiff/ ) and distributed under BSD license. 48 | * 49 | *

50 | * Running this on large files will probably require an increae of the default 51 | * maximum heap size (use java -Xmx200m) 52 | *

53 | * 54 | * @author Joe Desbonnet, jdesbonnet@gmail.com 55 | */ 56 | 57 | public class BSDiff { 58 | 59 | public static final int HEADER_SIZE = 32; 60 | 61 | // This is 62 | private static final byte[] MAGIC_BYTES = "BZDIFF40".getBytes(); 63 | 64 | /** 65 | * Read from input stream and fill the given buffer from the given offset up 66 | * to length len. 67 | */ 68 | private static boolean readFromStream(InputStream in, byte[] buf, int offset, int len) throws IOException { 69 | 70 | int totalBytesRead = 0; 71 | while (totalBytesRead < len) { 72 | int bytesRead = in.read(buf, offset + totalBytesRead, len - totalBytesRead); 73 | if (bytesRead < 0) { 74 | return false; 75 | } 76 | totalBytesRead += bytesRead; 77 | } 78 | return true; 79 | } 80 | 81 | 82 | private static void split(int[] arrayI, int[] arrayV, int start, int len, int h) { 83 | 84 | int i, j, k, x, tmp, jj, kk; 85 | 86 | if (len < 16) { 87 | for (k = start; k < start + len; k += j) { 88 | j = 1; 89 | x = arrayV[arrayI[k] + h]; 90 | for (i = 1; k + i < start + len; i++) { 91 | if (arrayV[arrayI[k + i] + h] < x) { 92 | x = arrayV[arrayI[k + i] + h]; 93 | j = 0; 94 | } 95 | 96 | if (arrayV[arrayI[k + i] + h] == x) { 97 | tmp = arrayI[k + j]; 98 | arrayI[k + j] = arrayI[k + i]; 99 | arrayI[k + i] = tmp; 100 | j++; 101 | } 102 | 103 | } 104 | 105 | for (i = 0; i < j; i++) { 106 | arrayV[arrayI[k + i]] = k + j - 1; 107 | } 108 | if (j == 1) { 109 | arrayI[k] = -1; 110 | } 111 | } 112 | 113 | return; 114 | } 115 | 116 | x = arrayV[arrayI[start + len / 2] + h]; 117 | jj = 0; 118 | kk = 0; 119 | for (i = start; i < start + len; i++) { 120 | if (arrayV[arrayI[i] + h] < x) { 121 | jj++; 122 | } 123 | if (arrayV[arrayI[i] + h] == x) { 124 | kk++; 125 | } 126 | } 127 | 128 | jj += start; 129 | kk += jj; 130 | 131 | i = start; 132 | j = 0; 133 | k = 0; 134 | while (i < jj) { 135 | if (arrayV[arrayI[i] + h] < x) { 136 | i++; 137 | } else if (arrayV[arrayI[i] + h] == x) { 138 | tmp = arrayI[i]; 139 | arrayI[i] = arrayI[jj + j]; 140 | arrayI[jj + j] = tmp; 141 | j++; 142 | } else { 143 | tmp = arrayI[i]; 144 | arrayI[i] = arrayI[kk + k]; 145 | arrayI[kk + k] = tmp; 146 | k++; 147 | } 148 | 149 | } 150 | 151 | while (jj + j < kk) { 152 | if (arrayV[arrayI[jj + j] + h] == x) { 153 | j++; 154 | } else { 155 | tmp = arrayI[jj + j]; 156 | arrayI[jj + j] = arrayI[kk + k]; 157 | arrayI[kk + k] = tmp; 158 | k++; 159 | } 160 | 161 | } 162 | 163 | if (jj > start) { 164 | split(arrayI, arrayV, start, jj - start, h); 165 | } 166 | 167 | for (i = 0; i < kk - jj; i++) { 168 | arrayV[arrayI[jj + i]] = kk - 1; 169 | } 170 | 171 | if (jj == kk - 1) { 172 | arrayI[jj] = -1; 173 | } 174 | 175 | if (start + len > kk) { 176 | split(arrayI, arrayV, kk, start + len - kk, h); 177 | } 178 | 179 | } 180 | 181 | /** 182 | * Fast suffix sporting. Larsson and Sadakane's qsufsort algorithm. See 183 | * http://www.cs.lth.se/Research/Algorithms/Papers/jesper5.ps 184 | */ 185 | private static void qsufsort(int[] arrayI, int[] arrayV, byte[] oldBuf, int oldsize) { 186 | 187 | // int oldsize = oldBuf.length; 188 | int[] buckets = new int[256]; 189 | 190 | // No need to do that in Java. 191 | // for ( int i = 0; i < 256; i++ ) { 192 | // buckets[i] = 0; 193 | // } 194 | 195 | for (int i = 0; i < oldsize; i++) { 196 | buckets[oldBuf[i] & 0xff]++; 197 | } 198 | 199 | for (int i = 1; i < 256; i++) { 200 | buckets[i] += buckets[i - 1]; 201 | } 202 | 203 | for (int i = 255; i > 0; i--) { 204 | buckets[i] = buckets[i - 1]; 205 | } 206 | 207 | buckets[0] = 0; 208 | 209 | for (int i = 0; i < oldsize; i++) { 210 | arrayI[++buckets[oldBuf[i] & 0xff]] = i; 211 | } 212 | 213 | arrayI[0] = oldsize; 214 | for (int i = 0; i < oldsize; i++) { 215 | arrayV[i] = buckets[oldBuf[i] & 0xff]; 216 | } 217 | arrayV[oldsize] = 0; 218 | 219 | for (int i = 1; i < 256; i++) { 220 | if (buckets[i] == buckets[i - 1] + 1) { 221 | arrayI[buckets[i]] = -1; 222 | } 223 | } 224 | 225 | arrayI[0] = -1; 226 | 227 | for (int h = 1; arrayI[0] != -(oldsize + 1); h += h) { 228 | int len = 0; 229 | int i; 230 | for (i = 0; i < oldsize + 1; ) { 231 | if (arrayI[i] < 0) { 232 | len -= arrayI[i]; 233 | i -= arrayI[i]; 234 | } else { 235 | // if(len) I[i-len]=-len; 236 | if (len != 0) { 237 | arrayI[i - len] = -len; 238 | } 239 | len = arrayV[arrayI[i]] + 1 - i; 240 | split(arrayI, arrayV, i, len, h); 241 | i += len; 242 | len = 0; 243 | } 244 | 245 | } 246 | 247 | if (len != 0) { 248 | arrayI[i - len] = -len; 249 | } 250 | } 251 | 252 | for (int i = 0; i < oldsize + 1; i++) { 253 | arrayI[arrayV[i]] = i; 254 | } 255 | } 256 | 257 | 258 | /** 259 | * 分别将 oldBufd[start..oldSize] 和 oldBufd[end..oldSize] 与 newBuf[newBufOffset...newSize] 进行匹配, 260 | * 返回他们中的最长匹配长度,并且将最长匹配的开始位置记录到pos.value中。 261 | */ 262 | private static int search(int[] arrayI, byte[] oldBuf, int oldSize, byte[] newBuf, int newSize, int newBufOffset, int start, int end, IntByRef pos) { 263 | 264 | if (end - start < 2) { 265 | int x = matchlen(oldBuf, oldSize, arrayI[start], newBuf, newSize, newBufOffset); 266 | int y = matchlen(oldBuf, oldSize, arrayI[end], newBuf, newSize, newBufOffset); 267 | 268 | if (x > y) { 269 | pos.value = arrayI[start]; 270 | return x; 271 | } else { 272 | pos.value = arrayI[end]; 273 | return y; 274 | } 275 | } 276 | 277 | // binary search 278 | int x = start + (end - start) / 2; 279 | if (memcmp(oldBuf, oldSize, arrayI[x], newBuf, newSize, newBufOffset) < 0) { 280 | return search(arrayI, oldBuf, oldSize, newBuf, newSize, newBufOffset, x, end, pos); // Calls itself recursively 281 | } else { 282 | return search(arrayI, oldBuf, oldSize, newBuf, newSize, newBufOffset, start, x, pos); 283 | } 284 | } 285 | 286 | 287 | /** 288 | * Count the number of bytes that match in oldBuf[oldOffset...oldSize] and newBuf[newOffset...newSize] 289 | */ 290 | private static int matchlen(byte[] oldBuf, int oldSize, int oldOffset, byte[] newBuf, int newSize, int newOffset) { 291 | 292 | int end = Math.min(oldSize - oldOffset, newSize - newOffset); 293 | for (int i = 0; i < end; i++) { 294 | if (oldBuf[oldOffset + i] != newBuf[newOffset + i]) { 295 | return i; 296 | } 297 | } 298 | return end; 299 | } 300 | 301 | /** 302 | * Compare two byte array segments to see if they are equal 303 | *

304 | * return 1 if s1[s1offset...s1Size] is bigger than s2[s2offset...s2Size] otherwise return -1 305 | */ 306 | private static int memcmp(byte[] s1, int s1Size, int s1offset, byte[] s2, int s2Size, int s2offset) { 307 | 308 | int n = s1Size - s1offset; 309 | 310 | if (n > (s2Size - s2offset)) { 311 | n = s2Size - s2offset; 312 | } 313 | 314 | for (int i = 0; i < n; i++) { 315 | 316 | if (s1[i + s1offset] != s2[i + s2offset]) { 317 | return s1[i + s1offset] < s2[i + s2offset] ? -1 : 1; 318 | } 319 | } 320 | return 0; 321 | } 322 | 323 | 324 | public static void bsdiff(File oldFile, File newFile, File diffFile) throws IOException { 325 | InputStream oldInputStream = new BufferedInputStream(new FileInputStream(oldFile)); 326 | InputStream newInputStream = new BufferedInputStream(new FileInputStream(newFile)); 327 | OutputStream diffOutputStream = new FileOutputStream(diffFile); 328 | try { 329 | byte[] diffBytes = bsdiff(oldInputStream, (int) oldFile.length(), newInputStream, (int) newFile.length()); 330 | diffOutputStream.write(diffBytes); 331 | } finally { 332 | diffOutputStream.close(); 333 | } 334 | } 335 | 336 | 337 | public static byte[] bsdiff(InputStream oldInputStream, int oldsize, InputStream newInputStream, int newsize) throws IOException { 338 | 339 | byte[] oldBuf = new byte[oldsize]; 340 | 341 | readFromStream(oldInputStream, oldBuf, 0, oldsize); 342 | oldInputStream.close(); 343 | 344 | byte[] newBuf = new byte[newsize]; 345 | readFromStream(newInputStream, newBuf, 0, newsize); 346 | newInputStream.close(); 347 | 348 | return bsdiff(oldBuf, newBuf); 349 | } 350 | 351 | 352 | public static byte[] bsdiff(byte[] oldBuf, byte[] newBuf) throws IOException { 353 | 354 | int oldsize = oldBuf.length; 355 | int newsize = newBuf.length; 356 | int[] arrayI = new int[oldsize + 1]; 357 | qsufsort(arrayI, new int[oldsize + 1], oldBuf, oldsize); 358 | 359 | // diff block 360 | int diffBLockLen = 0; 361 | byte[] diffBlock = new byte[newsize]; 362 | 363 | // extra block 364 | int extraBlockLen = 0; 365 | byte[] extraBlock = new byte[newsize]; 366 | 367 | /* 368 | * Diff file is composed as follows: 369 | * 370 | * Header (32 bytes) Data (from offset 32 to end of file) 371 | * 372 | * Header: 373 | * Offset 0, length 8 bytes: file magic "MicroMsg" 374 | * Offset 8, length 8 bytes: length of compressed ctrl block 375 | * Offset 16, length 8 bytes: length of compressed diff block 376 | * Offset 24, length 8 bytes: length of new file 377 | * 378 | * Data: 379 | * 32 (length ctrlBlockLen): ctrlBlock (bzip2) 380 | * 32 + ctrlBlockLen (length diffBlockLen): diffBlock (bzip2) 381 | * 32 + ctrlBlockLen + diffBlockLen (to end of file): extraBlock (bzip2) 382 | * 383 | * ctrlBlock comprises a set of records, each record 12 bytes. 384 | * A record comprises 3 x 32 bit integers. The ctrlBlock is not compressed. 385 | */ 386 | 387 | ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 388 | DataOutputStream diffOut = new DataOutputStream(byteOut); 389 | 390 | // Write as much of header as we have now. Size of ctrlBlock and diffBlock must be filled in later. 391 | diffOut.write(MAGIC_BYTES); 392 | diffOut.writeLong(-1); // place holder for ctrlBlockLen 393 | diffOut.writeLong(-1); // place holder for diffBlockLen 394 | diffOut.writeLong(newsize); 395 | diffOut.flush(); 396 | 397 | GZIPOutputStream bzip2Out = new GZIPOutputStream(diffOut); 398 | DataOutputStream dataOut = new DataOutputStream(bzip2Out); 399 | 400 | int oldscore, scsc; 401 | 402 | int overlap, ss, lens; 403 | int i; 404 | int scan = 0; 405 | int matchLen = 0; 406 | int lastscan = 0; 407 | int lastpos = 0; 408 | int lastoffset = 0; 409 | 410 | IntByRef pos = new IntByRef(); 411 | // int ctrlBlockLen = 0; 412 | 413 | while (scan < newsize) { 414 | oldscore = 0; 415 | 416 | for (scsc = scan += matchLen; scan < newsize; scan++) { 417 | // oldBuf[0...oldsize] newBuf[scan...newSize]. pos.value,scan 418 | matchLen = search(arrayI, oldBuf, oldsize, newBuf, newsize, scan, 0, oldsize, pos); 419 | 420 | for (; scsc < scan + matchLen; scsc++) { 421 | if ((scsc + lastoffset < oldsize) && (oldBuf[scsc + lastoffset] == newBuf[scsc])) { 422 | oldscore++; 423 | } 424 | } 425 | 426 | if (((matchLen == oldscore) && (matchLen != 0)) || (matchLen > oldscore + 8)) { 427 | break; 428 | } 429 | 430 | if ((scan + lastoffset < oldsize) && (oldBuf[scan + lastoffset] == newBuf[scan])) { 431 | oldscore--; 432 | } 433 | } 434 | 435 | if ((matchLen != oldscore) || (scan == newsize)) { 436 | 437 | int equalNum = 0; 438 | int sf = 0; 439 | int lenFromOld = 0; 440 | for (i = 0; (lastscan + i < scan) && (lastpos + i < oldsize); ) { 441 | if (oldBuf[lastpos + i] == newBuf[lastscan + i]) { 442 | equalNum++; 443 | } 444 | i++; 445 | if (equalNum * 2 - i > sf * 2 - lenFromOld) { 446 | sf = equalNum; 447 | lenFromOld = i; 448 | } 449 | } 450 | 451 | int lenb = 0; 452 | if (scan < newsize) { 453 | equalNum = 0; 454 | int sb = 0; 455 | for (i = 1; (scan >= lastscan + i) && (pos.value >= i); i++) { 456 | if (oldBuf[pos.value - i] == newBuf[scan - i]) { 457 | equalNum++; 458 | } 459 | if (equalNum * 2 - i > sb * 2 - lenb) { 460 | sb = equalNum; 461 | lenb = i; 462 | } 463 | } 464 | } 465 | 466 | if (lastscan + lenFromOld > scan - lenb) { 467 | overlap = (lastscan + lenFromOld) - (scan - lenb); 468 | equalNum = 0; 469 | ss = 0; 470 | lens = 0; 471 | for (i = 0; i < overlap; i++) { 472 | if (newBuf[lastscan + lenFromOld - overlap + i] == oldBuf[lastpos + lenFromOld - overlap + i]) { 473 | equalNum++; 474 | } 475 | if (newBuf[scan - lenb + i] == oldBuf[pos.value - lenb + i]) { 476 | equalNum--; 477 | } 478 | if (equalNum > ss) { 479 | ss = equalNum; 480 | lens = i + 1; 481 | } 482 | } 483 | 484 | lenFromOld += lens - overlap; 485 | lenb -= lens; 486 | } 487 | 488 | // ? byte casting introduced here -- might affect things 489 | for (i = 0; i < lenFromOld; i++) { 490 | diffBlock[diffBLockLen + i] = (byte) (newBuf[lastscan + i] - oldBuf[lastpos + i]); 491 | } 492 | 493 | for (i = 0; i < (scan - lenb) - (lastscan + lenFromOld); i++) { 494 | extraBlock[extraBlockLen + i] = newBuf[lastscan + lenFromOld + i]; 495 | } 496 | 497 | diffBLockLen += lenFromOld; 498 | extraBlockLen += (scan - lenb) - (lastscan + lenFromOld); 499 | 500 | // Write control block entry (3 x int) 501 | dataOut.writeLong(lenFromOld); // oldBuf 502 | dataOut.writeLong((scan - lenb) - (lastscan + lenFromOld)); // diffBufextraBlock 503 | dataOut.writeLong((pos.value - lenb) - (lastpos + lenFromOld)); // oldBuf 504 | 505 | lastscan = scan - lenb; 506 | lastpos = pos.value - lenb; 507 | lastoffset = pos.value - scan; 508 | } // end if 509 | } // end while loop 510 | 511 | dataOut.flush(); 512 | bzip2Out.finish(); 513 | 514 | // now compressed ctrlBlockLen 515 | int ctrlBlockLen = diffOut.size() - HEADER_SIZE; 516 | 517 | // GZIPOutputStream gzOut; 518 | 519 | /* 520 | * Write diff block 521 | */ 522 | bzip2Out = new GZIPOutputStream(diffOut); 523 | bzip2Out.write(diffBlock, 0, diffBLockLen); 524 | bzip2Out.finish(); 525 | bzip2Out.flush(); 526 | int diffBlockLen = diffOut.size() - ctrlBlockLen - HEADER_SIZE; 527 | // System.err.println( "Diff: diffBlockLen=" + diffBlockLen ); 528 | 529 | /* 530 | * Write extra block 531 | */ 532 | bzip2Out = new GZIPOutputStream(diffOut); 533 | bzip2Out.write(extraBlock, 0, extraBlockLen); 534 | bzip2Out.finish(); 535 | bzip2Out.flush(); 536 | 537 | diffOut.close(); 538 | 539 | /* 540 | * Write missing header info. 541 | */ 542 | ByteArrayOutputStream byteHeaderOut = new ByteArrayOutputStream(HEADER_SIZE); 543 | DataOutputStream headerOut = new DataOutputStream(byteHeaderOut); 544 | headerOut.write(MAGIC_BYTES); 545 | headerOut.writeLong(ctrlBlockLen); // place holder for ctrlBlockLen 546 | headerOut.writeLong(diffBlockLen); // place holder for diffBlockLen 547 | headerOut.writeLong(newsize); 548 | headerOut.close(); 549 | 550 | // Copy header information into the diff 551 | byte[] diffBytes = byteOut.toByteArray(); 552 | byte[] headerBytes = byteHeaderOut.toByteArray(); 553 | 554 | System.arraycopy(headerBytes, 0, diffBytes, 0, headerBytes.length); 555 | 556 | return diffBytes; 557 | } 558 | 559 | 560 | private static class IntByRef { 561 | private int value; 562 | } 563 | 564 | public static byte[] bspatch(byte[] olddata, byte[] diffdata) throws IOException { 565 | InputStream in = new ByteArrayInputStream(diffdata, 0, diffdata.length); 566 | DataInputStream header = new DataInputStream(in); 567 | 568 | byte[] magic = new byte[8]; 569 | header.read(magic); 570 | if (!new String(magic).equals("BZDIFF40")) { 571 | throw new IOException("Invalid magic"); 572 | } 573 | 574 | int ctrllen = (int) header.readLong(); 575 | int datalen = (int) header.readLong(); 576 | int newsize = (int) header.readLong(); 577 | header.close(); 578 | 579 | in = new ByteArrayInputStream(diffdata, 0, diffdata.length); 580 | in.skip(32); 581 | DataInputStream cpf = new DataInputStream(new GZIPInputStream(in)); 582 | 583 | in = new ByteArrayInputStream(diffdata, 0, diffdata.length); 584 | in.skip(32 + ctrllen); 585 | InputStream dpf = new GZIPInputStream(in); 586 | 587 | in = new ByteArrayInputStream(diffdata, 0, diffdata.length); 588 | in.skip(32 + ctrllen + datalen); 589 | InputStream epf = new GZIPInputStream(in); 590 | 591 | byte[] newdata = new byte[newsize]; 592 | 593 | int oldpos = 0; 594 | int newpos = 0; 595 | 596 | while (newpos < newsize) { 597 | int[] ctrl = new int[3]; 598 | for (int i = 0; i <= 2; i++) { 599 | ctrl[i] = (int) cpf.readLong(); 600 | } 601 | if (newpos + ctrl[0] > newsize) { 602 | throw new IOException("Invalid ctrl[0]"); 603 | } 604 | 605 | read(dpf, newdata, newpos, ctrl[0]); 606 | 607 | for (int i = 0; i < ctrl[0]; i++) { 608 | if ((oldpos + i >= 0) && (oldpos + i < olddata.length)) { 609 | newdata[newpos + i] += olddata[oldpos + i]; 610 | } 611 | } 612 | 613 | newpos += ctrl[0]; 614 | oldpos += ctrl[0]; 615 | 616 | if (newpos + ctrl[1] > newsize) { 617 | throw new IOException("Invalid ctrl[0]"); 618 | } 619 | 620 | read(epf, newdata, newpos, ctrl[1]); 621 | 622 | newpos += ctrl[1]; 623 | oldpos += ctrl[2]; 624 | } 625 | 626 | cpf.close(); 627 | dpf.close(); 628 | epf.close(); 629 | 630 | return newdata; 631 | } 632 | 633 | private static void read(InputStream in, byte[] buf, int off, int len) throws IOException { 634 | for (int i, n = 0; n < len; n += i) { 635 | if ((i = in.read(buf, off + n, len - n)) < 0) { 636 | throw new IOException("Unexpected EOF"); 637 | } 638 | } 639 | } 640 | 641 | } -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/Consumer.java: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter; 2 | 3 | /** 4 | * 转换抽象接口 5 | * 6 | * @author lizhangqu 7 | * @version V1.0 8 | * @since 2019-03-14 14:11 9 | */ 10 | public interface Consumer { 11 | 12 | /** 13 | * Performs this operation on the given arguments. 14 | * 15 | * @param t the first input argument 16 | * @param u the second input argument 17 | */ 18 | void accept(String variantName, String path, T t, U u); 19 | } 20 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/CustomClassTransform.java: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter; 2 | 3 | import com.android.SdkConstants; 4 | import com.android.annotations.NonNull; 5 | import com.android.build.api.transform.DirectoryInput; 6 | import com.android.build.api.transform.Format; 7 | import com.android.build.api.transform.JarInput; 8 | import com.android.build.api.transform.QualifiedContent; 9 | import com.android.build.api.transform.Status; 10 | import com.android.build.api.transform.Transform; 11 | import com.android.build.api.transform.TransformInput; 12 | import com.android.build.api.transform.TransformInvocation; 13 | import com.android.build.api.transform.TransformOutputProvider; 14 | import com.android.build.gradle.internal.pipeline.TransformManager; 15 | import com.android.utils.FileUtils; 16 | import com.google.common.collect.ImmutableSet; 17 | import com.google.common.io.Files; 18 | 19 | import org.gradle.api.Project; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.FileOutputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.OutputStream; 27 | import java.io.UncheckedIOException; 28 | import java.lang.reflect.ParameterizedType; 29 | import java.lang.reflect.Type; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import java.util.zip.ZipEntry; 33 | import java.util.zip.ZipInputStream; 34 | import java.util.zip.ZipOutputStream; 35 | 36 | /** 37 | * 自定义类转换 38 | * 39 | * @author lizhangqu 40 | * @version V1.0 41 | * @since 2019-03-14 13:55 42 | */ 43 | public class CustomClassTransform extends Transform { 44 | 45 | @NonNull 46 | private final String name; 47 | 48 | @NonNull 49 | private final Project project; 50 | 51 | @NonNull 52 | private final Class clazz; 53 | 54 | public CustomClassTransform(Project project, @NonNull Class clazz) { 55 | this.project = project; 56 | this.name = clazz.getName().replaceAll("\\.", "_"); 57 | this.clazz = clazz; 58 | } 59 | 60 | @NonNull 61 | @Override 62 | public String getName() { 63 | return name; 64 | } 65 | 66 | @NonNull 67 | @Override 68 | public Set getInputTypes() { 69 | return TransformManager.CONTENT_CLASS; 70 | } 71 | 72 | @NonNull 73 | @Override 74 | public Set getOutputTypes() { 75 | return TransformManager.CONTENT_CLASS; 76 | } 77 | 78 | @NonNull 79 | @Override 80 | public Set getScopes() { 81 | if (project.getPlugins().hasPlugin("com.android.application")) { 82 | return TransformManager.SCOPE_FULL_PROJECT; 83 | } else { 84 | return ImmutableSet.of(QualifiedContent.Scope.PROJECT); 85 | } 86 | } 87 | 88 | @Override 89 | public boolean isIncremental() { 90 | return true; 91 | } 92 | 93 | @Override 94 | public void transform(@NonNull TransformInvocation invocation) 95 | throws IOException { 96 | final TransformOutputProvider outputProvider = invocation.getOutputProvider(); 97 | assert outputProvider != null; 98 | 99 | if (!invocation.isIncremental()) { 100 | outputProvider.deleteAll(); 101 | } 102 | 103 | Consumer function = loadTransformFunction(clazz); 104 | String variantName = invocation.getContext().getVariantName(); 105 | 106 | for (TransformInput ti : invocation.getInputs()) { 107 | for (JarInput jarInput : ti.getJarInputs()) { 108 | File inputJar = jarInput.getFile(); 109 | File outputJar = 110 | outputProvider.getContentLocation( 111 | jarInput.getFile().toString(), 112 | jarInput.getContentTypes(), 113 | jarInput.getScopes(), 114 | Format.JAR); 115 | 116 | if (invocation.isIncremental()) { 117 | switch (jarInput.getStatus()) { 118 | case NOTCHANGED: 119 | break; 120 | case ADDED: 121 | case CHANGED: 122 | transformJar(function, variantName, inputJar, outputJar); 123 | break; 124 | case REMOVED: 125 | FileUtils.delete(outputJar); 126 | break; 127 | } 128 | } else { 129 | transformJar(function, variantName, inputJar, outputJar); 130 | } 131 | } 132 | for (DirectoryInput di : ti.getDirectoryInputs()) { 133 | File inputDir = di.getFile(); 134 | File outputDir = 135 | outputProvider.getContentLocation( 136 | di.getFile().toString(), 137 | di.getContentTypes(), 138 | di.getScopes(), 139 | Format.DIRECTORY); 140 | if (invocation.isIncremental()) { 141 | for (Map.Entry entry : di.getChangedFiles().entrySet()) { 142 | File inputFile = entry.getKey(); 143 | switch (entry.getValue()) { 144 | case NOTCHANGED: 145 | break; 146 | case ADDED: 147 | case CHANGED: 148 | if (!inputFile.isDirectory() 149 | && inputFile.getName() 150 | .endsWith(SdkConstants.DOT_CLASS)) { 151 | String filePath = FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir); 152 | File out = toOutputFile(outputDir, inputDir, inputFile); 153 | transformFile(function, variantName, filePath, inputFile, out); 154 | } 155 | break; 156 | case REMOVED: 157 | File outputFile = toOutputFile(outputDir, inputDir, inputFile); 158 | FileUtils.deleteIfExists(outputFile); 159 | break; 160 | } 161 | } 162 | } else { 163 | for (File input : FileUtils.getAllFiles(inputDir)) { 164 | if (input.getName().endsWith(SdkConstants.DOT_CLASS)) { 165 | String filePath = FileUtils.relativePossiblyNonExistingPath(input, inputDir); 166 | File out = toOutputFile(outputDir, inputDir, input); 167 | transformFile(function, variantName, filePath, input, out); 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | private Consumer loadTransformFunction(Class clazz) { 176 | Consumer consumer = null; 177 | try { 178 | consumer = clazz.asSubclass(Consumer.class).getConstructor(Project.class).newInstance(project); 179 | } catch (Exception e) { 180 | try { 181 | consumer = clazz.asSubclass(Consumer.class).newInstance(); 182 | } catch (Exception e1) { 183 | 184 | } 185 | } 186 | 187 | if (consumer == null) { 188 | throw new IllegalStateException( 189 | "Custom transform does not provide a BiConsumer to apply"); 190 | } 191 | 192 | Consumer uncheckedFunction = consumer; 193 | // Validate the generic arguments are valid: 194 | Type[] types = uncheckedFunction.getClass().getGenericInterfaces(); 195 | for (Type type : types) { 196 | if (type instanceof ParameterizedType) { 197 | ParameterizedType generic = (ParameterizedType) type; 198 | Type[] args = generic.getActualTypeArguments(); 199 | if (generic.getRawType().equals(Consumer.class) 200 | && args.length == 2 201 | && args[0].equals(InputStream.class) 202 | && args[1].equals(OutputStream.class)) { 203 | return (Consumer) uncheckedFunction; 204 | } 205 | } 206 | } 207 | throw new IllegalStateException( 208 | "Custom transform must provide a BiConsumer"); 209 | } 210 | 211 | private void transformJar( 212 | Consumer function, String variantName, File inputJar, File outputJar) 213 | throws IOException { 214 | Files.createParentDirs(outputJar); 215 | try (FileInputStream fis = new FileInputStream(inputJar); 216 | ZipInputStream zis = new ZipInputStream(fis); 217 | FileOutputStream fos = new FileOutputStream(outputJar); 218 | ZipOutputStream zos = new ZipOutputStream(fos)) { 219 | ZipEntry entry = zis.getNextEntry(); 220 | while (entry != null && !entry.getName().contains("../")) { 221 | if (!entry.isDirectory() && entry.getName().endsWith(SdkConstants.DOT_CLASS)) { 222 | zos.putNextEntry(new ZipEntry(entry.getName())); 223 | apply(function, variantName, entry.getName(), zis, zos); 224 | } else { 225 | // Do not copy resources 226 | } 227 | entry = zis.getNextEntry(); 228 | } 229 | } 230 | } 231 | 232 | private void transformFile( 233 | Consumer function, String variantName, String path, File inputFile, File outputFile) 234 | throws IOException { 235 | Files.createParentDirs(outputFile); 236 | try (FileInputStream fis = new FileInputStream(inputFile); 237 | FileOutputStream fos = new FileOutputStream(outputFile)) { 238 | apply(function, variantName, path, fis, fos); 239 | } 240 | } 241 | 242 | @NonNull 243 | private static File toOutputFile(File outputDir, File inputDir, File inputFile) { 244 | return new File(outputDir, FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir)); 245 | } 246 | 247 | private void apply( 248 | Consumer function, String variantName, String path, InputStream input, OutputStream out) 249 | throws IOException { 250 | try { 251 | function.accept(variantName, path, input, out); 252 | } catch (UncheckedIOException e) { 253 | throw e.getCause(); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/DynamicPatchRedirectTransform.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | import javassist.ClassPool 4 | import org.gradle.api.Project 5 | 6 | /** 7 | * 转换dynamic patch下载url 8 | * @author lizhangqu 9 | * @version V1.0 10 | * @since 2019-03-14 14:06 11 | */ 12 | class DynamicPatchRedirectTransform implements Consumer { 13 | 14 | private Project project 15 | 16 | DynamicPatchRedirectTransform(Project project) { 17 | this.project = project 18 | } 19 | 20 | @Override 21 | void accept(String variantName, String path, InputStream inputStream, OutputStream outputStream) { 22 | if (path?.contains("io/flutter/view/ResourceUpdater.class") && project.getPlugins().hasPlugin("com.android.application")) { 23 | 24 | ClassPool classPool = new ClassPool(true) 25 | TransformHelper.updateClassPath(classPool, project, variantName) 26 | 27 | def ctClass = classPool.makeClass(inputStream, false) 28 | if (ctClass.isFrozen()) { 29 | ctClass.defrost() 30 | } 31 | FlutterTransformExtension flutterTransformExtension = project.getExtensions().findByType(FlutterTransformExtension.class) 32 | if (classPool.getOrNull(flutterTransformExtension.patchClass) == null) { 33 | project.logger.error("${flutterTransformExtension.patchClass} is not in classpath, just ignore.") 34 | TransformHelper.copy(inputStream, outputStream) 35 | return 36 | } 37 | 38 | def downloadUrlctMethod = ctClass.getDeclaredMethod("buildUpdateDownloadURL") 39 | if (downloadUrlctMethod != null) { 40 | downloadUrlctMethod.setBody(""" 41 | { 42 | return ${flutterTransformExtension.patchClass}.${flutterTransformExtension.downloadUrlMethod}(context); 43 | } 44 | """) 45 | } 46 | 47 | def downloadModectMethod = ctClass.getDeclaredMethod("getDownloadMode") 48 | if (downloadModectMethod != null) { 49 | downloadModectMethod.setBody(""" 50 | { 51 | try { 52 | return io.flutter.view.ResourceUpdater.DownloadMode.valueOf(${ 53 | flutterTransformExtension.patchClass 54 | }.${ 55 | flutterTransformExtension.downloadModeMethod 56 | }(context)); 57 | } catch (Exception e) { 58 | return io.flutter.view.ResourceUpdater.DownloadMode.ON_RESTART; 59 | } 60 | } 61 | """) 62 | } 63 | 64 | def installModectMethod = ctClass.getDeclaredMethod("getInstallMode") 65 | if (installModectMethod != null) { 66 | installModectMethod.setBody(""" 67 | { 68 | try { 69 | return io.flutter.view.ResourceUpdater.InstallMode.valueOf(${ 70 | flutterTransformExtension.patchClass 71 | }.${ 72 | flutterTransformExtension.installModeMethod 73 | }(context)); 74 | } catch (Exception e) { 75 | return io.flutter.view.ResourceUpdater.InstallMode.ON_NEXT_RESTART; 76 | } 77 | } 78 | """) 79 | } 80 | 81 | TransformHelper.copy(new ByteArrayInputStream(ctClass.toBytecode()), outputStream) 82 | } else { 83 | TransformHelper.copy(inputStream, outputStream) 84 | } 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/FlutterPatchPlugin.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.Project 5 | import org.gradle.api.Plugin 6 | 7 | /** 8 | * @author lizhangqu 9 | * @version V1.0 10 | * @since 2019-03-22 12:52 11 | */ 12 | class FlutterPatchPlugin implements Plugin { 13 | 14 | @Override 15 | void apply(Project project) { 16 | if (!project.getPlugins().hasPlugin('com.android.application')) { 17 | throw new GradleException('apply plugin: \'com.android.application\' is required') 18 | } 19 | project.android.applicationVariants.all { def variant -> 20 | def taskConfiguration = new FlutterPatchTask.ConfigAction(project, variant) 21 | project.getTasks().create(taskConfiguration.getName(), taskConfiguration.getType(), taskConfiguration) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/FlutterPatchTask.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | import com.android.build.gradle.internal.dsl.SigningConfig 4 | import com.android.builder.model.Version 5 | import com.android.ide.common.signing.KeystoreHelper 6 | import com.google.common.base.Optional 7 | import com.google.common.base.Preconditions; 8 | import com.android.sdklib.BuildToolInfo 9 | import com.google.gson.Gson 10 | import com.google.gson.GsonBuilder 11 | import org.apache.commons.io.FileUtils 12 | import org.gradle.api.DefaultTask 13 | import org.gradle.api.GradleException 14 | import org.gradle.api.Project 15 | import org.gradle.api.tasks.InputFile 16 | import org.gradle.api.tasks.OutputDirectory 17 | import org.gradle.api.tasks.OutputFile 18 | import org.gradle.api.tasks.TaskAction 19 | import org.gradle.util.GFileUtils 20 | import org.zeroturnaround.zip.ZipUtil 21 | 22 | import java.security.PrivateKey 23 | import java.security.cert.X509Certificate 24 | import java.util.concurrent.LinkedBlockingDeque 25 | import java.util.concurrent.ThreadPoolExecutor 26 | import java.util.concurrent.TimeUnit 27 | import java.util.regex.Matcher 28 | import java.util.regex.Pattern 29 | import java.util.zip.CRC32 30 | import java.util.zip.ZipEntry 31 | import java.util.zip.ZipFile 32 | 33 | /** 34 | * flutter patch 生成具体实现 35 | * @author lizhangqu 36 | * @version V1.0 37 | * @since 2019-03-19 13:52 38 | */ 39 | class FlutterPatchTask extends DefaultTask { 40 | 41 | def variant 42 | 43 | @InputFile 44 | File baseLineApk 45 | @InputFile 46 | File apkFile 47 | 48 | @OutputDirectory 49 | File patchDir 50 | @OutputFile 51 | File patchFile 52 | 53 | @TaskAction 54 | void doFullTaskAction() { 55 | ZipFile newApk = new ZipFile(apkFile) 56 | ZipFile oldApk = new ZipFile(baseLineApk) 57 | GFileUtils.deleteQuietly(patchDir) 58 | GFileUtils.mkdirs(patchDir) 59 | 60 | boolean ignoreChanges = false 61 | if (project.hasProperty("ignoreChanges")) { 62 | ignoreChanges = project.property("ignoreChanges")?.toBoolean() 63 | } 64 | 65 | newApk.entries().each { ZipEntry newFile -> 66 | if (newFile.isDirectory()) { 67 | return 68 | } 69 | // Ignore changes to signature manifests. 70 | if (newFile.getName().startsWith('META-INF/')) { 71 | return 72 | } 73 | 74 | ZipEntry oldFile = oldApk.getEntry(newFile.getName()) 75 | if (oldFile != null && oldFile.crc == newFile.crc) { 76 | return 77 | } 78 | 79 | 80 | boolean usesAot = variant.getName() == 'profile' || variant.getName() == 'release' 81 | // Only allow certain changes. 82 | if (!newFile.getName().startsWith('assets/') && 83 | !(usesAot && newFile.getName().endsWith('.so'))) { 84 | if (ignoreChanges) { 85 | return 86 | } 87 | throw new GradleException("Error: Dynamic patching doesn't support changes to ${newFile.getName()}.") 88 | } 89 | 90 | final String name = newFile.getName() 91 | if (name.contains("_snapshot_") || name.endsWith(".so")) { 92 | byte[] oldBytes = oldApk.getInputStream(new ZipEntry(name)).bytes 93 | byte[] newBytes = newApk.getInputStream(new ZipEntry(name)).bytes 94 | FileUtils.writeByteArrayToFile(new File(patchDir, name + '.bzdiff40'), BSDiff.bsdiff(oldBytes, newBytes)) 95 | } else { 96 | FileUtils.writeByteArrayToFile(new File(patchDir, name), newApk.getInputStream(newFile).bytes) 97 | } 98 | 99 | } 100 | 101 | final List checksumFiles = [ 102 | 'assets/isolate_snapshot_data', 103 | 'assets/isolate_snapshot_instr', 104 | 'assets/flutter_assets/isolate_snapshot_data', 105 | ] 106 | CRC32 checksum = new CRC32() 107 | for (String fn in checksumFiles) { 108 | final ZipEntry oldFile = oldApk.getEntry(fn) 109 | if (oldFile != null) { 110 | checksum.update(oldApk.getInputStream(oldFile).bytes) 111 | } 112 | } 113 | long baselineChecksum = checksum.getValue() 114 | 115 | def buildTools 116 | def androidBuilder 117 | def variantData = variant.getMetaClass().getProperty(variant, 'variantData') 118 | try { 119 | androidBuilder = variantData.getScope().getGlobalScope().getAndroidBuilder() 120 | buildTools = androidBuilder.getTargetInfo().getBuildTools() 121 | } catch (Exception e) { 122 | Object gs = variantData.getScope().getGlobalScope() 123 | def sdkComponents = gs.metaClass.invokeMethod(gs, "getSdkComponents", null) 124 | buildTools = sdkComponents.metaClass.getProperty(sdkComponents, "buildToolInfoProvider").get() 125 | } 126 | 127 | def stdout = new ByteArrayOutputStream() 128 | project.exec { 129 | commandLine new File(buildTools.getPath(BuildToolInfo.PathId.AAPT)), "dump", "badging", baseLineApk.getAbsolutePath() 130 | standardOutput = stdout 131 | } 132 | 133 | Pattern versionCodePattern = Pattern.compile("versionCode='(.*?)'", Pattern.MULTILINE) 134 | Matcher matcher = versionCodePattern.matcher(stdout.toString()) 135 | matcher.find() 136 | String versionCode = matcher.group(1) 137 | if (versionCode == null || versionCode.length() == 0) { 138 | throw new GradleException("versionCode can't find.") 139 | } 140 | 141 | Gson gson = new GsonBuilder().setPrettyPrinting().create() 142 | Map manifestValues = new HashMap<>() 143 | manifestValues.put("baselineChecksum", baselineChecksum) 144 | manifestValues.put("buildNumber", versionCode) 145 | manifestValues.put("patchNumber", System.currentTimeMillis()) 146 | String manifestJson = gson.toJson(manifestValues) 147 | FileUtils.writeByteArrayToFile(new File(patchDir, 'manifest.json'), manifestJson.getBytes()) 148 | ZipUtil.pack(patchDir, patchFile) 149 | 150 | SigningConfig signingConfig = variantData.getVariantConfiguration().getSigningConfig() 151 | File signOut = new File(patchFile.getParentFile(), "signed_" + patchFile.getName()) 152 | //sign patch 153 | sign(project, androidBuilder, variantData.getScope(), patchFile, signingConfig, signOut) 154 | } 155 | 156 | /** 157 | * 对zip文件签名 158 | */ 159 | public void sign(Project project, def androidBuilder, def variantScope, File unsignedInputFile, def signingConfig, File signedOutputFile) { 160 | try { 161 | androidBuilder.signApk(unsignedInputFile, signingConfig, signedOutputFile) 162 | } catch (Throwable e) { 163 | signZip(getProject(), variantScope, unsignedInputFile, signingConfig, signedOutputFile) 164 | } 165 | 166 | if (!signedOutputFile.exists()) { 167 | throw new GradleException("signed output file is not exist: ${signedOutputFile.absolutePath}") 168 | } 169 | 170 | } 171 | 172 | 173 | class Predicate implements java.util.function.Predicate, com.google.common.base.Predicate { 174 | @Override 175 | boolean apply(String string) { 176 | return false 177 | } 178 | 179 | @Override 180 | boolean test(String string) { 181 | return false 182 | } 183 | } 184 | 185 | private static T resolveEnumValue(String value, Class type) { 186 | for (T constant : type.getEnumConstants()) { 187 | if (constant.toString().equalsIgnoreCase(value)) { 188 | return constant 189 | } 190 | } 191 | return null 192 | } 193 | 194 | private void signZip(Project project, def variantScope, File inFile, def signingConfig, File outFile) throws Exception { 195 | PrivateKey key; 196 | X509Certificate certificate; 197 | boolean v1SigningEnabled; 198 | boolean v2SigningEnabled; 199 | if (signingConfig != null && signingConfig.isSigningReady()) { 200 | def certificateInfo = KeystoreHelper.getCertificateInfo( 201 | signingConfig.getStoreType(), 202 | Preconditions.checkNotNull(signingConfig.getStoreFile()), 203 | Preconditions.checkNotNull(signingConfig.getStorePassword()), 204 | Preconditions.checkNotNull(signingConfig.getKeyPassword()), 205 | Preconditions.checkNotNull(signingConfig.getKeyAlias())); 206 | key = certificateInfo.getKey(); 207 | certificate = certificateInfo.getCertificate(); 208 | v1SigningEnabled = signingConfig.isV1SigningEnabled(); 209 | v2SigningEnabled = signingConfig.isV2SigningEnabled(); 210 | } else { 211 | key = null; 212 | certificate = null; 213 | v1SigningEnabled = false; 214 | v2SigningEnabled = false; 215 | } 216 | Class creationDataClass = null 217 | Class nativeLibrariesPackagingModeClass = null 218 | Class zFileOptionsClass = null 219 | Class bestAndDefaultDeflateExecutorCompressorClass = null 220 | Class apkZFileCreatorFactoryClass = null 221 | Class byteTrackerClass = null 222 | def compressEnum = null 223 | try { 224 | creationDataClass = Class.forName('com.android.apkzlib.zfile.ApkCreatorFactory$CreationData') 225 | nativeLibrariesPackagingModeClass = Class.forName('com.android.apkzlib.zfile.NativeLibrariesPackagingMode') 226 | zFileOptionsClass = Class.forName("com.android.apkzlib.zip.ZFileOptions") 227 | bestAndDefaultDeflateExecutorCompressorClass = Class.forName("com.android.apkzlib.zip.compress.BestAndDefaultDeflateExecutorCompressor") 228 | apkZFileCreatorFactoryClass = Class.forName("com.android.apkzlib.zfile.ApkZFileCreatorFactory") 229 | byteTrackerClass = Class.forName("com.android.apkzlib.zip.utils.ByteTracker") 230 | compressEnum = resolveEnumValue("COMPRESSED", Class.forName("com.android.apkzlib.zfile.NativeLibrariesPackagingMode")) 231 | } catch (Exception e) { 232 | creationDataClass = Class.forName('com.android.tools.build.apkzlib.zfile.ApkCreatorFactory$CreationData') 233 | nativeLibrariesPackagingModeClass = Class.forName('com.android.tools.build.apkzlib.zfile.NativeLibrariesPackagingMode') 234 | zFileOptionsClass = Class.forName("com.android.tools.build.apkzlib.zip.ZFileOptions") 235 | bestAndDefaultDeflateExecutorCompressorClass = Class.forName("com.android.tools.build.apkzlib.zip.compress.BestAndDefaultDeflateExecutorCompressor") 236 | apkZFileCreatorFactoryClass = Class.forName("com.android.tools.build.apkzlib.zfile.ApkZFileCreatorFactory") 237 | byteTrackerClass = Class.forName("com.android.tools.build.apkzlib.zip.utils.ByteTracker") 238 | compressEnum = resolveEnumValue("COMPRESSED", Class.forName("com.android.tools.build.apkzlib.zfile.NativeLibrariesPackagingMode")) 239 | 240 | } 241 | 242 | 243 | def creationDataConstructor = null 244 | try { 245 | creationDataConstructor = creationDataClass.getDeclaredConstructor( 246 | File.class, 247 | Class.forName("com.google.common.base.Optional"), 248 | String.class, 249 | String.class, 250 | nativeLibrariesPackagingModeClass, 251 | Class.forName("com.google.common.base.Predicate") 252 | ) 253 | } catch (Exception e) { 254 | try { 255 | creationDataConstructor = creationDataClass.getDeclaredConstructor( 256 | File.class, 257 | PrivateKey.class, 258 | X509Certificate.class, 259 | boolean.class, 260 | boolean.class, 261 | String.class, 262 | String.class, 263 | int.class, 264 | nativeLibrariesPackagingModeClass, 265 | Class.forName("com.google.common.base.Predicate") 266 | ) 267 | } catch (Exception e1) { 268 | try { 269 | creationDataConstructor = creationDataClass.getDeclaredConstructor( 270 | File.class, 271 | PrivateKey.class, 272 | X509Certificate.class, 273 | boolean.class, 274 | boolean.class, 275 | String.class, 276 | String.class, 277 | int.class, 278 | nativeLibrariesPackagingModeClass, 279 | Class.forName("java.util.function.Predicate") 280 | ) 281 | creationDataConstructor.setAccessible(true) 282 | } catch (Exception e2) { 283 | 284 | } 285 | } 286 | } 287 | 288 | 289 | def creationData = null 290 | if (creationDataConstructor == null) { 291 | //agp 3.5.0+ 292 | Class signingOptionsClass = Class.forName("com.android.tools.build.apkzlib.sign.SigningOptions") 293 | def signingOptions = signingOptionsClass.metaClass.invokeMethod(signingOptionsClass, "builder", null) 294 | .setKey(key) 295 | .setCertificates(certificate) 296 | .setV1SigningEnabled(v1SigningEnabled) 297 | .setV2SigningEnabled(v2SigningEnabled) 298 | .setMinSdkVersion(variantScope.getMinSdkVersion().getApiLevel()) 299 | .build() 300 | creationData = creationDataClass.metaClass.invokeMethod(creationDataClass, "builder", null) 301 | .setApkPath(outFile) 302 | .setSigningOptions(signingOptions) 303 | .setBuiltBy(null) 304 | .setCreatedBy("Android Gradle " + Version.ANDROID_GRADLE_PLUGIN_VERSION) 305 | .setNativeLibrariesPackagingMode(compressEnum) 306 | .setNoCompressPredicate(new Predicate()) 307 | .build() 308 | } else { 309 | try { 310 | Class signingOptions = Class.forName("com.android.tools.build.apkzlib.sign.SigningOptions") 311 | Optional optional = Optional.of(signingOptions.metaClass.invokeMethod(signingOptions, "builder", null) 312 | .setKey(key) 313 | .setCertificates(certificate) 314 | .setV1SigningEnabled(v1SigningEnabled) 315 | .setV2SigningEnabled(v2SigningEnabled) 316 | .setMinSdkVersion(variantScope.getMinSdkVersion().getApiLevel()) 317 | .build()) 318 | creationData = creationDataConstructor.newInstance(outFile, 319 | optional, 320 | null, 321 | "Android Gradle " + Version.ANDROID_GRADLE_PLUGIN_VERSION, 322 | compressEnum, 323 | new Predicate()) 324 | } catch (Exception e) { 325 | e.printStackTrace() 326 | creationData = creationDataConstructor.newInstance(outFile, 327 | key, 328 | certificate, 329 | v1SigningEnabled, 330 | v2SigningEnabled, 331 | null, 332 | "Android Gradle " + Version.ANDROID_GRADLE_PLUGIN_VERSION, 333 | variantScope.getMinSdkVersion().getApiLevel(), 334 | compressEnum, 335 | new Predicate()) 336 | } 337 | } 338 | 339 | def signedJarBuilder 340 | try { 341 | boolean keepTimestamps = false 342 | if (project.hasProperty("android.keepTimestampsInApk")) { 343 | Object value = project.property("android.keepTimestampsInApk"); 344 | if (value instanceof String) { 345 | keepTimestamps = Boolean.parseBoolean((String) value); 346 | } else if (value instanceof Boolean) { 347 | keepTimestamps = ((Boolean) value); 348 | } 349 | } 350 | def options = zFileOptionsClass.newInstance(); 351 | options.setNoTimestamps(!keepTimestamps); 352 | options.setCoverEmptySpaceUsingExtraField(true); 353 | ThreadPoolExecutor compressionExecutor = 354 | new ThreadPoolExecutor( 355 | 0, /* Number of always alive threads */ 356 | 2, 357 | 100, 358 | TimeUnit.MILLISECONDS, 359 | new LinkedBlockingDeque<>()); 360 | 361 | def compress = null 362 | try { 363 | compress = bestAndDefaultDeflateExecutorCompressorClass.getConstructor(Class.forName("java.util.concurrent.Executor"), double.class).newInstance(compressionExecutor, 1.0D) 364 | } catch (Exception e) { 365 | compress = bestAndDefaultDeflateExecutorCompressorClass.getConstructor(Class.forName("java.util.concurrent.Executor"), byteTrackerClass, double.class).newInstance(compressionExecutor, options.getTracker(), 1.0D) 366 | } 367 | 368 | options.setCompressor(compress); 369 | options.setAutoSortFiles(true); 370 | def factory = apkZFileCreatorFactoryClass.getConstructor(zFileOptionsClass).newInstance(options) 371 | signedJarBuilder = factory.make(creationData) 372 | signedJarBuilder.writeZip( 373 | inFile, 374 | null, 375 | null 376 | ); 377 | } finally { 378 | if (signedJarBuilder) { 379 | signedJarBuilder.close() 380 | } 381 | } 382 | } 383 | 384 | 385 | public static class ConfigAction extends TaskConfiguration { 386 | private def variant 387 | 388 | ConfigAction(Project project, def variant) { 389 | super(project) 390 | this.variant = variant 391 | } 392 | 393 | @Override 394 | String getName() { 395 | return "assemble${this.variant.getName().capitalize()}FlutterPatch" 396 | } 397 | 398 | @Override 399 | void execute(FlutterPatchTask task) { 400 | task.setGroup("flutter") 401 | task.setDescription("generate flutter patch file") 402 | task.variant = this.variant 403 | task.dependsOn project.tasks.findByName("assemble${this.variant.getName().capitalize()}") 404 | 405 | File baseLineApk = null 406 | if (project.hasProperty("baselineApk")) { 407 | String baseline = project.property("baselineApk") 408 | if (baseline.contains(":")) { 409 | try { 410 | //maybe maven 411 | def dependency = project.getDependencies().create(baseline) 412 | def configuration = project.getConfigurations().detachedConfiguration(dependency) 413 | configuration.setTransitive(false) 414 | baseLineApk = configuration.getSingleFile() 415 | } catch (Exception e) { 416 | baseLineApk = new File(baseline) 417 | } 418 | } else { 419 | baseLineApk = new File(baseline) 420 | } 421 | } 422 | if (baseLineApk == null) { 423 | baseLineApk = new File(project.projectDir, "baseline.apk") 424 | } 425 | task.baseLineApk = baseLineApk 426 | task.apkFile = new File(project.buildDir, "outputs/apk/${variant.getDirName()}/${project.name}-${this.variant.getName()}.apk") 427 | task.patchDir = new File(task.apkFile.getParentFile(), "patch") 428 | task.patchFile = new File(task.apkFile.getParentFile(), "flutter_patch.zip") 429 | 430 | } 431 | } 432 | } -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/FlutterTransformExtension.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | /** 4 | * @author lizhangqu 5 | * @version V1.0 6 | * @since 2019-03-22 12:52 7 | */ 8 | class FlutterTransformExtension { 9 | String patchClass = "io.github.lizhangqu.flutter.FlutterUpdate" 10 | String downloadUrlMethod = "getDownloadURL" 11 | String installModeMethod = "getInstallMode" 12 | String downloadModeMethod = "getDownloadMode" 13 | } -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/FlutterTransformPlugin.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | import com.android.build.gradle.BaseExtension 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | 7 | /** 8 | * @author lizhangqu 9 | * @version V1.0 10 | * @since 2019-03-22 12:52 11 | */ 12 | class FlutterTransformPlugin implements Plugin { 13 | 14 | @Override 15 | void apply(Project project) { 16 | BaseExtension android = project.getExtensions().getByType(BaseExtension.class) 17 | def transform = new CustomClassTransform(project, DynamicPatchRedirectTransform.class) 18 | android.registerTransform(transform) 19 | 20 | project.getExtensions().create("flutterTransform", FlutterTransformExtension.class) 21 | 22 | } 23 | } -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/TaskConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | import com.android.annotations.NonNull 4 | import org.gradle.api.Action 5 | import org.gradle.api.Project 6 | 7 | import java.lang.reflect.ParameterizedType 8 | import java.lang.reflect.Type 9 | 10 | /** 11 | * @author lizhangqu 12 | * @version V1.0 13 | * @since 2019-02-28 20:08 14 | */ 15 | 16 | 17 | public abstract class TaskConfiguration implements Action { 18 | protected Project project 19 | 20 | public TaskConfiguration(Project project) { 21 | this.project = project 22 | } 23 | /** 24 | * Return the name of the task to be configured. 25 | */ 26 | @NonNull 27 | abstract String getName(); 28 | 29 | /** 30 | * Return the class type of the task to be configured. 31 | */ 32 | Class getType() { 33 | Type rawType = null; 34 | Type type = this.getClass().getGenericSuperclass(); 35 | if (type instanceof ParameterizedType) { 36 | Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments(); 37 | if (actualTypeArguments != null && actualTypeArguments.length > 0) { 38 | rawType = actualTypeArguments[0]; 39 | } 40 | } else { 41 | Type[] genericInterfaces = this.getClass().getGenericInterfaces(); 42 | if (genericInterfaces != null && genericInterfaces.length > 0) { 43 | Type[] actualTypeArguments = ((ParameterizedType) genericInterfaces[0]).getActualTypeArguments(); 44 | if (actualTypeArguments != null && actualTypeArguments.length > 0) { 45 | rawType = actualTypeArguments[0]; 46 | } 47 | } 48 | } 49 | return rawType 50 | } 51 | 52 | /** 53 | * Configure the given newly-created task object. 54 | */ 55 | @Override 56 | abstract void execute(@NonNull T task); 57 | } -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io/github/lizhangqu/flutter/TransformHelper.groovy: -------------------------------------------------------------------------------- 1 | package io.github.lizhangqu.flutter 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.android.build.gradle.api.BaseVariant 5 | import javassist.ClassPool 6 | import org.gradle.api.GradleException 7 | import org.gradle.api.Project 8 | 9 | /** 10 | * 11 | * @author lizhangqu 12 | * @version V1.0 13 | * @since 2019-03-14 14:11 14 | */ 15 | public class TransformHelper { 16 | 17 | public static String getAndroidGradlePluginVersionCompat() { 18 | String version = null 19 | try { 20 | Class versionModel = Class.forName("com.android.builder.model.Version") 21 | def versionFiled = versionModel.getDeclaredField("ANDROID_GRADLE_PLUGIN_VERSION") 22 | versionFiled.setAccessible(true) 23 | version = versionFiled.get(null) 24 | } catch (Exception e) { 25 | 26 | } 27 | return version 28 | } 29 | 30 | /** 31 | * 反射获取枚举类,静态方法 32 | */ 33 | public static T resolveEnumValue(String value, Class type) { 34 | for (T constant : type.getEnumConstants()) { 35 | if (constant.toString().equalsIgnoreCase(value)) { 36 | return constant 37 | } 38 | } 39 | return null 40 | } 41 | 42 | 43 | public static def getCompileLibraries(Project project, String variantName) { 44 | 45 | BaseVariant foundVariant = null 46 | 47 | def variants = null; 48 | if (project.plugins.hasPlugin('com.android.application')) { 49 | variants = project.android.getApplicationVariants() 50 | } else if (project.plugins.hasPlugin('com.android.library')) { 51 | variants = project.android.getLibraryVariants() 52 | } 53 | 54 | variants?.all { BaseVariant variant -> 55 | if (variant.getName() == variantName) { 56 | foundVariant = variant 57 | } 58 | } 59 | if (foundVariant == null) { 60 | throw new GradleException("variant ${variantName} not found") 61 | } 62 | 63 | def variantData = foundVariant.getMetaClass().getProperty(foundVariant, 'variantData') 64 | 65 | if (getAndroidGradlePluginVersionCompat() >= '3.0.0') { 66 | Object compileClasspath = resolveEnumValue("COMPILE_CLASSPATH", Class.forName('com.android.build.gradle.internal.publishing.AndroidArtifacts$ConsumedConfigType')) 67 | Object allArtifactScope = resolveEnumValue("ALL", Class.forName('com.android.build.gradle.internal.publishing.AndroidArtifacts$ArtifactScope')) 68 | Object classes = resolveEnumValue("CLASSES", Class.forName('com.android.build.gradle.internal.publishing.AndroidArtifacts$ArtifactType')) 69 | return variantData.getScope().getArtifactFileCollection(compileClasspath, allArtifactScope, classes) 70 | } else { 71 | try { 72 | return foundVariant.getCompileLibraries() 73 | } catch (Exception e) { 74 | //maybe with com.android.library for aar 75 | return variantData.getScope().getGlobalScope().getAndroidBuilder().getCompileClasspath(variantData.getVariantConfiguration()); 76 | } 77 | } 78 | } 79 | 80 | 81 | public static void updateClassPath(ClassPool classPool, Project project, String variantName) { 82 | getCompileLibraries(project, variantName)?.each { 83 | try { 84 | classPool.insertClassPath(it.absolutePath) 85 | } catch (Exception e) { 86 | } 87 | } 88 | try { 89 | def javacTask = project.tasks.findByName("compile${variantName.capitalize()}JavaWithJavac") 90 | if(javacTask){ 91 | classPool.insertClassPath(javacTask.getDestinationDir().getAbsolutePath()) 92 | } 93 | } catch (Exception e) { 94 | } 95 | 96 | try { 97 | def kotlinTask = project.tasks.findByName("compile${variantName.capitalize()}Kotlin") 98 | if(kotlinTask){ 99 | classPool.insertClassPath(kotlinTask.getDestinationDir().getAbsolutePath()) 100 | } 101 | } catch (Exception e) { 102 | } 103 | 104 | AppExtension android = project.getExtensions().getByType(AppExtension.class) 105 | android?.getBootClasspath()?.each { 106 | try { 107 | classPool.insertClassPath(it.absolutePath) 108 | } catch (Exception e) { 109 | } 110 | } 111 | 112 | 113 | BaseVariant foundVariant = null 114 | 115 | def variants = null; 116 | if (project.plugins.hasPlugin('com.android.application')) { 117 | variants = project.android.getApplicationVariants() 118 | } else if (project.plugins.hasPlugin('com.android.library')) { 119 | variants = project.android.getLibraryVariants() 120 | } 121 | 122 | variants?.all { BaseVariant variant -> 123 | if (variant.getName() == variantName) { 124 | foundVariant = variant 125 | } 126 | } 127 | 128 | if (foundVariant != null) { 129 | try { 130 | def variantData = foundVariant.getMetaClass().getProperty(foundVariant, 'variantData') 131 | variantData?.getScope()?.getTryWithResourceRuntimeSupportJar()?.each { 132 | try { 133 | classPool.insertClassPath(it.absolutePath) 134 | } catch (Exception e) { 135 | } 136 | } 137 | } catch (Exception e1) { 138 | 139 | } 140 | 141 | } 142 | 143 | } 144 | 145 | public static void copy(InputStream inputStream, OutputStream outputStream) { 146 | int length = -1 147 | byte[] buffer = new byte[4096] 148 | while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) { 149 | outputStream.write(buffer, 0, length) 150 | outputStream.flush() 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.patch.properties: -------------------------------------------------------------------------------- 1 | implementation-class=io.github.lizhangqu.flutter.FlutterPatchPlugin -------------------------------------------------------------------------------- /buildSrc/src/main/resources/META-INF/gradle-plugins/flutter.transform.properties: -------------------------------------------------------------------------------- 1 | implementation-class=io.github.lizhangqu.flutter.FlutterTransformPlugin -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableBuildScriptClasspathCheck=false 2 | 3 | release.bintray=true 4 | #flutter.sdk=/Users/lizhangqu/Library/Flutter/v1.0.0/flutter -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizhangqu/plugin-flutter-patch/e19625224c0bb781c9a25ba5ec356a7af5c64f6a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Oct 13 13:54:35 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-4.6-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':buildSrc' 2 | include ':app' 3 | --------------------------------------------------------------------------------