// 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 static java.util.Arrays.asList; 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.support.annotation.NonNull; import android.support.annotation.WorkerThread; import android.util.Log; import io.flutter.BuildConfig; import io.flutter.util.PathUtils; import org.json.JSONObject; 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; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; 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 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 { @NonNull private final String mDataDirPath; @NonNull private final HashSet mResources; @NonNull private final AssetManager mAssetManager; @NonNull private final String mPackageName; @NonNull private final PackageManager mPackageManager; ExtractTask(@NonNull String dataDirPath, @NonNull HashSet 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 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 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 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 cpuAbis = new ArrayList(asList(Build.CPU_ABI, Build.CPU_ABI2)); cpuAbis.removeAll(asList(null, "")); return cpuAbis.toArray(new String[0]); } } }