Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / base / android / java / src / org / chromium / base / 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.base;
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 java.io.File;
17 import java.io.FileOutputStream;
18 import java.io.FilenameFilter;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.util.ArrayList;
23 import java.util.HashSet;
24 import java.util.Set;
25 import java.util.List;
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     private static ResourceIntercepter sIntercepter = null;
43
44     // By default, we attempt to extract a pak file for the users
45     // current device locale. Use setExtractImplicitLocale() to
46     // change this behavior.
47     private static boolean sExtractImplicitLocalePak = true;
48
49     public interface ResourceIntercepter {
50         Set<String> getInterceptableResourceList();
51         InputStream interceptLoadingForResource(String resource);
52     }
53
54     private class ExtractTask extends AsyncTask<Void, Void, Void> {
55         private static final int BUFFER_SIZE = 16 * 1024;
56
57         public ExtractTask() {
58         }
59
60         @Override
61         protected Void doInBackground(Void... unused) {
62             final File outputDir = getOutputDir();
63             if (!outputDir.exists() && !outputDir.mkdirs()) {
64                 Log.e(LOGTAG, "Unable to create pak resources directory!");
65                 return null;
66             }
67
68             String timestampFile = checkPakTimestamp(outputDir);
69             if (timestampFile != null) {
70                 deleteFiles();
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 = LocaleUtils.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(outputDir, 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 ? getAppDataDir() : outputDir, 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();
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(outputDir, 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(File outputDir) {
217             final String timestampPrefix = "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 timestampPrefix;
225             }
226
227             if (pi == null) {
228                 return timestampPrefix;
229             }
230
231             String expectedTimestamp = timestampPrefix + pi.versionCode + "-" + pi.lastUpdateTime;
232
233             String[] timestamps = outputDir.list(new FilenameFilter() {
234                 @Override
235                 public boolean accept(File dir, String name) {
236                     return name.startsWith(timestampPrefix);
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
258     private static ResourceExtractor sInstance;
259
260     public static ResourceExtractor get(Context context) {
261         if (sInstance == null) {
262             sInstance = new ResourceExtractor(context);
263         }
264         return sInstance;
265     }
266
267     /**
268      * Specifies the .pak files that should be extracted from the APK's asset resources directory
269      * and moved to {@link #getOutputDirFromContext(Context)}.
270      * @param mandatoryPaks The list of pak files to be loaded. If no pak files are
271      *     required, pass a single empty string.
272      */
273     public static void setMandatoryPaksToExtract(String... mandatoryPaks) {
274         assert (sInstance == null || sInstance.mExtractTask == null)
275                 : "Must be called before startExtractingResources is called";
276         sMandatoryPaks = mandatoryPaks;
277
278     }
279
280     /**
281      * Allow embedders to intercept the resource loading process. Embedders may
282      * want to load paks from res/raw instead of assets, since assets are not
283      * supported in Android library project.
284      * @param intercepter The instance of intercepter which provides the files list
285      * to intercept and the inputstream for the files it wants to intercept with.
286      */
287     public static void setResourceIntercepter(ResourceIntercepter intercepter) {
288         assert (sInstance == null || sInstance.mExtractTask == null)
289                 : "Must be called before startExtractingResources is called";
290         sIntercepter = intercepter;
291     }
292
293     /**
294      * By default the ResourceExtractor will attempt to extract a pak resource for the users
295      * currently specified locale. This behavior can be changed with this function and is
296      * only needed by tests.
297      * @param extract False if we should not attempt to extract a pak file for
298      *         the users currently selected locale and try to extract only the
299      *         pak files specified in sMandatoryPaks.
300      */
301     @VisibleForTesting
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     /**
309      * Marks all the 'pak' resources, packaged as assets, for extraction during
310      * running the tests.
311      */
312     @VisibleForTesting
313     public void setExtractAllPaksForTesting() {
314         List<String> pakFileAssets = new ArrayList<String>();
315         AssetManager manager = mContext.getResources().getAssets();
316         try {
317             String[] files = manager.list("");
318             for (String file : files) {
319                 if (file.endsWith(".pak")) pakFileAssets.add(file);
320             }
321         } catch (IOException e) {
322             Log.w(LOGTAG, "Exception while accessing assets: " + e.getMessage(), e);
323         }
324         setMandatoryPaksToExtract(pakFileAssets.toArray(new String[pakFileAssets.size()]));
325     }
326
327     private ResourceExtractor(Context context) {
328         mContext = context.getApplicationContext();
329     }
330
331     public void waitForCompletion() {
332         if (shouldSkipPakExtraction()) {
333             return;
334         }
335
336         assert mExtractTask != null;
337
338         try {
339             mExtractTask.get();
340             // ResourceExtractor is not needed any more.
341             // Release static objects to avoid leak of Context.
342             sIntercepter = null;
343             sInstance = null;
344         } catch (CancellationException e) {
345             // Don't leave the files in an inconsistent state.
346             deleteFiles();
347         } catch (ExecutionException e2) {
348             deleteFiles();
349         } catch (InterruptedException e3) {
350             deleteFiles();
351         }
352     }
353
354     /**
355      * This will extract the application pak resources in an
356      * AsyncTask. Call waitForCompletion() at the point resources
357      * are needed to block until the task completes.
358      */
359     public void startExtractingResources() {
360         if (mExtractTask != null) {
361             return;
362         }
363
364         if (shouldSkipPakExtraction()) {
365             return;
366         }
367
368         mExtractTask = new ExtractTask();
369         mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
370     }
371
372     private File getAppDataDir() {
373         return new File(PathUtils.getDataDirectory(mContext));
374     }
375
376     private File getOutputDir() {
377         return new File(getAppDataDir(), "paks");
378     }
379
380     /**
381      * Pak files (UI strings and other resources) should be updated along with
382      * Chrome. A version mismatch can lead to a rather broken user experience.
383      * The ICU data (icudtl.dat) is less version-sensitive, but still can
384      * lead to malfunction/UX misbehavior. So, we regard failing to update them
385      * as an error.
386      */
387     private void deleteFiles() {
388         File icudata = new File(getAppDataDir(), ICU_DATA_FILENAME);
389         if (icudata.exists() && !icudata.delete()) {
390             Log.e(LOGTAG, "Unable to remove the icudata " + icudata.getName());
391         }
392         File dir = getOutputDir();
393         if (dir.exists()) {
394             File[] files = dir.listFiles();
395             for (File file : files) {
396                 if (!file.delete()) {
397                     Log.e(LOGTAG, "Unable to remove existing resource " + file.getName());
398                 }
399             }
400         }
401     }
402
403     /**
404      * Pak extraction not necessarily required by the embedder; we allow them to skip
405      * this process if they call setMandatoryPaksToExtract with a single empty String.
406      */
407     private static boolean shouldSkipPakExtraction() {
408         // Must call setMandatoryPaksToExtract before beginning resource extraction.
409         assert sMandatoryPaks != null;
410         return sMandatoryPaks.length == 1 && "".equals(sMandatoryPaks[0]);
411     }
412 }