mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Minor refactoring of dynamic patching code. (#7325)
Minor refactoring of dynamic patching code. - Changes naming of manifest properties to be consistent with documentation. - Moves methods from inner class to outer class to make them more reusable.
This commit is contained in:
parent
868dc047b4
commit
6f763fbad7
@ -264,7 +264,7 @@ public class FlutterMain {
|
||||
Log.e(TAG, "Unable to read application info", e);
|
||||
}
|
||||
|
||||
if (metaData != null && metaData.getBoolean("DynamicUpdates")) {
|
||||
if (metaData != null && metaData.getBoolean("DynamicPatching")) {
|
||||
sResourceUpdater = new ResourceUpdater(context);
|
||||
sResourceUpdater.startUpdateDownloadOnce();
|
||||
sResourceUpdater.waitForDownloadCompletion();
|
||||
|
||||
@ -33,6 +33,8 @@ class ResourceExtractor {
|
||||
private static final String TAG = "ResourceExtractor";
|
||||
private static final String TIMESTAMP_PREFIX = "res_timestamp-";
|
||||
|
||||
private static final int BUFFER_SIZE = 16 * 1024;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
static long getVersionCode(PackageInfo packageInfo) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@ -43,11 +45,10 @@ class ResourceExtractor {
|
||||
}
|
||||
|
||||
private class ExtractTask extends AsyncTask<Void, Void, Void> {
|
||||
private static final int BUFFER_SIZE = 16 * 1024;
|
||||
|
||||
ExtractTask() { }
|
||||
|
||||
private void extractResources() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... unused) {
|
||||
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
|
||||
|
||||
JSONObject updateManifest = readUpdateManifest();
|
||||
@ -57,19 +58,19 @@ class ResourceExtractor {
|
||||
|
||||
final String timestamp = checkTimestamp(dataDir, updateManifest);
|
||||
if (timestamp == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteFiles();
|
||||
|
||||
if (updateManifest != null) {
|
||||
if (!extractUpdate(dataDir)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extractAPK(dataDir)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (timestamp != null) {
|
||||
@ -79,255 +80,7 @@ class ResourceExtractor {
|
||||
Log.w(TAG, "Failed to write resource timestamp");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if successfully unpacked APK resources,
|
||||
/// otherwise deletes all resources and returns false.
|
||||
private boolean extractAPK(File dataDir) {
|
||||
final AssetManager manager = mContext.getResources().getAssets();
|
||||
|
||||
byte[] buffer = null;
|
||||
for (String asset : mResources) {
|
||||
try {
|
||||
final File output = new File(dataDir, asset);
|
||||
if (output.exists()) {
|
||||
continue;
|
||||
}
|
||||
if (output.getParentFile() != null) {
|
||||
output.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try (InputStream is = manager.open(asset);
|
||||
OutputStream os = new FileOutputStream(output)) {
|
||||
if (buffer == null) {
|
||||
buffer = new byte[BUFFER_SIZE];
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
os.flush();
|
||||
Log.i(TAG, "Extracted baseline resource " + asset);
|
||||
}
|
||||
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
continue;
|
||||
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns true if successfully unpacked update resources or if there is no update,
|
||||
/// otherwise deletes all resources and returns false.
|
||||
private boolean extractUpdate(File dataDir) {
|
||||
if (FlutterMain.getUpdateInstallationPath() == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
||||
if (!updateFile.exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ZipFile zipFile;
|
||||
try {
|
||||
zipFile = new ZipFile(updateFile);
|
||||
|
||||
} catch (ZipException e) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] buffer = null;
|
||||
for (String asset : mResources) {
|
||||
ZipEntry entry = zipFile.getEntry(asset);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final File output = new File(dataDir, asset);
|
||||
if (output.exists()) {
|
||||
continue;
|
||||
}
|
||||
if (output.getParentFile() != null) {
|
||||
output.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try (InputStream is = zipFile.getInputStream(entry);
|
||||
OutputStream os = new FileOutputStream(output)) {
|
||||
if (buffer == null) {
|
||||
buffer = new byte[BUFFER_SIZE];
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
os.flush();
|
||||
Log.i(TAG, "Extracted override resource " + asset);
|
||||
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
continue;
|
||||
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns null if extracted resources are found and match the current APK version
|
||||
// and update version if any, otherwise returns the current APK and update version.
|
||||
private String checkTimestamp(File dataDir, JSONObject updateManifest) {
|
||||
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
PackageInfo packageInfo = null;
|
||||
|
||||
try {
|
||||
packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return TIMESTAMP_PREFIX;
|
||||
}
|
||||
|
||||
if (packageInfo == null) {
|
||||
return TIMESTAMP_PREFIX;
|
||||
}
|
||||
|
||||
String expectedTimestamp =
|
||||
TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
|
||||
|
||||
if (updateManifest != null) {
|
||||
String buildNumber = updateManifest.optString("buildNumber", null);
|
||||
if (buildNumber == null) {
|
||||
Log.w(TAG, "Invalid update manifest: buildNumber");
|
||||
} else {
|
||||
String patchNumber = updateManifest.optString("patchNumber", null);
|
||||
if (!buildNumber.equals(Long.toString(getVersionCode(packageInfo)))) {
|
||||
Log.w(TAG, "Outdated update file for " + getVersionCode(packageInfo));
|
||||
} else {
|
||||
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
||||
if (patchNumber != null) {
|
||||
expectedTimestamp += "-" + patchNumber + "-" + updateFile.lastModified();
|
||||
} else {
|
||||
expectedTimestamp += "-" + updateFile.lastModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String[] existingTimestamps = getExistingTimestamps(dataDir);
|
||||
|
||||
if (existingTimestamps == null) {
|
||||
Log.i(TAG, "No extracted resources found");
|
||||
return expectedTimestamp;
|
||||
}
|
||||
|
||||
if (existingTimestamps.length == 1) {
|
||||
Log.i(TAG, "Found extracted resources " + existingTimestamps[0]);
|
||||
}
|
||||
|
||||
if (existingTimestamps.length != 1
|
||||
|| !expectedTimestamp.equals(existingTimestamps[0])) {
|
||||
Log.i(TAG, "Resource version mismatch " + expectedTimestamp);
|
||||
return expectedTimestamp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns true if the downloaded update file was indeed built for this APK.
|
||||
private boolean validateUpdateManifest(JSONObject updateManifest) {
|
||||
if (updateManifest == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String baselineChecksum = updateManifest.optString("baselineChecksum", null);
|
||||
if (baselineChecksum == null) {
|
||||
Log.w(TAG, "Invalid update manifest: baselineChecksum");
|
||||
return false;
|
||||
}
|
||||
|
||||
final AssetManager manager = mContext.getResources().getAssets();
|
||||
try (InputStream is = manager.open("flutter_assets/isolate_snapshot_data")) {
|
||||
CRC32 checksum = new CRC32();
|
||||
|
||||
int count = 0;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
|
||||
checksum.update(buffer, 0, count);
|
||||
}
|
||||
|
||||
if (!baselineChecksum.equals(String.valueOf(checksum.getValue()))) {
|
||||
Log.w(TAG, "Mismatched update file for APK");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not read APK: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns null if no update manifest is found.
|
||||
private JSONObject readUpdateManifest() {
|
||||
if (FlutterMain.getUpdateInstallationPath() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
||||
if (!updateFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ZipFile zipFile = new ZipFile(updateFile);
|
||||
ZipEntry entry = zipFile.getEntry("manifest.json");
|
||||
if (entry == null) {
|
||||
Log.w(TAG, "Invalid update file: " + updateFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read and parse the entire JSON file as single operation.
|
||||
Scanner scanner = new Scanner(zipFile.getInputStream(entry));
|
||||
return new JSONObject(scanner.useDelimiter("\\A").next());
|
||||
|
||||
} catch (ZipException e) {
|
||||
Log.w(TAG, "Invalid update file: " + e);
|
||||
return null;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Invalid update file: " + e);
|
||||
return null;
|
||||
|
||||
} catch (JSONException e) {
|
||||
Log.w(TAG, "Invalid update file: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... unused) {
|
||||
extractResources();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -397,4 +150,249 @@ class ResourceExtractor {
|
||||
new File(dataDir, timestamp).delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns true if successfully unpacked APK resources,
|
||||
/// otherwise deletes all resources and returns false.
|
||||
private boolean extractAPK(File dataDir) {
|
||||
final AssetManager manager = mContext.getResources().getAssets();
|
||||
|
||||
byte[] buffer = null;
|
||||
for (String asset : mResources) {
|
||||
try {
|
||||
final File output = new File(dataDir, asset);
|
||||
if (output.exists()) {
|
||||
continue;
|
||||
}
|
||||
if (output.getParentFile() != null) {
|
||||
output.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try (InputStream is = manager.open(asset);
|
||||
OutputStream os = new FileOutputStream(output)) {
|
||||
if (buffer == null) {
|
||||
buffer = new byte[BUFFER_SIZE];
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
os.flush();
|
||||
Log.i(TAG, "Extracted baseline resource " + asset);
|
||||
}
|
||||
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
continue;
|
||||
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns true if successfully unpacked update resources or if there is no update,
|
||||
/// otherwise deletes all resources and returns false.
|
||||
private boolean extractUpdate(File dataDir) {
|
||||
if (FlutterMain.getUpdateInstallationPath() == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
||||
if (!updateFile.exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ZipFile zipFile;
|
||||
try {
|
||||
zipFile = new ZipFile(updateFile);
|
||||
|
||||
} catch (ZipException e) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] buffer = null;
|
||||
for (String asset : mResources) {
|
||||
ZipEntry entry = zipFile.getEntry(asset);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final File output = new File(dataDir, asset);
|
||||
if (output.exists()) {
|
||||
continue;
|
||||
}
|
||||
if (output.getParentFile() != null) {
|
||||
output.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try (InputStream is = zipFile.getInputStream(entry);
|
||||
OutputStream os = new FileOutputStream(output)) {
|
||||
if (buffer == null) {
|
||||
buffer = new byte[BUFFER_SIZE];
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
|
||||
os.write(buffer, 0, count);
|
||||
}
|
||||
|
||||
os.flush();
|
||||
Log.i(TAG, "Extracted override resource " + asset);
|
||||
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
continue;
|
||||
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
|
||||
deleteFiles();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns null if extracted resources are found and match the current APK version
|
||||
// and update version if any, otherwise returns the current APK and update version.
|
||||
private String checkTimestamp(File dataDir, JSONObject updateManifest) {
|
||||
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
PackageInfo packageInfo = null;
|
||||
|
||||
try {
|
||||
packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return TIMESTAMP_PREFIX;
|
||||
}
|
||||
|
||||
if (packageInfo == null) {
|
||||
return TIMESTAMP_PREFIX;
|
||||
}
|
||||
|
||||
String expectedTimestamp =
|
||||
TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
|
||||
|
||||
if (updateManifest != null) {
|
||||
String buildNumber = updateManifest.optString("buildNumber", null);
|
||||
if (buildNumber == null) {
|
||||
Log.w(TAG, "Invalid update manifest: buildNumber");
|
||||
} else {
|
||||
String patchNumber = updateManifest.optString("patchNumber", null);
|
||||
if (!buildNumber.equals(Long.toString(getVersionCode(packageInfo)))) {
|
||||
Log.w(TAG, "Outdated update file for " + getVersionCode(packageInfo));
|
||||
} else {
|
||||
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
||||
if (patchNumber != null) {
|
||||
expectedTimestamp += "-" + patchNumber + "-" + updateFile.lastModified();
|
||||
} else {
|
||||
expectedTimestamp += "-" + updateFile.lastModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String[] existingTimestamps = getExistingTimestamps(dataDir);
|
||||
|
||||
if (existingTimestamps == null) {
|
||||
Log.i(TAG, "No extracted resources found");
|
||||
return expectedTimestamp;
|
||||
}
|
||||
|
||||
if (existingTimestamps.length == 1) {
|
||||
Log.i(TAG, "Found extracted resources " + existingTimestamps[0]);
|
||||
}
|
||||
|
||||
if (existingTimestamps.length != 1
|
||||
|| !expectedTimestamp.equals(existingTimestamps[0])) {
|
||||
Log.i(TAG, "Resource version mismatch " + expectedTimestamp);
|
||||
return expectedTimestamp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns true if the downloaded update file was indeed built for this APK.
|
||||
private boolean validateUpdateManifest(JSONObject updateManifest) {
|
||||
if (updateManifest == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String baselineChecksum = updateManifest.optString("baselineChecksum", null);
|
||||
if (baselineChecksum == null) {
|
||||
Log.w(TAG, "Invalid update manifest: baselineChecksum");
|
||||
return false;
|
||||
}
|
||||
|
||||
final AssetManager manager = mContext.getResources().getAssets();
|
||||
try (InputStream is = manager.open("flutter_assets/isolate_snapshot_data")) {
|
||||
CRC32 checksum = new CRC32();
|
||||
|
||||
int count = 0;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
|
||||
checksum.update(buffer, 0, count);
|
||||
}
|
||||
|
||||
if (!baselineChecksum.equals(String.valueOf(checksum.getValue()))) {
|
||||
Log.w(TAG, "Mismatched update file for APK");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Could not read APK: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns null if no update manifest is found.
|
||||
private JSONObject readUpdateManifest() {
|
||||
if (FlutterMain.getUpdateInstallationPath() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
||||
if (!updateFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ZipFile zipFile = new ZipFile(updateFile);
|
||||
ZipEntry entry = zipFile.getEntry("manifest.json");
|
||||
if (entry == null) {
|
||||
Log.w(TAG, "Invalid update file: " + updateFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read and parse the entire JSON file as single operation.
|
||||
Scanner scanner = new Scanner(zipFile.getInputStream(entry));
|
||||
return new JSONObject(scanner.useDelimiter("\\A").next());
|
||||
|
||||
} catch (ZipException e) {
|
||||
Log.w(TAG, "Invalid update file: " + e);
|
||||
return null;
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Invalid update file: " + e);
|
||||
return null;
|
||||
|
||||
} catch (JSONException e) {
|
||||
Log.w(TAG, "Invalid update file: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +107,7 @@ public final class ResourceUpdater {
|
||||
}
|
||||
|
||||
public String getUpdateInstallationPath() {
|
||||
return context.getFilesDir().toString() + "/update.zip";
|
||||
return context.getFilesDir().toString() + "/patch.zip";
|
||||
}
|
||||
|
||||
public String buildUpdateDownloadURL() {
|
||||
@ -120,16 +120,16 @@ public final class ResourceUpdater {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (metaData == null || metaData.getString("UpdateServerURL") == null) {
|
||||
if (metaData == null || metaData.getString("PatchServerURL") == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(metaData.getString("UpdateServerURL") + "/" + getAPKVersion() + ".zip");
|
||||
uri = new URI(metaData.getString("PatchServerURL") + "/" + getAPKVersion() + ".zip");
|
||||
|
||||
} catch (URISyntaxException e) {
|
||||
Log.w(TAG, "Invalid AndroidManifest.xml UpdateServerURL: " + e.getMessage());
|
||||
Log.w(TAG, "Invalid AndroidManifest.xml PatchServerURL: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user