mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
375 lines
14 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|