1a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)// Copyright 2012 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)package org.chromium.content.browser;
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.Context;
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.SharedPreferences;
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.pm.PackageInfo;
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.pm.PackageManager;
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.content.res.AssetManager;
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.os.AsyncTask;
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.preference.PreferenceManager;
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import android.util.Log;
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import org.chromium.base.PathUtils;
17f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)import org.chromium.ui.base.LocalizationUtils;
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.io.File;
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.io.FileOutputStream;
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.io.FilenameFilter;
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.io.IOException;
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.io.InputStream;
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.io.OutputStream;
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.HashSet;
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.concurrent.CancellationException;
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.concurrent.ExecutionException;
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import java.util.regex.Pattern;
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)/**
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * Handles extracting the necessary resources bundled in an APK and moving them to a location on
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) * the file system accessible from the native code.
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) */
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)public class ResourceExtractor {
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final String LOGTAG = "ResourceExtractor";
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final String LAST_LANGUAGE = "Last language";
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static final String PAK_FILENAMES = "Pak filenames";
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static String[] sMandatoryPaks = null;
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
422a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    // By default, we attempt to extract a pak file for the users
432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    // current device locale. Use setExtractImplicitLocale() to
442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    // change this behavior.
452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    private static boolean sExtractImplicitLocalePak = true;
462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private class ExtractTask extends AsyncTask<Void, Void, Void> {
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        private static final int BUFFER_SIZE = 16 * 1024;
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        public ExtractTask() {
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        @Override
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        protected Void doInBackground(Void... unused) {
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (!mOutputDir.exists() && !mOutputDir.mkdirs()) {
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                Log.e(LOGTAG, "Unable to create pak resources directory!");
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return null;
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            String timestampFile = checkPakTimestamp();
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (timestampFile != null) {
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                deleteFiles(mContext);
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            HashSet<String> filenames = (HashSet<String>) prefs.getStringSet(
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    PAK_FILENAMES, new HashSet<String>());
68c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)            String currentLocale = LocalizationUtils.getDefaultLocale();
692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            String currentLanguage = currentLocale.split("-", 2)[0];
702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (prefs.getString(LAST_LANGUAGE, "").equals(currentLanguage)
725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    &&  filenames.size() >= sMandatoryPaks.length) {
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                boolean filesPresent = true;
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                for (String file : filenames) {
755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    if (!new File(mOutputDir, file).exists()) {
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        filesPresent = false;
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        break;
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    }
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                if (filesPresent) return null;
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            } else {
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                prefs.edit().putString(LAST_LANGUAGE, currentLanguage).apply();
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            StringBuilder p = new StringBuilder();
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            for (String mandatoryPak : sMandatoryPaks) {
872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                if (p.length() > 0) p.append('|');
882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                p.append("\\Q" + mandatoryPak + "\\E");
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            if (sExtractImplicitLocalePak) {
922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                if (p.length() > 0) p.append('|');
932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                // As well as the minimum required set of .paks above, we'll also add all .paks that
942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                // we have for the user's currently selected language.
955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                p.append(currentLanguage);
972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                p.append("(-\\w+)?\\.pak");
982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)            }
995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            Pattern paksToInstall = Pattern.compile(p.toString());
1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            AssetManager manager = mContext.getResources().getAssets();
1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            try {
1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // Loop through every asset file that we have in the APK, and look for the
1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // ones that we need to extract by trying to match the Patterns that we
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // created above.
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                byte[] buffer = null;
1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                String[] files = manager.list("");
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                for (String file : files) {
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    if (!paksToInstall.matcher(file).matches()) {
1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        continue;
1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    }
1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    File output = new File(mOutputDir, file);
1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    if (output.exists()) {
1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        continue;
1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    }
1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    InputStream is = null;
1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    OutputStream os = null;
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    try {
1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        is = manager.open(file);
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        os = new FileOutputStream(output);
1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        Log.i(LOGTAG, "Extracting resource " + file);
1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        if (buffer == null) {
1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            buffer = new byte[BUFFER_SIZE];
1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        }
1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        int count = 0;
1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            os.write(buffer, 0, count);
1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        }
1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        os.flush();
1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        // Ensure something reasonable was written.
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        if (output.length() == 0) {
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            throw new IOException(file + " extracted with 0 length!");
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        }
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        filenames.add(file);
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    } finally {
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        try {
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            if (is != null) {
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                is.close();
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            }
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        } finally {
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            if (os != null) {
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                os.close();
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                            }
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                        }
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    }
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            } catch (IOException e) {
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // TODO(benm): See crbug/152413.
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // Try to recover here, can we try again after deleting files instead of
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // returning null? It might be useful to gather UMA here too to track if
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // this happens with regularity.
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                Log.w(LOGTAG, "Exception unpacking required pak resources: " + e.getMessage());
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                deleteFiles(mContext);
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return null;
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            // Finished, write out a timestamp file if we need to.
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (timestampFile != null) {
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                try {
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    new File(mOutputDir, timestampFile).createNewFile();
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                } catch (IOException e) {
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    // Worst case we don't write a timestamp, so we'll re-extract the resource
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    // paks next start up.
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    Log.w(LOGTAG, "Failed to write resource pak timestamp!");
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            // TODO(yusufo): Figure out why remove is required here.
1735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            prefs.edit().remove(PAK_FILENAMES).apply();
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            prefs.edit().putStringSet(PAK_FILENAMES, filenames).apply();
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return null;
1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Looks for a timestamp file on disk that indicates the version of the APK that
1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // the resource paks were extracted from. Returns null if a timestamp was found
1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // and it indicates that the resources match the current APK. Otherwise returns
1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // a String that represents the filename of a timestamp to create.
1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // Note that we do this to avoid adding a BroadcastReceiver on
1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // android.content.Intent#ACTION_PACKAGE_CHANGED as that causes process churn
1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        // on (re)installation of *all* APK files.
1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        private String checkPakTimestamp() {
1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            final String TIMESTAMP_PREFIX = "pak_timestamp-";
1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            PackageManager pm = mContext.getPackageManager();
1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            PackageInfo pi = null;
1895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            try {
1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                pi = pm.getPackageInfo(mContext.getPackageName(), 0);
1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            } catch (PackageManager.NameNotFoundException e) {
1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return TIMESTAMP_PREFIX;
1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
1955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (pi == null) {
1975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return TIMESTAMP_PREFIX;
1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            String expectedTimestamp = TIMESTAMP_PREFIX + pi.versionCode + "-" + pi.lastUpdateTime;
2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            String[] timestamps = mOutputDir.list(new FilenameFilter() {
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                @Override
2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                public boolean accept(File dir, String name) {
2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    return name.startsWith(TIMESTAMP_PREFIX);
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            });
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (timestamps.length != 1) {
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // If there's no timestamp, nuke to be safe as we can't tell the age of the files.
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                // If there's multiple timestamps, something's gone wrong so nuke.
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return expectedTimestamp;
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if (!expectedTimestamp.equals(timestamps[0])) {
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return expectedTimestamp;
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            // timestamp file is already up-to date.
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return null;
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
224f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)    private final Context mContext;
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private ExtractTask mExtractTask;
226f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)    private final File mOutputDir;
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private static ResourceExtractor sInstance;
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static ResourceExtractor get(Context context) {
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (sInstance == null) {
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            sInstance = new ResourceExtractor(context);
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return sInstance;
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    /**
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * Specifies the .pak files that should be extracted from the APK's asset resources directory
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     * and moved to {@link #getOutputDirFromContext(Context)}.
240ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch     * @param mandatoryPaks The list of pak files to be loaded. If no pak files are
241ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch     *     required, pass a single empty string.
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)     */
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static void setMandatoryPaksToExtract(String... mandatoryPaks) {
2445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        assert (sInstance == null || sInstance.mExtractTask == null)
2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                : "Must be called before startExtractingResources is called";
2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        sMandatoryPaks = mandatoryPaks;
2472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    }
2492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)
2502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    /**
2512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     * By default the ResourceExtractor will attempt to extract a pak resource for the users
2522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     * currently specified locale. This behavior can be changed with this function and is
2532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     * only needed by tests.
2542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     * @param extract False if we should not attempt to extract a pak file for
2552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     *         the users currently selected locale and try to extract only the
2562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     *         pak files specified in sMandatoryPaks.
2572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)     */
2582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)    public static void setExtractImplicitLocaleForTesting(boolean extract) {
2592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        assert (sInstance == null || sInstance.mExtractTask == null)
2602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)                : "Must be called before startExtractingResources is called";
2612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)        sExtractImplicitLocalePak = extract;
2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    private ResourceExtractor(Context context) {
2655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        mContext = context;
2665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        mOutputDir = getOutputDirFromContext(mContext);
2675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public void waitForCompletion() {
270ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        if (shouldSkipPakExtraction()) {
271ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch            return;
272ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        }
273ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch
2745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        assert mExtractTask != null;
2755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        try {
2775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            mExtractTask.get();
278a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        } catch (CancellationException e) {
2795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            // Don't leave the files in an inconsistent state.
2805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            deleteFiles(mContext);
281a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        } catch (ExecutionException e2) {
2825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            deleteFiles(mContext);
283a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)        } catch (InterruptedException e3) {
2845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            deleteFiles(mContext);
2855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
2865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
2875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
288f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)    /**
289f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)     * This will extract the application pak resources in an
290f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)     * AsyncTask. Call waitForCompletion() at the point resources
291f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)     * are needed to block until the task completes.
292f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)     */
2935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public void startExtractingResources() {
2945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (mExtractTask != null) {
2955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            return;
2965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
2975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
298ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        if (shouldSkipPakExtraction()) {
299ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch            return;
300ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        }
301ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch
3025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        mExtractTask = new ExtractTask();
3035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
3045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
3055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static File getOutputDirFromContext(Context context) {
3075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return new File(PathUtils.getDataDirectory(context.getApplicationContext()), "paks");
3085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
3095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
3105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    public static void deleteFiles(Context context) {
3115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        File dir = getOutputDirFromContext(context);
3125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if (dir.exists()) {
3135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            File[] files = dir.listFiles();
3145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            for (File file : files) {
3155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                if (!file.delete()) {
3165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                    Log.w(LOGTAG, "Unable to remove existing resource " + file.getName());
3175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                }
3185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            }
3195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
3205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
321ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch
322ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch    /**
323ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch     * Pak extraction not necessarily required by the embedder; we allow them to skip
324ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch     * this process if they call setMandatoryPaksToExtract with a single empty String.
325ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch     */
326ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch    private static boolean shouldSkipPakExtraction() {
327ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        // Must call setMandatoryPaksToExtract before beginning resource extraction.
328ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        assert sMandatoryPaks != null;
329ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch        return sMandatoryPaks.length == 1 && "".equals(sMandatoryPaks[0]);
330ba5b9a6411cb1792fd21f0a078d7a25cd1ceec16Ben Murdoch    }
3315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
332