• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.TargetApi;
8 import android.content.Context;
9 import android.content.SharedPreferences;
10 import android.content.pm.PackageInfo;
11 import android.content.pm.PackageManager;
12 import android.os.AsyncTask;
13 import android.os.Build;
14 import android.os.Handler;
15 import android.os.Looper;
16 import android.os.Trace;
17 
18 import org.chromium.base.annotations.SuppressFBWarnings;
19 
20 import java.io.File;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.concurrent.CancellationException;
28 import java.util.concurrent.ExecutionException;
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 TAG = "cr.base";
37     private static final String ICU_DATA_FILENAME = "icudtl.dat";
38     private static final String V8_NATIVES_DATA_FILENAME = "natives_blob.bin";
39     private static final String V8_SNAPSHOT_DATA_FILENAME = "snapshot_blob.bin";
40     private static final String APP_VERSION_PREF = "org.chromium.base.ResourceExtractor.Version";
41 
42     private static ResourceEntry[] sResourcesToExtract = new ResourceEntry[0];
43 
44     /**
45      * Holds information about a res/raw file (e.g. locale .pak files).
46      */
47     public static final class ResourceEntry {
48         public final int resourceId;
49         public final String pathWithinApk;
50         public final String extractedFileName;
51 
ResourceEntry(int resourceId, String pathWithinApk, String extractedFileName)52         public ResourceEntry(int resourceId, String pathWithinApk, String extractedFileName) {
53             this.resourceId = resourceId;
54             this.pathWithinApk = pathWithinApk;
55             this.extractedFileName = extractedFileName;
56         }
57     }
58 
59     private class ExtractTask extends AsyncTask<Void, Void, Void> {
60         private static final int BUFFER_SIZE = 16 * 1024;
61 
62         private final List<Runnable> mCompletionCallbacks = new ArrayList<Runnable>();
63 
extractResourceHelper(InputStream is, File outFile, byte[] buffer)64         private void extractResourceHelper(InputStream is, File outFile, byte[] buffer)
65                 throws IOException {
66             OutputStream os = null;
67             try {
68                 os = new FileOutputStream(outFile);
69                 Log.i(TAG, "Extracting resource %s", outFile);
70 
71                 int count = 0;
72                 while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
73                     os.write(buffer, 0, count);
74                 }
75             } finally {
76                 try {
77                     if (os != null) {
78                         os.close();
79                     }
80                 } finally {
81                     if (is != null) {
82                         is.close();
83                     }
84                 }
85             }
86         }
87 
doInBackgroundImpl()88         private void doInBackgroundImpl() {
89             final File outputDir = getOutputDir();
90             if (!outputDir.exists() && !outputDir.mkdirs()) {
91                 Log.e(TAG, "Unable to create pak resources directory!");
92                 return;
93             }
94 
95             beginTraceSection("checkPakTimeStamp");
96             long curAppVersion = getApkVersion();
97             SharedPreferences sharedPrefs = ContextUtils.getAppSharedPreferences();
98             long prevAppVersion = sharedPrefs.getLong(APP_VERSION_PREF, 0);
99             boolean versionChanged = curAppVersion != prevAppVersion;
100             endTraceSection();
101 
102             if (versionChanged) {
103                 deleteFiles();
104                 // Use the version only to see if files should be deleted, not to skip extraction.
105                 // We've seen files be corrupted, so always attempt extraction.
106                 // http://crbug.com/606413
107                 sharedPrefs.edit().putLong(APP_VERSION_PREF, curAppVersion).apply();
108             }
109 
110             beginTraceSection("WalkAssets");
111             byte[] buffer = new byte[BUFFER_SIZE];
112             try {
113                 for (ResourceEntry entry : sResourcesToExtract) {
114                     File output = new File(outputDir, entry.extractedFileName);
115                     // TODO(agrieve): It would be better to check that .length == expectedLength.
116                     //     http://crbug.com/606413
117                     if (output.length() != 0) {
118                         continue;
119                     }
120                     beginTraceSection("ExtractResource");
121                     InputStream inputStream = mContext.getResources().openRawResource(
122                             entry.resourceId);
123                     try {
124                         extractResourceHelper(inputStream, output, buffer);
125                     } finally {
126                         endTraceSection(); // ExtractResource
127                     }
128                 }
129             } catch (IOException e) {
130                 // TODO(benm): See crbug/152413.
131                 // Try to recover here, can we try again after deleting files instead of
132                 // returning null? It might be useful to gather UMA here too to track if
133                 // this happens with regularity.
134                 Log.w(TAG, "Exception unpacking required pak resources: %s", e.getMessage());
135                 deleteFiles();
136                 return;
137             } finally {
138                 endTraceSection(); // WalkAssets
139             }
140         }
141 
142         @Override
doInBackground(Void... unused)143         protected Void doInBackground(Void... unused) {
144             // TODO(lizeb): Use chrome tracing here (and above in
145             // doInBackgroundImpl) when it will be possible. This is currently
146             // not doable since the native library is not loaded yet, and the
147             // TraceEvent calls are dropped before this point.
148             beginTraceSection("ResourceExtractor.ExtractTask.doInBackground");
149             try {
150                 doInBackgroundImpl();
151             } finally {
152                 endTraceSection();
153             }
154             return null;
155         }
156 
onPostExecuteImpl()157         private void onPostExecuteImpl() {
158             for (int i = 0; i < mCompletionCallbacks.size(); i++) {
159                 mCompletionCallbacks.get(i).run();
160             }
161             mCompletionCallbacks.clear();
162         }
163 
164         @Override
onPostExecute(Void result)165         protected void onPostExecute(Void result) {
166             beginTraceSection("ResourceExtractor.ExtractTask.onPostExecute");
167             try {
168                 onPostExecuteImpl();
169             } finally {
170                 endTraceSection();
171             }
172         }
173 
174         /** Returns a number that is different each time the apk changes. */
getApkVersion()175         private long getApkVersion() {
176             PackageManager pm = mContext.getPackageManager();
177             try {
178                 // More appropriate would be versionCode, but it doesn't change while developing.
179                 PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), 0);
180                 return pi.lastUpdateTime;
181             } catch (PackageManager.NameNotFoundException e) {
182                 throw new RuntimeException(e);
183             }
184         }
185 
186         @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
beginTraceSection(String section)187         private void beginTraceSection(String section) {
188             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return;
189             Trace.beginSection(section);
190         }
191 
192         @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
endTraceSection()193         private void endTraceSection() {
194             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return;
195             Trace.endSection();
196         }
197     }
198 
199     private final Context mContext;
200     private ExtractTask mExtractTask;
201 
202     private static ResourceExtractor sInstance;
203 
get(Context context)204     public static ResourceExtractor get(Context context) {
205         if (sInstance == null) {
206             sInstance = new ResourceExtractor(context);
207         }
208         return sInstance;
209     }
210 
211     /**
212      * Specifies the files that should be extracted from the APK.
213      * and moved to {@link #getOutputDir()}.
214      */
215     @SuppressFBWarnings("EI_EXPOSE_STATIC_REP2")
setResourcesToExtract(ResourceEntry[] entries)216     public static void setResourcesToExtract(ResourceEntry[] entries) {
217         assert (sInstance == null || sInstance.mExtractTask == null)
218                 : "Must be called before startExtractingResources is called";
219         sResourcesToExtract = entries;
220     }
221 
ResourceExtractor(Context context)222     private ResourceExtractor(Context context) {
223         mContext = context.getApplicationContext();
224     }
225 
226     /**
227      * Synchronously wait for the resource extraction to be completed.
228      * <p>
229      * This method is bad and you should feel bad for using it.
230      *
231      * @see #addCompletionCallback(Runnable)
232      */
waitForCompletion()233     public void waitForCompletion() {
234         if (shouldSkipPakExtraction()) {
235             return;
236         }
237 
238         assert mExtractTask != null;
239 
240         try {
241             mExtractTask.get();
242         } catch (CancellationException e) {
243             // Don't leave the files in an inconsistent state.
244             deleteFiles();
245         } catch (ExecutionException e2) {
246             deleteFiles();
247         } catch (InterruptedException e3) {
248             deleteFiles();
249         }
250     }
251 
252     /**
253      * Adds a callback to be notified upon the completion of resource extraction.
254      * <p>
255      * If the resource task has already completed, the callback will be posted to the UI message
256      * queue.  Otherwise, it will be executed after all the resources have been extracted.
257      * <p>
258      * This must be called on the UI thread.  The callback will also always be executed on
259      * the UI thread.
260      *
261      * @param callback The callback to be enqueued.
262      */
addCompletionCallback(Runnable callback)263     public void addCompletionCallback(Runnable callback) {
264         ThreadUtils.assertOnUiThread();
265 
266         Handler handler = new Handler(Looper.getMainLooper());
267         if (shouldSkipPakExtraction()) {
268             handler.post(callback);
269             return;
270         }
271 
272         assert mExtractTask != null;
273         assert !mExtractTask.isCancelled();
274         if (mExtractTask.getStatus() == AsyncTask.Status.FINISHED) {
275             handler.post(callback);
276         } else {
277             mExtractTask.mCompletionCallbacks.add(callback);
278         }
279     }
280 
281     /**
282      * This will extract the application pak resources in an
283      * AsyncTask. Call waitForCompletion() at the point resources
284      * are needed to block until the task completes.
285      */
startExtractingResources()286     public void startExtractingResources() {
287         if (mExtractTask != null) {
288             return;
289         }
290 
291         // If a previous release extracted resources, and the current release does not,
292         // deleteFiles() will not run and some files will be left. This currently
293         // can happen for ContentShell, but not for Chrome proper, since we always extract
294         // locale pak files.
295         if (shouldSkipPakExtraction()) {
296             return;
297         }
298 
299         mExtractTask = new ExtractTask();
300         mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
301     }
302 
getAppDataDir()303     private File getAppDataDir() {
304         return new File(PathUtils.getDataDirectory(mContext));
305     }
306 
getOutputDir()307     private File getOutputDir() {
308         return new File(getAppDataDir(), "paks");
309     }
310 
311     /**
312      * Pak files (UI strings and other resources) should be updated along with
313      * Chrome. A version mismatch can lead to a rather broken user experience.
314      * Failing to update the V8 snapshot files will lead to a version mismatch
315      * between V8 and the loaded snapshot which will cause V8 to crash, so this
316      * is treated as an error. The ICU data (icudtl.dat) is less
317      * version-sensitive, but still can lead to malfunction/UX misbehavior. So,
318      * we regard failing to update them as an error.
319      */
deleteFiles()320     private void deleteFiles() {
321         File icudata = new File(getAppDataDir(), ICU_DATA_FILENAME);
322         if (icudata.exists() && !icudata.delete()) {
323             Log.e(TAG, "Unable to remove the icudata %s", icudata.getName());
324         }
325         File v8_natives = new File(getAppDataDir(), V8_NATIVES_DATA_FILENAME);
326         if (v8_natives.exists() && !v8_natives.delete()) {
327             Log.e(TAG, "Unable to remove the v8 data %s", v8_natives.getName());
328         }
329         File v8_snapshot = new File(getAppDataDir(), V8_SNAPSHOT_DATA_FILENAME);
330         if (v8_snapshot.exists() && !v8_snapshot.delete()) {
331             Log.e(TAG, "Unable to remove the v8 data %s", v8_snapshot.getName());
332         }
333         File dir = getOutputDir();
334         if (dir.exists()) {
335             File[] files = dir.listFiles();
336 
337             if (files != null) {
338                 for (File file : files) {
339                     if (!file.delete()) {
340                         Log.e(TAG, "Unable to remove existing resource %s", file.getName());
341                     }
342                 }
343             }
344         }
345     }
346 
347     /**
348      * Pak extraction not necessarily required by the embedder.
349      */
shouldSkipPakExtraction()350     private static boolean shouldSkipPakExtraction() {
351         return sResourcesToExtract.length == 0;
352     }
353 }
354