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