Skip to content

Commit 18a4e33

Browse files
authored
Downloading and installation of dynamic updates on Android (flutter#7207)
1 parent 8e56b54 commit 18a4e33

File tree

6 files changed

+417
-14
lines changed

6 files changed

+417
-14
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java
454454
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceCleaner.java
455455
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceExtractor.java
456456
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourcePaths.java
457+
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceUpdater.java
457458
FILE: ../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java
458459
FILE: ../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java
459460
FILE: ../../../flutter/shell/platform/android/library_loader.cc

shell/platform/android/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ java_library("flutter_shell_java") {
140140
"io/flutter/view/ResourceCleaner.java",
141141
"io/flutter/view/ResourceExtractor.java",
142142
"io/flutter/view/ResourcePaths.java",
143+
"io/flutter/view/ResourceUpdater.java",
143144
"io/flutter/view/TextureRegistry.java",
144145
"io/flutter/view/VsyncWaiter.java",
145146
]

shell/platform/android/io/flutter/app/FlutterActivityDelegate.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,17 @@ public void onCreate(Bundle savedInstanceState) {
166166
if (loadIntent(activity.getIntent())) {
167167
return;
168168
}
169+
169170
if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
170171
String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
171172
if (appBundlePath != null) {
172173
FlutterRunArguments arguments = new FlutterRunArguments();
173-
arguments.bundlePath = appBundlePath;
174+
ArrayList<String> bundlePaths = new ArrayList<String>();
175+
if (FlutterMain.getUpdateInstallationPath() != null) {
176+
bundlePaths.add(FlutterMain.getUpdateInstallationPath());
177+
}
178+
bundlePaths.add(appBundlePath);
179+
arguments.bundlePaths = bundlePaths.toArray(new String[0]);
174180
arguments.entrypoint = "main";
175181
flutterView.runFromBundle(arguments);
176182
}
@@ -337,7 +343,12 @@ private boolean loadIntent(Intent intent) {
337343
}
338344
if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
339345
FlutterRunArguments args = new FlutterRunArguments();
340-
args.bundlePath = appBundlePath;
346+
ArrayList<String> bundlePaths = new ArrayList<String>();
347+
if (FlutterMain.getUpdateInstallationPath() != null) {
348+
bundlePaths.add(FlutterMain.getUpdateInstallationPath());
349+
}
350+
bundlePaths.add(appBundlePath);
351+
args.bundlePaths = bundlePaths.toArray(new String[0]);
341352
args.entrypoint = "main";
342353
flutterView.runFromBundle(args);
343354
}

shell/platform/android/io/flutter/view/FlutterMain.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ private static String fromFlutterAssets(String filePath) {
7777
private static String sFlutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR;
7878

7979
private static boolean sInitialized = false;
80+
private static ResourceUpdater sResourceUpdater;
8081
private static ResourceExtractor sResourceExtractor;
8182
private static boolean sIsPrecompiledAsBlobs;
8283
private static boolean sIsPrecompiledAsSharedLibrary;
@@ -254,6 +255,21 @@ private static void initResources(Context applicationContext) {
254255
Context context = applicationContext;
255256
new ResourceCleaner(context).start();
256257

258+
Bundle metaData = null;
259+
try {
260+
metaData = context.getPackageManager().getApplicationInfo(
261+
context.getPackageName(), PackageManager.GET_META_DATA).metaData;
262+
263+
} catch (PackageManager.NameNotFoundException e) {
264+
Log.e(TAG, "Unable to read application info", e);
265+
}
266+
267+
if (metaData != null && metaData.getBoolean("DynamicUpdates")) {
268+
sResourceUpdater = new ResourceUpdater(context);
269+
sResourceUpdater.startUpdateDownloadOnce();
270+
sResourceUpdater.waitForDownloadCompletion();
271+
}
272+
257273
sResourceExtractor = new ResourceExtractor(context);
258274

259275
String icuAssetPath = SHARED_ASSET_DIR + File.separator + SHARED_ASSET_ICU_DATA;
@@ -321,6 +337,10 @@ public static String findAppBundlePath(Context applicationContext) {
321337
return appBundle.exists() ? appBundle.getPath() : null;
322338
}
323339

340+
public static String getUpdateInstallationPath() {
341+
return sResourceUpdater == null ? null : sResourceUpdater.getUpdateInstallationPath();
342+
}
343+
324344
/**
325345
* Returns the file name for the given asset.
326346
* The returned file name can be used to access the asset in the APK

shell/platform/android/io/flutter/view/ResourceExtractor.java

Lines changed: 217 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@
1111
import android.os.AsyncTask;
1212
import android.util.Log;
1313
import io.flutter.util.PathUtils;
14+
import org.json.JSONException;
15+
import org.json.JSONObject;
1416

1517
import java.io.*;
1618
import java.util.Collection;
1719
import java.util.HashSet;
20+
import java.util.Scanner;
1821
import java.util.concurrent.CancellationException;
1922
import java.util.concurrent.ExecutionException;
23+
import java.util.zip.CRC32;
24+
import java.util.zip.ZipEntry;
25+
import java.util.zip.ZipException;
26+
import java.util.zip.ZipFile;
2027

2128
/**
22-
* A class to intialize the native code.
29+
* A class to initialize the native code.
2330
**/
2431
class ResourceExtractor {
2532
private static final String TAG = "ResourceExtractor";
@@ -33,18 +40,46 @@ private class ExtractTask extends AsyncTask<Void, Void, Void> {
3340
private void extractResources() {
3441
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
3542

36-
final String timestamp = checkTimestamp(dataDir);
43+
JSONObject updateManifest = readUpdateManifest();
44+
if (!validateUpdateManifest(updateManifest)) {
45+
updateManifest = null;
46+
}
47+
48+
final String timestamp = checkTimestamp(dataDir, updateManifest);
49+
if (timestamp == null) {
50+
return;
51+
}
52+
53+
deleteFiles();
54+
55+
if (updateManifest != null) {
56+
if (!extractUpdate(dataDir)) {
57+
return;
58+
}
59+
}
60+
61+
if (!extractAPK(dataDir)) {
62+
return;
63+
}
64+
3765
if (timestamp != null) {
38-
deleteFiles();
66+
try {
67+
new File(dataDir, timestamp).createNewFile();
68+
} catch (IOException e) {
69+
Log.w(TAG, "Failed to write resource timestamp");
70+
}
3971
}
72+
}
4073

74+
/// Returns true if successfully unpacked APK resources,
75+
/// otherwise deletes all resources and returns false.
76+
private boolean extractAPK(File dataDir) {
4177
final AssetManager manager = mContext.getResources().getAssets();
4278

4379
byte[] buffer = null;
4480
for (String asset : mResources) {
4581
try {
4682
final File output = new File(dataDir, asset);
47-
4883
if (output.exists()) {
4984
continue;
5085
}
@@ -62,28 +97,99 @@ private void extractResources() {
6297
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
6398
os.write(buffer, 0, count);
6499
}
100+
65101
os.flush();
102+
Log.i(TAG, "Extracted baseline resource " + asset);
66103
}
67104
}
105+
68106
} catch (FileNotFoundException fnfe) {
69107
continue;
108+
70109
} catch (IOException ioe) {
71110
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
72111
deleteFiles();
73-
return;
112+
return false;
74113
}
75114
}
76115

77-
if (timestamp != null) {
78-
try {
79-
new File(dataDir, timestamp).createNewFile();
80-
} catch (IOException e) {
81-
Log.w(TAG, "Failed to write resource timestamp");
116+
return true;
117+
}
118+
119+
/// Returns true if successfully unpacked update resources or if there is no update,
120+
/// otherwise deletes all resources and returns false.
121+
private boolean extractUpdate(File dataDir) {
122+
if (FlutterMain.getUpdateInstallationPath() == null) {
123+
return true;
124+
}
125+
126+
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
127+
if (!updateFile.exists()) {
128+
return true;
129+
}
130+
131+
ZipFile zipFile;
132+
try {
133+
zipFile = new ZipFile(updateFile);
134+
135+
} catch (ZipException e) {
136+
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
137+
deleteFiles();
138+
return false;
139+
140+
} catch (IOException e) {
141+
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
142+
deleteFiles();
143+
return false;
144+
}
145+
146+
byte[] buffer = null;
147+
for (String asset : mResources) {
148+
ZipEntry entry = zipFile.getEntry(asset);
149+
if (entry == null) {
150+
continue;
151+
}
152+
153+
final File output = new File(dataDir, asset);
154+
if (output.exists()) {
155+
continue;
156+
}
157+
if (output.getParentFile() != null) {
158+
output.getParentFile().mkdirs();
159+
}
160+
161+
try (InputStream is = zipFile.getInputStream(entry)) {
162+
try (OutputStream os = new FileOutputStream(output)) {
163+
if (buffer == null) {
164+
buffer = new byte[BUFFER_SIZE];
165+
}
166+
167+
int count = 0;
168+
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
169+
os.write(buffer, 0, count);
170+
}
171+
172+
os.flush();
173+
Log.i(TAG, "Extracted override resource " + asset);
174+
}
175+
176+
} catch (FileNotFoundException fnfe) {
177+
continue;
178+
179+
} catch (IOException ioe) {
180+
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
181+
deleteFiles();
182+
return false;
82183
}
83184
}
185+
186+
return true;
84187
}
85188

86-
private String checkTimestamp(File dataDir) {
189+
// Returns null if extracted resources are found and match the current APK version
190+
// and update version if any, otherwise returns the current APK and update version.
191+
private String checkTimestamp(File dataDir, JSONObject updateManifest) {
192+
87193
PackageManager packageManager = mContext.getPackageManager();
88194
PackageInfo packageInfo = null;
89195

@@ -100,20 +206,119 @@ private String checkTimestamp(File dataDir) {
100206
String expectedTimestamp =
101207
TIMESTAMP_PREFIX + packageInfo.versionCode + "-" + packageInfo.lastUpdateTime;
102208

209+
if (updateManifest != null) {
210+
String baselineVersion = updateManifest.optString("baselineVersion", null);
211+
if (baselineVersion == null) {
212+
Log.w(TAG, "Invalid update manifest: baselineVersion");
213+
}
214+
215+
String updateVersion = updateManifest.optString("updateVersion", null);
216+
if (updateVersion == null) {
217+
Log.w(TAG, "Invalid update manifest: updateVersion");
218+
}
219+
220+
if (baselineVersion != null && updateVersion != null) {
221+
if (!baselineVersion.equals(Integer.toString(packageInfo.versionCode))) {
222+
Log.w(TAG, "Outdated update file for " + packageInfo.versionCode);
223+
} else {
224+
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
225+
expectedTimestamp += "-" + updateVersion + "-" + updateFile.lastModified();
226+
}
227+
}
228+
}
229+
103230
final String[] existingTimestamps = getExistingTimestamps(dataDir);
104231

105232
if (existingTimestamps == null) {
106-
return null;
233+
Log.i(TAG, "No extracted resources found");
234+
return expectedTimestamp;
235+
}
236+
237+
if (existingTimestamps.length == 1) {
238+
Log.i(TAG, "Found extracted resources " + existingTimestamps[0]);
107239
}
108240

109241
if (existingTimestamps.length != 1
110242
|| !expectedTimestamp.equals(existingTimestamps[0])) {
243+
Log.i(TAG, "Resource version mismatch " + expectedTimestamp);
111244
return expectedTimestamp;
112245
}
113246

114247
return null;
115248
}
116249

250+
/// Returns true if the downloaded update file was indeed built for this APK.
251+
private boolean validateUpdateManifest(JSONObject updateManifest) {
252+
if (updateManifest == null) {
253+
return false;
254+
}
255+
256+
String baselineChecksum = updateManifest.optString("baselineChecksum", null);
257+
if (baselineChecksum == null) {
258+
Log.w(TAG, "Invalid update manifest: baselineChecksum");
259+
return false;
260+
}
261+
262+
final AssetManager manager = mContext.getResources().getAssets();
263+
try (InputStream is = manager.open("flutter_assets/isolate_snapshot_data")) {
264+
CRC32 checksum = new CRC32();
265+
266+
int count = 0;
267+
byte[] buffer = new byte[BUFFER_SIZE];
268+
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
269+
checksum.update(buffer, 0, count);
270+
}
271+
272+
if (!baselineChecksum.equals(String.valueOf(checksum.getValue()))) {
273+
Log.w(TAG, "Mismatched update file for APK");
274+
return false;
275+
}
276+
277+
return true;
278+
279+
} catch (IOException e) {
280+
Log.w(TAG, "Could not read APK: " + e);
281+
return false;
282+
}
283+
}
284+
285+
/// Returns null if no update manifest is found.
286+
private JSONObject readUpdateManifest() {
287+
if (FlutterMain.getUpdateInstallationPath() == null) {
288+
return null;
289+
}
290+
291+
File updateFile = new File(FlutterMain.getUpdateInstallationPath());
292+
if (!updateFile.exists()) {
293+
return null;
294+
}
295+
296+
try {
297+
ZipFile zipFile = new ZipFile(updateFile);
298+
ZipEntry entry = zipFile.getEntry("manifest.json");
299+
if (entry == null) {
300+
Log.w(TAG, "Invalid update file: " + updateFile);
301+
return null;
302+
}
303+
304+
// Read and parse the entire JSON file as single operation.
305+
Scanner scanner = new Scanner(zipFile.getInputStream(entry));
306+
return new JSONObject(scanner.useDelimiter("\\A").next());
307+
308+
} catch (ZipException e) {
309+
Log.w(TAG, "Invalid update file: " + e);
310+
return null;
311+
312+
} catch (IOException e) {
313+
Log.w(TAG, "Invalid update file: " + e);
314+
return null;
315+
316+
} catch (JSONException e) {
317+
Log.w(TAG, "Invalid update file: " + e);
318+
return null;
319+
}
320+
}
321+
117322
@Override
118323
protected Void doInBackground(Void... unused) {
119324
extractResources();

0 commit comments

Comments
 (0)