├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── markdown-navigator-enh.xml ├── markdown-navigator.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jyuesong │ │ └── flutter_hotpatchdemo │ │ ├── App.java │ │ ├── MainActivity.java │ │ └── PatchActivity.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 ├── art └── result.gif ├── build.gradle ├── flutter_hotpatch_module_f ├── .gitignore ├── .metadata ├── README.md ├── assets │ └── icon_second.png ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hotpatch ├── .gitignore ├── .idea │ ├── codeStyles │ │ └── Project.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ ├── runConfigurations.xml │ └── workspace.xml ├── build.gradle ├── consumer-rules.pro ├── local.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jyuesong │ │ └── flutter_hotpatch │ │ ├── FileUtils.java │ │ ├── FlutterHotPatchMain.java │ │ ├── FlutterHotPatchManager.java │ │ ├── activity │ │ └── HotPatchFlutterActivity.java │ │ └── loader │ │ ├── FlutterHotPatchLoader.java │ │ ├── ResourceCleaner.java │ │ ├── ResourceExtractor.java │ │ └── ResourcePaths.java │ └── res │ └── values │ └── strings.xml ├── key.jks ├── patch ├── app-release.apk ├── hotpatch-resource.zip └── libapp_fix.so └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 实验性项目,勿用于生产环境 2 | 3 | 效果: 4 | 5 | ![](https://github.com/jiang111/flutter_hotpatch/raw/master/art/result.gif) 6 | 7 | DEMO: 8 | > 1.下载patch文件夹中的app-release.apk,然后安装至手机,赋予相关的权限 9 | 10 | > 2.将libapp_fix.so和hotpatch-resource.zip拷贝至sd卡的根目录 11 | 12 | > 3.点击app中的启动老版本可查看老版本的内容,然后将app进程杀掉,再打开app,点击热更新,可实现sd卡中的libapp_fix.so和hotpatch-resource.zip动态加载 13 | 14 | 15 | 已经实现的功能: 16 | 17 | > 1.已实现flutter module中的代码和资源的热更新 18 | 19 | 已知问题: 20 | > 1.只能下发flutter代码,不能下发Android代码(这也不算问题) 21 | 22 | > 2.flutter engine版本最好一致 23 | 24 | > 3.点击热更新时必须保证fluttr engine没有初始化过,也就是app运行时没有初始化过flutter engine 25 | 26 | 原理: 27 | > 1.资源的热更新是通过Android自带的AssetManager,通过反射它的addAssetPath方法来将hotpatch-resource.zip中的资源实现累加 28 | 29 | > 2.dart代码层面的热更新通过生成的libapp_fix.so(so文件里包含的就是所有业务代码),修改FlutterLoader中的aotSharedLibraryFile字段实现,具体请参考源码 30 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.jyuesong.flutter_hotpatchdemo" 7 | minSdkVersion 17 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | ndk { 12 | //设置支持的SO库架构 13 | abiFilters 'armeabi-v7a' 14 | } 15 | 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_8 25 | targetCompatibility JavaVersion.VERSION_1_8 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation fileTree(dir: 'libs', include: ['*.jar']) 31 | implementation 'com.android.support:appcompat-v7:28.0.0' 32 | implementation project(path: ':hotpatch') 33 | implementation 'com.jyuesong.android.flutter:flutter_release:1.0' 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":1,"versionName":"1.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/jyuesong/flutter_hotpatchdemo/App.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatchdemo; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | import android.os.Environment; 7 | 8 | import com.jyuesong.flutter_hotpatch.FlutterHotPatchManager; 9 | 10 | import java.io.File; 11 | import java.lang.reflect.Method; 12 | 13 | import io.flutter.embedding.android.FlutterActivity; 14 | import io.flutter.embedding.engine.FlutterEngine; 15 | 16 | /** 17 | * created by NewTab 2020/4/29 18 | */ 19 | public class App extends Application { 20 | 21 | 22 | @Override 23 | public void onCreate() { 24 | super.onCreate(); 25 | 26 | registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { 27 | @Override 28 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 29 | 30 | } 31 | 32 | @Override 33 | public void onActivityStarted(Activity activity) { 34 | // if (activity instanceof FlutterActivity) { 35 | // try { 36 | // FlutterActivity flutterActivity = (FlutterActivity) activity; 37 | // Method flutterEngineMethod = FlutterActivity.class.getDeclaredMethod("getFlutterEngine"); 38 | // flutterEngineMethod.setAccessible(true); 39 | // 40 | // FlutterEngine flutterEngine = (FlutterEngine) flutterEngineMethod.invoke(flutterActivity); 41 | // FlutterHotPatchManager.patchResource(activity, flutterEngine, Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "hotpatch-resource.zip"); 42 | // } catch (Exception e) { 43 | // e.printStackTrace(); 44 | // } 45 | // } 46 | } 47 | 48 | @Override 49 | public void onActivityResumed(Activity activity) { 50 | 51 | } 52 | 53 | @Override 54 | public void onActivityPaused(Activity activity) { 55 | 56 | } 57 | 58 | @Override 59 | public void onActivityStopped(Activity activity) { 60 | 61 | } 62 | 63 | @Override 64 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 65 | 66 | } 67 | 68 | @Override 69 | public void onActivityDestroyed(Activity activity) { 70 | 71 | } 72 | }); 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/jyuesong/flutter_hotpatchdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatchdemo; 2 | 3 | 4 | import android.Manifest; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.os.Bundle; 8 | import android.os.Environment; 9 | import android.support.v4.app.ActivityCompat; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.view.View; 12 | import android.widget.TextView; 13 | import android.widget.Toast; 14 | 15 | import com.jyuesong.flutter_hotpatch.FlutterHotPatchManager; 16 | 17 | import io.flutter.embedding.android.FlutterActivity; 18 | 19 | public class MainActivity extends AppCompatActivity { 20 | private static String[] PERMISSIONS = { 21 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 22 | Manifest.permission.READ_EXTERNAL_STORAGE 23 | }; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_main); 29 | 30 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED 31 | || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 32 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 33 | || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { 34 | 35 | } else { 36 | // 联系人权限尚未被授予,直接请求 37 | ActivityCompat.requestPermissions(this, PERMISSIONS, 300); 38 | } 39 | } 40 | TextView fix = findViewById(R.id.fix); 41 | TextView start = findViewById(R.id.start); 42 | start.setOnClickListener(new View.OnClickListener() { 43 | @Override 44 | public void onClick(View v) { 45 | 46 | startActivity(FlutterActivity.withNewEngine().build(MainActivity.this)); 47 | 48 | } 49 | }); 50 | 51 | fix.setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | 55 | FlutterHotPatchManager.patch(MainActivity.this, 56 | Environment.getExternalStorageDirectory().getAbsolutePath(), "libapp_fix.so", new FlutterHotPatchManager.PatchInterface() { 57 | @Override 58 | public void success() { 59 | Toast.makeText(MainActivity.this, "修复成功", Toast.LENGTH_SHORT).show(); 60 | startActivity(new Intent(MainActivity.this, PatchActivity.class)); 61 | 62 | } 63 | 64 | @Override 65 | public void fail(String reason) { 66 | Toast.makeText(MainActivity.this, reason, Toast.LENGTH_SHORT).show(); 67 | 68 | } 69 | }); 70 | 71 | } 72 | }); 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/jyuesong/flutter_hotpatchdemo/PatchActivity.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatchdemo; 2 | 3 | import android.os.Environment; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.jyuesong.flutter_hotpatch.FlutterHotPatchManager; 7 | 8 | import java.io.File; 9 | 10 | import io.flutter.embedding.android.FlutterActivity; 11 | import io.flutter.embedding.android.FlutterEngineConfigurator; 12 | import io.flutter.embedding.engine.FlutterEngine; 13 | 14 | /** 15 | * created by NewTab 2020/5/29 16 | */ 17 | class PatchActivity extends FlutterActivity { 18 | 19 | 20 | @Override 21 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 22 | super.configureFlutterEngine(flutterEngine); 23 | try { 24 | FlutterHotPatchManager.patchResource(this, flutterEngine, Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "hotpatch-resource.zip"); 25 | } catch (Exception e) { 26 | e.printStackTrace(); 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | 9 | 10 | 11 | 18 | 19 | 26 | 27 | -------------------------------------------------------------------------------- /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/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | flutter_hotpatch 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /art/result.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/art/result.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.3' 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | maven { 21 | //TODO 修改为自己的目录 22 | url 'E:\\coding\\flutter_hotpatch_module_f\\build\\host\\outputs\\repo' 23 | } 24 | maven { 25 | url 'http://download.flutter.io' 26 | } 27 | } 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/README.md: -------------------------------------------------------------------------------- 1 | # flutter_hotpatch_module_f 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/assets/icon_second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/flutter_hotpatch_module_f/assets/icon_second.png -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() => runApp(MyApp()); 4 | 5 | class MyApp extends StatelessWidget { 6 | // This widget is the root of your application. 7 | @override 8 | Widget build(BuildContext context) { 9 | return MaterialApp( 10 | title: 'Flutter Demo', 11 | theme: ThemeData( 12 | // This is the theme of your application. 13 | // 14 | // Try running your application with "flutter run". You'll see the 15 | // application has a blue toolbar. Then, without quitting the app, try 16 | // changing the primarySwatch below to Colors.green and then invoke 17 | // "hot reload" (press "r" in the console where you ran "flutter run", 18 | // or simply save your changes to "hot reload" in a Flutter IDE). 19 | // Notice that the counter didn't reset back to zero; the application 20 | // is not restarted. 21 | primarySwatch: Colors.green, 22 | ), 23 | home: MyHomePage(title: 'You are the No:2 '), 24 | ); 25 | } 26 | } 27 | 28 | class MyHomePage extends StatefulWidget { 29 | MyHomePage({Key key, this.title}) : super(key: key); 30 | 31 | // This widget is the home page of your application. It is stateful, meaning 32 | // that it has a State object (defined below) that contains fields that affect 33 | // how it looks. 34 | 35 | // This class is the configuration for the state. It holds the values (in this 36 | // case the title) provided by the parent (in this case the App widget) and 37 | // used by the build method of the State. Fields in a Widget subclass are 38 | // always marked "final". 39 | 40 | final String title; 41 | 42 | @override 43 | _MyHomePageState createState() => _MyHomePageState(); 44 | } 45 | 46 | class _MyHomePageState extends State { 47 | int _counter = 0; 48 | 49 | void _incrementCounter() { 50 | setState(() { 51 | // This call to setState tells the Flutter framework that something has 52 | // changed in this State, which causes it to rerun the build method below 53 | // so that the display can reflect the updated values. If we changed 54 | // _counter without calling setState(), then the build method would not be 55 | // called again, and so nothing would appear to happen. 56 | _counter++; 57 | }); 58 | } 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | // This method is rerun every time setState is called, for instance as done 63 | // by the _incrementCounter method above. 64 | // 65 | // The Flutter framework has been optimized to make rerunning build methods 66 | // fast, so that you can just rebuild anything that needs updating rather 67 | // than having to individually change instances of widgets. 68 | return Scaffold( 69 | appBar: AppBar( 70 | // Here we take the value from the MyHomePage object that was created by 71 | // the App.build method, and use it to set our appbar title. 72 | title: Text(widget.title), 73 | ), 74 | body: Center( 75 | // Center is a layout widget. It takes a single child and positions it 76 | // in the middle of the parent. 77 | child: Column( 78 | // Column is also a layout widget. It takes a list of children and 79 | // arranges them vertically. By default, it sizes itself to fit its 80 | // children horizontally, and tries to be as tall as its parent. 81 | // 82 | // Invoke "debug painting" (press "p" in the console, choose the 83 | // "Toggle Debug Paint" action from the Flutter Inspector in Android 84 | // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) 85 | // to see the wireframe for each widget. 86 | // 87 | // Column has various properties to control how it sizes itself and 88 | // how it positions its children. Here we use mainAxisAlignment to 89 | // center the children vertically; the main axis here is the vertical 90 | // axis because Columns are vertical (the cross axis would be 91 | // horizontal). 92 | mainAxisAlignment: MainAxisAlignment.center, 93 | children: [ 94 | Text( 95 | 'You are the No:2 :', 96 | ), 97 | SizedBox( 98 | height: 15, 99 | ), 100 | Row( 101 | crossAxisAlignment: CrossAxisAlignment.center, 102 | children: [ 103 | Expanded( 104 | child: Container(), 105 | ), 106 | Image.asset( 107 | "assets/icon_second.png", 108 | ), 109 | SizedBox( 110 | width: 15, 111 | ), 112 | Text( 113 | '$_counter', 114 | style: Theme.of(context).textTheme.display1, 115 | ), 116 | Expanded( 117 | child: Container(), 118 | ), 119 | ], 120 | ) 121 | ], 122 | ), 123 | ), 124 | floatingActionButton: FloatingActionButton( 125 | onPressed: _incrementCounter, 126 | tooltip: 'Increment ', 127 | child: Icon(Icons.add), 128 | ), // This trailing comma makes auto-formatting nicer for build methods. 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.3" 60 | cupertino_icons: 61 | dependency: "direct main" 62 | description: 63 | name: cupertino_icons 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "0.1.3" 67 | flutter: 68 | dependency: "direct main" 69 | description: flutter 70 | source: sdk 71 | version: "0.0.0" 72 | flutter_test: 73 | dependency: "direct dev" 74 | description: flutter 75 | source: sdk 76 | version: "0.0.0" 77 | image: 78 | dependency: transitive 79 | description: 80 | name: image 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "2.1.4" 84 | matcher: 85 | dependency: transitive 86 | description: 87 | name: matcher 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "0.12.6" 91 | meta: 92 | dependency: transitive 93 | description: 94 | name: meta 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "1.1.8" 98 | path: 99 | dependency: transitive 100 | description: 101 | name: path 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "1.6.4" 105 | pedantic: 106 | dependency: transitive 107 | description: 108 | name: pedantic 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "1.8.0+1" 112 | petitparser: 113 | dependency: transitive 114 | description: 115 | name: petitparser 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "2.4.0" 119 | quiver: 120 | dependency: transitive 121 | description: 122 | name: quiver 123 | url: "https://pub.flutter-io.cn" 124 | source: hosted 125 | version: "2.0.5" 126 | sky_engine: 127 | dependency: transitive 128 | description: flutter 129 | source: sdk 130 | version: "0.0.99" 131 | source_span: 132 | dependency: transitive 133 | description: 134 | name: source_span 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.5.5" 138 | stack_trace: 139 | dependency: transitive 140 | description: 141 | name: stack_trace 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "1.9.3" 145 | stream_channel: 146 | dependency: transitive 147 | description: 148 | name: stream_channel 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "2.0.0" 152 | string_scanner: 153 | dependency: transitive 154 | description: 155 | name: string_scanner 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.0.5" 159 | term_glyph: 160 | dependency: transitive 161 | description: 162 | name: term_glyph 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "1.1.0" 166 | test_api: 167 | dependency: transitive 168 | description: 169 | name: test_api 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "0.2.11" 173 | typed_data: 174 | dependency: transitive 175 | description: 176 | name: typed_data 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "1.1.6" 180 | vector_math: 181 | dependency: transitive 182 | description: 183 | name: vector_math 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "2.0.8" 187 | xml: 188 | dependency: transitive 189 | description: 190 | name: xml 191 | url: "https://pub.flutter-io.cn" 192 | source: hosted 193 | version: "3.5.0" 194 | sdks: 195 | dart: ">=2.4.0 <3.0.0" 196 | -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_hotpatch_module_f 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | cupertino_icons: ^0.1.2 24 | 25 | dev_dependencies: 26 | flutter_test: 27 | sdk: flutter 28 | 29 | 30 | flutter: 31 | 32 | uses-material-design: true 33 | 34 | 35 | assets: 36 | - assets/ 37 | module: 38 | androidX: true 39 | androidPackage: com.jyuesong.android.flutter 40 | iosBundleIdentifier: com.jyuesong.ios.flutter -------------------------------------------------------------------------------- /flutter_hotpatch_module_f/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_hotpatch_module_f/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | # Automatically convert third-party libraries to use AndroidX 18 | 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 12 09:14:09 CST 2020 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-5.4.1-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 | -------------------------------------------------------------------------------- /hotpatch/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /hotpatch/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /hotpatch/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /hotpatch/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /hotpatch/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /hotpatch/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /hotpatch/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 55 | 56 | 57 | 1583981328918 58 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 1.8 99 | 100 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /hotpatch/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | defaultConfig { 7 | minSdkVersion 17 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | 12 | consumerProguardFiles 'consumer-rules.pro' 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | 26 | } 27 | repositories { 28 | 29 | maven { 30 | url 'http://download.flutter.io' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: 'libs', include: ['*.jar']) 36 | implementation 'com.llew:reflect:1.0.1' 37 | implementation 'com.android.support:appcompat-v7:28.0.0' 38 | implementation 'io.flutter:flutter_embedding_release:1.0.0-e1e6ced81d029258d449bdec2ba3cddca9c2ca0c' 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /hotpatch/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/hotpatch/consumer-rules.pro -------------------------------------------------------------------------------- /hotpatch/local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Thu Mar 12 10:48:48 CST 2020 8 | sdk.dir=D\:\\android_sdk 9 | -------------------------------------------------------------------------------- /hotpatch/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 | -------------------------------------------------------------------------------- /hotpatch/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatch; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.FileWriter; 9 | import java.nio.channels.FileChannel; 10 | 11 | /** 12 | * created by NewTab 2020/3/12 13 | */ 14 | public class FileUtils { 15 | private static final String TAG = "FileUtils"; 16 | 17 | public static void copyFile(File dir, String destFileName, File source, CopyInterface copyInterface) { 18 | new Thread(new Runnable() { 19 | @Override 20 | public void run() { 21 | if (!dir.exists()) { 22 | if (dir.mkdirs()) { 23 | Log.i(TAG, "mkdirs success: " + dir.getAbsolutePath()); 24 | } else { 25 | Log.i(TAG, "mkdirs failure: " + dir.getAbsolutePath()); 26 | } 27 | } else { 28 | Log.i(TAG, "dirs exists: " + dir.getAbsolutePath()); 29 | } 30 | try { 31 | File dest = new File(dir, destFileName); 32 | if (dest.exists() && !dest.delete()) { 33 | //存在就先清空 34 | FileWriter writer = new FileWriter(dest, false); 35 | writer.write(""); 36 | writer.flush(); 37 | writer.close(); 38 | } 39 | 40 | FileChannel inputChannel = new FileInputStream(source).getChannel(); 41 | FileChannel outputChannel = new FileOutputStream(dest).getChannel(); 42 | outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); 43 | inputChannel.close(); 44 | outputChannel.close(); 45 | 46 | Log.i(TAG, "copy file finish: " + dest.getAbsolutePath()); 47 | 48 | if (copyInterface != null) { 49 | copyInterface.success(dest); 50 | } 51 | } catch (Throwable error) { 52 | error.printStackTrace(); 53 | if (copyInterface != null) { 54 | copyInterface.fail(error.getMessage()); 55 | } 56 | } 57 | } 58 | }).start(); 59 | 60 | } 61 | 62 | public interface CopyInterface { 63 | void success(File destFile); 64 | 65 | void fail(String reason); 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/FlutterHotPatchMain.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatch; 2 | 3 | import android.content.Context; 4 | import android.os.Looper; 5 | 6 | import com.jyuesong.flutter_hotpatch.loader.FlutterHotPatchLoader; 7 | 8 | import java.io.File; 9 | 10 | import io.flutter.embedding.engine.loader.FlutterLoader; 11 | 12 | /** 13 | * created by NewTab 2020/3/12 14 | */ 15 | public class FlutterHotPatchMain { 16 | 17 | public static void startInitialization(Context context) { 18 | startInitialization(context, null); 19 | } 20 | 21 | public static void startInitialization(Context context, File aotFile) { 22 | startInitialization(context, aotFile, new FlutterLoader.Settings()); 23 | } 24 | 25 | public static void startInitialization(Context context, File aotFile, FlutterLoader.Settings settings) { 26 | if (Looper.myLooper() != Looper.getMainLooper()) return; 27 | if (aotFile == null || !aotFile.exists()) { 28 | FlutterHotPatchLoader.getInstance().startInitialization(context, settings); 29 | } else { 30 | FlutterHotPatchLoader.getInstance().startInitialization(context, aotFile, new FlutterLoader.Settings()); 31 | 32 | 33 | } 34 | 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/FlutterHotPatchManager.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatch; 2 | 3 | import android.app.Activity; 4 | import android.content.res.AssetManager; 5 | import android.os.Environment; 6 | import android.text.TextUtils; 7 | 8 | import java.io.File; 9 | import java.lang.reflect.Field; 10 | 11 | import io.flutter.embedding.engine.FlutterEngine; 12 | import io.flutter.embedding.engine.dart.DartExecutor; 13 | import io.flutter.embedding.engine.loader.FlutterLoader; 14 | import io.flutter.view.FlutterMain; 15 | 16 | /** 17 | * created by NewTab 2020/4/29 18 | */ 19 | public class FlutterHotPatchManager { 20 | 21 | 22 | public static void patchResource(Activity activity, FlutterEngine flutterEngine, String patchApkPath) throws Exception { 23 | 24 | if (patchApkPath == null) return; 25 | 26 | if (!new File(patchApkPath).exists()) return; 27 | 28 | DartExecutor dartExecutor = flutterEngine.getDartExecutor(); 29 | AssetManager assetManager = activity.getAssets(); 30 | AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, patchApkPath); 31 | Field assetsManagerField = DartExecutor.class.getDeclaredField("assetManager"); 32 | assetsManagerField.setAccessible(true); 33 | assetsManagerField.set(dartExecutor, assetManager); 34 | } 35 | 36 | 37 | public static void patch(Activity activity, String filePath, String fileName, PatchInterface patchInterface) { 38 | 39 | if (activity == null) { 40 | if (patchInterface != null) { 41 | patchInterface.fail("Context 不能为空"); 42 | } 43 | return; 44 | } 45 | if (TextUtils.isEmpty(filePath) || TextUtils.isEmpty(fileName)) { 46 | if (patchInterface != null) { 47 | patchInterface.fail("文件不能为空"); 48 | } 49 | return; 50 | } 51 | 52 | FileUtils.copyFile(activity.getApplicationContext().getFilesDir(), fileName, new File(filePath + File.separator + fileName), new FileUtils.CopyInterface() { 53 | @Override 54 | public void success(File destFile) { 55 | if (activity == null || activity.isDestroyed()) return; 56 | activity.runOnUiThread(new Runnable() { 57 | @Override 58 | public void run() { 59 | 60 | FlutterHotPatchMain.startInitialization(activity.getApplicationContext(), destFile); 61 | if (patchInterface != null) { 62 | patchInterface.success(); 63 | } 64 | } 65 | }); 66 | 67 | } 68 | 69 | @Override 70 | public void fail(String reason) { 71 | if (activity == null || activity.isDestroyed()) return; 72 | activity.runOnUiThread(new Runnable() { 73 | @Override 74 | public void run() { 75 | if (patchInterface != null) { 76 | patchInterface.fail(reason); 77 | } 78 | } 79 | }); 80 | } 81 | }); 82 | 83 | 84 | } 85 | 86 | 87 | public interface PatchInterface { 88 | void success(); 89 | 90 | void fail(String reason); 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/activity/HotPatchFlutterActivity.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatch.activity; 2 | 3 | import android.content.res.AssetManager; 4 | import android.os.Bundle; 5 | import android.os.Environment; 6 | import android.support.annotation.Nullable; 7 | 8 | import java.io.File; 9 | import java.lang.reflect.Field; 10 | 11 | import io.flutter.embedding.android.FlutterActivity; 12 | import io.flutter.embedding.engine.dart.DartExecutor; 13 | 14 | /** 15 | * created by NewTab 2020/4/29 16 | */ 17 | public class HotPatchFlutterActivity extends FlutterActivity { 18 | 19 | 20 | @Override 21 | protected void onCreate(@Nullable Bundle savedInstanceState) { 22 | 23 | super.onCreate(savedInstanceState); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/loader/FlutterHotPatchLoader.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatch.loader; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.PackageManager; 6 | import android.content.res.AssetManager; 7 | import android.os.Bundle; 8 | import android.os.Handler; 9 | import android.os.Looper; 10 | import android.os.SystemClock; 11 | import android.support.annotation.NonNull; 12 | import android.support.annotation.Nullable; 13 | import android.util.Log; 14 | import android.view.WindowManager; 15 | 16 | 17 | import com.llew.reflect.FieldUtils; 18 | 19 | import java.io.File; 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.List; 23 | 24 | import io.flutter.BuildConfig; 25 | import io.flutter.embedding.engine.FlutterJNI; 26 | import io.flutter.embedding.engine.loader.FlutterLoader; 27 | import io.flutter.util.PathUtils; 28 | import io.flutter.view.VsyncWaiter; 29 | 30 | /** 31 | * created by NewTab 2020/3/12 32 | */ 33 | public class FlutterHotPatchLoader extends FlutterLoader { 34 | 35 | private static FlutterHotPatchLoader instance; 36 | private File aotSharedLibraryFile; 37 | 38 | 39 | public static FlutterHotPatchLoader getInstance() { 40 | if (instance == null) { 41 | instance = new FlutterHotPatchLoader(); 42 | } 43 | return instance; 44 | } 45 | 46 | 47 | public void startInitialization(Context context, File aotFile, Settings settings) { 48 | aotSharedLibraryFile = aotFile; 49 | hookFlutterLoaderInstance(); 50 | FlutterLoader.getInstance().startInitialization(context, settings); 51 | } 52 | 53 | private void hookFlutterLoaderInstance() { 54 | 55 | try { 56 | if (isHookedFlutterLoader()) return; 57 | 58 | FlutterHotPatchLoader instance = FlutterHotPatchLoader.getInstance(); 59 | 60 | FieldUtils.writeStaticField(FlutterLoader.class, "instance", instance); 61 | 62 | if (isHookedFlutterLoader()) { 63 | Log.i(TAG, "Hook FlutterLoader success"); 64 | } else { 65 | Log.i(TAG, "Hook FlutterLoader failed"); 66 | 67 | } 68 | 69 | 70 | } catch (Exception e) { 71 | e.printStackTrace(); 72 | } 73 | 74 | 75 | } 76 | 77 | private boolean isHookedFlutterLoader() { 78 | return FlutterLoader.getInstance() instanceof FlutterHotPatchLoader; 79 | } 80 | 81 | private static final String TAG = "FlutterHotPatchLoader"; 82 | 83 | // Must match values in flutter::switches 84 | private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; 85 | private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; 86 | private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; 87 | private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; 88 | private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; 89 | 90 | // XML Attribute keys supported in AndroidManifest.xml 91 | private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = 92 | FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; 93 | private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = 94 | FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; 95 | private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = 96 | FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; 97 | private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = 98 | FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; 99 | 100 | // Resource names used for components of the precompiled snapshot. 101 | private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; 102 | private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; 103 | private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; 104 | private static final String DEFAULT_LIBRARY = "libflutter.so"; 105 | private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; 106 | private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; 107 | 108 | // Mutable because default values can be overridden via config properties 109 | private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; 110 | private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; 111 | private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; 112 | private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; 113 | 114 | private boolean initialized = false; 115 | @Nullable 116 | private ResourceExtractor resourceExtractor; 117 | @Nullable 118 | private FlutterLoader.Settings settings; 119 | 120 | /** 121 | * Starts initialization of the native system. 122 | * 123 | * @param applicationContext The Android application context. 124 | */ 125 | public void startInitialization(@NonNull Context applicationContext) { 126 | startInitialization(applicationContext, new FlutterLoader.Settings()); 127 | } 128 | 129 | /** 130 | * Starts initialization of the native system. 131 | *

132 | * This loads the Flutter engine's native library to enable subsequent JNI calls. This also 133 | * starts locating and unpacking Dart resources packaged in the app's APK. 134 | *

135 | * Calling this method multiple times has no effect. 136 | * 137 | * @param applicationContext The Android application context. 138 | * @param settings Configuration settings. 139 | */ 140 | public void startInitialization(@NonNull Context applicationContext, @NonNull FlutterLoader.Settings settings) { 141 | // Do not run startInitialization more than once. 142 | if (this.settings != null) { 143 | return; 144 | } 145 | if (Looper.myLooper() != Looper.getMainLooper()) { 146 | throw new IllegalStateException("startInitialization must be called on the main thread"); 147 | } 148 | 149 | this.settings = settings; 150 | 151 | long initStartTimestampMillis = SystemClock.uptimeMillis(); 152 | initConfig(applicationContext); 153 | initResources(applicationContext); 154 | 155 | System.loadLibrary("flutter"); 156 | 157 | VsyncWaiter 158 | .getInstance((WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE)) 159 | .init(); 160 | 161 | // We record the initialization time using SystemClock because at the start of the 162 | // initialization we have not yet loaded the native library to call into dart_tools_api.h. 163 | // To get Timeline timestamp of the start of initialization we simply subtract the delta 164 | // from the Timeline timestamp at the current moment (the assumption is that the overhead 165 | // of the JNI call is negligible). 166 | long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; 167 | FlutterJNI.nativeRecordStartTimestamp(initTimeMillis); 168 | } 169 | 170 | /** 171 | * Blocks until initialization of the native system has completed. 172 | *

173 | * Calling this method multiple times has no effect. 174 | * 175 | * @param applicationContext The Android application context. 176 | * @param args Flags sent to the Flutter runtime. 177 | */ 178 | public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) { 179 | if (initialized) { 180 | return; 181 | } 182 | if (Looper.myLooper() != Looper.getMainLooper()) { 183 | throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); 184 | } 185 | if (settings == null) { 186 | throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); 187 | } 188 | try { 189 | if (resourceExtractor != null) { 190 | resourceExtractor.waitForCompletion(); 191 | } 192 | 193 | List shellArgs = new ArrayList<>(); 194 | shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); 195 | 196 | ApplicationInfo applicationInfo = getApplicationInfo(applicationContext); 197 | shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); 198 | 199 | if (args != null) { 200 | Collections.addAll(shellArgs, args); 201 | } 202 | 203 | String kernelPath = null; 204 | if (io.flutter.BuildConfig.DEBUG || io.flutter.BuildConfig.JIT_RELEASE) { 205 | String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + flutterAssetsDir; 206 | kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; 207 | shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); 208 | shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData); 209 | shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData); 210 | } else { 211 | 212 | if (null != aotSharedLibraryFile 213 | && aotSharedLibraryFile.exists() 214 | && aotSharedLibraryFile.isFile() 215 | && aotSharedLibraryFile.canRead() 216 | && aotSharedLibraryFile.length() > 0) { 217 | shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getName()); 218 | 219 | shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryFile.getAbsolutePath()); 220 | 221 | Log.i(TAG, "initialize with fixed file: " + aotSharedLibraryFile.getAbsolutePath()); 222 | } else { 223 | 224 | 225 | shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName); 226 | 227 | // Most devices can load the AOT shared library based on the library name 228 | // with no directory path. Provide a fully qualified path to the library 229 | // as a workaround for devices where that fails. 230 | shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName); 231 | } 232 | } 233 | shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext)); 234 | if (settings.getLogTag() != null) { 235 | shellArgs.add("--log-tag=" + settings.getLogTag()); 236 | } 237 | 238 | String appStoragePath = PathUtils.getFilesDir(applicationContext); 239 | String engineCachesPath = PathUtils.getCacheDirectory(applicationContext); 240 | FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]), 241 | kernelPath, appStoragePath, engineCachesPath); 242 | 243 | initialized = true; 244 | } catch (Exception e) { 245 | Log.e(TAG, "Flutter initialization failed.", e); 246 | throw new RuntimeException(e); 247 | } 248 | } 249 | 250 | /** 251 | * Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background 252 | * thread, then invoking {@code callback} on the {@code callbackHandler}. 253 | */ 254 | public void ensureInitializationCompleteAsync( 255 | final @NonNull Context applicationContext, 256 | final @Nullable String[] args, 257 | final @NonNull Handler callbackHandler, 258 | final @NonNull Runnable callback 259 | ) { 260 | if (Looper.myLooper() != Looper.getMainLooper()) { 261 | throw new IllegalStateException("ensureInitializationComplete must be called on the main thread"); 262 | } 263 | if (settings == null) { 264 | throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization"); 265 | } 266 | if (initialized) { 267 | return; 268 | } 269 | new Thread(new Runnable() { 270 | @Override 271 | public void run() { 272 | if (resourceExtractor != null) { 273 | resourceExtractor.waitForCompletion(); 274 | } 275 | new Handler(Looper.getMainLooper()).post(new Runnable() { 276 | @Override 277 | public void run() { 278 | ensureInitializationComplete(applicationContext.getApplicationContext(), args); 279 | callbackHandler.post(callback); 280 | } 281 | }); 282 | } 283 | }).start(); 284 | } 285 | 286 | @NonNull 287 | private ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { 288 | try { 289 | return applicationContext 290 | .getPackageManager() 291 | .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); 292 | } catch (PackageManager.NameNotFoundException e) { 293 | throw new RuntimeException(e); 294 | } 295 | } 296 | 297 | /** 298 | * Initialize our Flutter config values by obtaining them from the 299 | * manifest XML file, falling back to default values. 300 | */ 301 | private void initConfig(@NonNull Context applicationContext) { 302 | Bundle metadata = getApplicationInfo(applicationContext).metaData; 303 | 304 | // There isn't a `` tag as a direct child of `` in 305 | // `AndroidManifest.xml`. 306 | if (metadata == null) { 307 | return; 308 | } 309 | 310 | aotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); 311 | flutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); 312 | 313 | vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); 314 | isolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); 315 | } 316 | 317 | /** 318 | * Extract assets out of the APK that need to be cached as uncompressed 319 | * files on disk. 320 | */ 321 | private void initResources(@NonNull Context applicationContext) { 322 | new ResourceCleaner(applicationContext).start(); 323 | 324 | if (io.flutter.BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { 325 | final String dataDirPath = PathUtils.getDataDirectory(applicationContext); 326 | final String packageName = applicationContext.getPackageName(); 327 | final PackageManager packageManager = applicationContext.getPackageManager(); 328 | final AssetManager assetManager = applicationContext.getResources().getAssets(); 329 | resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager); 330 | 331 | // In debug/JIT mode these assets will be written to disk and then 332 | // mapped into memory so they can be provided to the Dart VM. 333 | resourceExtractor 334 | .addResource(fullAssetPathFrom(vmSnapshotData)) 335 | .addResource(fullAssetPathFrom(isolateSnapshotData)) 336 | .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB)); 337 | 338 | resourceExtractor.start(); 339 | } 340 | } 341 | 342 | @NonNull 343 | public String findAppBundlePath() { 344 | return flutterAssetsDir; 345 | } 346 | 347 | /** 348 | * Returns the file name for the given asset. 349 | * The returned file name can be used to access the asset in the APK 350 | * through the {@link android.content.res.AssetManager} API. 351 | * 352 | * @param asset the name of the asset. The name can be hierarchical 353 | * @return the filename to be used with {@link android.content.res.AssetManager} 354 | */ 355 | @NonNull 356 | public String getLookupKeyForAsset(@NonNull String asset) { 357 | return fullAssetPathFrom(asset); 358 | } 359 | 360 | /** 361 | * Returns the file name for the given asset which originates from the 362 | * specified packageName. The returned file name can be used to access 363 | * the asset in the APK through the {@link android.content.res.AssetManager} API. 364 | * 365 | * @param asset the name of the asset. The name can be hierarchical 366 | * @param packageName the name of the package from which the asset originates 367 | * @return the file name to be used with {@link android.content.res.AssetManager} 368 | */ 369 | @NonNull 370 | public String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) { 371 | return getLookupKeyForAsset( 372 | "packages" + File.separator + packageName + File.separator + asset); 373 | } 374 | 375 | @NonNull 376 | private String fullAssetPathFrom(@NonNull String filePath) { 377 | return flutterAssetsDir + File.separator + filePath; 378 | } 379 | 380 | 381 | } 382 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/loader/ResourceCleaner.java: -------------------------------------------------------------------------------- 1 | package com.jyuesong.flutter_hotpatch.loader; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | import android.os.Handler; 6 | import android.util.Log; 7 | 8 | import java.io.File; 9 | import java.io.FilenameFilter; 10 | 11 | import io.flutter.BuildConfig; 12 | 13 | /** 14 | * created by NewTab 2020/3/10 15 | */ 16 | public class ResourceCleaner { 17 | 18 | private static final String TAG = "ResourceCleaner"; 19 | private static final long DELAY_MS = 5000; 20 | 21 | private static class CleanTask extends AsyncTask { 22 | private final File[] mFilesToDelete; 23 | 24 | CleanTask(File[] filesToDelete) { 25 | mFilesToDelete = filesToDelete; 26 | } 27 | 28 | boolean hasFilesToDelete() { 29 | return mFilesToDelete != null && mFilesToDelete.length > 0; 30 | } 31 | 32 | @Override 33 | protected Void doInBackground(Void... unused) { 34 | if (BuildConfig.DEBUG) { 35 | Log.i(TAG, "Cleaning " + mFilesToDelete.length + " resources."); 36 | } 37 | for (File file : mFilesToDelete) { 38 | if (file.exists()) { 39 | deleteRecursively(file); 40 | } 41 | } 42 | return null; 43 | } 44 | 45 | private void deleteRecursively(File parent) { 46 | if (parent.isDirectory()) { 47 | for (File child : parent.listFiles()) { 48 | deleteRecursively(child); 49 | } 50 | } 51 | parent.delete(); 52 | } 53 | } 54 | 55 | private final Context mContext; 56 | 57 | ResourceCleaner(Context context) { 58 | mContext = context; 59 | } 60 | 61 | void start() { 62 | File cacheDir = mContext.getCacheDir(); 63 | if (cacheDir == null) { 64 | return; 65 | } 66 | 67 | final ResourceCleaner.CleanTask task = new ResourceCleaner.CleanTask(cacheDir.listFiles(new FilenameFilter() { 68 | @Override 69 | public boolean accept(File dir, String name) { 70 | boolean result = name.startsWith(ResourcePaths.TEMPORARY_RESOURCE_PREFIX); 71 | return result; 72 | } 73 | })); 74 | 75 | if (!task.hasFilesToDelete()) { 76 | return; 77 | } 78 | 79 | new Handler().postDelayed(new Runnable() { 80 | @Override 81 | public void run() { 82 | task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 83 | } 84 | }, DELAY_MS); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/loader/ResourceExtractor.java: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package com.jyuesong.flutter_hotpatch.loader; 6 | 7 | import android.content.pm.PackageInfo; 8 | import android.content.pm.PackageManager; 9 | import android.content.res.AssetManager; 10 | import android.os.AsyncTask; 11 | import android.os.Build; 12 | import android.support.annotation.NonNull; 13 | import android.support.annotation.WorkerThread; 14 | import android.util.Log; 15 | 16 | 17 | import java.io.File; 18 | import java.io.FileNotFoundException; 19 | import java.io.FileOutputStream; 20 | import java.io.FilenameFilter; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.OutputStream; 24 | import java.util.ArrayList; 25 | import java.util.Collection; 26 | import java.util.HashSet; 27 | import java.util.concurrent.CancellationException; 28 | import java.util.concurrent.ExecutionException; 29 | 30 | import io.flutter.BuildConfig; 31 | 32 | import static java.util.Arrays.asList; 33 | 34 | /** 35 | * A class to initialize the native code. 36 | **/ 37 | class ResourceExtractor { 38 | private static final String TAG = "ResourceExtractor"; 39 | private static final String TIMESTAMP_PREFIX = "res_timestamp-"; 40 | private static final String[] SUPPORTED_ABIS = getSupportedAbis(); 41 | 42 | @SuppressWarnings("deprecation") 43 | static long getVersionCode(@NonNull PackageInfo packageInfo) { 44 | // Linter needs P (28) hardcoded or else it will fail these lines. 45 | if (Build.VERSION.SDK_INT >= 28) { 46 | return packageInfo.getLongVersionCode(); 47 | } else { 48 | return packageInfo.versionCode; 49 | } 50 | } 51 | 52 | private static class ExtractTask extends AsyncTask { 53 | @NonNull 54 | private final String mDataDirPath; 55 | @NonNull 56 | private final HashSet mResources; 57 | @NonNull 58 | private final AssetManager mAssetManager; 59 | @NonNull 60 | private final String mPackageName; 61 | @NonNull 62 | private final PackageManager mPackageManager; 63 | 64 | ExtractTask(@NonNull String dataDirPath, 65 | @NonNull HashSet resources, 66 | @NonNull String packageName, 67 | @NonNull PackageManager packageManager, 68 | @NonNull AssetManager assetManager) { 69 | mDataDirPath = dataDirPath; 70 | mResources = resources; 71 | mAssetManager = assetManager; 72 | mPackageName = packageName; 73 | mPackageManager = packageManager; 74 | } 75 | 76 | @Override 77 | protected Void doInBackground(Void... unused) { 78 | final File dataDir = new File(mDataDirPath); 79 | 80 | final String timestamp = checkTimestamp(dataDir, mPackageManager, mPackageName); 81 | if (timestamp == null) { 82 | return null; 83 | } 84 | 85 | deleteFiles(mDataDirPath, mResources); 86 | 87 | if (!extractAPK(dataDir)) { 88 | return null; 89 | } 90 | 91 | if (timestamp != null) { 92 | try { 93 | new File(dataDir, timestamp).createNewFile(); 94 | } catch (IOException e) { 95 | Log.w(TAG, "Failed to write resource timestamp"); 96 | } 97 | } 98 | 99 | return null; 100 | } 101 | 102 | 103 | /// Returns true if successfully unpacked APK resources, 104 | /// otherwise deletes all resources and returns false. 105 | @WorkerThread 106 | private boolean extractAPK(@NonNull File dataDir) { 107 | for (String asset : mResources) { 108 | try { 109 | final String resource = "assets/" + asset; 110 | final File output = new File(dataDir, asset); 111 | if (output.exists()) { 112 | continue; 113 | } 114 | if (output.getParentFile() != null) { 115 | output.getParentFile().mkdirs(); 116 | } 117 | 118 | try (InputStream is = mAssetManager.open(asset); 119 | OutputStream os = new FileOutputStream(output)) { 120 | copy(is, os); 121 | } 122 | if (BuildConfig.DEBUG) { 123 | Log.i(TAG, "Extracted baseline resource " + resource); 124 | } 125 | } catch (FileNotFoundException fnfe) { 126 | continue; 127 | 128 | } catch (IOException ioe) { 129 | Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage()); 130 | deleteFiles(mDataDirPath, mResources); 131 | return false; 132 | } 133 | } 134 | 135 | return true; 136 | } 137 | } 138 | 139 | @NonNull 140 | private final String mDataDirPath; 141 | @NonNull 142 | private final String mPackageName; 143 | @NonNull 144 | private final PackageManager mPackageManager; 145 | @NonNull 146 | private final AssetManager mAssetManager; 147 | @NonNull 148 | private final HashSet mResources; 149 | private ExtractTask mExtractTask; 150 | 151 | ResourceExtractor(@NonNull String dataDirPath, 152 | @NonNull String packageName, 153 | @NonNull PackageManager packageManager, 154 | @NonNull AssetManager assetManager) { 155 | mDataDirPath = dataDirPath; 156 | mPackageName = packageName; 157 | mPackageManager = packageManager; 158 | mAssetManager = assetManager; 159 | mResources = new HashSet<>(); 160 | } 161 | 162 | ResourceExtractor addResource(@NonNull String resource) { 163 | mResources.add(resource); 164 | return this; 165 | } 166 | 167 | ResourceExtractor addResources(@NonNull Collection resources) { 168 | mResources.addAll(resources); 169 | return this; 170 | } 171 | 172 | ResourceExtractor start() { 173 | if (BuildConfig.DEBUG && mExtractTask != null) { 174 | Log.e(TAG, "Attempted to start resource extraction while another extraction was in progress."); 175 | } 176 | mExtractTask = new ExtractTask(mDataDirPath, mResources, mPackageName, mPackageManager, mAssetManager); 177 | mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 178 | return this; 179 | } 180 | 181 | void waitForCompletion() { 182 | if (mExtractTask == null) { 183 | return; 184 | } 185 | 186 | try { 187 | mExtractTask.get(); 188 | } catch (CancellationException | ExecutionException | InterruptedException e) { 189 | deleteFiles(mDataDirPath, mResources); 190 | } 191 | } 192 | 193 | private static String[] getExistingTimestamps(File dataDir) { 194 | return dataDir.list(new FilenameFilter() { 195 | @Override 196 | public boolean accept(File dir, String name) { 197 | return name.startsWith(TIMESTAMP_PREFIX); 198 | } 199 | }); 200 | } 201 | 202 | private static void deleteFiles(@NonNull String dataDirPath, @NonNull HashSet resources) { 203 | final File dataDir = new File(dataDirPath); 204 | for (String resource : resources) { 205 | final File file = new File(dataDir, resource); 206 | if (file.exists()) { 207 | file.delete(); 208 | } 209 | } 210 | final String[] existingTimestamps = getExistingTimestamps(dataDir); 211 | if (existingTimestamps == null) { 212 | return; 213 | } 214 | for (String timestamp : existingTimestamps) { 215 | new File(dataDir, timestamp).delete(); 216 | } 217 | } 218 | 219 | // Returns null if extracted resources are found and match the current APK version 220 | // and update version if any, otherwise returns the current APK and update version. 221 | private static String checkTimestamp(@NonNull File dataDir, 222 | @NonNull PackageManager packageManager, 223 | @NonNull String packageName) { 224 | PackageInfo packageInfo = null; 225 | 226 | try { 227 | packageInfo = packageManager.getPackageInfo(packageName, 0); 228 | } catch (PackageManager.NameNotFoundException e) { 229 | return TIMESTAMP_PREFIX; 230 | } 231 | 232 | if (packageInfo == null) { 233 | return TIMESTAMP_PREFIX; 234 | } 235 | 236 | String expectedTimestamp = 237 | TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime; 238 | 239 | final String[] existingTimestamps = getExistingTimestamps(dataDir); 240 | 241 | if (existingTimestamps == null) { 242 | if (BuildConfig.DEBUG) { 243 | Log.i(TAG, "No extracted resources found"); 244 | } 245 | return expectedTimestamp; 246 | } 247 | 248 | if (existingTimestamps.length == 1) { 249 | if (BuildConfig.DEBUG) { 250 | Log.i(TAG, "Found extracted resources " + existingTimestamps[0]); 251 | } 252 | } 253 | 254 | if (existingTimestamps.length != 1 255 | || !expectedTimestamp.equals(existingTimestamps[0])) { 256 | if (BuildConfig.DEBUG) { 257 | Log.i(TAG, "Resource version mismatch " + expectedTimestamp); 258 | } 259 | return expectedTimestamp; 260 | } 261 | 262 | return null; 263 | } 264 | 265 | private static void copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { 266 | byte[] buf = new byte[16 * 1024]; 267 | for (int i; (i = in.read(buf)) >= 0; ) { 268 | out.write(buf, 0, i); 269 | } 270 | } 271 | 272 | @SuppressWarnings("deprecation") 273 | private static String[] getSupportedAbis() { 274 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 275 | return Build.SUPPORTED_ABIS; 276 | } else { 277 | ArrayList cpuAbis = new ArrayList(asList(Build.CPU_ABI, Build.CPU_ABI2)); 278 | cpuAbis.removeAll(asList(null, "")); 279 | return cpuAbis.toArray(new String[0]); 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /hotpatch/src/main/java/com/jyuesong/flutter_hotpatch/loader/ResourcePaths.java: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package com.jyuesong.flutter_hotpatch.loader; 6 | 7 | import android.content.Context; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | 12 | class ResourcePaths { 13 | // The filename prefix used by Chromium temporary file APIs. 14 | public static final String TEMPORARY_RESOURCE_PREFIX = ".org.chromium.Chromium."; 15 | 16 | // Return a temporary file that will be cleaned up by the ResourceCleaner. 17 | public static File createTempFile(Context context, String suffix) throws IOException { 18 | return File.createTempFile(TEMPORARY_RESOURCE_PREFIX, "_" + suffix, 19 | context.getCacheDir()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /hotpatch/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | flutter_hotpatch 3 | 4 | -------------------------------------------------------------------------------- /key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/key.jks -------------------------------------------------------------------------------- /patch/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/patch/app-release.apk -------------------------------------------------------------------------------- /patch/hotpatch-resource.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/patch/hotpatch-resource.zip -------------------------------------------------------------------------------- /patch/libapp_fix.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiang111/flutter_hotpatch/466efcd1784092ce2a073a32d9c9fb51090740b7/patch/libapp_fix.so -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':hotpatch' 2 | --------------------------------------------------------------------------------