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