Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ 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
Expand Down
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ 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",
]
Expand Down
15 changes: 13 additions & 2 deletions shell/platform/android/io/flutter/app/FlutterActivityDelegate.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,17 @@ public void onCreate(Bundle savedInstanceState) {
if (loadIntent(activity.getIntent())) {
return;
}

if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
if (appBundlePath != null) {
FlutterRunArguments arguments = new FlutterRunArguments();
arguments.bundlePath = appBundlePath;
ArrayList<String> bundlePaths = new ArrayList<String>();
if (FlutterMain.getUpdateInstallationPath() != null) {
bundlePaths.add(FlutterMain.getUpdateInstallationPath());
}
bundlePaths.add(appBundlePath);
arguments.bundlePaths = bundlePaths.toArray(new String[0]);
arguments.entrypoint = "main";
flutterView.runFromBundle(arguments);
}
Expand Down Expand Up @@ -337,7 +343,12 @@ private boolean loadIntent(Intent intent) {
}
if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
FlutterRunArguments args = new FlutterRunArguments();
args.bundlePath = appBundlePath;
ArrayList<String> bundlePaths = new ArrayList<String>();
if (FlutterMain.getUpdateInstallationPath() != null) {
bundlePaths.add(FlutterMain.getUpdateInstallationPath());
}
bundlePaths.add(appBundlePath);
args.bundlePaths = bundlePaths.toArray(new String[0]);
args.entrypoint = "main";
flutterView.runFromBundle(args);
}
Expand Down
20 changes: 20 additions & 0 deletions shell/platform/android/io/flutter/view/FlutterMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private static String fromFlutterAssets(String filePath) {
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;
Expand Down Expand Up @@ -254,6 +255,21 @@ private static void initResources(Context applicationContext) {
Context context = applicationContext;
new ResourceCleaner(context).start();

Bundle metaData = null;
try {
metaData = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_META_DATA).metaData;

} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to read application info", e);
}

if (metaData != null && metaData.getBoolean("DynamicUpdates")) {
sResourceUpdater = new ResourceUpdater(context);
sResourceUpdater.startUpdateDownloadOnce();
sResourceUpdater.waitForDownloadCompletion();
}

sResourceExtractor = new ResourceExtractor(context);

String icuAssetPath = SHARED_ASSET_DIR + File.separator + SHARED_ASSET_ICU_DATA;
Expand Down Expand Up @@ -321,6 +337,10 @@ public static String findAppBundlePath(Context applicationContext) {
return appBundle.exists() ? appBundle.getPath() : null;
}

public static String getUpdateInstallationPath() {
return sResourceUpdater == null ? null : sResourceUpdater.getUpdateInstallationPath();
}

/**
* Returns the file name for the given asset.
* The returned file name can be used to access the asset in the APK
Expand Down
229 changes: 217 additions & 12 deletions shell/platform/android/io/flutter/view/ResourceExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@
import android.os.AsyncTask;
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 intialize the native code.
* A class to initialize the native code.
**/
class ResourceExtractor {
private static final String TAG = "ResourceExtractor";
Expand All @@ -33,18 +40,46 @@ private class ExtractTask extends AsyncTask<Void, Void, Void> {
private void extractResources() {
final File dataDir = new File(PathUtils.getDataDirectory(mContext));

final String timestamp = checkTimestamp(dataDir);
JSONObject updateManifest = readUpdateManifest();
if (!validateUpdateManifest(updateManifest)) {
updateManifest = null;
}

final String timestamp = checkTimestamp(dataDir, updateManifest);
if (timestamp == null) {
return;
}

deleteFiles();

if (updateManifest != null) {
if (!extractUpdate(dataDir)) {
return;
}
}

if (!extractAPK(dataDir)) {
return;
}

if (timestamp != null) {
deleteFiles();
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
}
}
}

/// 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;
}
Expand All @@ -62,28 +97,99 @@ private void extractResources() {
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;
return false;
}
}

if (timestamp != null) {
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
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)) {
try (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;
}

private String checkTimestamp(File dataDir) {
// 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;

Expand All @@ -100,20 +206,119 @@ private String checkTimestamp(File dataDir) {
String expectedTimestamp =
TIMESTAMP_PREFIX + packageInfo.versionCode + "-" + packageInfo.lastUpdateTime;

if (updateManifest != null) {
String baselineVersion = updateManifest.optString("baselineVersion", null);
if (baselineVersion == null) {
Log.w(TAG, "Invalid update manifest: baselineVersion");
}

String updateVersion = updateManifest.optString("updateVersion", null);
if (updateVersion == null) {
Log.w(TAG, "Invalid update manifest: updateVersion");
}

if (baselineVersion != null && updateVersion != null) {
if (!baselineVersion.equals(Integer.toString(packageInfo.versionCode))) {
Log.w(TAG, "Outdated update file for " + packageInfo.versionCode);
} else {
final File updateFile = new File(FlutterMain.getUpdateInstallationPath());
expectedTimestamp += "-" + updateVersion + "-" + updateFile.lastModified();
}
}
}

final String[] existingTimestamps = getExistingTimestamps(dataDir);

if (existingTimestamps == null) {
return 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;
}
}

@Override
protected Void doInBackground(Void... unused) {
extractResources();
Expand Down
Loading