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