未验证 提交 8b5f7763 编写于 作者: S Stanislav Baranov 提交者: GitHub

Remove support for downloading dynamic patches. (#8663)

上级 1bcb96bf
......@@ -547,7 +547,6 @@ 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
......
......@@ -167,7 +167,6 @@ 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",
]
......
......@@ -32,7 +32,6 @@ import io.flutter.view.FlutterMain;
import io.flutter.view.FlutterNativeView;
import io.flutter.view.FlutterRunArguments;
import io.flutter.view.FlutterView;
import io.flutter.view.ResourceUpdater;
import org.json.JSONObject;
import java.io.File;
......@@ -213,7 +212,6 @@ public final class FlutterActivityDelegate
@Override
public void onResume() {
Application app = (Application) activity.getApplicationContext();
FlutterMain.onResume(app);
if (app instanceof FlutterApplication) {
FlutterApplication flutterApp = (FlutterApplication) app;
flutterApp.setCurrentActivity(activity);
......@@ -354,14 +352,6 @@ public final class FlutterActivityDelegate
if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
FlutterRunArguments args = new FlutterRunArguments();
ArrayList<String> bundlePaths = new ArrayList<>();
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater != null) {
File patchFile = resourceUpdater.getInstalledPatch();
JSONObject manifest = resourceUpdater.readManifest(patchFile);
if (resourceUpdater.validateManifest(manifest)) {
bundlePaths.add(patchFile.getPath());
}
}
bundlePaths.add(appBundlePath);
args.bundlePaths = bundlePaths.toArray(new String[0]);
args.entrypoint = "main";
......
......@@ -74,7 +74,6 @@ public class FlutterMain {
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;
......@@ -152,17 +151,7 @@ public class FlutterMain {
initAot(applicationContext);
initResources(applicationContext);
if (sResourceUpdater == null) {
System.loadLibrary("flutter");
} else {
sResourceExtractor.waitForCompletion();
File lib = new File(PathUtils.getDataDirectory(applicationContext), DEFAULT_LIBRARY);
if (lib.exists()) {
System.load(lib.getAbsolutePath());
} else {
System.loadLibrary("flutter");
}
}
System.loadLibrary("flutter");
// We record the initialization time using SystemClock because at the start of the
// initialization we have not yet loaded the native library to call into dart_tools_api.h.
......@@ -310,21 +299,6 @@ public class FlutterMain {
Log.e(TAG, "Unable to read application info", e);
}
if (metaData != null && metaData.getBoolean("DynamicPatching")) {
sResourceUpdater = new ResourceUpdater(context);
// Also checking for ON_RESUME here since it's more efficient than waiting for actual
// onResume. Even though actual onResume is imminent when the app has just restarted,
// it's better to start downloading now, in parallel with the rest of initialization,
// and avoid a second application restart a bit later when actual onResume happens.
if (sResourceUpdater.getDownloadMode() == ResourceUpdater.DownloadMode.ON_RESTART ||
sResourceUpdater.getDownloadMode() == ResourceUpdater.DownloadMode.ON_RESUME) {
sResourceUpdater.startUpdateDownloadOnce();
if (sResourceUpdater.getInstallMode() == ResourceUpdater.InstallMode.IMMEDIATE) {
sResourceUpdater.waitForDownloadCompletion();
}
}
}
sResourceExtractor = new ResourceExtractor(context);
sResourceExtractor
......@@ -346,22 +320,9 @@ public class FlutterMain {
.addResource(sAotIsolateSnapshotInstr);
}
if (sResourceUpdater != null) {
sResourceExtractor
.addResource(DEFAULT_LIBRARY);
}
sResourceExtractor.start();
}
public static void onResume(Context context) {
if (sResourceUpdater != null) {
if (sResourceUpdater.getDownloadMode() == ResourceUpdater.DownloadMode.ON_RESUME) {
sResourceUpdater.startUpdateDownloadOnce();
}
}
}
/**
* Returns a list of the file names at the root of the application's asset
* path.
......@@ -403,15 +364,6 @@ public class FlutterMain {
return appBundle.exists() ? appBundle.getPath() : null;
}
/**
* Returns the main internal interface for the dynamic patching subsystem.
*
* If this is null, it means that dynamic patching is disabled in this app.
*/
public static ResourceUpdater getResourceUpdater() {
return sResourceUpdater;
}
/**
* Returns the file name for the given asset.
* The returned file name can be used to access the asset in the APK
......
......@@ -52,64 +52,26 @@ class ResourceExtractor {
protected Void doInBackground(Void... unused) {
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater != null) {
// Protect patch file from being overwritten by downloader while
// it's being extracted since downloading happens asynchronously.
resourceUpdater.getInstallationLock().lock();
final String timestamp = checkTimestamp(dataDir);
if (timestamp == null) {
return null;
}
try {
if (resourceUpdater != null) {
File updateFile = resourceUpdater.getDownloadedPatch();
File activeFile = resourceUpdater.getInstalledPatch();
if (updateFile.exists()) {
JSONObject manifest = resourceUpdater.readManifest(updateFile);
if (resourceUpdater.validateManifest(manifest)) {
// Graduate patch file as active for asset manager.
if (activeFile.exists() && !activeFile.delete()) {
Log.w(TAG, "Could not delete file " + activeFile);
return null;
}
if (!updateFile.renameTo(activeFile)) {
Log.w(TAG, "Could not create file " + activeFile);
return null;
}
}
}
}
final String timestamp = checkTimestamp(dataDir);
if (timestamp == null) {
return null;
}
deleteFiles();
if (!extractUpdate(dataDir)) {
return null;
}
deleteFiles();
if (!extractAPK(dataDir)) {
return null;
}
if (!extractAPK(dataDir)) {
return null;
}
if (timestamp != null) {
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
}
if (timestamp != null) {
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
}
}
return null;
} finally {
if (resourceUpdater != null) {
resourceUpdater.getInstallationLock().unlock();
}
}
return null;
}
}
......@@ -213,133 +175,6 @@ class ResourceExtractor {
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) {
final AssetManager manager = mContext.getResources().getAssets();
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater == null) {
return true;
}
File updateFile = resourceUpdater.getInstalledPatch();
if (!updateFile.exists()) {
return true;
}
JSONObject manifest = resourceUpdater.readManifest(updateFile);
if (!resourceUpdater.validateManifest(manifest)) {
// Obsolete patch file, nothing to install.
return true;
}
ZipFile zipFile;
try {
zipFile = new ZipFile(updateFile);
} catch (IOException e) {
Log.w(TAG, "Exception unpacking resources: " + e.getMessage());
deleteFiles();
return false;
}
for (String asset : mResources) {
String resource = null;
ZipEntry entry = null;
if (asset.endsWith(".so")) {
// Replicate library lookup logic.
for (String abi : SUPPORTED_ABIS) {
resource = "lib/" + abi + "/" + asset;
entry = zipFile.getEntry(resource);
if (entry == null) {
entry = zipFile.getEntry(resource + ".bzdiff40");
if (entry == null) {
continue;
}
}
// Stop after the first match.
break;
}
}
if (entry == null) {
resource = "assets/" + asset;
entry = zipFile.getEntry(resource);
if (entry == null) {
entry = zipFile.getEntry(resource + ".bzdiff40");
if (entry == null) {
continue;
}
}
}
final File output = new File(dataDir, asset);
if (output.exists()) {
continue;
}
if (output.getParentFile() != null) {
output.getParentFile().mkdirs();
}
try {
if (entry.getName().endsWith(".bzdiff40")) {
ByteArrayOutputStream diff = new ByteArrayOutputStream();
try (InputStream is = zipFile.getInputStream(entry)) {
copy(is, diff);
}
ByteArrayOutputStream orig = new ByteArrayOutputStream();
if (asset.endsWith(".so")) {
ZipFile apkFile = new ZipFile(getAPKPath());
if (apkFile == null) {
throw new IOException("Could not find APK");
}
ZipEntry origEntry = apkFile.getEntry(resource);
if (origEntry == null) {
throw new IOException("Could not find APK resource " + resource);
}
try (InputStream is = apkFile.getInputStream(origEntry)) {
copy(is, orig);
}
} else {
try (InputStream is = manager.open(asset)) {
copy(is, orig);
} catch (FileNotFoundException e) {
throw new IOException("Could not find APK resource " + resource);
}
}
try (OutputStream os = new FileOutputStream(output)) {
os.write(BSDiff.bspatch(orig.toByteArray(), diff.toByteArray()));
}
} else {
try (InputStream is = zipFile.getInputStream(entry);
OutputStream os = new FileOutputStream(output)) {
copy(is, os);
}
}
Log.i(TAG, "Extracted override resource " + entry.getName());
} 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) {
......@@ -359,20 +194,6 @@ class ResourceExtractor {
String expectedTimestamp =
TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater != null) {
File patchFile = resourceUpdater.getInstalledPatch();
JSONObject manifest = resourceUpdater.readManifest(patchFile);
if (resourceUpdater.validateManifest(manifest)) {
String patchNumber = manifest.optString("patchNumber", null);
if (patchNumber != null) {
expectedTimestamp += "-" + patchNumber + "-" + patchFile.lastModified();
} else {
expectedTimestamp += "-" + patchFile.lastModified();
}
}
}
final String[] existingTimestamps = getExistingTimestamps(dataDir);
if (existingTimestamps == null) {
......
// 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;
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册