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 |