OLD | NEW |
| (Empty) |
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 | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.content.browser; | |
6 | |
7 import android.content.Context; | |
8 import android.content.SharedPreferences; | |
9 import android.content.pm.PackageInfo; | |
10 import android.content.pm.PackageManager; | |
11 import android.content.res.AssetManager; | |
12 import android.os.AsyncTask; | |
13 import android.preference.PreferenceManager; | |
14 import android.util.Log; | |
15 | |
16 import org.chromium.base.PathUtils; | |
17 import org.chromium.ui.base.LocalizationUtils; | |
18 | |
19 import java.io.File; | |
20 import java.io.FileOutputStream; | |
21 import java.io.FilenameFilter; | |
22 import java.io.IOException; | |
23 import java.io.InputStream; | |
24 import java.io.OutputStream; | |
25 import java.util.HashSet; | |
26 import java.util.concurrent.CancellationException; | |
27 import java.util.concurrent.ExecutionException; | |
28 import java.util.regex.Pattern; | |
29 | |
30 /** | |
31 * Handles extracting the necessary resources bundled in an APK and moving them
to a location on | |
32 * the file system accessible from the native code. | |
33 */ | |
34 public class ResourceExtractor { | |
35 | |
36 private static final String LOGTAG = "ResourceExtractor"; | |
37 private static final String LAST_LANGUAGE = "Last language"; | |
38 private static final String PAK_FILENAMES = "Pak filenames"; | |
39 private static final String ICU_DATA_FILENAME = "icudtl.dat"; | |
40 | |
41 private static String[] sMandatoryPaks = null; | |
42 | |
43 // By default, we attempt to extract a pak file for the users | |
44 // current device locale. Use setExtractImplicitLocale() to | |
45 // change this behavior. | |
46 private static boolean sExtractImplicitLocalePak = true; | |
47 | |
48 private class ExtractTask extends AsyncTask<Void, Void, Void> { | |
49 private static final int BUFFER_SIZE = 16 * 1024; | |
50 | |
51 public ExtractTask() { | |
52 } | |
53 | |
54 @Override | |
55 protected Void doInBackground(Void... unused) { | |
56 final File outputDir = getOutputDir(); | |
57 if (!outputDir.exists() && !outputDir.mkdirs()) { | |
58 Log.e(LOGTAG, "Unable to create pak resources directory!"); | |
59 return null; | |
60 } | |
61 | |
62 String timestampFile = checkPakTimestamp(outputDir); | |
63 if (timestampFile != null) { | |
64 deleteFiles(); | |
65 } | |
66 | |
67 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferen
ces(mContext); | |
68 HashSet<String> filenames = (HashSet<String>) prefs.getStringSet( | |
69 PAK_FILENAMES, new HashSet<String>()); | |
70 String currentLocale = LocalizationUtils.getDefaultLocale(); | |
71 String currentLanguage = currentLocale.split("-", 2)[0]; | |
72 | |
73 if (prefs.getString(LAST_LANGUAGE, "").equals(currentLanguage) | |
74 && filenames.size() >= sMandatoryPaks.length) { | |
75 boolean filesPresent = true; | |
76 for (String file : filenames) { | |
77 if (!new File(outputDir, file).exists()) { | |
78 filesPresent = false; | |
79 break; | |
80 } | |
81 } | |
82 if (filesPresent) return null; | |
83 } else { | |
84 prefs.edit().putString(LAST_LANGUAGE, currentLanguage).apply(); | |
85 } | |
86 | |
87 StringBuilder p = new StringBuilder(); | |
88 for (String mandatoryPak : sMandatoryPaks) { | |
89 if (p.length() > 0) p.append('|'); | |
90 p.append("\\Q" + mandatoryPak + "\\E"); | |
91 } | |
92 | |
93 if (sExtractImplicitLocalePak) { | |
94 if (p.length() > 0) p.append('|'); | |
95 // As well as the minimum required set of .paks above, we'll als
o add all .paks that | |
96 // we have for the user's currently selected language. | |
97 | |
98 p.append(currentLanguage); | |
99 p.append("(-\\w+)?\\.pak"); | |
100 } | |
101 | |
102 Pattern paksToInstall = Pattern.compile(p.toString()); | |
103 | |
104 AssetManager manager = mContext.getResources().getAssets(); | |
105 try { | |
106 // Loop through every asset file that we have in the APK, and lo
ok for the | |
107 // ones that we need to extract by trying to match the Patterns
that we | |
108 // created above. | |
109 byte[] buffer = null; | |
110 String[] files = manager.list(""); | |
111 for (String file : files) { | |
112 if (!paksToInstall.matcher(file).matches()) { | |
113 continue; | |
114 } | |
115 boolean isICUData = file.equals(ICU_DATA_FILENAME); | |
116 File output = new File(isICUData ? getAppDataDir() : outputD
ir, file); | |
117 if (output.exists()) { | |
118 continue; | |
119 } | |
120 | |
121 InputStream is = null; | |
122 OutputStream os = null; | |
123 try { | |
124 is = manager.open(file); | |
125 os = new FileOutputStream(output); | |
126 Log.i(LOGTAG, "Extracting resource " + file); | |
127 if (buffer == null) { | |
128 buffer = new byte[BUFFER_SIZE]; | |
129 } | |
130 | |
131 int count = 0; | |
132 while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1)
{ | |
133 os.write(buffer, 0, count); | |
134 } | |
135 os.flush(); | |
136 | |
137 // Ensure something reasonable was written. | |
138 if (output.length() == 0) { | |
139 throw new IOException(file + " extracted with 0 leng
th!"); | |
140 } | |
141 | |
142 if (!isICUData) { | |
143 filenames.add(file); | |
144 } else { | |
145 // icudata needs to be accessed by a renderer proces
s. | |
146 output.setReadable(true, false); | |
147 } | |
148 } finally { | |
149 try { | |
150 if (is != null) { | |
151 is.close(); | |
152 } | |
153 } finally { | |
154 if (os != null) { | |
155 os.close(); | |
156 } | |
157 } | |
158 } | |
159 } | |
160 } catch (IOException e) { | |
161 // TODO(benm): See crbug/152413. | |
162 // Try to recover here, can we try again after deleting files in
stead of | |
163 // returning null? It might be useful to gather UMA here too to
track if | |
164 // this happens with regularity. | |
165 Log.w(LOGTAG, "Exception unpacking required pak resources: " + e
.getMessage()); | |
166 deleteFiles(); | |
167 return null; | |
168 } | |
169 | |
170 // Finished, write out a timestamp file if we need to. | |
171 | |
172 if (timestampFile != null) { | |
173 try { | |
174 new File(outputDir, timestampFile).createNewFile(); | |
175 } catch (IOException e) { | |
176 // Worst case we don't write a timestamp, so we'll re-extrac
t the resource | |
177 // paks next start up. | |
178 Log.w(LOGTAG, "Failed to write resource pak timestamp!"); | |
179 } | |
180 } | |
181 // TODO(yusufo): Figure out why remove is required here. | |
182 prefs.edit().remove(PAK_FILENAMES).apply(); | |
183 prefs.edit().putStringSet(PAK_FILENAMES, filenames).apply(); | |
184 return null; | |
185 } | |
186 | |
187 // Looks for a timestamp file on disk that indicates the version of the
APK that | |
188 // the resource paks were extracted from. Returns null if a timestamp wa
s found | |
189 // and it indicates that the resources match the current APK. Otherwise
returns | |
190 // a String that represents the filename of a timestamp to create. | |
191 // Note that we do this to avoid adding a BroadcastReceiver on | |
192 // android.content.Intent#ACTION_PACKAGE_CHANGED as that causes process
churn | |
193 // on (re)installation of *all* APK files. | |
194 private String checkPakTimestamp(File outputDir) { | |
195 final String timestampPrefix = "pak_timestamp-"; | |
196 PackageManager pm = mContext.getPackageManager(); | |
197 PackageInfo pi = null; | |
198 | |
199 try { | |
200 pi = pm.getPackageInfo(mContext.getPackageName(), 0); | |
201 } catch (PackageManager.NameNotFoundException e) { | |
202 return timestampPrefix; | |
203 } | |
204 | |
205 if (pi == null) { | |
206 return timestampPrefix; | |
207 } | |
208 | |
209 String expectedTimestamp = timestampPrefix + pi.versionCode + "-" +
pi.lastUpdateTime; | |
210 | |
211 String[] timestamps = outputDir.list(new FilenameFilter() { | |
212 @Override | |
213 public boolean accept(File dir, String name) { | |
214 return name.startsWith(timestampPrefix); | |
215 } | |
216 }); | |
217 | |
218 if (timestamps.length != 1) { | |
219 // If there's no timestamp, nuke to be safe as we can't tell the
age of the files. | |
220 // If there's multiple timestamps, something's gone wrong so nuk
e. | |
221 return expectedTimestamp; | |
222 } | |
223 | |
224 if (!expectedTimestamp.equals(timestamps[0])) { | |
225 return expectedTimestamp; | |
226 } | |
227 | |
228 // timestamp file is already up-to date. | |
229 return null; | |
230 } | |
231 } | |
232 | |
233 private final Context mContext; | |
234 private ExtractTask mExtractTask; | |
235 | |
236 private static ResourceExtractor sInstance; | |
237 | |
238 public static ResourceExtractor get(Context context) { | |
239 if (sInstance == null) { | |
240 sInstance = new ResourceExtractor(context); | |
241 } | |
242 return sInstance; | |
243 } | |
244 | |
245 /** | |
246 * Specifies the .pak files that should be extracted from the APK's asset re
sources directory | |
247 * and moved to {@link #getOutputDirFromContext(Context)}. | |
248 * @param mandatoryPaks The list of pak files to be loaded. If no pak files
are | |
249 * required, pass a single empty string. | |
250 */ | |
251 public static void setMandatoryPaksToExtract(String... mandatoryPaks) { | |
252 assert (sInstance == null || sInstance.mExtractTask == null) | |
253 : "Must be called before startExtractingResources is called"; | |
254 sMandatoryPaks = mandatoryPaks; | |
255 | |
256 } | |
257 | |
258 /** | |
259 * By default the ResourceExtractor will attempt to extract a pak resource f
or the users | |
260 * currently specified locale. This behavior can be changed with this functi
on and is | |
261 * only needed by tests. | |
262 * @param extract False if we should not attempt to extract a pak file for | |
263 * the users currently selected locale and try to extract only the | |
264 * pak files specified in sMandatoryPaks. | |
265 */ | |
266 public static void setExtractImplicitLocaleForTesting(boolean extract) { | |
267 assert (sInstance == null || sInstance.mExtractTask == null) | |
268 : "Must be called before startExtractingResources is called"; | |
269 sExtractImplicitLocalePak = extract; | |
270 } | |
271 | |
272 private ResourceExtractor(Context context) { | |
273 mContext = context.getApplicationContext(); | |
274 } | |
275 | |
276 public void waitForCompletion() { | |
277 if (shouldSkipPakExtraction()) { | |
278 return; | |
279 } | |
280 | |
281 assert mExtractTask != null; | |
282 | |
283 try { | |
284 mExtractTask.get(); | |
285 } catch (CancellationException e) { | |
286 // Don't leave the files in an inconsistent state. | |
287 deleteFiles(); | |
288 } catch (ExecutionException e2) { | |
289 deleteFiles(); | |
290 } catch (InterruptedException e3) { | |
291 deleteFiles(); | |
292 } | |
293 } | |
294 | |
295 /** | |
296 * This will extract the application pak resources in an | |
297 * AsyncTask. Call waitForCompletion() at the point resources | |
298 * are needed to block until the task completes. | |
299 */ | |
300 public void startExtractingResources() { | |
301 if (mExtractTask != null) { | |
302 return; | |
303 } | |
304 | |
305 if (shouldSkipPakExtraction()) { | |
306 return; | |
307 } | |
308 | |
309 mExtractTask = new ExtractTask(); | |
310 mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); | |
311 } | |
312 | |
313 private File getAppDataDir() { | |
314 return new File(PathUtils.getDataDirectory(mContext)); | |
315 } | |
316 | |
317 private File getOutputDir() { | |
318 return new File(getAppDataDir(), "paks"); | |
319 } | |
320 | |
321 /** | |
322 * Pak files (UI strings and other resources) should be updated along with | |
323 * Chrome. A version mismatch can lead to a rather broken user experience. | |
324 * The ICU data (icudtl.dat) is less version-sensitive, but still can | |
325 * lead to malfunction/UX misbehavior. So, we regard failing to update them | |
326 * as an error. | |
327 */ | |
328 private void deleteFiles() { | |
329 File icudata = new File(getAppDataDir(), ICU_DATA_FILENAME); | |
330 if (icudata.exists() && !icudata.delete()) { | |
331 Log.e(LOGTAG, "Unable to remove the icudata " + icudata.getName()); | |
332 } | |
333 File dir = getOutputDir(); | |
334 if (dir.exists()) { | |
335 File[] files = dir.listFiles(); | |
336 for (File file : files) { | |
337 if (!file.delete()) { | |
338 Log.e(LOGTAG, "Unable to remove existing resource " + file.g
etName()); | |
339 } | |
340 } | |
341 } | |
342 } | |
343 | |
344 /** | |
345 * Pak extraction not necessarily required by the embedder; we allow them to
skip | |
346 * this process if they call setMandatoryPaksToExtract with a single empty S
tring. | |
347 */ | |
348 private static boolean shouldSkipPakExtraction() { | |
349 // Must call setMandatoryPaksToExtract before beginning resource extract
ion. | |
350 assert sMandatoryPaks != null; | |
351 return sMandatoryPaks.length == 1 && "".equals(sMandatoryPaks[0]); | |
352 } | |
353 } | |
OLD | NEW |