From e19a1c6836d51b035508c132e785e19a3dcd3ac5 Mon Sep 17 00:00:00 2001 From: Josh Hou Date: Mon, 27 Dec 2021 16:57:09 +0800 Subject: [PATCH] APP languages detection Parse the contents of LocaleConfig's XML to obtain the app-specific supported locales Bug: 203015582 Bug: 193787310 Test: atest LocaleConfigTest Local test APK Change-Id: Ie9314e4c09f881f834bf7ea3cc0780d1545e6ce7 --- core/api/current.txt | 12 ++ core/java/android/app/LocaleConfig.java | 166 ++++++++++++++++++ .../android/content/pm/ApplicationInfo.java | 24 +++ .../content/pm/parsing/ParsingPackage.java | 2 + .../pm/parsing/ParsingPackageImpl.java | 16 ++ .../pm/parsing/ParsingPackageRead.java | 7 + .../pm/parsing/ParsingPackageUtils.java | 1 + core/res/res/values/attrs.xml | 9 +- core/res/res/values/attrs_manifest.xml | 4 + core/res/res/values/public.xml | 1 + .../parsing/parcelling/AndroidPackageTest.kt | 1 + 11 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 core/java/android/app/LocaleConfig.java diff --git a/core/api/current.txt b/core/api/current.txt index 98d3a91c99446..5eac832a958ab 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -971,6 +971,7 @@ package android { field public static final int listSeparatorTextViewStyle = 16843272; // 0x1010208 field public static final int listViewStyle = 16842868; // 0x1010074 field public static final int listViewWhiteStyle = 16842869; // 0x1010075 + field public static final int localeConfig; field public static final int lockTaskMode = 16844013; // 0x10104ed field public static final int logo = 16843454; // 0x10102be field public static final int logoDescription = 16844009; // 0x10104e9 @@ -5685,6 +5686,17 @@ package android.app { method @Deprecated public android.view.Window startActivity(String, android.content.Intent); } + public class LocaleConfig { + ctor public LocaleConfig(@NonNull android.content.Context); + method public int getStatus(); + method @Nullable public android.os.LocaleList getSupportedLocales(); + field public static final int STATUS_NOT_SPECIFIED = 1; // 0x1 + field public static final int STATUS_PARSING_FAILED = 2; // 0x2 + field public static final int STATUS_SUCCESS = 0; // 0x0 + field public static final String TAG_LOCALE = "locale"; + field public static final String TAG_LOCALE_CONFIG = "locale-config"; + } + public class LocaleManager { method @NonNull public android.os.LocaleList getApplicationLocales(); method public void setApplicationLocales(@NonNull android.os.LocaleList); diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java new file mode 100644 index 0000000000000..436eac37b4fb3 --- /dev/null +++ b/core/java/android/app/LocaleConfig.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.LocaleList; +import android.util.AttributeSet; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashSet; +import java.util.Set; + +/** + * The LocaleConfig of an application. + * Defined in an XML resource file with an {@code } element and + * referenced in the manifest via {@code android:localeConfig} on + * {@code }. + * + * For more information, see TODO(b/214154050): add link to guide + * + * @attr ref android.R.styleable#LocaleConfig_Locale_name + * @attr ref android.R.styleable#AndroidManifestApplication_localeConfig + */ +public class LocaleConfig { + + private static final String TAG = "LocaleConfig"; + public static final String TAG_LOCALE_CONFIG = "locale-config"; + public static final String TAG_LOCALE = "locale"; + private LocaleList mLocales; + private int mStatus; + + /** + * succeeded reading the LocaleConfig structure stored in an XML file. + */ + public static final int STATUS_SUCCESS = 0; + /** + * No android:localeConfig tag on . + */ + public static final int STATUS_NOT_SPECIFIED = 1; + /** + * Malformed input in the XML file where the LocaleConfig was stored. + */ + public static final int STATUS_PARSING_FAILED = 2; + + /** @hide */ + @IntDef(prefix = { "STATUS_" }, value = { + STATUS_SUCCESS, + STATUS_NOT_SPECIFIED, + STATUS_PARSING_FAILED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Status{} + + /** + * Returns the LocaleConfig for the provided application context + * + * @param context the context of the application + * + * @see Context#createPackageContext(String, int). + */ + public LocaleConfig(@NonNull Context context) { + int resId = 0; + Resources res = context.getResources(); + try { + //Get the resource id + resId = new ApplicationInfo(context.getApplicationInfo()).getLocaleConfigRes(); + //Get the parser to read XML data + XmlResourceParser parser = res.getXml(resId); + parseLocaleConfig(parser, res); + } catch (Resources.NotFoundException e) { + Slog.w(TAG, "The resource file pointed to by the given resource ID isn't found.", e); + mStatus = STATUS_NOT_SPECIFIED; + } catch (XmlPullParserException | IOException e) { + Slog.w(TAG, "Failed to parse XML configuration from " + + res.getResourceEntryName(resId), e); + mStatus = STATUS_PARSING_FAILED; + } + } + + /** + * Parse the XML content and get the locales supported by the application + */ + private void parseLocaleConfig(XmlResourceParser parser, Resources res) + throws IOException, XmlPullParserException { + XmlUtils.beginDocument(parser, TAG_LOCALE_CONFIG); + int outerDepth = parser.getDepth(); + AttributeSet attrs = Xml.asAttributeSet(parser); + Set localeNames = new HashSet(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + if (TAG_LOCALE.equals(parser.getName())) { + final TypedArray attributes = res.obtainAttributes( + attrs, com.android.internal.R.styleable.LocaleConfig_Locale); + String nameAttr = attributes.getString( + com.android.internal.R.styleable.LocaleConfig_Locale_name); + localeNames.add(nameAttr); + attributes.recycle(); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + mStatus = STATUS_SUCCESS; + mLocales = LocaleList.forLanguageTags(String.join(",", localeNames)); + } + + /** + * Returns the locales supported by the specified application. + * + *

