Skip to content

Commit

Permalink
Downloading and installation of dynamic updates on Android (flutter#7207
Browse files Browse the repository at this point in the history
)
  • Loading branch information
sbaranov authored Dec 13, 2018
1 parent 8e56b54 commit 18a4e33
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 14 deletions.
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceCleaner.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceExtractor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourcePaths.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/ResourceUpdater.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java
FILE: ../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java
FILE: ../../../flutter/shell/platform/android/library_loader.cc
Expand Down
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ java_library("flutter_shell_java") {
"io/flutter/view/ResourceCleaner.java",
"io/flutter/view/ResourceExtractor.java",
"io/flutter/view/ResourcePaths.java",
"io/flutter/view/ResourceUpdater.java",
"io/flutter/view/TextureRegistry.java",
"io/flutter/view/VsyncWaiter.java",
]
Expand Down
15 changes: 13 additions & 2 deletions shell/platform/android/io/flutter/app/FlutterActivityDelegate.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,17 @@ public void onCreate(Bundle savedInstanceState) {
if (loadIntent(activity.getIntent())) {
return;
}

if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
if (appBundlePath != null) {
FlutterRunArguments arguments = new FlutterRunArguments();
arguments.bundlePath = appBundlePath;
ArrayList<String> bundlePaths = new ArrayList<String>();
if (FlutterMain.getUpdateInstallationPath() != null) {
bundlePaths.add(FlutterMain.getUpdateInstallationPath());
}
bundlePaths.add(appBundlePath);
arguments.bundlePaths = bundlePaths.toArray(new String[0]);
arguments.entrypoint = "main";
flutterView.runFromBundle(arguments);
}
Expand Down Expand Up @@ -337,7 +343,12 @@ private boolean loadIntent(Intent intent) {
}
if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
FlutterRunArguments args = new FlutterRunArguments();
args.bundlePath = appBundlePath;
ArrayList<String> bundlePaths = new ArrayList<String>();
if (FlutterMain.getUpdateInstallationPath() != null) {
bundlePaths.add(FlutterMain.getUpdateInstallationPath());
}
bundlePaths.add(appBundlePath);
args.bundlePaths = bundlePaths.toArray(new String[0]);
args.entrypoint = "main";
flutterView.runFromBundle(args);
}
Expand Down
20 changes: 20 additions & 0 deletions shell/platform/android/io/flutter/view/FlutterMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private static String fromFlutterAssets(String filePath) {
private static String sFlutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR;

private static boolean sInitialized = false;
private static ResourceUpdater sResourceUpdater;
private static ResourceExtractor sResourceExtractor;
private static boolean sIsPrecompiledAsBlobs;
private static boolean sIsPrecompiledAsSharedLibrary;
Expand Down Expand Up @@ -254,6 +255,21 @@ private static void initResources(Context applicationContext) {
Context context = applicationContext;
new ResourceCleaner(context).start();

Bundle metaData = null;
try {
metaData = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_META_DATA).metaData;

} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to read application info", e);
}

if (metaData != null && metaData.getBoolean("DynamicUpdates")) {
sResourceUpdater = new ResourceUpdater(context);
sResourceUpdater.startUpdateDownloadOnce();
sResourceUpdater.waitForDownloadCompletion();
}

sResourceExtractor = new ResourceExtractor(context);

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

public static String getUpdateInstallationPath() {
return sResourceUpdater == null ? null : sResourceUpdater.getUpdateInstallationPath();
}

/**
* Returns the file name for the given asset.
* The returned file name can be used to access the asset in the APK
Expand Down
229 changes: 217 additions & 12 deletions shell/platform/android/io/flutter/view/ResourceExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@
import android.os.AsyncTask;
import android.util.Log;
import io.flutter.util.PathUtils;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Scanner;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

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

final String timestamp = checkTimestamp(dataDir);
JSONObject updateManifest = readUpdateManifest();
if (!validateUpdateManifest(updateManifest)) {
updateManifest = null;
}

final String timestamp = checkTimestamp(dataDir, updateManifest);
if (timestamp == null) {
return;
}

deleteFiles();

if (updateManifest != null) {
if (!extractUpdate(dataDir)) {
return;
}
}

if (!extractAPK(dataDir)) {
return;
}

if (timestamp != null) {
deleteFiles();
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
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;
}
Expand All @@ -62,28 +97,99 @@ private void extractResources() {
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;
return false;
}
}

if (timestamp != null) {
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
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)) {
try (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;
}

private String checkTimestamp(File dataDir) {
// 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;

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

if (updateManifest != null) {
String baselineVersion = updateManifest.optString("baselineVersion", null);
if (baselineVersion == null) {
Log.w(TAG, "Invalid update manifest: baselineVersion");
}

String updateVersion = updateManifest.optString("updateVersion", null);
if (updateVersion == null) {
Log.w(TAG, "Invalid update manifest: updateVersion");
}

if (baselineVersion != null && updateVersion != null) {
if (!baselineVersion.equals(Integer.toString(packageInfo.versionCode))) {
Log.w(TAG, "Outdated update file for " + packageInfo.versionCode);
} else {
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
expectedTimestamp += "-" + updateVersion + "-" + updateFile.lastModified();
}
}
}

final String[] existingTimestamps = getExistingTimestamps(dataDir);

if (existingTimestamps == null) {
return 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();
Expand Down
Loading

0 comments on commit 18a4e33

Please sign in to comment.