Chromium Code Reviews| Index: content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java |
| diff --git a/content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java b/content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..3cc615ef67ee5940959fc64060100f9a2c94e43a |
| --- /dev/null |
| +++ b/content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java |
| @@ -0,0 +1,295 @@ |
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package org.chromium.content.browser; |
| + |
| +import android.content.Context; |
| +import android.content.SharedPreferences; |
| +import android.content.pm.PackageInfo; |
| +import android.content.pm.PackageManager; |
| +import android.content.res.AssetManager; |
| +import android.os.AsyncTask; |
| +import android.preference.PreferenceManager; |
| +import android.util.Log; |
| + |
| +import java.io.File; |
| +import java.io.FileOutputStream; |
| +import java.io.FilenameFilter; |
| +import java.io.IOException; |
| +import java.io.InputStream; |
| +import java.io.OutputStream; |
| +import java.util.HashSet; |
| +import java.util.Locale; |
| +import java.util.concurrent.CancellationException; |
| +import java.util.concurrent.ExecutionException; |
| +import java.util.regex.Pattern; |
| + |
| +/** |
| + * Handles extracting the necessary resources bundled in the Chrome APK and moving them to a |
|
Yaron
2012/07/12 01:00:59
This isn't specific to chrome anymore.
Ted C
2012/07/12 01:20:18
Done.
|
| + * location on the file system accessible from the native code. |
| + */ |
| +public class ResourceExtractor { |
| + |
| + private static final String LOGTAG = "ResourceExtractor"; |
| + private static final String LAST_LANGUAGE = "Last language"; |
| + private static final String PAK_FILENAMES = "Pak filenames"; |
| + |
| + private static String[] sMandatoryPaks = null; |
| + |
| + private class ExtractTask extends AsyncTask<Void, Void, Void> { |
| + private static final int BUFFER_SIZE = 16 * 1024; |
| + |
| + public ExtractTask() { |
| + } |
| + |
| + @Override |
| + protected Void doInBackground(Void... unused) { |
| + if (sMandatoryPaks == null) { |
| + assert false : "No pak files specified. Call setMandatoryPaksToExtract before " |
| + + "beginning the resource extrations"; |
| + return null; |
| + } |
| + if (!mOutputDir.exists() && !mOutputDir.mkdirs()) { |
| + Log.e(LOGTAG, "Unable to create pak resources directory!"); |
| + return null; |
| + } |
| + |
| + String timestampFile = checkPakTimestamp(); |
| + if (timestampFile != null) { |
| + deleteFiles(mContext); |
| + } |
| + |
| + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); |
| + HashSet<String> filenames = (HashSet<String>) prefs.getStringSet( |
| + PAK_FILENAMES, new HashSet<String>()); |
| + String currentLanguage = Locale.getDefault().getLanguage(); |
| + if (prefs.getString(LAST_LANGUAGE, "").equals(currentLanguage) |
| + && filenames.size() >= sMandatoryPaks.length) { |
| + boolean filesPresent = true; |
| + for (String file : filenames) { |
| + if (!new File(mOutputDir, file).exists()) { |
| + filesPresent = false; |
| + break; |
| + } |
| + } |
| + if (filesPresent) return null; |
| + } else { |
| + prefs.edit().putString(LAST_LANGUAGE, currentLanguage).apply(); |
| + } |
| + |
| + StringBuilder p = new StringBuilder(); |
| + for (String mandatoryPak : sMandatoryPaks) { |
| + p.append("\\Q" + mandatoryPak + "\\E|"); |
| + } |
| + |
| + // As well as the minimum required set of .paks above, we'll also add all .paks that |
| + // we have for the users currently selected language. |
| + |
| + // Android uses non-standard language codes for Indonesian, Hebrew, and Yiddish: |
| + // http://developer.android.com/reference/java/util/Locale.html. Correct these codes |
| + // so that we unpack the correct .pak file (see http://b/6504501). |
|
Yaron
2012/07/12 01:00:59
Please file crbug
Ted C
2012/07/12 01:20:18
Done.
|
| + if (currentLanguage.equals("in")) { |
| + currentLanguage = "id"; |
| + } else if (currentLanguage.equals("iw")) { |
| + currentLanguage = "he"; |
| + } |
| + // Uncomment these lines to support Yiddish: |
|
Yaron
2012/07/12 01:00:59
??? Probably should remove this..
Ted C
2012/07/12 01:20:18
Done.
|
| + //else if (currentLanguage.equals("ji")) { |
| + // currentLanguage = "yi"; |
| + //} |
| + |
| + p.append(currentLanguage); |
| + p.append("(-\\w+)?\\.pak"); |
| + Pattern paksToInstall = Pattern.compile(p.toString()); |
| + |
| + AssetManager manager = mContext.getResources().getAssets(); |
| + try { |
| + // Loop through every asset file that we have in the APK, and look for the |
| + // ones that we need to extract by trying to match the Patterns that we |
| + // created above. |
| + byte[] buffer = null; |
| + String[] files = manager.list(""); |
| + for (String file : files) { |
| + if (!paksToInstall.matcher(file).matches()) { |
| + continue; |
| + } |
| + File output = new File(mOutputDir, file); |
| + if (output.exists()) { |
| + continue; |
| + } |
| + |
| + InputStream is = null; |
| + OutputStream os = null; |
| + try { |
| + is = manager.open(file); |
| + os = new FileOutputStream(output); |
| + Log.i(LOGTAG, "Extracting resource " + file); |
| + if (buffer == null) { |
| + buffer = new byte[BUFFER_SIZE]; |
| + } |
| + |
| + int count = 0; |
| + while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) { |
| + os.write(buffer, 0, count); |
| + } |
| + filenames.add(file); |
| + } finally { |
| + try { |
| + if (is != null) { |
| + is.close(); |
| + } |
| + } finally { |
| + if (os != null) { |
| + os.close(); |
| + } |
| + } |
| + } |
| + } |
| + } catch (IOException e) { |
| + Log.w(LOGTAG, "Exception unpacking required pak resources: " + e.getMessage()); |
| + deleteFiles(mContext); |
| + return null; |
| + } |
| + |
| + // Finished, write out a timestamp file if we need to. |
| + |
| + if (timestampFile != null) { |
| + try { |
| + new File(mOutputDir, timestampFile).createNewFile(); |
| + } catch (IOException e) { |
| + // Worst case we don't write a timestamp, so we'll re-extract the resource |
| + // paks next start up. |
| + Log.w(LOGTAG, "Failed to write resource pak timestamp!"); |
| + } |
| + } |
| + // TODO(yusufo): Figure out why remove is required here. |
| + prefs.edit().remove(PAK_FILENAMES).apply(); |
| + prefs.edit().putStringSet(PAK_FILENAMES, filenames).apply(); |
| + return null; |
| + } |
| + |
| + // Looks for a timestamp file on disk that indicates the version of the APK that |
| + // the resource paks were extracted from. Returns null if a timestamp was found |
| + // and it indicates that the resources match the current APK. Otherwise returns |
| + // a String that represents the filename of a timestamp to create. |
| + // Note that we do this to avoid adding a BroadcastReceiver on |
| + // android.content.Intent#ACTION_PACKAGE_CHANGED as that causes process churn |
| + // on (re)installation of *all* APK files. |
| + private String checkPakTimestamp() { |
| + final String TIMESTAMP_PREFIX = "pak_timestamp-"; |
| + PackageManager pm = mContext.getPackageManager(); |
| + PackageInfo pi = null; |
| + |
| + try { |
| + pi = pm.getPackageInfo(mContext.getPackageName(), 0); |
| + } catch (PackageManager.NameNotFoundException e) { |
| + return TIMESTAMP_PREFIX; |
| + } |
| + |
| + if (pi == null) { |
| + return TIMESTAMP_PREFIX; |
| + } |
| + |
| + String expectedTimestamp = TIMESTAMP_PREFIX + pi.versionCode + "-" + pi.lastUpdateTime; |
| + |
| + String[] timestamps = mOutputDir.list(new FilenameFilter() { |
| + @Override |
| + public boolean accept(File dir, String name) { |
| + return name.startsWith(TIMESTAMP_PREFIX); |
| + } |
| + }); |
| + |
| + if (timestamps.length != 1) { |
| + // If there's no timestamp, nuke to be safe as we can't tell the age of the files. |
| + // If there's multiple timestamps, something's gone wrong so nuke. |
| + return expectedTimestamp; |
| + } |
| + |
| + if (!expectedTimestamp.equals(timestamps[0])) { |
| + return expectedTimestamp; |
| + } |
| + |
| + // timestamp file is already up-to date. |
| + return null; |
| + } |
| + } |
| + |
| + private Context mContext; |
| + private ExtractTask mExtractTask; |
| + private File mOutputDir; |
| + |
| + private static ResourceExtractor sInstance; |
| + |
| + public static ResourceExtractor get(Context context) { |
| + if (sInstance == null) { |
| + sInstance = new ResourceExtractor(context); |
| + } |
| + return sInstance; |
| + } |
| + |
| + /** |
| + * Specifies the .pak files that should be extracted from the APK's asset resources directory |
| + * and moved to {@link #getOutputDirFromContext(Context)}. |
| + * @param mandatoryPaks The list of pak files to be loaded. |
| + */ |
| + public static void setMandatoryPaksToExtract(String... mandatoryPaks) { |
| + assert (sInstance == null || sInstance.mExtractTask == null) |
| + : "Must be called before startExtractingResources is called"; |
| + sMandatoryPaks = mandatoryPaks; |
| + } |
| + |
| + private ResourceExtractor(Context context) { |
| + mContext = context; |
| + mOutputDir = getOutputDirFromContext(mContext); |
| + } |
| + |
| + public void waitForCompletion() { |
| + assert mExtractTask != null; |
| + |
| + try { |
| + mExtractTask.get(); |
| + } |
| + catch (CancellationException e) { |
| + // Don't leave the files in a inconsistent state. |
| + deleteFiles(mContext); |
| + } |
| + catch (ExecutionException e2) { |
| + deleteFiles(mContext); |
| + } |
| + catch (InterruptedException e3) { |
| + deleteFiles(mContext); |
| + } |
| + } |
| + |
| + // This will extract the application pak resources in an |
| + // AsyncTask. Call waitForCompletion() at the point resources |
| + // are needed to block until the task completes. |
| + public void startExtractingResources() { |
| + if (mExtractTask != null) { |
| + return; |
| + } |
| + |
| + mExtractTask = new ExtractTask(); |
| + mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| + } |
| + |
| + public static File getOutputDirFromContext(Context context) { |
| + // Note that the "chrome" base directory here must match the one in PathUtils.java |
| + // and "paks" must match the value used in chrome_paths.cc. |
| + // TODO(benm): Clean this up by removing the duplication. |
| + return new File(context.getDir("chrome", Context.MODE_PRIVATE), "paks"); |
|
Yaron
2012/07/12 01:00:59
Hmm. This shouldn't mention chrome.
Ted C
2012/07/12 01:20:18
This needs to match the value in PathUtils.java.
Yaron
2012/07/12 01:32:17
Follow up sgtm
|
| + } |
| + |
| + public static void deleteFiles(Context context) { |
| + File dir = getOutputDirFromContext(context); |
| + if (dir.exists()) { |
| + File[] files = dir.listFiles(); |
| + for (File file : files) { |
| + if (!file.delete()) { |
| + Log.w(LOGTAG, "Unable to remove existing resource " + file.getName()); |
| + } |
| + } |
| + } |
| + } |
| +} |