mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This change adds explicit validation of dynamic patches in all places where they're used, instead of only validating it in some places which wasn't as reliable because some of the code paths were missed. This change also moves utility functions that deal with validating patches from ResourceExtractor to ResourceUpdater, to make them available as API for other places in code that need this validation.
371 lines
14 KiB
Java
371 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;
|
|
}
|
|
|
|
final AssetManager manager = context.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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|