From 6f763fbad7cd090657d6465e0e5f752dddaff31c Mon Sep 17 00:00:00 2001 From: Stanislav Baranov Date: Fri, 28 Dec 2018 13:23:38 -0800 Subject: [PATCH] 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. --- .../android/io/flutter/view/FlutterMain.java | 2 +- .../io/flutter/view/ResourceExtractor.java | 506 +++++++++--------- .../io/flutter/view/ResourceUpdater.java | 8 +- 3 files changed, 257 insertions(+), 259 deletions(-) diff --git a/shell/platform/android/io/flutter/view/FlutterMain.java b/shell/platform/android/io/flutter/view/FlutterMain.java index 7594edb33..b01f243e3 100644 --- a/shell/platform/android/io/flutter/view/FlutterMain.java +++ b/shell/platform/android/io/flutter/view/FlutterMain.java @@ -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(); diff --git a/shell/platform/android/io/flutter/view/ResourceExtractor.java b/shell/platform/android/io/flutter/view/ResourceExtractor.java index ecf0ef5bc..0aaf58ec4 100644 --- a/shell/platform/android/io/flutter/view/ResourceExtractor.java +++ b/shell/platform/android/io/flutter/view/ResourceExtractor.java @@ -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 { - 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; + } + } } diff --git a/shell/platform/android/io/flutter/view/ResourceUpdater.java b/shell/platform/android/io/flutter/view/ResourceUpdater.java index 7a2ee45b1..679388e6e 100644 --- a/shell/platform/android/io/flutter/view/ResourceUpdater.java +++ b/shell/platform/android/io/flutter/view/ResourceUpdater.java @@ -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; }