Commit bdb7c6bf authored by David 'Digit' Turner's avatar David 'Digit' Turner Committed by Commit Bot

android: Enable language-based splits for app bundles.

This is done by adding a new |enable_language_splits| variable to
the android_app_bundle GN template. Note that splitting by
screen_density or ABI currently doesn't make sense for Chromium,
but could be added in the future.

Only the chrome_public_bundle target uses language-based
splits in this CL.

BUG=846633,820459
R=agrieve@chromium.org, yfriedman@chromium.org
TBR=twellington@chromium.org,tedchoc@chromium.org

Change-Id: Ic89f9dd4b8c78e8b2127bb19208db18bc8027eab
Reviewed-on: https://chromium-review.googlesource.com/1125934
Commit-Queue: David Turner <digit@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#574888}
parent ebe635eb
......@@ -55,6 +55,8 @@ def _ParseArgs(args):
parser.add_argument('--uncompress-shared-libraries', action='append',
help='Whether to store native libraries uncompressed. '
'This is a string to allow @FileArg usage.')
parser.add_argument('--split-dimensions',
help="GN-list of split dimensions to support.")
parser.add_argument('--keystore-path', help='Keystore path')
parser.add_argument('--keystore-password', help='Keystore password')
parser.add_argument('--key-name', help='Keystore key name')
......@@ -74,14 +76,15 @@ def _ParseArgs(args):
# Merge all uncompressed assets into a set.
uncompressed_list = []
for l in options.uncompressed_assets:
for entry in build_utils.ParseGnList(l):
# Each entry has the following format: 'zipPath' or 'srcPath:zipPath'
pos = entry.find(':')
if pos >= 0:
uncompressed_list.append(entry[pos + 1:])
else:
uncompressed_list.append(entry)
if options.uncompressed_assets:
for l in options.uncompressed_assets:
for entry in build_utils.ParseGnList(l):
# Each entry has the following format: 'zipPath' or 'srcPath:zipPath'
pos = entry.find(':')
if pos >= 0:
uncompressed_list.append(entry[pos + 1:])
else:
uncompressed_list.append(entry)
options.uncompressed_assets = set(uncompressed_list)
......@@ -89,9 +92,17 @@ def _ParseArgs(args):
if options.uncompress_shared_libraries:
uncompressed_libs = set(options.uncompress_shared_libraries)
if len(uncompressed_libs) > 1:
raise Exception('Inconsistent uses of --uncompress-native-libs!')
parser.error('Inconsistent uses of --uncompress-native-libs!')
options.uncompress_shared_libraries = 'True' in uncompressed_libs
# Check that all split dimensions are valid
if options.split_dimensions:
options.split_dimensions = build_utils.ParseGnList(options.split_dimensions)
for dim in options.split_dimensions:
if dim.upper() not in _ALL_SPLIT_DIMENSIONS:
parser.error('Invalid split dimension "%s" (expected one of: %s)' % (
dim, ', '.join(x.lower() for x in _ALL_SPLIT_DIMENSIONS)))
return options
......@@ -160,19 +171,20 @@ def _RewriteLanguageAssetPath(src_path):
return src_path
locale = src_path[len(_LOCALES_SUBDIR):-4]
locale = resource_utils.CHROME_TO_ANDROID_LOCALE_MAP.get(locale, locale)
android_locale = resource_utils.CHROME_TO_ANDROID_LOCALE_MAP.get(
locale, locale)
# The locale format is <lang>-<region> or <lang>. Extract the language.
pos = locale.find('-')
pos = android_locale.find('-')
if pos >= 0:
language = locale[:pos]
android_language = android_locale[:pos]
else:
language = locale
android_language = android_locale
if language == _FALLBACK_LANGUAGE:
if android_language == _FALLBACK_LANGUAGE:
return 'assets/locales/%s.pak' % locale
return 'assets/locales#lang_%s/%s.pak' % (language, locale)
return 'assets/locales#lang_%s/%s.pak' % (android_language, locale)
def _SplitModuleForAssetTargeting(src_module_zip, tmp_dir, split_dimensions):
......@@ -224,10 +236,9 @@ def main(args):
args = build_utils.ExpandFileArgs(args)
options = _ParseArgs(args)
# TODO(crbug.com/846633): Enable language-based configuration splits once
# Chromium detects the appropriate fallback locales when needed.
# split_dimensions = [ 'LANGUAGE' ]
split_dimensions = []
if options.split_dimensions:
split_dimensions = [x.upper() for x in options.split_dimensions]
bundle_config = _GenerateBundleConfigJson(options.uncompressed_assets,
options.uncompress_shared_libraries,
......
......@@ -27,6 +27,7 @@ EMPTY_ANDROID_MANIFEST_PATH = os.path.join(
# A variation of this lists also exists in:
# //base/android/java/src/org/chromium/base/LocaleUtils.java
# //ui/android/java/src/org/chromium/base/LocalizationUtils.java
CHROME_TO_ANDROID_LOCALE_MAP = {
'en-GB': 'en-rGB',
'en-US': 'en-rUS',
......
......@@ -3445,6 +3445,9 @@ if (enable_java_templates) {
#
# NOTE: This creates a new target with the name "${target_name}_apks"
#
# enable_language_splits: Optional. If true, enable APK splits based
# on languages.
#
# sign_bundle: Optional. If true, sign the bundle. Default is false
# because signing is very slow, and there is no reason to do it
# unless one wants to upload the bundle to the Play Store (e.g.
......@@ -3529,6 +3532,12 @@ if (enable_java_templates) {
_sign_bundle = defined(invoker.sign_bundle) && invoker.sign_bundle
_generate_apks = defined(invoker.generate_apks) && invoker.generate_apks
_split_dimensions = []
if (defined(invoker.enable_language_splits) &&
invoker.enable_language_splits) {
_split_dimensions += [ "language" ]
}
if (_sign_bundle || _generate_apks) {
_keystore_path = android_keystore_path
_keystore_password = android_keystore_password
......@@ -3580,6 +3589,9 @@ if (enable_java_templates) {
_bundle_keystore_name,
]
}
if (_split_dimensions != []) {
args += [ "--split-dimensions=$_split_dimensions" ]
}
foreach(build_config, _all_module_build_configs) {
_rebased_build_config = rebase_path(build_config, root_build_dir)
args += [
......
......@@ -1557,6 +1557,8 @@ android_app_bundle("chrome_public_bundle") {
# To ease local testing, also generate the .apks archive containing split
# APKs for this bundle.
generate_apks = true
enable_language_splits = true
}
android_app_bundle("chrome_modern_public_bundle") {
......
......@@ -111,5 +111,33 @@ public class LocalizationUtils {
return uiLocale;
}
/**
* Return one default locale-specific PAK file name associated with a given language.
*
* @param language Language name (e.g. "en").
* @return A Chromium-specific locale name (e.g. "en-US") matching the input language
* that can be used to access compressed locale pak files.
*/
public static String getDefaultCompressedPakLocaleForLanguage(String language) {
// IMPORTANT: Keep in sync with the mapping found in:
// //build/android/gyp/resource_utils.py
// NOTE: All languages provide locale files named '<language>.pak', except
// for a few exceptions listed below. E.g. for the English language,
// the 'en-US.pak' and 'en-GB.pak' files are provided, and there is
// no 'en.pak' file.
switch (language) {
case "en":
return "en-US";
case "pt":
return "pt-PT";
case "zh":
return "zh-CN";
default:
// NOTE: for Spanish (es), both es.pak and es-419.pak are used. Hence this works.
return language;
}
}
private static native int nativeGetFirstStrongCharacterDirection(String string);
}
......@@ -149,20 +149,87 @@ public class ResourceExtractor {
activeLocales.add(locale);
}
}
if (activeLocales.isEmpty() && BuildConfig.COMPRESSED_LOCALES.length > 0) {
if (activeLocales.isEmpty()) {
assert BuildConfig.COMPRESSED_LOCALES.length > 0;
assert Arrays.asList(BuildConfig.COMPRESSED_LOCALES).contains(FALLBACK_LOCALE);
activeLocales.add(FALLBACK_LOCALE);
}
// * For regular APKs, the locale pak files are stored under:
// base.apk!/assets/locales/<locale>.pak
//
// where <locale> is a Chromium-specific locale name.
//
// * When using app bundles, the locale pak files are stored in
// language-specific directories that look like:
// <split>.apk!/assets/locales#lang_<lang>/<locale>.pak
//
// Where <lang> is an Android-specific ISO-639-1 language identifier.
//
// Moreover, when the bundle uses APK splits, there is no guarantee that the split
// corresponding to the current device locale is installed yet, but the one matching
// uiLanguage should be there, since the value is determined by loading a resource string
// from the current application's asset manager.
//
AssetManager assetManager = ContextUtils.getApplicationAssets();
String localesSrcDir;
String langSpecificPath = COMPRESSED_LOCALES_DIR + "#lang_" + uiLanguage;
String defaultLocalePakName =
LocalizationUtils.getDefaultCompressedPakLocaleForLanguage(uiLanguage) + ".pak";
if (assetPathHasFile(assetManager, langSpecificPath, defaultLocalePakName)) {
// This is an app bundle, and the split containing the pak files for
// the current locale is installed.
localesSrcDir = langSpecificPath;
} else if (assetPathHasFile(
assetManager, COMPRESSED_LOCALES_DIR, activeLocales.get(0) + ".pak")) {
// This is a regular APK, and all pak files are available.
localesSrcDir = COMPRESSED_LOCALES_DIR;
} else {
// This is an app bundle, but the split containing the pak files for the current UI
// locale is *not* installed yet. This should never happen in theory, and there is
// little that can be done at this point, so return an empty list. Nothing will get
// extracted, and Chromium may later crash when trying to access the PAK file from
// native code.
Log.e(TAG, "Android Locale: %s misses split for .pak files: %s", defaultLocale,
Arrays.toString(activeLocales.toArray()));
return new String[] {};
}
// Return the list of locale pak file paths corresponding to the current language.
String[] localePakFiles = new String[activeLocales.size()];
for (int n = 0; n < activeLocales.size(); ++n) {
localePakFiles[n] = COMPRESSED_LOCALES_DIR + '/' + activeLocales.get(n) + ".pak";
localePakFiles[n] = localesSrcDir + '/' + activeLocales.get(n) + ".pak";
}
Log.i(TAG, "Using app bundle locale directory: " + localesSrcDir);
Log.i(TAG, "UI Language: %s requires .pak files: %s", uiLanguage,
Arrays.toString(activeLocales.toArray()));
return localePakFiles;
}
/**
* Check that an AssetManager instance has a specific asset file.
*
* @param assetManager The application's AssetManager instance.
* @param assetPath Asset directory path (e.g. "assets/locales").
* @param assetFile Asset file name inside assetPath.
* @return true iff the asset file is available.
*/
private static boolean assetPathHasFile(
AssetManager assetManager, String assetPath, String assetFile) {
String assetFilePath = assetPath + '/' + assetFile;
try {
InputStream input = assetManager.open(assetFilePath);
input.close();
Log.i(TAG, "Found asset file: " + assetFilePath);
return true;
} catch (IOException e) {
Log.i(TAG, "Missing asset file: " + assetFilePath);
return false;
}
}
/**
* Synchronously wait for the resource extraction to be completed.
* <p>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment