| // 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]); |
| } |
| } |
| } |