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