Note: The locale format should follow the + * IETF BCP47 regular expression + * + * @return the {@link LocaleList} + */ + public @Nullable LocaleList getSupportedLocales() { + return mLocales; + } + + /** + * Get the status of reading the resource file where the LocaleConfig was stored. + * + *

Distinguish "the application didn't provide the resource file" from "the application + * provided malformed input" if {@link #getSupportedLocales()} returns {@code null}. + * + * @return {@code STATUS_SUCCESS} if the LocaleConfig structure existed in an XML file was + * successfully read, or {@code STATUS_NOT_SPECIFIED} if no android:localeConfig tag on + * pointing to an XML file that stores the LocaleConfig, or + * {@code STATUS_PARSING_FAILED} if the application provided malformed input for the + * LocaleConfig structure. + * + * @see #STATUS_SUCCESS + * @see #STATUS_NOT_SPECIFIED + * @see #STATUS_PARSING_FAILED + * + */ + public @Status int getStatus() { + return mStatus; + } +} diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 76b4e5c4dba0c..9e9dd1edd5777 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -1543,6 +1543,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { @Nullable private ArrayMap mAppClassNamesByProcess; + /** + * Resource file providing the application's locales configuration. + */ + private int localeConfigRes; + public void dump(Printer pw, String prefix) { dump(pw, prefix, DUMP_FLAG_ALL); } @@ -1660,6 +1665,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { pw.println(prefix + "requestRawExternalStorageAccess=" + requestRawExternalStorageAccess); } + if (localeConfigRes != 0) { + pw.println(prefix + "localeConfigRes=0x" + + Integer.toHexString(localeConfigRes)); + } } pw.println(prefix + "createTimestamp=" + createTimestamp); super.dumpBack(pw, prefix); @@ -1891,6 +1900,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { memtagMode = orig.memtagMode; nativeHeapZeroInitialized = orig.nativeHeapZeroInitialized; requestRawExternalStorageAccess = orig.requestRawExternalStorageAccess; + localeConfigRes = orig.localeConfigRes; createTimestamp = System.currentTimeMillis(); } @@ -1993,6 +2003,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeString(mAppClassNamesByProcess.valueAt(i)); } } + dest.writeInt(localeConfigRes); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR @@ -2088,6 +2099,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { mAppClassNamesByProcess.put(source.readString(), source.readString()); } } + localeConfigRes = source.readInt(); } /** @@ -2631,4 +2643,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } return className; } + + /** @hide */ public void setLocaleConfigRes(int value) { localeConfigRes = value; } + + /** + * Return the resource id pointing to the resource file that provides the application's locales + * configuration. + * + * @hide + */ + public int getLocaleConfigRes() { + return localeConfigRes; + } } diff --git a/core/java/android/content/pm/parsing/ParsingPackage.java b/core/java/android/content/pm/parsing/ParsingPackage.java index fc9f1a55d84c9..e635e9199cd60 100644 --- a/core/java/android/content/pm/parsing/ParsingPackage.java +++ b/core/java/android/content/pm/parsing/ParsingPackage.java @@ -375,6 +375,8 @@ public interface ParsingPackage extends ParsingPackageRead { ParsingPackage setResetEnabledSettingsOnAppDataCleared( boolean resetEnabledSettingsOnAppDataCleared); + ParsingPackage setLocaleConfigRes(int localeConfigRes); + // TODO(b/135203078): This class no longer has access to ParsedPackage, find a replacement // for moving to the next step @CallSuper diff --git a/core/java/android/content/pm/parsing/ParsingPackageImpl.java b/core/java/android/content/pm/parsing/ParsingPackageImpl.java index dbd3d5c4a7e79..5ecf3beb788ee 100644 --- a/core/java/android/content/pm/parsing/ParsingPackageImpl.java +++ b/core/java/android/content/pm/parsing/ParsingPackageImpl.java @@ -559,6 +559,8 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, private UUID mStorageUuid; private long mLongVersionCode; + private int mLocaleConfigRes; + @VisibleForTesting public ParsingPackageImpl(@NonNull String packageName, @NonNull String baseApkPath, @NonNull String path, @Nullable TypedArray manifestArray) { @@ -1136,6 +1138,7 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, appInfo.setSplitResourcePaths(splitCodePaths); appInfo.setVersionCode(mLongVersionCode); appInfo.setAppClassNamesByProcess(buildAppClassNamesByProcess()); + appInfo.setLocaleConfigRes(mLocaleConfigRes); return appInfo; } @@ -1314,6 +1317,7 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, dest.writeInt(this.memtagMode); dest.writeInt(this.nativeHeapZeroInitialized); sForBoolean.parcel(this.requestRawExternalStorageAccess, dest, flags); + dest.writeInt(this.mLocaleConfigRes); } public ParsingPackageImpl(Parcel in) { @@ -1461,6 +1465,7 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, this.memtagMode = in.readInt(); this.nativeHeapZeroInitialized = in.readInt(); this.requestRawExternalStorageAccess = sForBoolean.unparcel(in); + this.mLocaleConfigRes = in.readInt(); assignDerivedFields(); } @@ -2279,6 +2284,11 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, return nativeHeapZeroInitialized; } + @Override + public int getLocaleConfigRes() { + return mLocaleConfigRes; + } + @Nullable @Override public Boolean hasRequestRawExternalStorageAccess() { @@ -2936,4 +2946,10 @@ public class ParsingPackageImpl implements ParsingPackage, ParsingPackageHidden, resetEnabledSettingsOnAppDataCleared); return this; } + + @Override + public ParsingPackageImpl setLocaleConfigRes(int value) { + mLocaleConfigRes = value; + return this; + } } diff --git a/core/java/android/content/pm/parsing/ParsingPackageRead.java b/core/java/android/content/pm/parsing/ParsingPackageRead.java index c8113efcc7c1d..a5e98d633f136 100644 --- a/core/java/android/content/pm/parsing/ParsingPackageRead.java +++ b/core/java/android/content/pm/parsing/ParsingPackageRead.java @@ -343,4 +343,11 @@ public interface ParsingPackageRead extends PkgWithoutStateAppInfo, PkgWithoutSt * @see R.styleable#AndroidManifestApplication_resetEnabledSettingsOnAppDataCleared */ boolean isResetEnabledSettingsOnAppDataCleared(); + + /** + * The resource ID used to provide the application's locales configuration. + * + * @see R.styleable#AndroidManifestApplication_localeConfig + */ + int getLocaleConfigRes(); } diff --git a/core/java/android/content/pm/parsing/ParsingPackageUtils.java b/core/java/android/content/pm/parsing/ParsingPackageUtils.java index 16deaa05afcea..594575d8fe87a 100644 --- a/core/java/android/content/pm/parsing/ParsingPackageUtils.java +++ b/core/java/android/content/pm/parsing/ParsingPackageUtils.java @@ -2331,6 +2331,7 @@ public class ParsingPackageUtils { .setTheme(resId(R.styleable.AndroidManifestApplication_theme, sa)) .setDataExtractionRules( resId(R.styleable.AndroidManifestApplication_dataExtractionRules, sa)) + .setLocaleConfigRes(resId(R.styleable.AndroidManifestApplication_localeConfig, sa)) // Strings .setClassLoaderName(string(R.styleable.AndroidManifestApplication_classLoader, sa)) .setRequiredAccountType(string(R.styleable.AndroidManifestApplication_requiredAccountType, sa)) diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index bfc1c83e8fc59..311964953d53d 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -9645,4 +9645,11 @@ - + + + + + + + diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 06f347f4daff6..6b267032e77ec 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1519,6 +1519,9 @@ + + + @@ -1801,6 +1804,7 @@ + + diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index d7e3195765ef9..8cf6fe3d118d7 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -130,6 +130,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::getLabelRes, AndroidPackage::getLargestWidthLimitDp, AndroidPackage::getLogo, + AndroidPackage::getLocaleConfigRes, AndroidPackage::getManageSpaceActivityName, AndroidPackage::getMemtagMode, AndroidPackage::getMinSdkVersion,