Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(53)

Side by Side Diff: content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java

Issue 548023002: Migrate ResourceExtractor.java inside org.chromium.base package. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rolling the AOSP deps Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698