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 |
--------------------------------------------------------------------------------