mirror of
https://github.com/encounter/engine.git
synced 2026-03-30 11:09:55 -07:00
8cca33aea8
This change introduces manifest properties that control when dynamic patches are downloaded and installed in the application lifecycle. Application developer can choose whether between install on restart, install on resume, or immediate forced install of dynamic patches.
412 lines
13 KiB
Java
412 lines
13 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.Build;
|
|
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 initialize the native code.
|
|
**/
|
|
class ResourceExtractor {
|
|
private static final String TAG = "ResourceExtractor";
|
|
private static final String TIMESTAMP_PREFIX = "res_timestamp-";
|
|
|
|
private static final int BUFFER_SIZE = 16 * 1024;
|
|
|
|
@SuppressWarnings("deprecation")
|
|
static long getVersionCode(PackageInfo packageInfo) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
return packageInfo.getLongVersionCode();
|
|
} else {
|
|
return packageInfo.versionCode;
|
|
}
|
|
}
|
|
|
|
private class ExtractTask extends AsyncTask<Void, Void, Void> {
|
|
ExtractTask() { }
|
|
|
|
@Override
|
|
protected Void doInBackground(Void... unused) {
|
|
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
|
|
|
|
JSONObject updateManifest = readUpdateManifest();
|
|
if (!validateUpdateManifest(updateManifest)) {
|
|
updateManifest = null;
|
|
}
|
|
|
|
final String timestamp = checkTimestamp(dataDir, updateManifest);
|
|
if (timestamp == null) {
|
|
return null;
|
|
}
|
|
|
|
deleteFiles();
|
|
|
|
if (updateManifest != null) {
|
|
if (!extractUpdate(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");
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private final Context mContext;
|
|
private final HashSet<String> mResources;
|
|
private ExtractTask mExtractTask;
|
|
|
|
ResourceExtractor(Context context) {
|
|
mContext = context;
|
|
mResources = new HashSet<>();
|
|
}
|
|
|
|
ResourceExtractor addResource(String resource) {
|
|
mResources.add(resource);
|
|
return this;
|
|
}
|
|
|
|
ResourceExtractor addResources(Collection<String> resources) {
|
|
mResources.addAll(resources);
|
|
return this;
|
|
}
|
|
|
|
ResourceExtractor start() {
|
|
assert mExtractTask == null;
|
|
mExtractTask = new ExtractTask();
|
|
mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
return this;
|
|
}
|
|
|
|
void waitForCompletion() {
|
|
if (mExtractTask == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
mExtractTask.get();
|
|
} catch (CancellationException e) {
|
|
deleteFiles();
|
|
} catch (ExecutionException e2) {
|
|
deleteFiles();
|
|
} catch (InterruptedException e3) {
|
|
deleteFiles();
|
|
}
|
|
}
|
|
|
|
boolean filesMatch() {
|
|
JSONObject updateManifest = readUpdateManifest();
|
|
if (!validateUpdateManifest(updateManifest)) {
|
|
updateManifest = null;
|
|
}
|
|
|
|
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
|
|
final String timestamp = checkTimestamp(dataDir, updateManifest);
|
|
return (timestamp == null);
|
|
}
|
|
|
|
private String[] getExistingTimestamps(File dataDir) {
|
|
return dataDir.list(new FilenameFilter() {
|
|
@Override
|
|
public boolean accept(File dir, String name) {
|
|
return name.startsWith(TIMESTAMP_PREFIX);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void deleteFiles() {
|
|
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
|
|
for (String resource : mResources) {
|
|
final File file = new File(dataDir, resource);
|
|
if (file.exists()) {
|
|
file.delete();
|
|
}
|
|
}
|
|
final String[] existingTimestamps = getExistingTimestamps(dataDir);
|
|
if (existingTimestamps == null) {
|
|
return;
|
|
}
|
|
for (String timestamp : existingTimestamps) {
|
|
new File(dataDir, timestamp).delete();
|
|
}
|
|
}
|
|
|
|
|
|
/// 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;
|
|
}
|
|
if (output.getParentFile() != null) {
|
|
output.getParentFile().mkdirs();
|
|
}
|
|
|
|
try (InputStream is = manager.open(asset);
|
|
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 baseline resource " + asset);
|
|
}
|
|
|
|
} catch (FileNotFoundException fnfe) {
|
|
continue;
|
|
|
|
} catch (IOException ioe) {
|
|
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
|
|
deleteFiles();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
|
|
try {
|
|
packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), 0);
|
|
} catch (PackageManager.NameNotFoundException e) {
|
|
return TIMESTAMP_PREFIX;
|
|
}
|
|
|
|
if (packageInfo == null) {
|
|
return TIMESTAMP_PREFIX;
|
|
}
|
|
|
|
String expectedTimestamp =
|
|
TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
|
|
|
|
if (updateManifest != null) {
|
|
String buildNumber = updateManifest.optString("buildNumber", null);
|
|
if (buildNumber == null) {
|
|
Log.w(TAG, "Invalid update manifest: buildNumber");
|
|
} else {
|
|
String patchNumber = updateManifest.optString("patchNumber", null);
|
|
if (!buildNumber.equals(Long.toString(getVersionCode(packageInfo)))) {
|
|
Log.w(TAG, "Outdated update file for " + getVersionCode(packageInfo));
|
|
} else {
|
|
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
|
|
if (patchNumber != null) {
|
|
expectedTimestamp += "-" + patchNumber + "-" + updateFile.lastModified();
|
|
} else {
|
|
expectedTimestamp += "-" + updateFile.lastModified();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final String[] existingTimestamps = getExistingTimestamps(dataDir);
|
|
|
|
if (existingTimestamps == 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;
|
|
}
|
|
}
|
|
}
|