EMMA Coverage Report (generated Fri Aug 23 16:39:17 PDT 2013)
[all classes][org.chromium.content.browser]

COVERAGE SUMMARY FOR SOURCE FILE [ResourceExtractor.java]

nameclass, %method, %block, %line, %
ResourceExtractor.java100% (3/3)100% (19/19)77%  (489/634)75%  (100.2/134)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class ResourceExtractor100% (1/1)100% (14/14)66%  (130/196)68%  (30.4/45)
waitForCompletion (): void 100% (1/1)41%  (13/32)32%  (3.9/12)
setExtractImplicitLocaleForTesting (boolean): void 100% (1/1)47%  (7/15)78%  (2.3/3)
setMandatoryPaksToExtract (String []): void 100% (1/1)47%  (7/15)78%  (2.3/3)
deleteFiles (Context): void 100% (1/1)49%  (20/41)66%  (4.6/7)
shouldSkipPakExtraction (): boolean 100% (1/1)73%  (16/22)68%  (1.4/2)
<static initializer> 100% (1/1)83%  (10/12)94%  (2.8/3)
startExtractingResources (): void 100% (1/1)90%  (19/21)71%  (5/7)
ResourceExtractor (Context): void 100% (1/1)100% (11/11)100% (4/4)
access$000 (ResourceExtractor): File 100% (1/1)100% (3/3)100% (1/1)
access$100 (ResourceExtractor): Context 100% (1/1)100% (3/3)100% (1/1)
access$200 (): String [] 100% (1/1)100% (2/2)100% (1/1)
access$300 (): boolean 100% (1/1)100% (2/2)100% (1/1)
get (Context): ResourceExtractor 100% (1/1)100% (9/9)100% (3/3)
getOutputDirFromContext (Context): File 100% (1/1)100% (8/8)100% (1/1)
     
class ResourceExtractor$ExtractTask100% (1/1)100% (3/3)82%  (349/428)78%  (68.8/88)
doInBackground (Void []): Void 100% (1/1)80%  (285/357)78%  (54.8/70)
checkPakTimestamp (): String 100% (1/1)89%  (58/65)75%  (12/16)
ResourceExtractor$ExtractTask (ResourceExtractor): void 100% (1/1)100% (6/6)100% (2/2)
     
class ResourceExtractor$ExtractTask$1100% (1/1)100% (2/2)100% (10/10)100% (2/2)
ResourceExtractor$ExtractTask$1 (ResourceExtractor$ExtractTask): void 100% (1/1)100% (6/6)100% (1/1)
accept (File, String): boolean 100% (1/1)100% (4/4)100% (1/1)

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

[all classes][org.chromium.content.browser]
EMMA 2.0.5312 (C) Vladimir Roubtsov