Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(862)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java

Issue 10693157: Add pak file support for content shell and android apks. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebase Created 8 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..793f8c1a9d0b72ea8269c68d0922dafe7fee568d
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/ResourceExtractor.java
@@ -0,0 +1,293 @@
+// 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 an APK and moving them to a 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 extractions";
+ 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 crbug.com/136933).
+ if (currentLanguage.equals("in")) {
+ currentLanguage = "id";
+ } else if (currentLanguage.equals("iw")) {
+ currentLanguage = "he";
+ } 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 an 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");
+ }
+
+ 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());
+ }
+ }
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698