| OLD | NEW |
| 1 // Copyright 2012 The Chromium Authors. All rights reserved. | 1 // Copyright 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.base; | 5 package org.chromium.base; |
| 6 | 6 |
| 7 import android.annotation.TargetApi; | 7 import android.annotation.TargetApi; |
| 8 import android.content.Context; | 8 import android.content.Context; |
| 9 import android.content.SharedPreferences; |
| 9 import android.content.pm.PackageInfo; | 10 import android.content.pm.PackageInfo; |
| 10 import android.content.pm.PackageManager; | 11 import android.content.pm.PackageManager; |
| 11 import android.content.res.AssetManager; | 12 import android.content.res.AssetManager; |
| 12 import android.content.res.Resources; | |
| 13 import android.content.res.TypedArray; | |
| 14 import android.os.AsyncTask; | 13 import android.os.AsyncTask; |
| 15 import android.os.Build; | 14 import android.os.Build; |
| 16 import android.os.Handler; | 15 import android.os.Handler; |
| 17 import android.os.Looper; | 16 import android.os.Looper; |
| 18 import android.os.Trace; | 17 import android.os.Trace; |
| 18 import android.preference.PreferenceManager; |
| 19 import android.util.Log; | 19 import android.util.Log; |
| 20 | 20 |
| 21 import java.io.File; | 21 import java.io.File; |
| 22 import java.io.FileOutputStream; | 22 import java.io.FileOutputStream; |
| 23 import java.io.FilenameFilter; | 23 import java.io.FilenameFilter; |
| 24 import java.io.IOException; | 24 import java.io.IOException; |
| 25 import java.io.InputStream; | 25 import java.io.InputStream; |
| 26 import java.io.OutputStream; | 26 import java.io.OutputStream; |
| 27 import java.util.ArrayList; | 27 import java.util.ArrayList; |
| 28 import java.util.List; | 28 import java.util.List; |
| 29 import java.util.Locale; | |
| 30 import java.util.concurrent.CancellationException; | 29 import java.util.concurrent.CancellationException; |
| 31 import java.util.concurrent.ExecutionException; | 30 import java.util.concurrent.ExecutionException; |
| 31 import java.util.regex.Pattern; |
| 32 | 32 |
| 33 /** | 33 /** |
| 34 * Handles extracting the necessary resources bundled in an APK and moving them
to a location on | 34 * Handles extracting the necessary resources bundled in an APK and moving them
to a location on |
| 35 * the file system accessible from the native code. | 35 * the file system accessible from the native code. |
| 36 */ | 36 */ |
| 37 public class ResourceExtractor { | 37 public class ResourceExtractor { |
| 38 | 38 |
| 39 private static final String LOGTAG = "ResourceExtractor"; | 39 private static final String LOGTAG = "ResourceExtractor"; |
| 40 private static final String LAST_LANGUAGE = "Last language"; |
| 41 private static final String PAK_FILENAMES_LEGACY_NOREUSE = "Pak filenames"; |
| 40 private static final String ICU_DATA_FILENAME = "icudtl.dat"; | 42 private static final String ICU_DATA_FILENAME = "icudtl.dat"; |
| 41 private static final String V8_NATIVES_DATA_FILENAME = "natives_blob.bin"; | 43 private static final String V8_NATIVES_DATA_FILENAME = "natives_blob.bin"; |
| 42 private static final String V8_SNAPSHOT_DATA_FILENAME = "snapshot_blob.bin"; | 44 private static final String V8_SNAPSHOT_DATA_FILENAME = "snapshot_blob.bin"; |
| 43 | 45 |
| 44 private static String[] sMandatoryPaks = null; | 46 private static String[] sMandatoryPaks = null; |
| 45 private static int sLocalePaksResId = -1; | |
| 46 | 47 |
| 47 /** | 48 // By default, we attempt to extract a pak file for the users |
| 48 * Applies the reverse mapping done by locale_pak_resources.py. | 49 // current device locale. Use setExtractImplicitLocale() to |
| 49 */ | 50 // change this behavior. |
| 50 private static String toChromeLocaleName(String srcFileName) { | 51 private static boolean sExtractImplicitLocalePak = true; |
| 51 String[] parts = srcFileName.split("_"); | 52 |
| 52 if (parts.length > 1) { | 53 private static boolean isAppDataFile(String file) { |
| 53 int dotIdx = parts[1].indexOf('.'); | 54 return ICU_DATA_FILENAME.equals(file) |
| 54 return parts[0] + "-" + parts[1].substring(0, dotIdx).toUpperCase(Lo
cale.ENGLISH) | 55 || V8_NATIVES_DATA_FILENAME.equals(file) |
| 55 + parts[1].substring(dotIdx); | 56 || V8_SNAPSHOT_DATA_FILENAME.equals(file); |
| 56 } | |
| 57 return srcFileName; | |
| 58 } | 57 } |
| 59 | 58 |
| 60 private class ExtractTask extends AsyncTask<Void, Void, Void> { | 59 private class ExtractTask extends AsyncTask<Void, Void, Void> { |
| 61 private static final int BUFFER_SIZE = 16 * 1024; | 60 private static final int BUFFER_SIZE = 16 * 1024; |
| 62 | 61 |
| 63 private final List<Runnable> mCompletionCallbacks = new ArrayList<Runnab
le>(); | 62 private final List<Runnable> mCompletionCallbacks = new ArrayList<Runnab
le>(); |
| 64 | 63 |
| 65 private void extractResourceHelper(InputStream is, File outFile, byte[]
buffer) | 64 public ExtractTask() { |
| 66 throws IOException { | |
| 67 OutputStream os = null; | |
| 68 try { | |
| 69 os = new FileOutputStream(outFile); | |
| 70 Log.i(LOGTAG, "Extracting resource " + outFile); | |
| 71 | |
| 72 int count = 0; | |
| 73 while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) { | |
| 74 os.write(buffer, 0, count); | |
| 75 } | |
| 76 os.flush(); | |
| 77 | |
| 78 // Ensure something reasonable was written. | |
| 79 if (outFile.length() == 0) { | |
| 80 throw new IOException(outFile + " extracted with 0 length!")
; | |
| 81 } | |
| 82 } finally { | |
| 83 try { | |
| 84 if (is != null) { | |
| 85 is.close(); | |
| 86 } | |
| 87 } finally { | |
| 88 if (os != null) { | |
| 89 os.close(); | |
| 90 } | |
| 91 } | |
| 92 } | |
| 93 } | 65 } |
| 94 | 66 |
| 95 private void doInBackgroundImpl() { | 67 private void doInBackgroundImpl() { |
| 96 final File outputDir = getOutputDir(); | 68 final File outputDir = getOutputDir(); |
| 69 final File appDataDir = getAppDataDir(); |
| 97 if (!outputDir.exists() && !outputDir.mkdirs()) { | 70 if (!outputDir.exists() && !outputDir.mkdirs()) { |
| 98 Log.e(LOGTAG, "Unable to create pak resources directory!"); | 71 Log.e(LOGTAG, "Unable to create pak resources directory!"); |
| 99 return; | 72 return; |
| 100 } | 73 } |
| 101 | 74 |
| 102 String timestampFile = null; | 75 String timestampFile = null; |
| 103 beginTraceSection("checkPakTimeStamp"); | 76 beginTraceSection("checkPakTimeStamp"); |
| 104 try { | 77 try { |
| 105 timestampFile = checkPakTimestamp(outputDir); | 78 timestampFile = checkPakTimestamp(outputDir); |
| 106 } finally { | 79 } finally { |
| 107 endTraceSection(); | 80 endTraceSection(); |
| 108 } | 81 } |
| 109 if (timestampFile != null) { | 82 if (timestampFile != null) { |
| 110 deleteFiles(); | 83 deleteFiles(); |
| 111 } | 84 } |
| 112 | 85 |
| 86 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferen
ces(mContext); |
| 87 String currentLocale = LocaleUtils.getDefaultLocale(); |
| 88 String currentLanguage = currentLocale.split("-", 2)[0]; |
| 89 // If everything we need is already there (and the locale hasn't |
| 90 // changed), quick exit. |
| 91 if (prefs.getString(LAST_LANGUAGE, "").equals(currentLanguage)) { |
| 92 boolean filesPresent = true; |
| 93 for (String file : sMandatoryPaks) { |
| 94 File directory = isAppDataFile(file) ? appDataDir : outputDi
r; |
| 95 if (!new File(directory, file).exists()) { |
| 96 filesPresent = false; |
| 97 break; |
| 98 } |
| 99 } |
| 100 if (filesPresent) return; |
| 101 } else { |
| 102 prefs.edit().putString(LAST_LANGUAGE, currentLanguage).apply(); |
| 103 } |
| 104 |
| 105 StringBuilder p = new StringBuilder(); |
| 106 for (String mandatoryPak : sMandatoryPaks) { |
| 107 if (p.length() > 0) p.append('|'); |
| 108 p.append("\\Q" + mandatoryPak + "\\E"); |
| 109 } |
| 110 |
| 111 if (sExtractImplicitLocalePak) { |
| 112 if (p.length() > 0) p.append('|'); |
| 113 // As well as the minimum required set of .paks above, we'll |
| 114 // also add all .paks that we have for the user's currently |
| 115 // selected language. |
| 116 p.append(currentLanguage); |
| 117 p.append("(-\\w+)?\\.pak"); |
| 118 } |
| 119 |
| 120 Pattern paksToInstall = Pattern.compile(p.toString()); |
| 121 |
| 122 AssetManager manager = mContext.getResources().getAssets(); |
| 113 beginTraceSection("WalkAssets"); | 123 beginTraceSection("WalkAssets"); |
| 114 AssetManager assetManager = mContext.getAssets(); | |
| 115 byte[] buffer = new byte[BUFFER_SIZE]; | |
| 116 try { | 124 try { |
| 117 // Extract all files that don't already exist. | 125 // Loop through every asset file that we have in the APK, and lo
ok for the |
| 118 for (String fileName : sMandatoryPaks) { | 126 // ones that we need to extract by trying to match the Patterns
that we |
| 119 File output = new File(outputDir, fileName); | 127 // created above. |
| 128 byte[] buffer = null; |
| 129 String[] files = manager.list(""); |
| 130 for (String file : files) { |
| 131 if (!paksToInstall.matcher(file).matches()) { |
| 132 continue; |
| 133 } |
| 134 File output = new File(isAppDataFile(file) ? appDataDir : ou
tputDir, file); |
| 120 if (output.exists()) { | 135 if (output.exists()) { |
| 121 continue; | 136 continue; |
| 122 } | 137 } |
| 138 |
| 139 InputStream is = null; |
| 140 OutputStream os = null; |
| 123 beginTraceSection("ExtractResource"); | 141 beginTraceSection("ExtractResource"); |
| 124 InputStream inputStream = assetManager.open(fileName); | |
| 125 try { | 142 try { |
| 126 extractResourceHelper(inputStream, output, buffer); | 143 is = manager.open(file); |
| 127 } finally { | 144 os = new FileOutputStream(output); |
| 128 endTraceSection(); // ExtractResource | 145 Log.i(LOGTAG, "Extracting resource " + file); |
| 129 } | 146 if (buffer == null) { |
| 130 } | 147 buffer = new byte[BUFFER_SIZE]; |
| 148 } |
| 131 | 149 |
| 132 if (sLocalePaksResId != 0) { | 150 int count = 0; |
| 133 // locale_paks yields the current language's pak file paths. | 151 while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1)
{ |
| 134 Resources resources = mContext.getResources(); | 152 os.write(buffer, 0, count); |
| 135 TypedArray resIds = resources.obtainTypedArray(sLocalePaksRe
sId); | 153 } |
| 136 try { | 154 os.flush(); |
| 137 int len = resIds.length(); | 155 |
| 138 for (int i = 0; i < len; ++i) { | 156 // Ensure something reasonable was written. |
| 139 int resId = resIds.getResourceId(i, 0); | 157 if (output.length() == 0) { |
| 140 String resPath = resources.getString(resId); | 158 throw new IOException(file + " extracted with 0 leng
th!"); |
| 141 String srcBaseName = new File(resPath).getName(); | 159 } |
| 142 String dstBaseName = toChromeLocaleName(srcBaseName)
; | 160 |
| 143 File output = new File(outputDir, dstBaseName); | 161 if (isAppDataFile(file)) { |
| 144 if (output.exists()) { | 162 // icu and V8 data need to be accessed by a renderer |
| 145 continue; | 163 // process. |
| 146 } | 164 output.setReadable(true, false); |
| 147 beginTraceSection("ExtractResource"); | |
| 148 InputStream inputStream = resources.openRawResource(
resId); | |
| 149 try { | |
| 150 extractResourceHelper(inputStream, output, buffe
r); | |
| 151 } finally { | |
| 152 endTraceSection(); // ExtractResource | |
| 153 } | |
| 154 } | 165 } |
| 155 } finally { | 166 } finally { |
| 156 resIds.recycle(); | 167 try { |
| 168 if (is != null) { |
| 169 is.close(); |
| 170 } |
| 171 } finally { |
| 172 if (os != null) { |
| 173 os.close(); |
| 174 } |
| 175 endTraceSection(); // ExtractResource |
| 176 } |
| 157 } | 177 } |
| 158 } | 178 } |
| 159 } catch (IOException e) { | 179 } catch (IOException e) { |
| 160 // TODO(benm): See crbug/152413. | 180 // TODO(benm): See crbug/152413. |
| 161 // Try to recover here, can we try again after deleting files in
stead of | 181 // Try to recover here, can we try again after deleting files in
stead of |
| 162 // returning null? It might be useful to gather UMA here too to
track if | 182 // returning null? It might be useful to gather UMA here too to
track if |
| 163 // this happens with regularity. | 183 // this happens with regularity. |
| 164 Log.w(LOGTAG, "Exception unpacking required pak resources: " + e
.getMessage()); | 184 Log.w(LOGTAG, "Exception unpacking required pak resources: " + e
.getMessage()); |
| 165 deleteFiles(); | 185 deleteFiles(); |
| 166 return; | 186 return; |
| (...skipping 109 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 276 private static ResourceExtractor sInstance; | 296 private static ResourceExtractor sInstance; |
| 277 | 297 |
| 278 public static ResourceExtractor get(Context context) { | 298 public static ResourceExtractor get(Context context) { |
| 279 if (sInstance == null) { | 299 if (sInstance == null) { |
| 280 sInstance = new ResourceExtractor(context); | 300 sInstance = new ResourceExtractor(context); |
| 281 } | 301 } |
| 282 return sInstance; | 302 return sInstance; |
| 283 } | 303 } |
| 284 | 304 |
| 285 /** | 305 /** |
| 286 * Specifies the files that should be extracted from the APK. | 306 * Specifies the .pak files that should be extracted from the APK's asset re
sources directory |
| 287 * and moved to {@link #getOutputDirFromContext(Context)}. | 307 * and moved to {@link #getOutputDirFromContext(Context)}. |
| 288 * @param localePaksResId Resource ID for the locale_paks string array. Pass | 308 * @param mandatoryPaks The list of pak files to be loaded. If no pak files
are |
| 289 * in 0 to disable locale pak extraction. | 309 * required, pass a single empty string. |
| 290 * @param paths The list of paths to be extracted. | |
| 291 */ | 310 */ |
| 292 public static void setMandatoryPaksToExtract(int localePaksResId, String...
paths) { | 311 public static void setMandatoryPaksToExtract(String... mandatoryPaks) { |
| 293 // TODO(agrieve): Remove the need to call this once all files are loaded
from the apk. | 312 // TODO(agrieve): Remove the need to call this once all files are loaded
from the apk. |
| 294 assert (sInstance == null || sInstance.mExtractTask == null) | 313 assert (sInstance == null || sInstance.mExtractTask == null) |
| 295 : "Must be called before startExtractingResources is called"; | 314 : "Must be called before startExtractingResources is called"; |
| 296 sLocalePaksResId = localePaksResId; | 315 sMandatoryPaks = mandatoryPaks; |
| 297 sMandatoryPaks = paths; | 316 |
| 298 } | 317 } |
| 299 | 318 |
| 300 /** | 319 /** |
| 320 * By default the ResourceExtractor will attempt to extract a pak resource f
or the users |
| 321 * currently specified locale. This behavior can be changed with this functi
on and is |
| 322 * only needed by tests. |
| 323 * @param extract False if we should not attempt to extract a pak file for |
| 324 * the users currently selected locale and try to extract only the |
| 325 * pak files specified in sMandatoryPaks. |
| 326 */ |
| 327 @VisibleForTesting |
| 328 public static void setExtractImplicitLocaleForTesting(boolean extract) { |
| 329 assert (sInstance == null || sInstance.mExtractTask == null) |
| 330 : "Must be called before startExtractingResources is called"; |
| 331 sExtractImplicitLocalePak = extract; |
| 332 } |
| 333 |
| 334 /** |
| 301 * Marks all the 'pak' resources, packaged as assets, for extraction during | 335 * Marks all the 'pak' resources, packaged as assets, for extraction during |
| 302 * running the tests. | 336 * running the tests. |
| 303 */ | 337 */ |
| 304 @VisibleForTesting | 338 @VisibleForTesting |
| 305 public void setExtractAllPaksAndV8SnapshotForTesting() { | 339 public void setExtractAllPaksAndV8SnapshotForTesting() { |
| 306 List<String> pakAndSnapshotFileAssets = new ArrayList<String>(); | 340 List<String> pakAndSnapshotFileAssets = new ArrayList<String>(); |
| 307 AssetManager manager = mContext.getResources().getAssets(); | 341 AssetManager manager = mContext.getResources().getAssets(); |
| 308 try { | 342 try { |
| 309 String[] files = manager.list(""); | 343 String[] files = manager.list(""); |
| 310 for (String file : files) { | 344 for (String file : files) { |
| 311 if (file.endsWith(".pak")) pakAndSnapshotFileAssets.add(file); | 345 if (file.endsWith(".pak")) pakAndSnapshotFileAssets.add(file); |
| 312 } | 346 } |
| 313 } catch (IOException e) { | 347 } catch (IOException e) { |
| 314 Log.w(LOGTAG, "Exception while accessing assets: " + e.getMessage(),
e); | 348 Log.w(LOGTAG, "Exception while accessing assets: " + e.getMessage(),
e); |
| 315 } | 349 } |
| 316 setMandatoryPaksToExtract(0, pakAndSnapshotFileAssets.toArray( | 350 setMandatoryPaksToExtract(pakAndSnapshotFileAssets.toArray( |
| 317 new String[pakAndSnapshotFileAssets.size()])); | 351 new String[pakAndSnapshotFileAssets.size()])); |
| 318 } | 352 } |
| 319 | 353 |
| 320 private ResourceExtractor(Context context) { | 354 private ResourceExtractor(Context context) { |
| 321 mContext = context.getApplicationContext(); | 355 mContext = context.getApplicationContext(); |
| 322 } | 356 } |
| 323 | 357 |
| 324 /** | 358 /** |
| 325 * Synchronously wait for the resource extraction to be completed. | 359 * Synchronously wait for the resource extraction to be completed. |
| 326 * <p> | 360 * <p> |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 379 /** | 413 /** |
| 380 * This will extract the application pak resources in an | 414 * This will extract the application pak resources in an |
| 381 * AsyncTask. Call waitForCompletion() at the point resources | 415 * AsyncTask. Call waitForCompletion() at the point resources |
| 382 * are needed to block until the task completes. | 416 * are needed to block until the task completes. |
| 383 */ | 417 */ |
| 384 public void startExtractingResources() { | 418 public void startExtractingResources() { |
| 385 if (mExtractTask != null) { | 419 if (mExtractTask != null) { |
| 386 return; | 420 return; |
| 387 } | 421 } |
| 388 | 422 |
| 389 // If a previous release extracted resources, and the current release do
es not, | |
| 390 // deleteFiles() will not run and some files will be left. This currentl
y | |
| 391 // can happen for ContentShell, but not for Chrome proper, since we alwa
ys extract | |
| 392 // locale pak files. | |
| 393 if (shouldSkipPakExtraction()) { | 423 if (shouldSkipPakExtraction()) { |
| 394 return; | 424 return; |
| 395 } | 425 } |
| 396 | 426 |
| 397 mExtractTask = new ExtractTask(); | 427 mExtractTask = new ExtractTask(); |
| 398 mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); | 428 mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| 399 } | 429 } |
| 400 | 430 |
| 401 private File getAppDataDir() { | 431 private File getAppDataDir() { |
| 402 return new File(PathUtils.getDataDirectory(mContext)); | 432 return new File(PathUtils.getDataDirectory(mContext)); |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 435 File[] files = dir.listFiles(); | 465 File[] files = dir.listFiles(); |
| 436 for (File file : files) { | 466 for (File file : files) { |
| 437 if (!file.delete()) { | 467 if (!file.delete()) { |
| 438 Log.e(LOGTAG, "Unable to remove existing resource " + file.g
etName()); | 468 Log.e(LOGTAG, "Unable to remove existing resource " + file.g
etName()); |
| 439 } | 469 } |
| 440 } | 470 } |
| 441 } | 471 } |
| 442 } | 472 } |
| 443 | 473 |
| 444 /** | 474 /** |
| 445 * Pak extraction not necessarily required by the embedder. | 475 * Pak extraction not necessarily required by the embedder; we allow them to
skip |
| 476 * this process if they call setMandatoryPaksToExtract with a single empty S
tring. |
| 446 */ | 477 */ |
| 447 private static boolean shouldSkipPakExtraction() { | 478 private static boolean shouldSkipPakExtraction() { |
| 448 assert (sLocalePaksResId != -1 && sMandatoryPaks != null) | 479 // Must call setMandatoryPaksToExtract before beginning resource extract
ion. |
| 449 : "setMandatoryPaksToExtract() must be called before startExtracting
Resources()"; | 480 assert sMandatoryPaks != null; |
| 450 return sMandatoryPaks.length == 0 && sLocalePaksResId == 0; | 481 return sMandatoryPaks.length == 1 && "".equals(sMandatoryPaks[0]); |
| 451 } | 482 } |
| 452 } | 483 } |
| OLD | NEW |