Stanislav Baranov aa275820bc
Support for loading dynamic patches in AOT mode. (#7744)
* Dynamic patching support for AOT mode.

* Cleanup.
2019-02-08 10:47:01 -08:00

375 lines
14 KiB
Java

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.view;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.Math;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.Scanner;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.json.JSONException;
import org.json.JSONObject;
public final class ResourceUpdater {
private static final String TAG = "ResourceUpdater";
private static final int BUFFER_SIZE = 16 * 1024;
// Controls when to check if a new patch is available for download, and start downloading.
// Note that by default the application will not block to wait for the download to finish.
// Patches are downloaded in the background, but the developer can also use [InstallMode]
// to control whether to block on download completion, in order to install patches sooner.
enum DownloadMode {
// Check for and download patch on application restart (but not necessarily apply it).
// This is the default setting which will also check for new patches least frequently.
ON_RESTART,
// Check for and download patch on application resume (but not necessarily apply it).
// By definition, this setting will check for new patches both on restart and resume.
ON_RESUME
}
// Controls when to check that a new patch has been downloaded and needs to be applied.
enum InstallMode {
// Wait for next application restart before applying downloaded patch. With this
// setting, the application will not block to wait for patch download to finish.
// The application can be restarted later either by the user, or by the system,
// for any reason, at which point the newly downloaded patch will get applied.
// This is the default setting, and is the least disruptive way to apply patches.
ON_NEXT_RESTART,
// Apply patch as soon as it's downloaded. This will block to wait for new patch
// download to finish, and will immediately apply it. This setting increases the
// urgency with which patches are installed, but may also affect startup latency.
// For now, this setting is only effective when download happens during restart.
// Patches downloaded during resume will not get installed immediately as that
// requires force restarting the app (which might be implemented in the future).
IMMEDIATE
}
/// Lock that prevents replacement of the install file by the downloader
/// while this file is being extracted, since these can happen in parallel.
Lock getInstallationLock() {
return installationLock;
}
// Patch file that's fully installed and is ready to serve assets.
// This file represents the final stage in the installation process.
public File getInstalledPatch() {
return new File(context.getFilesDir().toString() + "/patch.zip");
}
// Patch file that's finished downloading and is ready to be installed.
// This is a separate file in order to prevent serving assets from patch
// that failed installing for any reason, such as mismatched APK version.
File getDownloadedPatch() {
return new File(getInstalledPatch().getPath() + ".install");
}
private class DownloadTask extends AsyncTask<String, String, Void> {
@Override
protected Void doInBackground(String... unused) {
try {
URL unresolvedURL = new URL(buildUpdateDownloadURL());
// Download to transient file to avoid extracting incomplete download.
File localFile = new File(getInstalledPatch().getPath() + ".download");
long startMillis = new Date().getTime();
Log.i(TAG, "Checking for updates at " + unresolvedURL);
HttpURLConnection connection =
(HttpURLConnection)unresolvedURL.openConnection();
long lastDownloadTime = Math.max(
getDownloadedPatch().lastModified(),
getInstalledPatch().lastModified());
if (lastDownloadTime != 0) {
Log.i(TAG, "Active update timestamp " + lastDownloadTime);
connection.setIfModifiedSince(lastDownloadTime);
}
URL resolvedURL = connection.getURL();
Log.i(TAG, "Resolved update URL " + resolvedURL);
int responseCode = connection.getResponseCode();
Log.i(TAG, "HTTP response code " + responseCode);
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
Log.i(TAG, "Latest update not found on server");
return null;
}
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.i(TAG, "Already have latest update");
return null;
}
try (InputStream input = connection.getInputStream()) {
Log.i(TAG, "Downloading update " + unresolvedURL);
try (OutputStream output = new FileOutputStream(localFile)) {
int count;
byte[] data = new byte[1024];
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
long totalMillis = new Date().getTime() - startMillis;
Log.i(TAG, "Update downloaded in " + totalMillis / 100 / 10. + "s");
}
}
// Wait renaming the file if extraction is in progress.
installationLock.lock();
try {
File updateFile = getDownloadedPatch();
// Graduate downloaded file as ready for installation.
if (updateFile.exists() && !updateFile.delete()) {
Log.w(TAG, "Could not delete file " + updateFile);
return null;
}
if (!localFile.renameTo(updateFile)) {
Log.w(TAG, "Could not create file " + updateFile);
return null;
}
return null;
} finally {
installationLock.unlock();
}
} catch (IOException e) {
Log.w(TAG, "Could not download update " + e.getMessage());
return null;
}
}
}
private final Context context;
private DownloadTask downloadTask;
private final Lock installationLock = new ReentrantLock();
public ResourceUpdater(Context context) {
this.context = context;
}
private String getAPKVersion() {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
return packageInfo == null ? null : Long.toString(ResourceExtractor.getVersionCode(packageInfo));
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
private String buildUpdateDownloadURL() {
Bundle metaData;
try {
metaData = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_META_DATA).metaData;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
if (metaData == null || metaData.getString("PatchServerURL") == null) {
return null;
}
URI uri;
try {
uri = new URI(metaData.getString("PatchServerURL") + "/" + getAPKVersion() + ".zip");
} catch (URISyntaxException e) {
Log.w(TAG, "Invalid AndroidManifest.xml PatchServerURL: " + e.getMessage());
return null;
}
return uri.normalize().toString();
}
DownloadMode getDownloadMode() {
Bundle metaData;
try {
metaData = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_META_DATA).metaData;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
if (metaData == null) {
return DownloadMode.ON_RESTART;
}
String patchDownloadMode = metaData.getString("PatchDownloadMode");
if (patchDownloadMode == null) {
return DownloadMode.ON_RESTART;
}
try {
return DownloadMode.valueOf(patchDownloadMode);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Invalid PatchDownloadMode " + patchDownloadMode);
return DownloadMode.ON_RESTART;
}
}
InstallMode getInstallMode() {
Bundle metaData;
try {
metaData = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_META_DATA).metaData;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
if (metaData == null) {
return InstallMode.ON_NEXT_RESTART;
}
String patchInstallMode = metaData.getString("PatchInstallMode");
if (patchInstallMode == null) {
return InstallMode.ON_NEXT_RESTART;
}
try {
return InstallMode.valueOf(patchInstallMode);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Invalid PatchInstallMode " + patchInstallMode);
return InstallMode.ON_NEXT_RESTART;
}
}
/// Returns manifest JSON from ZIP file, or null if not found.
public JSONObject readManifest(File updateFile) {
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 (IOException | JSONException e) {
Log.w(TAG, "Invalid update file: " + e);
return null;
}
}
/// Returns true if the patch file was indeed built for this APK.
public boolean validateManifest(JSONObject manifest) {
if (manifest == null) {
return false;
}
String buildNumber = manifest.optString("buildNumber", null);
if (buildNumber == null) {
Log.w(TAG, "Invalid update manifest: missing buildNumber");
return false;
}
if (!buildNumber.equals(getAPKVersion())) {
Log.w(TAG, "Outdated update file for build " + getAPKVersion());
return false;
}
String baselineChecksum = manifest.optString("baselineChecksum", null);
if (baselineChecksum == null) {
Log.w(TAG, "Invalid update manifest: missing baselineChecksum");
return false;
}
CRC32 checksum = new CRC32();
String[] checksumFiles = {
"isolate_snapshot_data",
"isolate_snapshot_instr",
"flutter_assets/isolate_snapshot_data",
};
for (String fn : checksumFiles) {
AssetManager manager = context.getResources().getAssets();
try (InputStream is = manager.open(fn)) {
int count = 0;
byte[] buffer = new byte[BUFFER_SIZE];
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
checksum.update(buffer, 0, count);
}
} catch (IOException e) {
// Skip missing files.
}
}
if (!baselineChecksum.equals(String.valueOf(checksum.getValue()))) {
Log.w(TAG, "Mismatched update file for APK");
return false;
}
return true;
}
void startUpdateDownloadOnce() {
if (downloadTask != null) {
return;
}
downloadTask = new DownloadTask();
downloadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
void waitForDownloadCompletion() {
if (downloadTask == null) {
return;
}
try {
downloadTask.get();
downloadTask = null;
} catch (CancellationException e) {
Log.w(TAG, "Download cancelled: " + e.getMessage());
return;
} catch (ExecutionException e) {
Log.w(TAG, "Download exception: " + e.getMessage());
return;
} catch (InterruptedException e) {
Log.w(TAG, "Download interrupted: " + e.getMessage());
return;
}
}
}