1 // Copyright 2013 The Flutter Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package io.flutter.view; 6 7 import static java.util.Arrays.asList; 8 9 import android.content.Context; 10 import android.content.pm.PackageInfo; 11 import android.content.pm.PackageManager; 12 import android.content.res.AssetManager; 13 import android.os.AsyncTask; 14 import android.os.Build; 15 import android.support.annotation.NonNull; 16 import android.support.annotation.WorkerThread; 17 import android.util.Log; 18 19 import io.flutter.BuildConfig; 20 import io.flutter.util.PathUtils; 21 22 import org.json.JSONObject; 23 24 import java.io.*; 25 import java.util.ArrayList; 26 import java.util.Collection; 27 import java.util.HashSet; 28 import java.util.concurrent.CancellationException; 29 import java.util.concurrent.ExecutionException; 30 import java.util.zip.GZIPInputStream; 31 import java.util.zip.ZipEntry; 32 import java.util.zip.ZipFile; 33 34 /** 35 * A class to initialize the native code. 36 **/ 37 class ResourceExtractor { 38 private static final String TAG = "ResourceExtractor"; 39 private static final String TIMESTAMP_PREFIX = "res_timestamp-"; 40 private static final String[] SUPPORTED_ABIS = getSupportedAbis(); 41 42 @SuppressWarnings("deprecation") getVersionCode(@onNull PackageInfo packageInfo)43 static long getVersionCode(@NonNull PackageInfo packageInfo) { 44 // Linter needs P (28) hardcoded or else it will fail these lines. 45 if (Build.VERSION.SDK_INT >= 28) { 46 return packageInfo.getLongVersionCode(); 47 } else { 48 return packageInfo.versionCode; 49 } 50 } 51 52 private static class ExtractTask extends AsyncTask<Void, Void, Void> { 53 @NonNull 54 private final String mDataDirPath; 55 @NonNull 56 private final HashSet<String> mResources; 57 @NonNull 58 private final AssetManager mAssetManager; 59 @NonNull 60 private final String mPackageName; 61 @NonNull 62 private final PackageManager mPackageManager; 63 ExtractTask(@onNull String dataDirPath, @NonNull HashSet<String> resources, @NonNull String packageName, @NonNull PackageManager packageManager, @NonNull AssetManager assetManager)64 ExtractTask(@NonNull String dataDirPath, 65 @NonNull HashSet<String> resources, 66 @NonNull String packageName, 67 @NonNull PackageManager packageManager, 68 @NonNull AssetManager assetManager) { 69 mDataDirPath = dataDirPath; 70 mResources = resources; 71 mAssetManager = assetManager; 72 mPackageName = packageName; 73 mPackageManager = packageManager; 74 } 75 76 @Override doInBackground(Void... unused)77 protected Void doInBackground(Void... unused) { 78 final File dataDir = new File(mDataDirPath); 79 80 final String timestamp = checkTimestamp(dataDir, mPackageManager, mPackageName); 81 if (timestamp == null) { 82 return null; 83 } 84 85 deleteFiles(mDataDirPath, mResources); 86 87 if (!extractAPK(dataDir)) { 88 return null; 89 } 90 91 if (timestamp != null) { 92 try { 93 new File(dataDir, timestamp).createNewFile(); 94 } catch (IOException e) { 95 Log.w(TAG, "Failed to write resource timestamp"); 96 } 97 } 98 99 return null; 100 } 101 102 103 /// Returns true if successfully unpacked APK resources, 104 /// otherwise deletes all resources and returns false. 105 @WorkerThread extractAPK(@onNull File dataDir)106 private boolean extractAPK(@NonNull File dataDir) { 107 for (String asset : mResources) { 108 try { 109 final String resource = "assets/" + asset; 110 final File output = new File(dataDir, asset); 111 if (output.exists()) { 112 continue; 113 } 114 if (output.getParentFile() != null) { 115 output.getParentFile().mkdirs(); 116 } 117 118 try (InputStream is = mAssetManager.open(asset); 119 OutputStream os = new FileOutputStream(output)) { 120 copy(is, os); 121 } 122 if (BuildConfig.DEBUG) { 123 Log.i(TAG, "Extracted baseline resource " + resource); 124 } 125 } catch (FileNotFoundException fnfe) { 126 continue; 127 128 } catch (IOException ioe) { 129 Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage()); 130 deleteFiles(mDataDirPath, mResources); 131 return false; 132 } 133 } 134 135 return true; 136 } 137 } 138 139 @NonNull 140 private final String mDataDirPath; 141 @NonNull 142 private final String mPackageName; 143 @NonNull 144 private final PackageManager mPackageManager; 145 @NonNull 146 private final AssetManager mAssetManager; 147 @NonNull 148 private final HashSet<String> mResources; 149 private ExtractTask mExtractTask; 150 ResourceExtractor(@onNull String dataDirPath, @NonNull String packageName, @NonNull PackageManager packageManager, @NonNull AssetManager assetManager)151 ResourceExtractor(@NonNull String dataDirPath, 152 @NonNull String packageName, 153 @NonNull PackageManager packageManager, 154 @NonNull AssetManager assetManager) { 155 mDataDirPath = dataDirPath; 156 mPackageName = packageName; 157 mPackageManager = packageManager; 158 mAssetManager = assetManager; 159 mResources = new HashSet<>(); 160 } 161 addResource(@onNull String resource)162 ResourceExtractor addResource(@NonNull String resource) { 163 mResources.add(resource); 164 return this; 165 } 166 addResources(@onNull Collection<String> resources)167 ResourceExtractor addResources(@NonNull Collection<String> resources) { 168 mResources.addAll(resources); 169 return this; 170 } 171 start()172 ResourceExtractor start() { 173 if (BuildConfig.DEBUG && mExtractTask != null) { 174 Log.e(TAG, "Attempted to start resource extraction while another extraction was in progress."); 175 } 176 mExtractTask = new ExtractTask(mDataDirPath, mResources, mPackageName, mPackageManager, mAssetManager); 177 mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 178 return this; 179 } 180 waitForCompletion()181 void waitForCompletion() { 182 if (mExtractTask == null) { 183 return; 184 } 185 186 try { 187 mExtractTask.get(); 188 } catch (CancellationException | ExecutionException | InterruptedException e) { 189 deleteFiles(mDataDirPath, mResources); 190 } 191 } 192 getExistingTimestamps(File dataDir)193 private static String[] getExistingTimestamps(File dataDir) { 194 return dataDir.list(new FilenameFilter() { 195 @Override 196 public boolean accept(File dir, String name) { 197 return name.startsWith(TIMESTAMP_PREFIX); 198 } 199 }); 200 } 201 202 private static void deleteFiles(@NonNull String dataDirPath, @NonNull HashSet<String> resources) { 203 final File dataDir = new File(dataDirPath); 204 for (String resource : resources) { 205 final File file = new File(dataDir, resource); 206 if (file.exists()) { 207 file.delete(); 208 } 209 } 210 final String[] existingTimestamps = getExistingTimestamps(dataDir); 211 if (existingTimestamps == null) { 212 return; 213 } 214 for (String timestamp : existingTimestamps) { 215 new File(dataDir, timestamp).delete(); 216 } 217 } 218 219 // Returns null if extracted resources are found and match the current APK version 220 // and update version if any, otherwise returns the current APK and update version. 221 private static String checkTimestamp(@NonNull File dataDir, 222 @NonNull PackageManager packageManager, 223 @NonNull String packageName) { 224 PackageInfo packageInfo = null; 225 226 try { 227 packageInfo = packageManager.getPackageInfo(packageName, 0); 228 } catch (PackageManager.NameNotFoundException e) { 229 return TIMESTAMP_PREFIX; 230 } 231 232 if (packageInfo == null) { 233 return TIMESTAMP_PREFIX; 234 } 235 236 String expectedTimestamp = 237 TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime; 238 239 final String[] existingTimestamps = getExistingTimestamps(dataDir); 240 241 if (existingTimestamps == null) { 242 if (BuildConfig.DEBUG) { 243 Log.i(TAG, "No extracted resources found"); 244 } 245 return expectedTimestamp; 246 } 247 248 if (existingTimestamps.length == 1) { 249 if (BuildConfig.DEBUG) { 250 Log.i(TAG, "Found extracted resources " + existingTimestamps[0]); 251 } 252 } 253 254 if (existingTimestamps.length != 1 255 || !expectedTimestamp.equals(existingTimestamps[0])) { 256 if (BuildConfig.DEBUG) { 257 Log.i(TAG, "Resource version mismatch " + expectedTimestamp); 258 } 259 return expectedTimestamp; 260 } 261 262 return null; 263 } 264 265 private static void copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { 266 byte[] buf = new byte[16 * 1024]; 267 for (int i; (i = in.read(buf)) >= 0; ) { 268 out.write(buf, 0, i); 269 } 270 } 271 272 @SuppressWarnings("deprecation") 273 private static String[] getSupportedAbis() { 274 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 275 return Build.SUPPORTED_ABIS; 276 } else { 277 ArrayList<String> cpuAbis = new ArrayList<String>(asList(Build.CPU_ABI, Build.CPU_ABI2)); 278 cpuAbis.removeAll(asList(null, "")); 279 return cpuAbis.toArray(new String[0]); 280 } 281 } 282 } 283