blob: a0b7382edf3da09fa0cd22faf1657f0657b15214 [file] [log] [blame]
// 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.embedding.engine.loader;
import static java.util.Arrays.asList;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import io.flutter.BuildConfig;
import io.flutter.Log;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
/** 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 String[] SUPPORTED_ABIS = getSupportedAbis();
@SuppressWarnings("deprecation")
static long getVersionCode(@NonNull PackageInfo packageInfo) {
// Linter needs P (28) hardcoded or else it will fail these lines.
if (Build.VERSION.SDK_INT >= 28) {
return packageInfo.getLongVersionCode();
} else {
return packageInfo.versionCode;
}
}
private static class ExtractTask extends AsyncTask<Void, Void, Void> {
@NonNull private final String mDataDirPath;
@NonNull private final HashSet<String> mResources;
@NonNull private final AssetManager mAssetManager;
@NonNull private final String mPackageName;
@NonNull private final PackageManager mPackageManager;
ExtractTask(
@NonNull String dataDirPath,
@NonNull HashSet<String> resources,
@NonNull String packageName,
@NonNull PackageManager packageManager,
@NonNull AssetManager assetManager) {
mDataDirPath = dataDirPath;
mResources = resources;
mAssetManager = assetManager;
mPackageName = packageName;
mPackageManager = packageManager;
}
@Override
protected Void doInBackground(Void... unused) {
final File dataDir = new File(mDataDirPath);
final String timestamp = checkTimestamp(dataDir, mPackageManager, mPackageName);
if (timestamp == null) {
return null;
}
deleteFiles(mDataDirPath, mResources);
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;
}
/// Returns true if successfully unpacked APK resources,
/// otherwise deletes all resources and returns false.
@WorkerThread
private boolean extractAPK(@NonNull File dataDir) {
for (String asset : mResources) {
try {
final String resource = "assets/" + asset;
final File output = new File(dataDir, asset);
if (output.exists()) {
continue;
}
if (output.getParentFile() != null) {
output.getParentFile().mkdirs();
}
try (InputStream is = mAssetManager.open(asset);
OutputStream os = new FileOutputStream(output)) {
copy(is, os);
}
if (BuildConfig.DEBUG) {
Log.i(TAG, "Extracted baseline resource " + resource);
}
} catch (FileNotFoundException fnfe) {
continue;
} catch (IOException ioe) {
Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
deleteFiles(mDataDirPath, mResources);
return false;
}
}
return true;
}
}
@NonNull private final String mDataDirPath;
@NonNull private final String mPackageName;
@NonNull private final PackageManager mPackageManager;
@NonNull private final AssetManager mAssetManager;
@NonNull private final HashSet<String> mResources;
private ExtractTask mExtractTask;
ResourceExtractor(
@NonNull String dataDirPath,
@NonNull String packageName,
@NonNull PackageManager packageManager,
@NonNull AssetManager assetManager) {
mDataDirPath = dataDirPath;
mPackageName = packageName;
mPackageManager = packageManager;
mAssetManager = assetManager;
mResources = new HashSet<>();
}
ResourceExtractor addResource(@NonNull String resource) {
mResources.add(resource);
return this;
}
ResourceExtractor addResources(@NonNull Collection<String> resources) {
mResources.addAll(resources);
return this;
}
ResourceExtractor start() {
if (BuildConfig.DEBUG && mExtractTask != null) {
Log.e(
TAG, "Attempted to start resource extraction while another extraction was in progress.");
}
mExtractTask =
new ExtractTask(mDataDirPath, mResources, mPackageName, mPackageManager, mAssetManager);
mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return this;
}
void waitForCompletion() {
if (mExtractTask == null) {
return;
}
try {
mExtractTask.get();
} catch (CancellationException | ExecutionException | InterruptedException e) {
deleteFiles(mDataDirPath, mResources);
}
}
private static String[] getExistingTimestamps(File dataDir) {
return dataDir.list(
new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.startsWith(TIMESTAMP_PREFIX);
}
});
}
private static void deleteFiles(@NonNull String dataDirPath, @NonNull HashSet<String> resources) {
final File dataDir = new File(dataDirPath);
for (String resource : resources) {
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 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 static String checkTimestamp(
@NonNull File dataDir, @NonNull PackageManager packageManager, @NonNull String packageName) {
PackageInfo packageInfo = null;
try {
packageInfo = packageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
return TIMESTAMP_PREFIX;
}
if (packageInfo == null) {
return TIMESTAMP_PREFIX;
}
String expectedTimestamp =
TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
final String[] existingTimestamps = getExistingTimestamps(dataDir);
if (existingTimestamps == null) {
if (BuildConfig.DEBUG) {
Log.i(TAG, "No extracted resources found");
}
return expectedTimestamp;
}
if (existingTimestamps.length == 1) {
if (BuildConfig.DEBUG) {
Log.i(TAG, "Found extracted resources " + existingTimestamps[0]);
}
}
if (existingTimestamps.length != 1 || !expectedTimestamp.equals(existingTimestamps[0])) {
if (BuildConfig.DEBUG) {
Log.i(TAG, "Resource version mismatch " + expectedTimestamp);
}
return expectedTimestamp;
}
return null;
}
private static void copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
byte[] buf = new byte[16 * 1024];
for (int i; (i = in.read(buf)) >= 0; ) {
out.write(buf, 0, i);
}
}
@SuppressWarnings("deprecation")
private static String[] getSupportedAbis() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return Build.SUPPORTED_ABIS;
} else {
ArrayList<String> cpuAbis = new ArrayList<String>(asList(Build.CPU_ABI, Build.CPU_ABI2));
cpuAbis.removeAll(asList(null, ""));
return cpuAbis.toArray(new String[0]);
}
}
}