forked from firka/flutter
Remove support for downloading dynamic patches. (flutter/engine#8663)
This commit is contained in:
committed by
GitHub
parent
16f8fb6f4d
commit
4b86efc066
@@ -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;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
deleteFiles();
|
||||
|
||||
if (!extractAPK(dataDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (resourceUpdater != null) {
|
||||
resourceUpdater.getInstallationLock().unlock();
|
||||
}
|
||||
}
|
||||
if (timestamp != null) {
|
||||
try {
|
||||
new File(dataDir, timestamp).createNewFile();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to write resource timestamp");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user