[automerger skipped] DO NOT MERGE Track tzdb 2023d update. [SC_V2 CTS] am: 9c14af3c90 -s ours am: 25d9d020f2 -s ours am: 684fd74c35 -s ours am: d43668518d -s ours am: ff127c78f4 -s ours

am skip reason: contains skip directive

Original change: https://android-review.googlesource.com/c/platform/external/icu/+/2985516

Change-Id: Ic1b8e1e5c33fcd8f14b948cc904bdc2093dd0cda
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index d7fb36b..f1c850e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -53,7 +53,6 @@
     visibility: [
         "//external/icu:__subpackages__",
     ],
-    path: "NOTICE",
     srcs: ["NOTICE"],
 }
 
@@ -68,10 +67,6 @@
         "-Wno-unneeded-internal-declaration",
         "-Wno-deprecated-declarations",
     ],
-    // Upstream requires C++11 as the minimum version.
-    cppflags: [
-        "-std=c++11",
-    ],
     target: {
         android: {
             cflags: [
diff --git a/METADATA b/METADATA
index 5c3f89c..25aa1d6 100644
--- a/METADATA
+++ b/METADATA
@@ -1,3 +1,17 @@
+name: "ICU"
+description:
+    "International Components for Unicode"
+
 third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://icu.unicode.org/"
+  }
+  url {
+    type: GIT
+    value: "https://github.com/unicode-org/icu"
+  }
+  version: "72"
+  last_upgrade_date { year: 2022 month: 11 day: 1 }
   license_type: RECIPROCAL
 }
diff --git a/OWNERS b/OWNERS
index 18026a4..5737ec8 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,9 +1,17 @@
-icu-team+reviews@google.com
-mscherer@google.com
-roubert@google.com
-
-android-libcore-team+review@google.com
+# Default reviewers
 vichang@google.com
-nfuller@google.com
-nikitai@google.com
-ngeoffray@google.com
+mingaleev@google.com
+mscherer@google.com
+oth@google.com
+
+# Bug component: 24949
+# libcore team is the larger team owning the library.
+include platform/libcore:/OWNERS
+
+libcore-bugs-triage@google.com
+# g2.corp.android-icu-maintainers@google.com
+# android-libcore-team+review@google.com
+# icu-team+reviews@google.com
+
+# Others in the ICU team
+roubert@google.com
diff --git a/README.android b/README.android
index 5b15c78..c243801 100644
--- a/README.android
+++ b/README.android
@@ -30,3 +30,5 @@
                      libandroidicu and libjavacore to initialize ICU4C.
 
 tools/             - Code / data maintenance tools. See tools/README.android.
+
+libicu/            - NDK headers and the shim implementation in the libicu.so.
diff --git a/README.version b/README.version
index 70eadc7..3939e1b 100644
--- a/README.version
+++ b/README.version
@@ -1,3 +1,3 @@
 URL: https://github.com/unicode-org/icu
-Version: 70.1
+Version: 72.1
 BugComponent: 23970
diff --git a/android_icu4j/Android.bp b/android_icu4j/Android.bp
index d912c19..e0e2851 100644
--- a/android_icu4j/Android.bp
+++ b/android_icu4j/Android.bp
@@ -77,7 +77,7 @@
     name: "timezone-host",
     visibility: [
         "//packages/modules/RuntimeI18n/apex",
-        "//system/timezone/distro/core",
+        "//system/timezone/input_tools/version",
     ],
     srcs: [
         "libcore_bridge/src/java/com/android/i18n/timezone/TzDataSetVersion.java",
@@ -111,8 +111,14 @@
         javacflags: [
             "-Xep:MissingOverride:OFF", // Ignore missing @Override.
             "-Xep:ConstantOverflow:WARN", // Known constant overflow in SplittableRandom
+            "-Xep:BoxedPrimitiveEquality:WARN",
+            "-Xep:ComparableType:WARN",
+            "-Xep:EmptyTopLevelDeclaration:WARN",
         ],
     },
+    lint: {
+        warning_checks: ["SuspiciousIndentation"],
+    },
 }
 
 // A separated core library that contains ICU4J because ICU4J will be in a different APEX module,
@@ -192,6 +198,9 @@
             "-Xep:MissingOverride:OFF",
         ],
     },
+    lint: {
+        warning_checks: ["SuspiciousIndentation"],
+    },
 
     public: {
         enabled: true,
@@ -255,6 +264,10 @@
 
     // Don't copy any output files to the dist.
     no_dist: true,
+
+    lint: {
+        warning_checks: ["SuspiciousIndentation"],
+    },
 }
 
 // Referenced implicitly from i18n.module.intra.core.api.
@@ -367,6 +380,10 @@
 
     // Don't copy any output files to the dist.
     no_dist: true,
+
+    lint: {
+        warning_checks: ["SuspiciousIndentation"],
+    },
 }
 
 java_sdk_library {
@@ -397,6 +414,10 @@
 
     // Don't copy any output files to the dist.
     no_dist: true,
+
+    lint: {
+        warning_checks: ["SuspiciousIndentation"],
+    },
 }
 
 //==========================================================
@@ -434,6 +455,42 @@
     system_modules: "art-module-intra-core-api-stubs-system-modules",
 
     errorprone: {
-        javacflags: ["-Xep:EqualsNull:WARN"],
+        javacflags: [
+            "-Xep:EqualsNull:WARN",
+            "-Xep:ArrayToString:WARN",
+            "-Xep:SelfEquals:WARN",
+            "-Xep:SelfComparison:WARN",
+            "-Xep:ReturnValueIgnored:WARN",
+            "-Xep:IdentityBinaryExpression:WARN",
+            "-Xep:BoxedPrimitiveEquality:WARN",
+            "-Xep:ComparableType:WARN",
+        ],
     },
 }
+
+java_api_contribution {
+    name: "i18n-intra-core-stubs",
+    api_surface: "intra-core",
+    api_file: "api/intra/current.txt",
+    visibility: [
+        "//build/orchestrator/apis",
+    ],
+}
+
+java_api_contribution {
+    name: "i18n-public-stubs",
+    api_surface: "public",
+    api_file: "api/public/current.txt",
+    visibility: [
+        "//build/orchestrator/apis",
+    ],
+}
+
+java_api_contribution {
+    name: "i18n-module-lib-stubs",
+    api_surface: "module-lib",
+    api_file: "api/stable_platform/current.txt",
+    visibility: [
+        "//build/orchestrator/apis",
+    ],
+}
diff --git a/android_icu4j/api/legacy_platform/current.txt b/android_icu4j/api/legacy_platform/current.txt
index 337a712..ca3fafb 100644
--- a/android_icu4j/api/legacy_platform/current.txt
+++ b/android_icu4j/api/legacy_platform/current.txt
@@ -88,8 +88,6 @@
   }
 
   public final class TimeZoneDataFiles {
-    method public static String getDataTimeZoneFile(String);
-    method public static String getDataTimeZoneRootDir();
     method public static String getTimeZoneModuleTzVersionFile();
     method public static com.android.i18n.timezone.TzDataSetVersion readTimeZoneModuleVersion() throws java.io.IOException, com.android.i18n.timezone.TzDataSetVersion.TzDataSetException;
   }
diff --git a/android_icu4j/api/public/current.txt b/android_icu4j/api/public/current.txt
index 93473d7..fd2e6fe 100644
--- a/android_icu4j/api/public/current.txt
+++ b/android_icu4j/api/public/current.txt
@@ -50,6 +50,7 @@
     method public static double getUnicodeNumericValue(int);
     method public static android.icu.util.VersionInfo getUnicodeVersion();
     method public static boolean hasBinaryProperty(int, int);
+    method public static boolean hasBinaryProperty(CharSequence, int);
     method public static boolean isBMP(int);
     method public static boolean isBaseForm(int);
     method public static boolean isDefined(int);
@@ -468,6 +469,8 @@
     field public static final int ARABIC_EXTENDED_A_ID = 210; // 0xd2
     field public static final android.icu.lang.UCharacter.UnicodeBlock ARABIC_EXTENDED_B;
     field public static final int ARABIC_EXTENDED_B_ID = 309; // 0x135
+    field public static final android.icu.lang.UCharacter.UnicodeBlock ARABIC_EXTENDED_C;
+    field public static final int ARABIC_EXTENDED_C_ID = 321; // 0x141
     field public static final int ARABIC_ID = 12; // 0xc
     field public static final android.icu.lang.UCharacter.UnicodeBlock ARABIC_MATHEMATICAL_ALPHABETIC_SYMBOLS;
     field public static final int ARABIC_MATHEMATICAL_ALPHABETIC_SYMBOLS_ID = 211; // 0xd3
@@ -562,6 +565,8 @@
     field public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_ID = 274; // 0x112
     field public static final android.icu.lang.UCharacter.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_G;
     field public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_G_ID = 302; // 0x12e
+    field public static final android.icu.lang.UCharacter.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_H;
+    field public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_H_ID = 322; // 0x142
     field public static final int CJK_UNIFIED_IDEOGRAPHS_ID = 71; // 0x47
     field public static final android.icu.lang.UCharacter.UnicodeBlock COMBINING_DIACRITICAL_MARKS;
     field public static final android.icu.lang.UCharacter.UnicodeBlock COMBINING_DIACRITICAL_MARKS_EXTENDED;
@@ -600,6 +605,8 @@
     field public static final int CYRILLIC_EXTENDED_B_ID = 160; // 0xa0
     field public static final android.icu.lang.UCharacter.UnicodeBlock CYRILLIC_EXTENDED_C;
     field public static final int CYRILLIC_EXTENDED_C_ID = 265; // 0x109
+    field public static final android.icu.lang.UCharacter.UnicodeBlock CYRILLIC_EXTENDED_D;
+    field public static final int CYRILLIC_EXTENDED_D_ID = 323; // 0x143
     field public static final int CYRILLIC_ID = 9; // 0x9
     field public static final android.icu.lang.UCharacter.UnicodeBlock CYRILLIC_SUPPLEMENT;
     field public static final android.icu.lang.UCharacter.UnicodeBlock CYRILLIC_SUPPLEMENTARY;
@@ -609,6 +616,8 @@
     field public static final int DESERET_ID = 90; // 0x5a
     field public static final android.icu.lang.UCharacter.UnicodeBlock DEVANAGARI;
     field public static final android.icu.lang.UCharacter.UnicodeBlock DEVANAGARI_EXTENDED;
+    field public static final android.icu.lang.UCharacter.UnicodeBlock DEVANAGARI_EXTENDED_A;
+    field public static final int DEVANAGARI_EXTENDED_A_ID = 324; // 0x144
     field public static final int DEVANAGARI_EXTENDED_ID = 179; // 0xb3
     field public static final int DEVANAGARI_ID = 15; // 0xf
     field public static final android.icu.lang.UCharacter.UnicodeBlock DINGBATS;
@@ -727,6 +736,8 @@
     field public static final int JAVANESE_ID = 181; // 0xb5
     field public static final android.icu.lang.UCharacter.UnicodeBlock KAITHI;
     field public static final int KAITHI_ID = 193; // 0xc1
+    field public static final android.icu.lang.UCharacter.UnicodeBlock KAKTOVIK_NUMERALS;
+    field public static final int KAKTOVIK_NUMERALS_ID = 325; // 0x145
     field public static final android.icu.lang.UCharacter.UnicodeBlock KANA_EXTENDED_A;
     field public static final int KANA_EXTENDED_A_ID = 275; // 0x113
     field public static final android.icu.lang.UCharacter.UnicodeBlock KANA_EXTENDED_B;
@@ -743,6 +754,8 @@
     field public static final int KATAKANA_ID = 63; // 0x3f
     field public static final android.icu.lang.UCharacter.UnicodeBlock KATAKANA_PHONETIC_EXTENSIONS;
     field public static final int KATAKANA_PHONETIC_EXTENSIONS_ID = 107; // 0x6b
+    field public static final android.icu.lang.UCharacter.UnicodeBlock KAWI;
+    field public static final int KAWI_ID = 326; // 0x146
     field public static final android.icu.lang.UCharacter.UnicodeBlock KAYAH_LI;
     field public static final int KAYAH_LI_ID = 162; // 0xa2
     field public static final android.icu.lang.UCharacter.UnicodeBlock KHAROSHTHI;
@@ -869,6 +882,8 @@
     field public static final int MYANMAR_ID = 28; // 0x1c
     field public static final android.icu.lang.UCharacter.UnicodeBlock NABATAEAN;
     field public static final int NABATAEAN_ID = 239; // 0xef
+    field public static final android.icu.lang.UCharacter.UnicodeBlock NAG_MUNDARI;
+    field public static final int NAG_MUNDARI_ID = 327; // 0x147
     field public static final android.icu.lang.UCharacter.UnicodeBlock NANDINAGARI;
     field public static final int NANDINAGARI_ID = 294; // 0x126
     field public static final android.icu.lang.UCharacter.UnicodeBlock NEWA;
@@ -1224,6 +1239,7 @@
     field public static final int AGE = 16384; // 0x4000
     field public static final int ALPHABETIC = 0; // 0x0
     field public static final int ASCII_HEX_DIGIT = 1; // 0x1
+    field public static final int BASIC_EMOJI = 65; // 0x41
     field public static final int BIDI_CLASS = 4096; // 0x1000
     field public static final int BIDI_CONTROL = 2; // 0x2
     field public static final int BIDI_MIRRORED = 3; // 0x3
@@ -1252,6 +1268,7 @@
     field public static final int EAST_ASIAN_WIDTH = 4100; // 0x1004
     field public static final int EMOJI = 57; // 0x39
     field public static final int EMOJI_COMPONENT = 61; // 0x3d
+    field public static final int EMOJI_KEYCAP_SEQUENCE = 66; // 0x42
     field public static final int EMOJI_MODIFIER = 59; // 0x3b
     field public static final int EMOJI_MODIFIER_BASE = 60; // 0x3c
     field public static final int EMOJI_PRESENTATION = 58; // 0x3a
@@ -1309,6 +1326,11 @@
     field public static final int QUOTATION_MARK = 25; // 0x19
     field public static final int RADICAL = 26; // 0x1a
     field public static final int REGIONAL_INDICATOR = 62; // 0x3e
+    field public static final int RGI_EMOJI = 71; // 0x47
+    field public static final int RGI_EMOJI_FLAG_SEQUENCE = 68; // 0x44
+    field public static final int RGI_EMOJI_MODIFIER_SEQUENCE = 67; // 0x43
+    field public static final int RGI_EMOJI_TAG_SEQUENCE = 69; // 0x45
+    field public static final int RGI_EMOJI_ZWJ_SEQUENCE = 70; // 0x46
     field public static final int SCRIPT = 4106; // 0x100a
     field public static final int SCRIPT_EXTENSIONS = 28672; // 0x7000
     field public static final int SEGMENT_STARTER = 41; // 0x29
@@ -1431,6 +1453,7 @@
     field public static final int KANNADA = 21; // 0x15
     field public static final int KATAKANA = 22; // 0x16
     field public static final int KATAKANA_OR_HIRAGANA = 54; // 0x36
+    field public static final int KAWI = 198; // 0xc6
     field public static final int KAYAH_LI = 79; // 0x4f
     field public static final int KHAROSHTHI = 57; // 0x39
     field public static final int KHITAN_SMALL_SCRIPT = 191; // 0xbf
@@ -1477,6 +1500,7 @@
     field public static final int MULTANI = 164; // 0xa4
     field public static final int MYANMAR = 28; // 0x1c
     field public static final int NABATAEAN = 143; // 0x8f
+    field public static final int NAG_MUNDARI = 199; // 0xc7
     field public static final int NAKHI_GEBA = 132; // 0x84
     field public static final int NANDINAGARI = 187; // 0xbb
     field public static final int NEWA = 170; // 0xaa
@@ -1708,6 +1732,7 @@
   public abstract class FractionPrecision extends android.icu.number.Precision {
     method public android.icu.number.Precision withMaxDigits(int);
     method public android.icu.number.Precision withMinDigits(int);
+    method public android.icu.number.Precision withSignificantDigits(int, int, android.icu.number.NumberFormatter.RoundingPriority);
   }
 
   public class IntegerWidth {
@@ -1756,16 +1781,28 @@
     enum_constant public static final android.icu.number.NumberFormatter.GroupingStrategy THOUSANDS;
   }
 
+  public enum NumberFormatter.RoundingPriority {
+    enum_constant public static final android.icu.number.NumberFormatter.RoundingPriority RELAXED;
+    enum_constant public static final android.icu.number.NumberFormatter.RoundingPriority STRICT;
+  }
+
   public enum NumberFormatter.SignDisplay {
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay ACCOUNTING;
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay ACCOUNTING_ALWAYS;
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay ACCOUNTING_EXCEPT_ZERO;
+    enum_constant public static final android.icu.number.NumberFormatter.SignDisplay ACCOUNTING_NEGATIVE;
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay ALWAYS;
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay AUTO;
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay EXCEPT_ZERO;
+    enum_constant public static final android.icu.number.NumberFormatter.SignDisplay NEGATIVE;
     enum_constant public static final android.icu.number.NumberFormatter.SignDisplay NEVER;
   }
 
+  public enum NumberFormatter.TrailingZeroDisplay {
+    enum_constant public static final android.icu.number.NumberFormatter.TrailingZeroDisplay AUTO;
+    enum_constant public static final android.icu.number.NumberFormatter.TrailingZeroDisplay HIDE_IF_WHOLE;
+  }
+
   public enum NumberFormatter.UnitWidth {
     enum_constant public static final android.icu.number.NumberFormatter.UnitWidth FORMAL;
     enum_constant public static final android.icu.number.NumberFormatter.UnitWidth FULL_NAME;
@@ -1839,6 +1876,7 @@
     method public static android.icu.number.FractionPrecision minMaxFraction(int, int);
     method public static android.icu.number.Precision minMaxSignificantDigits(int, int);
     method public static android.icu.number.Precision minSignificantDigits(int);
+    method public android.icu.number.Precision trailingZeroDisplay(android.icu.number.NumberFormatter.TrailingZeroDisplay);
     method public static android.icu.number.Precision unlimited();
   }
 
@@ -3845,6 +3883,7 @@
     method public int getRangeCount();
     method public int getRangeEnd(int);
     method public int getRangeStart(int);
+    method public boolean hasStrings();
     method public int indexOf(int);
     method public boolean isEmpty();
     method public boolean isFrozen();
@@ -3909,6 +3948,7 @@
     method public boolean nextRange();
     method public void reset(android.icu.text.UnicodeSet);
     method public void reset();
+    method public android.icu.text.UnicodeSetIterator skipToStrings();
     field public static int IS_STRING;
     field public int codepoint;
     field public int codepointEnd;
@@ -4483,12 +4523,14 @@
     method public android.icu.util.MeasureUnit.Complexity getComplexity();
     method public int getDimensionality();
     method public String getIdentifier();
+    method public android.icu.util.MeasureUnit.MeasurePrefix getPrefix();
     method public String getSubtype();
     method public String getType();
     method public android.icu.util.MeasureUnit product(android.icu.util.MeasureUnit);
     method public android.icu.util.MeasureUnit reciprocal();
     method public java.util.List<android.icu.util.MeasureUnit> splitToSingleUnits();
     method public android.icu.util.MeasureUnit withDimensionality(int);
+    method public android.icu.util.MeasureUnit withPrefix(android.icu.util.MeasureUnit.MeasurePrefix);
     field public static final android.icu.util.MeasureUnit ACRE;
     field public static final android.icu.util.MeasureUnit ACRE_FOOT;
     field public static final android.icu.util.MeasureUnit AMPERE;
@@ -4547,6 +4589,7 @@
     field public static final android.icu.util.TimeUnit HOUR;
     field public static final android.icu.util.MeasureUnit INCH;
     field public static final android.icu.util.MeasureUnit INCH_HG;
+    field public static final android.icu.util.MeasureUnit ITEM;
     field public static final android.icu.util.MeasureUnit JOULE;
     field public static final android.icu.util.MeasureUnit KARAT;
     field public static final android.icu.util.MeasureUnit KELVIN;
@@ -4560,6 +4603,7 @@
     field public static final android.icu.util.MeasureUnit KILOMETER_PER_HOUR;
     field public static final android.icu.util.MeasureUnit KILOWATT;
     field public static final android.icu.util.MeasureUnit KILOWATT_HOUR;
+    field public static final android.icu.util.MeasureUnit KILOWATT_HOUR_PER_100_KILOMETER;
     field public static final android.icu.util.MeasureUnit KNOT;
     field public static final android.icu.util.MeasureUnit LIGHT_YEAR;
     field public static final android.icu.util.MeasureUnit LITER;
@@ -4588,6 +4632,7 @@
     field public static final android.icu.util.MeasureUnit MILLIAMPERE;
     field public static final android.icu.util.MeasureUnit MILLIBAR;
     field public static final android.icu.util.MeasureUnit MILLIGRAM;
+    field public static final android.icu.util.MeasureUnit MILLIGRAM_OFGLUCOSE_PER_DECILITER;
     field public static final android.icu.util.MeasureUnit MILLIGRAM_PER_DECILITER;
     field public static final android.icu.util.MeasureUnit MILLILITER;
     field public static final android.icu.util.MeasureUnit MILLIMETER;
@@ -4647,6 +4692,40 @@
     enum_constant public static final android.icu.util.MeasureUnit.Complexity SINGLE;
   }
 
+  public enum MeasureUnit.MeasurePrefix {
+    method public int getBase();
+    method public int getPower();
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix ATTO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix CENTI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix DECI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix DEKA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix EXA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix EXBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix FEMTO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix GIBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix GIGA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix HECTO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix KIBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix KILO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix MEBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix MEGA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix MICRO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix MILLI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix NANO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix ONE;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix PEBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix PETA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix PICO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix TEBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix TERA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix YOBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix YOCTO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix YOTTA;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix ZEBI;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix ZEPTO;
+    enum_constant public static final android.icu.util.MeasureUnit.MeasurePrefix ZETTA;
+  }
+
   public class Output<T> {
     ctor public Output();
     ctor public Output(T);
@@ -4955,6 +5034,7 @@
     field public static final android.icu.util.VersionInfo UNICODE_12_1;
     field public static final android.icu.util.VersionInfo UNICODE_13_0;
     field public static final android.icu.util.VersionInfo UNICODE_14_0;
+    field public static final android.icu.util.VersionInfo UNICODE_15_0;
     field public static final android.icu.util.VersionInfo UNICODE_1_0;
     field public static final android.icu.util.VersionInfo UNICODE_1_0_1;
     field public static final android.icu.util.VersionInfo UNICODE_1_1_0;
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java
index 1dea86a..6f3b16e 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java
@@ -32,7 +32,6 @@
 public final class TimeZoneDataFiles {
     private static final String ANDROID_ROOT_ENV = AndroidDataFiles.ANDROID_ROOT_ENV;
     private static final String ANDROID_TZDATA_ROOT_ENV = AndroidDataFiles.ANDROID_TZDATA_ROOT_ENV;
-    private static final String ANDROID_DATA_ENV = AndroidDataFiles.ANDROID_DATA_ENV;
 
     private TimeZoneDataFiles() {}
 
@@ -40,28 +39,12 @@
      * Returns time zone file paths for the specified file name in an array in the order they
      * should be tried. See {@link AndroidDataFiles#generateIcuDataPath()} for ICU files instead.
      * <ul>
-     * <li>[0] - the location of the file in the /data partition (may not exist).</li>
-     * <li>[1] - the location of the file from the time zone module under /apex (must exist).</li>
+     * <li>[0] - the location of the file from the time zone module under /apex (must exist).</li>
      * </ul>
      */
     // VisibleForTesting
     public static String[] getTimeZoneFilePaths(String fileName) {
-        return new String[] {
-                getDataTimeZoneFile(fileName),
-                getTimeZoneModuleTzFile(fileName),
-        };
-    }
-
-    // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
-    @libcore.api.CorePlatformApi
-    public static String getDataTimeZoneRootDir() {
-        return System.getenv(ANDROID_DATA_ENV) + "/misc/zoneinfo/";
-    }
-
-    // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
-    @libcore.api.CorePlatformApi
-    public static String getDataTimeZoneFile(String fileName) {
-        return getDataTimeZoneRootDir() + "current/" + fileName;
+        return new String[] { getTimeZoneModuleTzFile(fileName) };
     }
 
     public static String getTimeZoneModuleTzFile(String fileName) {
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TzDataSetVersion.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TzDataSetVersion.java
index 5af6d45..4c07f84 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TzDataSetVersion.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TzDataSetVersion.java
@@ -61,7 +61,7 @@
      * version to 1 when doing so.
      */
     // @VisibleForTesting : Keep this inline-able: it is used from CTS tests.
-    public static final int CURRENT_FORMAT_MAJOR_VERSION = 6; // Android T
+    public static final int CURRENT_FORMAT_MAJOR_VERSION = 7; // Android U
 
     /**
      * Returns the major tz data format version supported by this device.
diff --git a/android_icu4j/resources/android/icu/ICUConfig.properties b/android_icu4j/resources/android/icu/ICUConfig.properties
index 9d26445..ae97732 100644
--- a/android_icu4j/resources/android/icu/ICUConfig.properties
+++ b/android_icu4j/resources/android/icu/ICUConfig.properties
@@ -63,3 +63,10 @@
 # LocaleDisplayNames implementation class
 # @internal
 # android.icu.text.LocaleDisplayNames.impl = com.ibm.icu.impl.LocaleDisplayNamesImpl
+
+#
+# [Internal Use Only]
+# Enable ML phrase breaking
+# Android patch, http://b/219529457, for ML-based phrase line breaking
+# @internal
+android.icu.impl.breakiter.useMLPhraseBreaking = false
diff --git a/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java b/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
index 7925b3c..24c5487 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
@@ -75,6 +75,10 @@
             cpStart=cpLimit=limit;
         }
 
+        public void moveTo(int i) {
+            cpStart=cpLimit=i;
+        }
+
         /**
          * Iterate forward through the string to fetch the next code point
          * to be case-mapped, and set the context indexes for it.
@@ -194,6 +198,13 @@
         return options | newOption;
     }
 
+    private static final char ACUTE = '\u0301';
+
+    private static final int U_GC_M_MASK =
+            (1 << UCharacterCategory.NON_SPACING_MARK) |
+            (1 << UCharacterCategory.COMBINING_SPACING_MARK) |
+            (1 << UCharacterCategory.ENCLOSING_MARK);
+
     private static final int LNS =
             (1 << UCharacterCategory.UPPERCASE_LETTER) |
             (1 << UCharacterCategory.LOWERCASE_LETTER) |
@@ -731,34 +742,25 @@
                     }
 
                     if(titleStart<index) {
-                        int titleLimit=iter.getCPLimit();
                         // titlecase c which is from [titleStart..titleLimit[
                         c = UCaseProps.INSTANCE.toFullTitle(c, iter, dest, caseLocale);
                         appendResult(c, dest, iter.getCPLength(), options, edits);
 
                         // Special case Dutch IJ titlecasing
+                        int titleLimit;
                         if (titleStart+1 < index && caseLocale == UCaseProps.LOC_DUTCH) {
-                            char c1 = src.charAt(titleStart);
-                            if ((c1 == 'i' || c1 == 'I')) {
-                                char c2 = src.charAt(titleStart+1);
-                                if (c2 == 'j') {
-                                    dest.append('J');
-                                    if (edits != null) {
-                                        edits.addReplace(1, 1);
-                                    }
-                                    c = iter.nextCaseMapCP();
-                                    titleLimit++;
-                                    assert c == c2;
-                                    assert titleLimit == iter.getCPLimit();
-                                } else if (c2 == 'J') {
-                                    // Keep the capital J from getting lowercased.
-                                    appendUnchanged(src, titleStart + 1, 1, dest, options, edits);
-                                    c = iter.nextCaseMapCP();
-                                    titleLimit++;
-                                    assert c == c2;
-                                    assert titleLimit == iter.getCPLimit();
-                                }
+                            if (c < 0) {
+                                c = ~c;
                             }
+                            if (c == 'I' || c == 'Í') {
+                                titleLimit = maybeTitleDutchIJ(src, c, titleStart + 1, index, dest, options, edits);
+                                iter.moveTo(titleLimit);
+                            }
+                            else {
+                                titleLimit = iter.getCPLimit();
+                            }
+                        } else {
+                            titleLimit = iter.getCPLimit();
                         }
 
                         // lowercase [titleLimit..index[
@@ -784,6 +786,84 @@
         }
     }
 
+    /**
+     * Input: c is a letter I with or without acute accent.
+     * start is the index in src after c, and is less than segmentLimit.
+     * If a plain i/I is followed by a plain j/J,
+     * or an i/I with acute (precomposed or decomposed) is followed by a j/J with acute,
+     * then we output accordingly.
+     *
+     * @return the src index after the titlecased sequence, or the start index if no Dutch IJ
+     * @throws IOException
+     */
+    private static <A extends Appendable> int maybeTitleDutchIJ(
+            CharSequence src, int c, int start, int segmentLimit,
+            A dest, int options, Edits edits) throws IOException {
+        assert start < segmentLimit;
+
+        int index = start;
+        boolean withAcute = false;
+
+        // If the conditions are met, then the following variables tell us what to output.
+        int unchanged1 = 0;  // code units before the j, or the whole sequence (0..3)
+        boolean doTitleJ = false;  // true if the j needs to be titlecased
+        int unchanged2 = 0;  // after the j (0 or 1)
+
+        // next character after the first letter
+        char c2 = src.charAt(index++);
+
+        // Is the first letter an i/I with accent?
+        if (c == 'I') {
+            if (c2 == ACUTE) {
+                withAcute = true;
+                unchanged1 = 1;
+                if (index == segmentLimit) { return start; }
+                c2 = src.charAt(index++);
+            }
+        } else {  // Í
+            withAcute = true;
+        }
+        // Is the next character a j/J?
+        if (c2 == 'j') {
+            doTitleJ = true;
+        } else if (c2 == 'J') {
+            ++unchanged1;
+        } else {
+            return start;
+        }
+        // A plain i/I must be followed by a plain j/J.
+        // An i/I with acute must be followed by a j/J with acute.
+        if (withAcute) {
+            if (index == segmentLimit || src.charAt(index++) != ACUTE) { return start; }
+            if (doTitleJ) {
+                unchanged2 = 1;
+            } else {
+                ++unchanged1;
+            }
+        }
+        // There must not be another combining mark.
+        if (index < segmentLimit) {
+            int cp = Character.codePointAt(src, index);
+            int bit = 1 << UCharacter.getType(cp);
+            if ((bit & U_GC_M_MASK) != 0) {
+                return start;
+            }
+        }
+        // Output the rest of the Dutch IJ.
+        appendUnchanged(src, start, unchanged1, dest, options, edits);
+        start += unchanged1;
+        if (doTitleJ) {
+            dest.append('J');
+            if (edits != null) {
+                edits.addReplace(1, 1);
+            }
+            ++start;
+        }
+        appendUnchanged(src, start, unchanged2, dest, options, edits);
+        assert start + unchanged2 == index;
+        return index;
+    }
+
     public static String fold(int options, CharSequence src) {
         if (src.length() <= 100 && (options & OMIT_UNCHANGED_TEXT) == 0) {
             if (src.length() == 0) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUConfig.java b/android_icu4j/src/main/java/android/icu/impl/ICUConfig.java
index 5e47011..7fa7ff4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUConfig.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUConfig.java
@@ -78,7 +78,7 @@
             val = System.getProperty(name);
         }
 
-        if (val == null) {
+        if (val == null || val.equals("")) {
             val = CONFIG_PROPS.getProperty(name, def);
         }
         return val;
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java b/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
index db3af94..8113915 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
@@ -17,6 +17,7 @@
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.EnumMap;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -430,7 +431,7 @@
         }
         UResource.Key key = new UResource.Key();
         ReaderValue readerValue = new ReaderValue();
-        rb.getAllItemsWithFallback(key, readerValue, sink);
+        rb.getAllItemsWithFallback(key, readerValue, sink, this);
     }
 
     /**
@@ -475,7 +476,7 @@
     }
 
     private void getAllItemsWithFallback(
-            UResource.Key key, ReaderValue readerValue, UResource.Sink sink) {
+            UResource.Key key, ReaderValue readerValue, UResource.Sink sink, UResourceBundle requested) {
         // We recursively enumerate child-first,
         // only storing parent items in the absence of child items.
         // The sink needs to store a placeholder value for the no-fallback/no-inheritance marker
@@ -504,10 +505,10 @@
                 // if we had followed an alias.
                 String[] pathKeys = new String[depth];
                 getResPathKeys(pathKeys, depth);
-                rb = findResourceWithFallback(pathKeys, 0, parentBundle, null);
+                rb = findResourceWithFallback(pathKeys, 0, parentBundle, requested);
             }
             if (rb != null) {
-                rb.getAllItemsWithFallback(key, readerValue, sink);
+                rb.getAllItemsWithFallback(key, readerValue, sink, requested);
             }
         }
     }
@@ -1252,10 +1253,10 @@
         localeID = ULocale.getBaseName(localeID);
         ICUResourceBundle b;
         if (openType == OpenType.LOCALE_DEFAULT_ROOT) {
-            b = instantiateBundle(baseName, localeID, ULocale.getDefault().getBaseName(),
+            b = instantiateBundle(baseName, localeID, null, ULocale.getDefault().getBaseName(),
                     root, openType);
         } else {
-            b = instantiateBundle(baseName, localeID, null, root, openType);
+            b = instantiateBundle(baseName, localeID, null, null, root, openType);
         }
         if(b==null){
             throw new MissingResourceException(
@@ -1269,8 +1270,99 @@
                 (localeID.length() == lang.length() || localeID.charAt(lang.length()) == '_');
     }
 
+    private static final Comparator<String[]> COMPARE_FIRST_ELEMENT = new Comparator<String[]>() {
+        @Override
+        public int compare(String[] pair1, String[] pair2) {
+            return pair1[0].compareTo(pair2[0]);
+        }
+    };
+
+    private static String getExplicitParent(String localeID) {
+        return LocaleFallbackData.PARENT_LOCALE_TABLE.get(localeID);
+    }
+
+    private static String getDefaultScript(String language, String region) {
+        String localeID = language + "_" + region;
+        String result = LocaleFallbackData.DEFAULT_SCRIPT_TABLE.get(localeID);
+        if (result == null) {
+            result = LocaleFallbackData.DEFAULT_SCRIPT_TABLE.get(language);
+        }
+        if (result == null) {
+            result = "Latn";
+        }
+        return result;
+    }
+
+    private static String getParentLocaleID(String name, String origName, OpenType openType) {
+        // early out if the locale ID has a variant code or ends with _
+        if (name.endsWith("_") || !ULocale.getVariant(name).isEmpty()) {
+            int lastUnderbarPos = name.lastIndexOf('_');
+            if (lastUnderbarPos >= 0) {
+                return name.substring(0, lastUnderbarPos);
+            } else {
+                return null;
+            }
+        }
+
+        // TODO: Is there a better way to break the locale ID up into its consituent parts?
+        ULocale nameLocale = new ULocale(name);
+        String language = nameLocale.getLanguage();
+        String script = nameLocale.getScript();
+        String region = nameLocale.getCountry();
+
+        // if our open type is LOCALE_DEFAULT_ROOT, first look the locale ID up in the parent locale table; if that
+        // table specifies a parent for it, return that (we don't do this for the other open types-- if we're not
+        // falling back through the system default locale, we also want to do straight truncation fallback instead
+        // of looking things up in the parent locale table-- see https://www.unicode.org/reports/tr35/tr35.html#Parent_Locales:
+        // "Collation data, however, is an exception...")
+        if (openType == OpenType.LOCALE_DEFAULT_ROOT) {
+            String parentID = getExplicitParent(name);
+            if (parentID != null) {
+                return parentID.equals("root") ? null : parentID;
+            }
+        }
+
+        // if it's not in the parent locale table, figure out the fallback script algorithmically
+        // (see CLDR-15265 for an explanation of the algorithm)
+        if (!script.isEmpty() && !region.isEmpty()) {
+            // if "name" has both script and region, is the script the default script?
+            // - if so, remove it and keep the region
+            // - if not, remove the region and keep the script
+            if (getDefaultScript(language, region).equals(script)) {
+                return language + "_" + region;
+            } else {
+                return language + "_" + script;
+            }
+        } else if (!region.isEmpty()) {
+            // if "name" has region but not script, did the original locale ID specify a script?
+            // - if yes, replace the region with the script from the original locale ID
+            // - if no, replace the region with the default script for that language and region
+            String origNameScript = ULocale.getScript(origName);
+            if (!origNameScript.isEmpty()) {
+                return language + "_" + origNameScript;
+            } else {
+                return language + "_" + getDefaultScript(language, region);
+            }
+        } else if (!script.isEmpty()) {
+            // if "name" has script but not region (and our open type is LOCALE_DEFAULT_ROOT), is the script the
+            // default script for the language?
+            // - if so, remove it from the locale ID
+            // - if not, return "root" (bypassing the system default locale ID)
+            // (we don't do this for other open types for the same reason we don't look things up in the parent
+            // locale table for other open types-- see the reference to UTS #35 above)
+            if (openType != OpenType.LOCALE_DEFAULT_ROOT || getDefaultScript(language, null).equals(script)) {
+                return language;
+            } else {
+                return /*"root"*/null;
+            }
+        } else {
+            // if "name" just contains a language code, return null so the calling code falls back to "root"
+            return null;
+        }
+    }
+
     private static ICUResourceBundle instantiateBundle(
-            final String baseName, final String localeID, final String defaultID,
+            final String baseName, final String localeID, final String origLocaleID, final String defaultID,
             final ClassLoader root, final OpenType openType) {
         assert localeID.indexOf('@') < 0;
         assert defaultID == null || defaultID.indexOf('@') < 0;
@@ -1316,17 +1408,15 @@
                 if (openType == OpenType.LOCALE_DEFAULT_ROOT && localeName.equals(defaultID)) {
                     localOpenType = OpenType.LOCALE_ROOT;
                 }
-                int i = localeName.lastIndexOf('_');
-                if (i != -1) {
-                    // Chop off the last underscore and the subtag after that.
-                    String temp = localeName.substring(0, i);
-                    b = instantiateBundle(baseName, temp, defaultID, root, localOpenType);
+                String origLocaleName = (origLocaleID != null) ? origLocaleID : localeName;
+                String fallbackLocaleID = getParentLocaleID(localeName, origLocaleName, openType);
+                if (fallbackLocaleID != null) {
+                    b = instantiateBundle(baseName, fallbackLocaleID, origLocaleName, defaultID, root, localOpenType);
                 }else{
-                    // No underscore, only a base language subtag.
                     if(localOpenType == OpenType.LOCALE_DEFAULT_ROOT &&
                             !localeIDStartsWithLangSubtag(defaultID, localeName)) {
                         // Go to the default locale before root.
-                        b = instantiateBundle(baseName, defaultID, defaultID, root, localOpenType);
+                        b = instantiateBundle(baseName, defaultID, null, defaultID, root, localOpenType);
                     } else if(localOpenType != OpenType.LOCALE_ONLY && !rootLocale.isEmpty()) {
                         // Ultimately go to root.
                         b = ICUResourceBundle.createBundle(baseName, rootLocale, root);
@@ -1340,11 +1430,11 @@
                 // TODO: C++ uresbund.cpp also checks for %%ParentIsRoot. Why not Java?
                 String parentLocaleName = ((ICUResourceBundleImpl.ResourceTable)b).findString("%%Parent");
                 if (parentLocaleName != null) {
-                    parent = instantiateBundle(baseName, parentLocaleName, defaultID, root, openType);
+                    parent = instantiateBundle(baseName, parentLocaleName, null, defaultID, root, openType);
                 } else if (i != -1) {
-                    parent = instantiateBundle(baseName, localeName.substring(0, i), defaultID, root, openType);
+                    parent = instantiateBundle(baseName, localeName.substring(0, i), null, defaultID, root, openType);
                 } else if (!localeName.equals(rootLocale)){
-                    parent = instantiateBundle(baseName, rootLocale, defaultID, root, openType);
+                    parent = instantiateBundle(baseName, rootLocale, null, defaultID, root, openType);
                 }
 
                 if (!b.equals(parent)){
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundleReader.java b/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundleReader.java
index ce7b1c7..daadfeb 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundleReader.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundleReader.java
@@ -15,6 +15,7 @@
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.IntBuffer;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 
 import android.icu.util.ICUException;
@@ -446,13 +447,12 @@
     }
 
     private static String makeKeyStringFromBytes(byte[] keyBytes, int keyOffset) {
-        StringBuilder sb = new StringBuilder();
-        byte b;
-        while((b = keyBytes[keyOffset]) != 0) {
-            ++keyOffset;
-            sb.append((char)b);
+        int end = keyOffset;
+        while(keyBytes[end] != 0) {
+            ++end;
         }
-        return sb.toString();
+        int len = end - keyOffset;
+        return new String(keyBytes, keyOffset, len, StandardCharsets.ISO_8859_1);
     }
     private String getKey16String(int keyOffset) {
         if(keyOffset < localKeyLimit) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java b/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java
new file mode 100644
index 0000000..6ed1cad
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java
@@ -0,0 +1,576 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+//
+// Internal static data tables used by ICUResourceBundle.java
+// WARNING: This file is mechanically generated by the CLDR-to-ICU tool
+// (see tools/cldr/cldr-to-icu/src/main/java/org/unicode/tool/cldrtoicu/generator/ResourcFallbackCodeGenerator.java).
+// DO NOT HAND EDIT!!!
+
+package android.icu.impl;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class LocaleFallbackData {
+    //======================================================================
+    // Default script table
+    public static final Map<String, String> DEFAULT_SCRIPT_TABLE = buildDefaultScriptTable();
+
+    private static Map<String, String> buildDefaultScriptTable() {
+        Map<String, String> t = new HashMap<>();
+        t.put("ab", "Cyrl");
+        t.put("abq", "Cyrl");
+        t.put("adp", "Tibt");
+        t.put("ady", "Cyrl");
+        t.put("ae", "Avst");
+        t.put("aeb", "Arab");
+        t.put("aho", "Ahom");
+        t.put("ajt", "Arab");
+        t.put("akk", "Xsux");
+        t.put("alt", "Cyrl");
+        t.put("am", "Ethi");
+        t.put("apc", "Arab");
+        t.put("apd", "Arab");
+        t.put("ar", "Arab");
+        t.put("arc", "Armi");
+        t.put("arq", "Arab");
+        t.put("ars", "Arab");
+        t.put("ary", "Arab");
+        t.put("arz", "Arab");
+        t.put("as", "Beng");
+        t.put("ase", "Sgnw");
+        t.put("av", "Cyrl");
+        t.put("avl", "Arab");
+        t.put("awa", "Deva");
+        t.put("az_IQ", "Arab");
+        t.put("az_IR", "Arab");
+        t.put("az_RU", "Cyrl");
+        t.put("ba", "Cyrl");
+        t.put("bal", "Arab");
+        t.put("bap", "Deva");
+        t.put("bax", "Bamu");
+        t.put("bcq", "Ethi");
+        t.put("be", "Cyrl");
+        t.put("bej", "Arab");
+        t.put("bfq", "Taml");
+        t.put("bft", "Arab");
+        t.put("bfy", "Deva");
+        t.put("bg", "Cyrl");
+        t.put("bgc", "Deva");
+        t.put("bgn", "Arab");
+        t.put("bgx", "Grek");
+        t.put("bhb", "Deva");
+        t.put("bhi", "Deva");
+        t.put("bho", "Deva");
+        t.put("bji", "Ethi");
+        t.put("bjj", "Deva");
+        t.put("blt", "Tavt");
+        t.put("bn", "Beng");
+        t.put("bo", "Tibt");
+        t.put("bpy", "Beng");
+        t.put("bqi", "Arab");
+        t.put("bra", "Deva");
+        t.put("brh", "Arab");
+        t.put("brx", "Deva");
+        t.put("bsq", "Bass");
+        t.put("bst", "Ethi");
+        t.put("btv", "Deva");
+        t.put("bua", "Cyrl");
+        t.put("byn", "Ethi");
+        t.put("ccp", "Cakm");
+        t.put("ce", "Cyrl");
+        t.put("chm", "Cyrl");
+        t.put("chr", "Cher");
+        t.put("cja", "Arab");
+        t.put("cjm", "Cham");
+        t.put("ckb", "Arab");
+        t.put("cmg", "Soyo");
+        t.put("cop", "Copt");
+        t.put("cr", "Cans");
+        t.put("crh", "Cyrl");
+        t.put("crk", "Cans");
+        t.put("crl", "Cans");
+        t.put("csw", "Cans");
+        t.put("ctd", "Pauc");
+        t.put("cu", "Cyrl");
+        t.put("cv", "Cyrl");
+        t.put("dar", "Cyrl");
+        t.put("dcc", "Arab");
+        t.put("dgl", "Arab");
+        t.put("dmf", "Medf");
+        t.put("doi", "Deva");
+        t.put("drh", "Mong");
+        t.put("drs", "Ethi");
+        t.put("dty", "Deva");
+        t.put("dv", "Thaa");
+        t.put("dz", "Tibt");
+        t.put("egy", "Egyp");
+        t.put("eky", "Kali");
+        t.put("el", "Grek");
+        t.put("esg", "Gonm");
+        t.put("ett", "Ital");
+        t.put("fa", "Arab");
+        t.put("fia", "Arab");
+        t.put("fub", "Arab");
+        t.put("gan", "Hans");
+        t.put("gbm", "Deva");
+        t.put("gbz", "Arab");
+        t.put("gez", "Ethi");
+        t.put("ggn", "Deva");
+        t.put("gjk", "Arab");
+        t.put("gju", "Arab");
+        t.put("glk", "Arab");
+        t.put("gmv", "Ethi");
+        t.put("gof", "Ethi");
+        t.put("gom", "Deva");
+        t.put("gon", "Telu");
+        t.put("got", "Goth");
+        t.put("grc", "Cprt");
+        t.put("grt", "Beng");
+        t.put("gu", "Gujr");
+        t.put("gvr", "Deva");
+        t.put("gwc", "Arab");
+        t.put("gwt", "Arab");
+        t.put("ha_CM", "Arab");
+        t.put("ha_SD", "Arab");
+        t.put("hak", "Hans");
+        t.put("haz", "Arab");
+        t.put("hdy", "Ethi");
+        t.put("he", "Hebr");
+        t.put("hi", "Deva");
+        t.put("hlu", "Hluw");
+        t.put("hmd", "Plrd");
+        t.put("hnd", "Arab");
+        t.put("hne", "Deva");
+        t.put("hnj", "Hmnp");
+        t.put("hno", "Arab");
+        t.put("hoc", "Deva");
+        t.put("hoj", "Deva");
+        t.put("hsn", "Hans");
+        t.put("hy", "Armn");
+        t.put("ii", "Yiii");
+        t.put("inh", "Cyrl");
+        t.put("iu", "Cans");
+        t.put("iw", "Hebr");
+        t.put("ja", "Jpan");
+        t.put("ji", "Hebr");
+        t.put("jml", "Deva");
+        t.put("ka", "Geor");
+        t.put("kaa", "Cyrl");
+        t.put("kaw", "Kawi");
+        t.put("kbd", "Cyrl");
+        t.put("kby", "Arab");
+        t.put("kdt", "Thai");
+        t.put("kfr", "Deva");
+        t.put("kfy", "Deva");
+        t.put("khb", "Talu");
+        t.put("khn", "Deva");
+        t.put("kht", "Mymr");
+        t.put("khw", "Arab");
+        t.put("kjg", "Laoo");
+        t.put("kk", "Cyrl");
+        t.put("kk_AF", "Arab");
+        t.put("kk_CN", "Arab");
+        t.put("kk_IR", "Arab");
+        t.put("kk_MN", "Arab");
+        t.put("km", "Khmr");
+        t.put("kn", "Knda");
+        t.put("ko", "Kore");
+        t.put("koi", "Cyrl");
+        t.put("kok", "Deva");
+        t.put("kqy", "Ethi");
+        t.put("krc", "Cyrl");
+        t.put("kru", "Deva");
+        t.put("ks", "Arab");
+        t.put("ktb", "Ethi");
+        t.put("ku_LB", "Arab");
+        t.put("kum", "Cyrl");
+        t.put("kv", "Cyrl");
+        t.put("kvx", "Arab");
+        t.put("kxc", "Ethi");
+        t.put("kxl", "Deva");
+        t.put("kxm", "Thai");
+        t.put("kxp", "Arab");
+        t.put("ky", "Cyrl");
+        t.put("ky_CN", "Arab");
+        t.put("kzh", "Arab");
+        t.put("lab", "Lina");
+        t.put("lad", "Hebr");
+        t.put("lah", "Arab");
+        t.put("lbe", "Cyrl");
+        t.put("lcp", "Thai");
+        t.put("lep", "Lepc");
+        t.put("lez", "Cyrl");
+        t.put("lif", "Deva");
+        t.put("lis", "Lisu");
+        t.put("lki", "Arab");
+        t.put("lmn", "Telu");
+        t.put("lo", "Laoo");
+        t.put("lrc", "Arab");
+        t.put("luz", "Arab");
+        t.put("lwl", "Thai");
+        t.put("lzh", "Hans");
+        t.put("mag", "Deva");
+        t.put("mai", "Deva");
+        t.put("man_GN", "Nkoo");
+        t.put("mde", "Arab");
+        t.put("mdf", "Cyrl");
+        t.put("mdx", "Ethi");
+        t.put("mfa", "Arab");
+        t.put("mgp", "Deva");
+        t.put("mk", "Cyrl");
+        t.put("mki", "Arab");
+        t.put("ml", "Mlym");
+        t.put("mn", "Cyrl");
+        t.put("mn_CN", "Mong");
+        t.put("mni", "Beng");
+        t.put("mnw", "Mymr");
+        t.put("mr", "Deva");
+        t.put("mrd", "Deva");
+        t.put("mrj", "Cyrl");
+        t.put("mro", "Mroo");
+        t.put("ms_CC", "Arab");
+        t.put("mtr", "Deva");
+        t.put("mvy", "Arab");
+        t.put("mwr", "Deva");
+        t.put("mww", "Hmnp");
+        t.put("my", "Mymr");
+        t.put("mym", "Ethi");
+        t.put("myv", "Cyrl");
+        t.put("myz", "Mand");
+        t.put("mzn", "Arab");
+        t.put("nan", "Hans");
+        t.put("ne", "Deva");
+        t.put("new", "Deva");
+        t.put("nnp", "Wcho");
+        t.put("nod", "Lana");
+        t.put("noe", "Deva");
+        t.put("non", "Runr");
+        t.put("nqo", "Nkoo");
+        t.put("nsk", "Cans");
+        t.put("nst", "Tnsa");
+        t.put("oj", "Cans");
+        t.put("ojs", "Cans");
+        t.put("or", "Orya");
+        t.put("oru", "Arab");
+        t.put("os", "Cyrl");
+        t.put("osa", "Osge");
+        t.put("ota", "Arab");
+        t.put("otk", "Orkh");
+        t.put("oui", "Ougr");
+        t.put("pa", "Guru");
+        t.put("pa_PK", "Arab");
+        t.put("pal", "Phli");
+        t.put("peo", "Xpeo");
+        t.put("phl", "Arab");
+        t.put("phn", "Phnx");
+        t.put("pka", "Brah");
+        t.put("pnt", "Grek");
+        t.put("ppa", "Deva");
+        t.put("pra", "Khar");
+        t.put("prd", "Arab");
+        t.put("ps", "Arab");
+        t.put("raj", "Deva");
+        t.put("rhg", "Rohg");
+        t.put("rif", "Tfng");
+        t.put("rjs", "Deva");
+        t.put("rkt", "Beng");
+        t.put("rmt", "Arab");
+        t.put("ru", "Cyrl");
+        t.put("rue", "Cyrl");
+        t.put("ryu", "Kana");
+        t.put("sa", "Deva");
+        t.put("sah", "Cyrl");
+        t.put("sat", "Olck");
+        t.put("saz", "Saur");
+        t.put("sck", "Deva");
+        t.put("scl", "Arab");
+        t.put("sd", "Arab");
+        t.put("sd_IN", "Deva");
+        t.put("sdh", "Arab");
+        t.put("sga", "Ogam");
+        t.put("sgw", "Ethi");
+        t.put("shi", "Tfng");
+        t.put("shn", "Mymr");
+        t.put("shu", "Arab");
+        t.put("si", "Sinh");
+        t.put("skr", "Arab");
+        t.put("smp", "Samr");
+        t.put("sog", "Sogd");
+        t.put("sou", "Thai");
+        t.put("sr", "Cyrl");
+        t.put("srb", "Sora");
+        t.put("srx", "Deva");
+        t.put("swb", "Arab");
+        t.put("swv", "Deva");
+        t.put("syl", "Beng");
+        t.put("syr", "Syrc");
+        t.put("ta", "Taml");
+        t.put("taj", "Deva");
+        t.put("tcy", "Knda");
+        t.put("tdd", "Tale");
+        t.put("tdg", "Deva");
+        t.put("tdh", "Deva");
+        t.put("te", "Telu");
+        t.put("tg", "Cyrl");
+        t.put("tg_PK", "Arab");
+        t.put("th", "Thai");
+        t.put("thl", "Deva");
+        t.put("thq", "Deva");
+        t.put("thr", "Deva");
+        t.put("ti", "Ethi");
+        t.put("tig", "Ethi");
+        t.put("tkt", "Deva");
+        t.put("trw", "Arab");
+        t.put("tsd", "Grek");
+        t.put("tsf", "Deva");
+        t.put("tsj", "Tibt");
+        t.put("tt", "Cyrl");
+        t.put("tts", "Thai");
+        t.put("txg", "Tang");
+        t.put("txo", "Toto");
+        t.put("tyv", "Cyrl");
+        t.put("udi", "Aghb");
+        t.put("udm", "Cyrl");
+        t.put("ug", "Arab");
+        t.put("ug_KZ", "Cyrl");
+        t.put("ug_MN", "Cyrl");
+        t.put("uga", "Ugar");
+        t.put("uk", "Cyrl");
+        t.put("unr", "Beng");
+        t.put("unr_NP", "Deva");
+        t.put("unx", "Beng");
+        t.put("ur", "Arab");
+        t.put("uz_AF", "Arab");
+        t.put("uz_CN", "Cyrl");
+        t.put("vai", "Vaii");
+        t.put("wal", "Ethi");
+        t.put("wbq", "Telu");
+        t.put("wbr", "Deva");
+        t.put("wni", "Arab");
+        t.put("wsg", "Gong");
+        t.put("wtm", "Deva");
+        t.put("wuu", "Hans");
+        t.put("xco", "Chrs");
+        t.put("xcr", "Cari");
+        t.put("xlc", "Lyci");
+        t.put("xld", "Lydi");
+        t.put("xmf", "Geor");
+        t.put("xmn", "Mani");
+        t.put("xmr", "Merc");
+        t.put("xna", "Narb");
+        t.put("xnr", "Deva");
+        t.put("xpr", "Prti");
+        t.put("xsa", "Sarb");
+        t.put("xsr", "Deva");
+        t.put("yi", "Hebr");
+        t.put("yue", "Hant");
+        t.put("yue_CN", "Hans");
+        t.put("zdj", "Arab");
+        t.put("zgh", "Tfng");
+        t.put("zh", "Hans");
+        t.put("zh_AU", "Hant");
+        t.put("zh_BN", "Hant");
+        t.put("zh_GB", "Hant");
+        t.put("zh_GF", "Hant");
+        t.put("zh_HK", "Hant");
+        t.put("zh_ID", "Hant");
+        t.put("zh_MO", "Hant");
+        t.put("zh_PA", "Hant");
+        t.put("zh_PF", "Hant");
+        t.put("zh_PH", "Hant");
+        t.put("zh_SR", "Hant");
+        t.put("zh_TH", "Hant");
+        t.put("zh_TW", "Hant");
+        t.put("zh_US", "Hant");
+        t.put("zh_VN", "Hant");
+        t.put("zhx", "Nshu");
+        t.put("zkt", "Kits");
+        return Collections.unmodifiableMap(t);
+    }
+
+    //======================================================================
+    // Parent locale table
+    public static final Map<String, String> PARENT_LOCALE_TABLE = buildParentLocaleTable();
+
+    private static Map<String, String> buildParentLocaleTable() {
+        Map<String, String> t = new HashMap<>();
+        t.put("az_Arab", "root");
+        t.put("az_Cyrl", "root");
+        t.put("bal_Latn", "root");
+        t.put("blt_Latn", "root");
+        t.put("bm_Nkoo", "root");
+        t.put("bs_Cyrl", "root");
+        t.put("byn_Latn", "root");
+        t.put("cu_Glag", "root");
+        t.put("dje_Arab", "root");
+        t.put("dyo_Arab", "root");
+        t.put("en_150", "en_001");
+        t.put("en_AG", "en_001");
+        t.put("en_AI", "en_001");
+        t.put("en_AT", "en_150");
+        t.put("en_AU", "en_001");
+        t.put("en_BB", "en_001");
+        t.put("en_BE", "en_150");
+        t.put("en_BM", "en_001");
+        t.put("en_BS", "en_001");
+        t.put("en_BW", "en_001");
+        t.put("en_BZ", "en_001");
+        t.put("en_CC", "en_001");
+        t.put("en_CH", "en_150");
+        t.put("en_CK", "en_001");
+        t.put("en_CM", "en_001");
+        t.put("en_CX", "en_001");
+        t.put("en_CY", "en_001");
+        t.put("en_DE", "en_150");
+        t.put("en_DG", "en_001");
+        t.put("en_DK", "en_150");
+        t.put("en_DM", "en_001");
+        t.put("en_Dsrt", "root");
+        t.put("en_ER", "en_001");
+        t.put("en_FI", "en_150");
+        t.put("en_FJ", "en_001");
+        t.put("en_FK", "en_001");
+        t.put("en_FM", "en_001");
+        t.put("en_GB", "en_001");
+        t.put("en_GD", "en_001");
+        t.put("en_GG", "en_001");
+        t.put("en_GH", "en_001");
+        t.put("en_GI", "en_001");
+        t.put("en_GM", "en_001");
+        t.put("en_GY", "en_001");
+        t.put("en_HK", "en_001");
+        t.put("en_IE", "en_001");
+        t.put("en_IL", "en_001");
+        t.put("en_IM", "en_001");
+        t.put("en_IN", "en_001");
+        t.put("en_IO", "en_001");
+        t.put("en_JE", "en_001");
+        t.put("en_JM", "en_001");
+        t.put("en_KE", "en_001");
+        t.put("en_KI", "en_001");
+        t.put("en_KN", "en_001");
+        t.put("en_KY", "en_001");
+        t.put("en_LC", "en_001");
+        t.put("en_LR", "en_001");
+        t.put("en_LS", "en_001");
+        t.put("en_MG", "en_001");
+        t.put("en_MO", "en_001");
+        t.put("en_MS", "en_001");
+        t.put("en_MT", "en_001");
+        t.put("en_MU", "en_001");
+        t.put("en_MV", "en_001");
+        t.put("en_MW", "en_001");
+        t.put("en_MY", "en_001");
+        t.put("en_NA", "en_001");
+        t.put("en_NF", "en_001");
+        t.put("en_NG", "en_001");
+        t.put("en_NL", "en_150");
+        t.put("en_NR", "en_001");
+        t.put("en_NU", "en_001");
+        t.put("en_NZ", "en_001");
+        t.put("en_PG", "en_001");
+        t.put("en_PK", "en_001");
+        t.put("en_PN", "en_001");
+        t.put("en_PW", "en_001");
+        t.put("en_RW", "en_001");
+        t.put("en_SB", "en_001");
+        t.put("en_SC", "en_001");
+        t.put("en_SD", "en_001");
+        t.put("en_SE", "en_150");
+        t.put("en_SG", "en_001");
+        t.put("en_SH", "en_001");
+        t.put("en_SI", "en_150");
+        t.put("en_SL", "en_001");
+        t.put("en_SS", "en_001");
+        t.put("en_SX", "en_001");
+        t.put("en_SZ", "en_001");
+        t.put("en_Shaw", "root");
+        t.put("en_TC", "en_001");
+        t.put("en_TK", "en_001");
+        t.put("en_TO", "en_001");
+        t.put("en_TT", "en_001");
+        t.put("en_TV", "en_001");
+        t.put("en_TZ", "en_001");
+        t.put("en_UG", "en_001");
+        t.put("en_VC", "en_001");
+        t.put("en_VG", "en_001");
+        t.put("en_VU", "en_001");
+        t.put("en_WS", "en_001");
+        t.put("en_ZA", "en_001");
+        t.put("en_ZM", "en_001");
+        t.put("en_ZW", "en_001");
+        t.put("es_AR", "es_419");
+        t.put("es_BO", "es_419");
+        t.put("es_BR", "es_419");
+        t.put("es_BZ", "es_419");
+        t.put("es_CL", "es_419");
+        t.put("es_CO", "es_419");
+        t.put("es_CR", "es_419");
+        t.put("es_CU", "es_419");
+        t.put("es_DO", "es_419");
+        t.put("es_EC", "es_419");
+        t.put("es_GT", "es_419");
+        t.put("es_HN", "es_419");
+        t.put("es_MX", "es_419");
+        t.put("es_NI", "es_419");
+        t.put("es_PA", "es_419");
+        t.put("es_PE", "es_419");
+        t.put("es_PR", "es_419");
+        t.put("es_PY", "es_419");
+        t.put("es_SV", "es_419");
+        t.put("es_US", "es_419");
+        t.put("es_UY", "es_419");
+        t.put("es_VE", "es_419");
+        t.put("ff_Adlm", "root");
+        t.put("ff_Arab", "root");
+        t.put("ha_Arab", "root");
+        t.put("hi_Latn", "en_IN");
+        t.put("ht", "fr_HT");
+        t.put("iu_Latn", "root");
+        t.put("kk_Arab", "root");
+        t.put("ks_Deva", "root");
+        t.put("ku_Arab", "root");
+        t.put("ky_Arab", "root");
+        t.put("ky_Latn", "root");
+        t.put("ml_Arab", "root");
+        t.put("mn_Mong", "root");
+        t.put("mni_Mtei", "root");
+        t.put("ms_Arab", "root");
+        t.put("nb", "no");
+        t.put("nn", "no");
+        t.put("pa_Arab", "root");
+        t.put("pt_AO", "pt_PT");
+        t.put("pt_CH", "pt_PT");
+        t.put("pt_CV", "pt_PT");
+        t.put("pt_FR", "pt_PT");
+        t.put("pt_GQ", "pt_PT");
+        t.put("pt_GW", "pt_PT");
+        t.put("pt_LU", "pt_PT");
+        t.put("pt_MO", "pt_PT");
+        t.put("pt_MZ", "pt_PT");
+        t.put("pt_ST", "pt_PT");
+        t.put("pt_TL", "pt_PT");
+        t.put("sat_Deva", "root");
+        t.put("sd_Deva", "root");
+        t.put("sd_Khoj", "root");
+        t.put("sd_Sind", "root");
+        t.put("shi_Latn", "root");
+        t.put("so_Arab", "root");
+        t.put("sr_Latn", "root");
+        t.put("sw_Arab", "root");
+        t.put("tg_Arab", "root");
+        t.put("ug_Cyrl", "root");
+        t.put("uz_Arab", "root");
+        t.put("uz_Cyrl", "root");
+        t.put("vai_Latn", "root");
+        t.put("wo_Arab", "root");
+        t.put("yo_Arab", "root");
+        t.put("yue_Hans", "root");
+        t.put("zh_Hant", "root");
+        t.put("zh_Hant_MO", "zh_Hant_HK");
+        return Collections.unmodifiableMap(t);
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java b/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
index f1c0ac5..cef1174 100644
--- a/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
@@ -134,6 +134,7 @@
      */
     /* tables updated per http://lcweb.loc.gov/standards/iso639-2/
        to include the revisions up to 2001/7/27 *CWB*/
+    /* Subsequent hand addition of selected languages */
     /* The 3 character codes are the terminology codes like RFC 3066.
        This is compatible with prior ICU codes */
     /* "in" "iw" "ji" "jw" & "sh" have been withdrawn but are still in
@@ -145,183 +146,181 @@
     /* This list MUST be in sorted order, and MUST contain the two-letter codes
     if one exists otherwise use the three letter code */
     private static final String[] _languages = {
-        "aa",  "ab",  "ace", "ach", "ada", "ady", "ae",  "af",  
-        "afa", "afh", "agq", "ain", "ak",  "akk", "ale", "alg", 
-        "alt", "am",  "an",  "ang", "anp", "apa", "ar",  "arc", 
-        "arn", "arp", "ars", "art", "arw", "as",  "asa", "ast",
-        "ath", "aus", "av",  "awa", "ay",  "az",  
-        "ba",  "bad", "bai", "bal", "ban", "bas", "bat", "bax", 
-        "bbj", "be",  "bej", "bem", "ber", "bez", "bfd", "bg",  
-        "bh",  "bho", "bi",  "bik", "bin", "bkm", "bla", "bm",  
-        "bn",  "bnt", "bo",  "br",  "bra", "brx", "bs",  "bss", 
-        "btk", "bua", "bug", "bum", "byn", "byv", 
-        "ca",  "cad", "cai", "car", "cau", "cay", "cch", "ce",  
-        "ceb", "cel", "cgg", "ch",  "chb", "chg", "chk", "chm", 
-        "chn", "cho", "chp", "chr", "chy", "ckb", "cmc", "co",  
-        "cop", "cpe", "cpf", "cpp", "cr",  "crh", "crp", "cs",  
-        "csb", "cu",  "cus", "cv",  "cy",  
-        "da",  "dak", "dar", "dav", "day", "de",  "del", "den", 
-        "dgr", "din", "dje", "doi", "dra", "dsb", "dua", "dum", 
-        "dv",  "dyo", "dyu", "dz",  "dzg", 
-        "ebu", "ee",  "efi", "egy", "eka", "el",  "elx", "en",  
-        "enm", "eo",  "es",  "et",  "eu",  "ewo", 
-        "fa",  "fan", "fat", "ff",  "fi",  "fil", "fiu", "fj",  
-        "fo",  "fon", "fr",  "frm", "fro", "frr", "frs", "fur", 
-        "fy",  
-        "ga",  "gaa", "gay", "gba", "gd",  "gem", "gez", "gil", 
-        "gl",  "gmh", "gn",  "goh", "gon", "gor", "got", "grb", 
-        "grc", "gsw", "gu",  "guz", "gv",  "gwi", 
-        "ha",  "hai", "haw", "he",  "hi",  "hil", "him", "hit", 
-        "hmn", "ho",  "hr",  "hsb", "ht",  "hu",  "hup", "hy",  
-        "hz",  
-        "ia",  "iba", "ibb", "id",  "ie",  "ig",  "ii",  "ijo", 
-        "ik",  "ilo", "inc", "ine", "inh", "io",  "ira", "iro", 
-        "is",  "it",  "iu",  
-        "ja",  "jbo", "jgo", "jmc", "jpr", "jrb", "jv",  
-        "ka",  "kaa", "kab", "kac", "kaj", "kam", "kar", "kaw", 
-        "kbd", "kbl", "kcg", "kde", "kea", "kfo", "kg",  "kha", 
-        "khi", "kho", "khq", "ki",  "kj",  "kk",  "kkj", "kl",  
-        "kln", "km",  "kmb", "kn",  "ko",  "kok", "kos", "kpe", 
-        "kr",  "krc", "krl", "kro", "kru", "ks",  "ksb", "ksf", 
-        "ksh", "ku",  "kum", "kut", "kv",  "kw",  "ky",  
-        "la",  "lad", "lag", "lah", "lam", "lb",  "lez", "lg",  
-        "li",  "lkt", "ln",  "lo",  "lol", "loz", "lt",  "lu",  
-        "lua", "lui", "lun", "luo", "lus", "luy", "lv",  
-        "mad", "maf", "mag", "mai", "mak", "man", "map", "mas", 
-        "mde", "mdf", "mdr", "men", "mer", "mfe", "mg",  "mga", 
-        "mgh", "mgo", "mh",  "mi",  "mic", "min", "mis", "mk",  
-        "mkh", "ml",  "mn",  "mnc", "mni", "mno", "mo",  "moh", 
-        "mos", "mr",  "ms",  "mt",  "mua", "mul", "mun", "mus", 
-        "mwl", "mwr", "my",  "mye", "myn", "myv", 
-        "na",  "nah", "nai", "nap", "naq", "nb",  "nd",  "nds", 
-        "ne",  "new", "ng",  "nia", "nic", "niu", "nl",  "nmg", 
-        "nn",  "nnh", "no",  "nog", "non", "nqo", "nr",  "nso", 
-        "nub", "nus", "nv",  "nwc", "ny",  "nym", "nyn", "nyo", 
-        "nzi", 
-        "oc",  "oj",  "om",  "or",  "os",  "osa", "ota", "oto", 
-        "pa",  "paa", "pag", "pal", "pam", "pap", "pau", "peo", 
-        "phi", "phn", "pi",  "pl",  "pon", "pra", "pro", "ps",  
-        "pt",  
-        "qu",  
-        "raj", "rap", "rar", "rm",  "rn",  "ro",  "roa", "rof", 
-        "rom", "ru",  "rup", "rw",  "rwk", 
-        "sa",  "sad", "sah", "sai", "sal", "sam", "saq", "sas", 
-        "sat", "sba", "sbp", "sc",  "scn", "sco", "sd",  "se",  
-        "see", "seh", "sel", "sem", "ses", "sg",  "sga", "sgn", 
-        "shi", "shn", "shu", "si",  "sid", "sio", "sit", 
-        "sk",  "sl",  "sla", "sm",  "sma", "smi", "smj", "smn", 
-        "sms", "sn",  "snk", "so",  "sog", "son", "sq",  "sr",  
-        "srn", "srr", "ss",  "ssa", "ssy", "st",  "su",  "suk", 
-        "sus", "sux", "sv",  "sw",  "swb", "swc", "syc", "syr", 
-        "ta",  "tai", "te",  "tem", "teo", "ter", "tet", "tg",  
-        "th",  "ti",  "tig", "tiv", "tk",  "tkl", "tl",  "tlh", 
-        "tli", "tmh", "tn",  "to",  "tog", "tpi", "tr",  "trv", 
-        "ts",  "tsi", "tt",  "tum", "tup", "tut", "tvl", "tw",  
-        "twq", "ty",  "tyv", "tzm", 
-        "udm", "ug",  "uga", "uk",  "umb", "und", "ur",  "uz",  
-        "vai", "ve",  "vi",  "vo",  "vot", "vun", 
-        "wa",  "wae", "wak", "wal", "war", "was", "wen", "wo",  
-        "xal", "xh",  "xog", 
-        "yao", "yap", "yav", "ybb", "yi",  "yo",  "ypk", "yue", 
-        "za",  "zap", "zbl", "zen", "zh",  "znd", "zu",  "zun", 
+        "aa",  "ab",  "ace", "ach", "ada", "ady", "ae",  "aeb",
+        "af",  "afh", "agq", "ain", "ak",  "akk", "akz", "ale",
+        "aln", "alt", "am",  "an",  "ang", "anp", "ar",  "arc",
+        "arn", "aro", "arp", "arq", "ars", "arw", "ary", "arz", "as",
+        "asa", "ase", "ast", "av",  "avk", "awa", "ay",  "az",
+        "ba",  "bal", "ban", "bar", "bas", "bax", "bbc", "bbj",
+        "be",  "bej", "bem", "bew", "bez", "bfd", "bfq", "bg",
+        "bgc", "bgn", "bho", "bi",  "bik", "bin", "bjn", "bkm", "bla",
+        "bm",  "bn",  "bo",  "bpy", "bqi", "br",  "bra", "brh",
+        "brx", "bs",  "bss", "bua", "bug", "bum", "byn", "byv",
+        "ca",  "cad", "car", "cay", "cch", "ccp", "ce",  "ceb", "cgg",
+        "ch",  "chb", "chg", "chk", "chm", "chn", "cho", "chp",
+        "chr", "chy", "ckb", "co",  "cop", "cps", "cr",  "crh",
+        "cs",  "csb", "cu",  "cv",  "cy",
+        "da",  "dak", "dar", "dav", "day", "de",  "del", "den",
+        "dgr", "din", "dje", "doi", "dra", "dsb", "dua", "dum",
+        "dv",  "dyo", "dyu", "dz",  "dzg",
+        "ebu", "ee",  "efi", "egy", "eka", "el",  "elx", "en",
+        "enm", "eo",  "es",  "et",  "eu",  "ewo",
+        "fa",  "fan", "fat", "ff",  "fi",  "fil", "fiu", "fj",
+        "fo",  "fon", "fr",  "frm", "fro", "frr", "frs", "fur",
+        "fy",
+        "ga",  "gaa", "gay", "gba", "gd",  "gem", "gez", "gil",
+        "gl",  "gmh", "gn",  "goh", "gon", "gor", "got", "grb",
+        "grc", "gsw", "gu",  "guz", "gv",  "gwi",
+        "ha",  "hai", "haw", "he",  "hi",  "hil", "him", "hit",
+        "hmn", "ho",  "hr",  "hsb", "ht",  "hu",  "hup", "hy",
+        "hz",
+        "ia",  "iba", "ibb", "id",  "ie",  "ig",  "ii",  "ijo",
+        "ik",  "ilo", "inc", "ine", "inh", "io",  "ira", "iro",
+        "is",  "it",  "iu",
+        "ja",  "jbo", "jgo", "jmc", "jpr", "jrb", "jv",
+        "ka",  "kaa", "kab", "kac", "kaj", "kam", "kar", "kaw",
+        "kbd", "kbl", "kcg", "kde", "kea", "kfo", "kg",  "kha",
+        "khi", "kho", "khq", "ki",  "kj",  "kk",  "kkj", "kl",
+        "kln", "km",  "kmb", "kn",  "ko",  "kok", "kos", "kpe",
+        "kr",  "krc", "krl", "kro", "kru", "ks",  "ksb", "ksf",
+        "ksh", "ku",  "kum", "kut", "kv",  "kw",  "ky",
+        "la",  "lad", "lag", "lah", "lam", "lb",  "lez", "lg",
+        "li",  "lkt", "ln",  "lo",  "lol", "loz", "lt",  "lu",
+        "lua", "lui", "lun", "luo", "lus", "luy", "lv",
+        "mad", "maf", "mag", "mai", "mak", "man", "map", "mas",
+        "mde", "mdf", "mdr", "men", "mer", "mfe", "mg",  "mga",
+        "mgh", "mgo", "mh",  "mi",  "mic", "min", "mis", "mk",
+        "mkh", "ml",  "mn",  "mnc", "mni", "mno", "moh",
+        "mos", "mr",  "ms",  "mt",  "mua", "mul", "mun", "mus",
+        "mwl", "mwr", "my",  "mye", "myn", "myv",
+        "na",  "nah", "nai", "nap", "naq", "nb",  "nd",  "nds",
+        "ne",  "new", "ng",  "nia", "nic", "niu", "nl",  "nmg",
+        "nn",  "nnh", "no",  "nog", "non", "nqo", "nr",  "nso",
+        "nub", "nus", "nv",  "nwc", "ny",  "nym", "nyn", "nyo",
+        "nzi",
+        "oc",  "oj",  "om",  "or",  "os",  "osa", "ota", "oto",
+        "pa",  "paa", "pag", "pal", "pam", "pap", "pau", "peo",
+        "phi", "phn", "pi",  "pl",  "pon", "pra", "pro", "ps",
+        "pt",
+        "qu",
+        "raj", "rap", "rar", "rm",  "rn",  "ro",  "roa", "rof",
+        "rom", "ru",  "rup", "rw",  "rwk",
+        "sa",  "sad", "sah", "sai", "sal", "sam", "saq", "sas",
+        "sat", "sba", "sbp", "sc",  "scn", "sco", "sd",  "se",
+        "see", "seh", "sel", "sem", "ses", "sg",  "sga", "sgn",
+        "shi", "shn", "shu", "si",  "sid", "sio", "sit",
+        "sk",  "sl",  "sla", "sm",  "sma", "smi", "smj", "smn",
+        "sms", "sn",  "snk", "so",  "sog", "son", "sq",  "sr",
+        "srn", "srr", "ss",  "ssa", "ssy", "st",  "su",  "suk",
+        "sus", "sux", "sv",  "sw",  "swb", "syc", "syr",
+        "ta",  "tai", "te",  "tem", "teo", "ter", "tet", "tg",
+        "th",  "ti",  "tig", "tiv", "tk",  "tkl", "tlh",
+        "tli", "tmh", "tn",  "to",  "tog", "tpi", "tr",  "trv",
+        "ts",  "tsi", "tt",  "tum", "tup", "tut", "tvl", "tw",
+        "twq", "ty",  "tyv", "tzm",
+        "udm", "ug",  "uga", "uk",  "umb", "und", "ur",  "uz",
+        "vai", "ve",  "vi",  "vo",  "vot", "vun",
+        "wa",  "wae", "wak", "wal", "war", "was", "wen", "wo",
+        "xal", "xh",  "xog",
+        "yao", "yap", "yav", "ybb", "yi",  "yo",  "ypk", "yue",
+        "za",  "zap", "zbl", "zen", "zh",  "znd", "zu",  "zun",
         "zxx", "zza" };
-        
+
     private static final String[] _replacementLanguages = {
-        "id", "he", "yi", "jv", "sr", /* replacement language codes */
+        "id", "he", "yi", "jv", "ro", "sr", /* replacement language codes */
     };
-    
+
     private static final String[] _obsoleteLanguages = {
-        "in", "iw", "ji", "jw", "sh", /* obsolete language codes */
+        "in", "iw", "ji", "jw", "mo", "sh", /* obsolete language codes */
     };
-    
+
     /* This list MUST contain a three-letter code for every two-letter code in the
     list above, and they MUST ne in the same order (i.e., the same language must
     be in the same place in both lists)! */
     private static final String[] _languages3 = {
-        "aar", "abk", "ace", "ach", "ada", "ady", "ave", "afr", 
-        "afa", "afh", "agq", "ain", "aka", "akk", "ale", "alg", 
-        "alt", "amh", "arg", "ang", "anp", "apa", "ara", "arc", 
-        "arn", "arp", "ars", "art", "arw", "asm", "asa", "ast",
-        "ath", "aus", "ava", "awa", "aym", "aze", 
-        "bak", "bad", "bai", "bal", "ban", "bas", "bat", "bax", 
-        "bbj", "bel", "bej", "bem", "ber", "bez", "bfd", "bul", 
-        "bih", "bho", "bis", "bik", "bin", "bkm", "bla", "bam", 
-        "ben", "bnt", "bod", "bre", "bra", "brx", "bos", "bss", 
-        "btk", "bua", "bug", "bum", "byn", "byv", 
-        "cat", "cad", "cai", "car", "cau", "cay", "cch", "che", 
-        "ceb", "cel", "cgg", "cha", "chb", "chg", "chk", "chm", 
-        "chn", "cho", "chp", "chr", "chy", "ckb", "cmc", "cos", 
-        "cop", "cpe", "cpf", "cpp", "cre", "crh", "crp", "ces", 
-        "csb", "chu", "cus", "chv", "cym", 
-        "dan", "dak", "dar", "dav", "day", "deu", "del", "den", 
-        "dgr", "din", "dje", "doi", "dra", "dsb", "dua", "dum", 
-        "div", "dyo", "dyu", "dzo", "dzg", 
-        "ebu", "ewe", "efi", "egy", "eka", "ell", "elx", "eng", 
-        "enm", "epo", "spa", "est", "eus", "ewo", 
-        "fas", "fan", "fat", "ful", "fin", "fil", "fiu", "fij", 
-        "fao", "fon", "fra", "frm", "fro", "frr", "frs", "fur", 
-        "fry", 
-        "gle", "gaa", "gay", "gba", "gla", "gem", "gez", "gil", 
-        "glg", "gmh", "grn", "goh", "gon", "gor", "got", "grb", 
-        "grc", "gsw", "guj", "guz", "glv", "gwi", 
-        "hau", "hai", "haw", "heb", "hin", "hil", "him", "hit", 
-        "hmn", "hmo", "hrv", "hsb", "hat", "hun", "hup", "hye", 
-        "her", 
-        "ina", "iba", "ibb", "ind", "ile", "ibo", "iii", "ijo", 
-        "ipk", "ilo", "inc", "ine", "inh", "ido", "ira", "iro", 
-        "isl", "ita", "iku", 
-        "jpn", "jbo", "jgo", "jmc", "jpr", "jrb", "jav", 
-        "kat", "kaa", "kab", "kac", "kaj", "kam", "kar", "kaw", 
-        "kbd", "kbl", "kcg", "kde", "kea", "kfo", "kon", "kha", 
-        "khi", "kho", "khq", "kik", "kua", "kaz", "kkj", "kal", 
-        "kln", "khm", "kmb", "kan", "kor", "kok", "kos", "kpe", 
-        "kau", "krc", "krl", "kro", "kru", "kas", "ksb", "ksf", 
-        "ksh", "kur", "kum", "kut", "kom", "cor", "kir", 
-        "lat", "lad", "lag", "lah", "lam", "ltz", "lez", "lug", 
-        "lim", "lkt", "lin", "lao", "lol", "loz", "lit", "lub", 
-        "lua", "lui", "lun", "luo", "lus", "luy", "lav", 
-        "mad", "maf", "mag", "mai", "mak", "man", "map", "mas", 
-        "mde", "mdf", "mdr", "men", "mer", "mfe", "mlg", "mga", 
-        "mgh", "mgo", "mah", "mri", "mic", "min", "mis", "mkd", 
-        "mkh", "mal", "mon", "mnc", "mni", "mno", "mol", "moh", 
-        "mos", "mar", "msa", "mlt", "mua", "mul", "mun", "mus", 
-        "mwl", "mwr", "mya", "mye", "myn", "myv", 
-        "nau", "nah", "nai", "nap", "naq", "nob", "nde", "nds", 
-        "nep", "new", "ndo", "nia", "nic", "niu", "nld", "nmg", 
-        "nno", "nnh", "nor", "nog", "non", "nqo", "nbl", "nso", 
-        "nub", "nus", "nav", "nwc", "nya", "nym", "nyn", "nyo", 
-        "nzi", 
-        "oci", "oji", "orm", "ori", "oss", "osa", "ota", "oto", 
-        "pan", "paa", "pag", "pal", "pam", "pap", "pau", "peo", 
-        "phi", "phn", "pli", "pol", "pon", "pra", "pro", "pus", 
-        "por", 
-        "que", 
-        "raj", "rap", "rar", "roh", "run", "ron", "roa", "rof", 
-        "rom", "rus", "rup", "kin", "rwk", 
-        "san", "sad", "sah", "sai", "sal", "sam", "saq", "sas", 
-        "sat", "sba", "sbp", "srd", "scn", "sco", "snd", "sme", 
-        "see", "seh", "sel", "sem", "ses", "sag", "sga", "sgn", 
-        "shi", "shn", "shu", "sin", "sid", "sio", "sit", 
-        "slk", "slv", "sla", "smo", "sma", "smi", "smj", "smn", 
-        "sms", "sna", "snk", "som", "sog", "son", "sqi", "srp", 
-        "srn", "srr", "ssw", "ssa", "ssy", "sot", "sun", "suk", 
-        "sus", "sux", "swe", "swa", "swb", "swc", "syc", "syr", 
-        "tam", "tai", "tel", "tem", "teo", "ter", "tet", "tgk", 
-        "tha", "tir", "tig", "tiv", "tuk", "tkl", "tgl", "tlh", 
-        "tli", "tmh", "tsn", "ton", "tog", "tpi", "tur", "trv", 
-        "tso", "tsi", "tat", "tum", "tup", "tut", "tvl", "twi", 
-        "twq", "tah", "tyv", "tzm", 
-        "udm", "uig", "uga", "ukr", "umb", "und", "urd", "uzb", 
-        "vai", "ven", "vie", "vol", "vot", "vun", 
-        "wln", "wae", "wak", "wal", "war", "was", "wen", "wol", 
-        "xal", "xho", "xog", 
-        "yao", "yap", "yav", "ybb", "yid", "yor", "ypk", "yue", 
-        "zha", "zap", "zbl", "zen", "zho", "znd", "zul", "zun", 
+        "aar", "abk", "ace", "ach", "ada", "ady", "ave", "aeb",
+        "afr", "afh", "agq", "ain", "aka", "akk", "akz", "ale",
+        "aln", "alt", "amh", "arg", "ang", "anp", "ara", "arc",
+        "arn", "aro", "arp", "arq", "ars", "arw", "ary", "arz", "asm",
+        "asa", "ase", "ast", "ava", "avk", "awa", "aym", "aze",
+        "bak", "bal", "ban", "bar", "bas", "bax", "bbc", "bbj",
+        "bel", "bej", "bem", "bew", "bez", "bfd", "bfq", "bul",
+        "bgc", "bgn", "bho", "bis", "bik", "bin", "bjn", "bkm", "bla",
+        "bam", "ben", "bod", "bpy", "bqi", "bre", "bra", "brh",
+        "brx", "bos", "bss", "bua", "bug", "bum", "byn", "byv",
+        "cat", "cad", "car", "cay", "cch", "ccp", "che", "ceb", "cgg",
+        "cha", "chb", "chg", "chk", "chm", "chn", "cho", "chp",
+        "chr", "chy", "ckb", "cos", "cop", "cps", "cre", "crh",
+        "ces", "csb", "chu", "chv", "cym",
+        "dan", "dak", "dar", "dav", "day", "deu", "del", "den",
+        "dgr", "din", "dje", "doi", "dra", "dsb", "dua", "dum",
+        "div", "dyo", "dyu", "dzo", "dzg",
+        "ebu", "ewe", "efi", "egy", "eka", "ell", "elx", "eng",
+        "enm", "epo", "spa", "est", "eus", "ewo",
+        "fas", "fan", "fat", "ful", "fin", "fil", "fiu", "fij",
+        "fao", "fon", "fra", "frm", "fro", "frr", "frs", "fur",
+        "fry",
+        "gle", "gaa", "gay", "gba", "gla", "gem", "gez", "gil",
+        "glg", "gmh", "grn", "goh", "gon", "gor", "got", "grb",
+        "grc", "gsw", "guj", "guz", "glv", "gwi",
+        "hau", "hai", "haw", "heb", "hin", "hil", "him", "hit",
+        "hmn", "hmo", "hrv", "hsb", "hat", "hun", "hup", "hye",
+        "her",
+        "ina", "iba", "ibb", "ind", "ile", "ibo", "iii", "ijo",
+        "ipk", "ilo", "inc", "ine", "inh", "ido", "ira", "iro",
+        "isl", "ita", "iku",
+        "jpn", "jbo", "jgo", "jmc", "jpr", "jrb", "jav",
+        "kat", "kaa", "kab", "kac", "kaj", "kam", "kar", "kaw",
+        "kbd", "kbl", "kcg", "kde", "kea", "kfo", "kon", "kha",
+        "khi", "kho", "khq", "kik", "kua", "kaz", "kkj", "kal",
+        "kln", "khm", "kmb", "kan", "kor", "kok", "kos", "kpe",
+        "kau", "krc", "krl", "kro", "kru", "kas", "ksb", "ksf",
+        "ksh", "kur", "kum", "kut", "kom", "cor", "kir",
+        "lat", "lad", "lag", "lah", "lam", "ltz", "lez", "lug",
+        "lim", "lkt", "lin", "lao", "lol", "loz", "lit", "lub",
+        "lua", "lui", "lun", "luo", "lus", "luy", "lav",
+        "mad", "maf", "mag", "mai", "mak", "man", "map", "mas",
+        "mde", "mdf", "mdr", "men", "mer", "mfe", "mlg", "mga",
+        "mgh", "mgo", "mah", "mri", "mic", "min", "mis", "mkd",
+        "mkh", "mal", "mon", "mnc", "mni", "mno", "moh",
+        "mos", "mar", "msa", "mlt", "mua", "mul", "mun", "mus",
+        "mwl", "mwr", "mya", "mye", "myn", "myv",
+        "nau", "nah", "nai", "nap", "naq", "nob", "nde", "nds",
+        "nep", "new", "ndo", "nia", "nic", "niu", "nld", "nmg",
+        "nno", "nnh", "nor", "nog", "non", "nqo", "nbl", "nso",
+        "nub", "nus", "nav", "nwc", "nya", "nym", "nyn", "nyo",
+        "nzi",
+        "oci", "oji", "orm", "ori", "oss", "osa", "ota", "oto",
+        "pan", "paa", "pag", "pal", "pam", "pap", "pau", "peo",
+        "phi", "phn", "pli", "pol", "pon", "pra", "pro", "pus",
+        "por",
+        "que",
+        "raj", "rap", "rar", "roh", "run", "ron", "roa", "rof",
+        "rom", "rus", "rup", "kin", "rwk",
+        "san", "sad", "sah", "sai", "sal", "sam", "saq", "sas",
+        "sat", "sba", "sbp", "srd", "scn", "sco", "snd", "sme",
+        "see", "seh", "sel", "sem", "ses", "sag", "sga", "sgn",
+        "shi", "shn", "shu", "sin", "sid", "sio", "sit",
+        "slk", "slv", "sla", "smo", "sma", "smi", "smj", "smn",
+        "sms", "sna", "snk", "som", "sog", "son", "sqi", "srp",
+        "srn", "srr", "ssw", "ssa", "ssy", "sot", "sun", "suk",
+        "sus", "sux", "swe", "swa", "swb", "syc", "syr",
+        "tam", "tai", "tel", "tem", "teo", "ter", "tet", "tgk",
+        "tha", "tir", "tig", "tiv", "tuk", "tkl", "tlh",
+        "tli", "tmh", "tsn", "ton", "tog", "tpi", "tur", "trv",
+        "tso", "tsi", "tat", "tum", "tup", "tut", "tvl", "twi",
+        "twq", "tah", "tyv", "tzm",
+        "udm", "uig", "uga", "ukr", "umb", "und", "urd", "uzb",
+        "vai", "ven", "vie", "vol", "vot", "vun",
+        "wln", "wae", "wak", "wal", "war", "was", "wen", "wol",
+        "xal", "xho", "xog",
+        "yao", "yap", "yav", "ybb", "yid", "yor", "ypk", "yue",
+        "zha", "zap", "zbl", "zen", "zho", "znd", "zul", "zun",
         "zxx", "zza" };
 
     private static final String[] _obsoleteLanguages3 = {
-        /* "in",  "iw",  "ji",  "jw",  "sh", */
-        "ind", "heb", "yid", "jaw", "srp",
+        /* "in",  "iw",  "ji",  "jw",  "mo",  "sh", */
+        "ind", "heb", "yid", "jaw", "ron", "srp"
     };
 
     /* ZR(ZAR) is now CD(COD) and FX(FXX) is PS(PSE) as per
@@ -340,13 +339,13 @@
         "BJ",  "BL",  "BM",  "BN",  "BO",  "BQ",  "BR",  "BS",  "BT",  "BV",
         "BW",  "BY",  "BZ",  "CA",  "CC",  "CD",  "CF",  "CG",
         "CH",  "CI",  "CK",  "CL",  "CM",  "CN",  "CO",  "CR",
-        "CU",  "CV",  "CW",  "CX",  "CY",  "CZ",  "DE",  "DJ",  "DK",
-        "DM",  "DO",  "DZ",  "EC",  "EE",  "EG",  "EH",  "ER",
+        "CU",  "CV",  "CW",  "CX",  "CY",  "CZ",  "DE",  "DG",  "DJ",  "DK",
+        "DM",  "DO",  "DZ",  "EA",  "EC",  "EE",  "EG",  "EH",  "ER",
         "ES",  "ET",  "FI",  "FJ",  "FK",  "FM",  "FO",  "FR",
         "GA",  "GB",  "GD",  "GE",  "GF",  "GG",  "GH",  "GI",  "GL",
         "GM",  "GN",  "GP",  "GQ",  "GR",  "GS",  "GT",  "GU",
         "GW",  "GY",  "HK",  "HM",  "HN",  "HR",  "HT",  "HU",
-        "ID",  "IE",  "IL",  "IM",  "IN",  "IO",  "IQ",  "IR",  "IS",
+        "IC",  "ID",  "IE",  "IL",  "IM",  "IN",  "IO",  "IQ",  "IR",  "IS",
         "IT",  "JE",  "JM",  "JO",  "JP",  "KE",  "KG",  "KH",  "KI",
         "KM",  "KN",  "KP",  "KR",  "KW",  "KY",  "KZ",  "LA",
         "LB",  "LC",  "LI",  "LK",  "LR",  "LS",  "LT",  "LU",
@@ -363,22 +362,22 @@
         "TK",  "TL",  "TM",  "TN",  "TO",  "TR",  "TT",  "TV",
         "TW",  "TZ",  "UA",  "UG",  "UM",  "US",  "UY",  "UZ",
         "VA",  "VC",  "VE",  "VG",  "VI",  "VN",  "VU",  "WF",
-        "WS",  "YE",  "YT",  "ZA",  "ZM",  "ZW" };
-    
+        "WS",  "XK",  "YE",  "YT",  "ZA",  "ZM",  "ZW" };
+
     private static final String[] _deprecatedCountries = {
         "AN", "BU", "CS", "DD", "DY", "FX", "HV", "NH", "RH", "SU", "TP", "UK", "VD", "YD", "YU", "ZR" /* deprecated country list */
     };
-    
+
     private static final String[] _replacementCountries = {
     /*  "AN", "BU", "CS", "DD", "DY", "FX", "HV", "NH", "RH", "SU", "TP", "UK", "VD", "YD", "YU", "ZR" */
-        "CW", "MM", "RS", "DE", "BJ", "FR", "BF", "VU", "ZW", "RU", "TL", "GB", "VN", "YE", "RS", "CD"  /* replacement country codes */      
+        "CW", "MM", "RS", "DE", "BJ", "FR", "BF", "VU", "ZW", "RU", "TL", "GB", "VN", "YE", "RS", "CD"  /* replacement country codes */
     };
-    
+
     /* this table is used for three letter codes */
     private static final String[] _obsoleteCountries = {
         "AN",  "BU", "CS", "FX", "RO", "SU", "TP", "YD", "YU", "ZR",   /* obsolete country codes */
     };
-    
+
     /* This list MUST contain a three-letter code for every two-letter code in
     the above list, and they MUST be listed in the same order! */
     private static final String[] _countries3 = {
@@ -394,10 +393,10 @@
         "BWA", "BLR", "BLZ", "CAN", "CCK", "COD", "CAF", "COG",
     /*  "CH",  "CI",  "CK",  "CL",  "CM",  "CN",  "CO",  "CR",     */
         "CHE", "CIV", "COK", "CHL", "CMR", "CHN", "COL", "CRI",
-    /*  "CU",  "CV",  "CW",  "CX",  "CY",  "CZ",  "DE",  "DJ",  "DK",     */
-        "CUB", "CPV", "CUW", "CXR", "CYP", "CZE", "DEU", "DJI", "DNK",
-    /*  "DM",  "DO",  "DZ",  "EC",  "EE",  "EG",  "EH",  "ER",     */
-        "DMA", "DOM", "DZA", "ECU", "EST", "EGY", "ESH", "ERI",
+    /*  "CU",  "CV",  "CW",  "CX",  "CY",  "CZ",  "DE",  "DG",  "DJ",  "DK",     */
+        "CUB", "CPV", "CUW", "CXR", "CYP", "CZE", "DEU", "DGA", "DJI", "DNK",
+    /*  "DM",  "DO",  "DZ",  "EA",  "EC",  "EE",  "EG",  "EH",  "ER",     */
+        "DMA", "DOM", "DZA", "XEA", "ECU", "EST", "EGY", "ESH", "ERI",
     /*  "ES",  "ET",  "FI",  "FJ",  "FK",  "FM",  "FO",  "FR",     */
         "ESP", "ETH", "FIN", "FJI", "FLK", "FSM", "FRO", "FRA",
     /*  "GA",  "GB",  "GD",  "GE",  "GF",  "GG",  "GH",  "GI",  "GL",     */
@@ -406,8 +405,8 @@
         "GMB", "GIN", "GLP", "GNQ", "GRC", "SGS", "GTM", "GUM",
     /*  "GW",  "GY",  "HK",  "HM",  "HN",  "HR",  "HT",  "HU",     */
         "GNB", "GUY", "HKG", "HMD", "HND", "HRV", "HTI", "HUN",
-    /*  "ID",  "IE",  "IL",  "IM",  "IN",  "IO",  "IQ",  "IR",  "IS" */
-        "IDN", "IRL", "ISR", "IMN", "IND", "IOT", "IRQ", "IRN", "ISL",
+    /*  "IC",  "ID",  "IE",  "IL",  "IM",  "IN",  "IO",  "IQ",  "IR",  "IS" */
+        "XIC", "IDN", "IRL", "ISR", "IMN", "IND", "IOT", "IRQ", "IRN", "ISL",
     /*  "IT",  "JE",  "JM",  "JO",  "JP",  "KE",  "KG",  "KH",  "KI",     */
         "ITA", "JEY", "JAM", "JOR", "JPN", "KEN", "KGZ", "KHM", "KIR",
     /*  "KM",  "KN",  "KP",  "KR",  "KW",  "KY",  "KZ",  "LA",     */
@@ -440,9 +439,9 @@
         "TWN", "TZA", "UKR", "UGA", "UMI", "USA", "URY", "UZB",
     /*  "VA",  "VC",  "VE",  "VG",  "VI",  "VN",  "VU",  "WF",     */
         "VAT", "VCT", "VEN", "VGB", "VIR", "VNM", "VUT", "WLF",
-    /*  "WS",  "YE",  "YT",  "ZA",  "ZM",  "ZW",          */
-        "WSM", "YEM", "MYT", "ZAF", "ZMB", "ZWE" };
-    
+    /*  "WS",  "XK",  "YE",  "YT",  "ZA",  "ZM",  "ZW",          */
+        "WSM", "XKK", "YEM", "MYT", "ZAF", "ZMB", "ZWE" };
+
     private static final String[] _obsoleteCountries3 = {
     /*  "AN",  "BU",  "CS",  "FX",  "RO",  "SU",  "TP",  "YD",  "YU",  "ZR" */
         "ANT", "BUR", "SCG", "FXX", "ROM", "SUN", "TMP", "YMD", "YUG", "ZAR",
diff --git a/android_icu4j/src/main/java/android/icu/impl/Normalizer2Impl.java b/android_icu4j/src/main/java/android/icu/impl/Normalizer2Impl.java
index 8cf25cc..af129db 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Normalizer2Impl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Normalizer2Impl.java
@@ -915,8 +915,8 @@
     public static final int MIN_YES_YES_WITH_CC=0xfe02;
     public static final int JAMO_VT=0xfe00;
     public static final int MIN_NORMAL_MAYBE_YES=0xfc00;
-    public static final int JAMO_L=2;  // offset=1 hasCompBoundaryAfter=FALSE
-    public static final int INERT=1;  // offset=0 hasCompBoundaryAfter=TRUE
+    public static final int JAMO_L=2;  // offset=1 hasCompBoundaryAfter=false
+    public static final int INERT=1;  // offset=0 hasCompBoundaryAfter=true
 
     // norm16 bit 0 is comp-boundary-after.
     public static final int HAS_COMP_BOUNDARY_AFTER=1;
diff --git a/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java b/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
index 1d200a9..4673179 100644
--- a/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
+++ b/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
@@ -307,9 +307,9 @@
     public boolean useDaylightTime() {
         // If DST was observed in 1942 (for example) but has never been
         // observed from 1943 to the present, most clients will expect
-        // this method to return FALSE.  This method determines whether
+        // this method to return false.  This method determines whether
         // DST is in use in the current year (at any point in the year)
-        // and returns TRUE if so.
+        // and returns true if so.
         long current = System.currentTimeMillis();
 
         if (finalZone != null && current >= finalStartMillis) {
@@ -322,7 +322,7 @@
         long start = Grego.fieldsToDay(fields[0], 0, 1) * SECONDS_PER_DAY;
         long limit = Grego.fieldsToDay(fields[0] + 1, 0, 1) * SECONDS_PER_DAY;
 
-        // Return TRUE if DST is observed at any time during the current
+        // Return true if DST is observed at any time during the current
         // year.
         for (int i = 0; i < transitionCount; ++i) {
             if (transitionTimes64[i] >= limit) {
@@ -351,7 +351,7 @@
             }
         }
 
-        // Return TRUE if DST is observed at any future time
+        // Return true if DST is observed at any future time
         long currentSec = Grego.floorDivide(current, Grego.MILLIS_PER_SECOND);
         int trsIdx = transitionCount - 1;
         if (dstOffsetAt(trsIdx) != 0) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/PatternProps.java b/android_icu4j/src/main/java/android/icu/impl/PatternProps.java
index 5bdb37c..a3f3941 100644
--- a/android_icu4j/src/main/java/android/icu/impl/PatternProps.java
+++ b/android_icu4j/src/main/java/android/icu/impl/PatternProps.java
@@ -21,7 +21,7 @@
  * Pattern_Syntax includes some unassigned code points.
  * <p>
  * [:Pattern_White_Space:] =
- *   [\u0009-\u000D\ \u0085\u200E\u200F\u2028\u2029]
+ *   [\u0009-\u000D\ \u0020\u0085\u200E\u200F\u2028\u2029]
  * <p>
  * [:Pattern_Syntax:] =
  *   [!-/\:-@\[-\^`\{-~\u00A1-\u00A7\u00A9\u00AB\u00AC\u00AE
diff --git a/android_icu4j/src/main/java/android/icu/impl/Trie2.java b/android_icu4j/src/main/java/android/icu/impl/Trie2.java
index c3a07d5..4942126 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Trie2.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Trie2.java
@@ -196,8 +196,8 @@
      * @param is   an InputStream containing the serialized form
      *             of a UTrie, version 1 or 2.  The stream must support mark() and reset().
      *             The position of the input stream will be left unchanged.
-     * @param littleEndianOk If FALSE, only big-endian (Java native) serialized forms are recognized.
-     *                    If TRUE, little-endian serialized forms are recognized as well.
+     * @param littleEndianOk If false, only big-endian (Java native) serialized forms are recognized.
+     *                    If true, little-endian serialized forms are recognized as well.
      * @return     the Trie version of the serialized form, or 0 if it is not
      *             recognized as a serialized UTrie
      * @throws     IOException on errors in reading from the input stream.
diff --git a/android_icu4j/src/main/java/android/icu/impl/Trie2Writable.java b/android_icu4j/src/main/java/android/icu/impl/Trie2Writable.java
index d7c7971..2bf5129 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Trie2Writable.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Trie2Writable.java
@@ -358,7 +358,7 @@
     }
 
     /**
-     * initialValue is ignored if overwrite=TRUE
+     * initialValue is ignored if overwrite=true
      * @hide draft / provisional / internal are hidden on Android
      */
     private void fillBlock(int block, /*UChar32*/ int start, /*UChar32*/ int limit,
@@ -381,7 +381,7 @@
     /**
      * Set a value in a range of code points [start..end].
      * All code points c with start<=c<=end will get the value if
-     * overwrite is TRUE or if the old value is the initial value.
+     * overwrite is true or if the old value is the initial value.
      *
      * @param start the first code point to get the value
      * @param end the last code point to get the value (inclusive)
@@ -514,7 +514,7 @@
       * Set the values from a Trie2.Range.
       * 
       * All code points within the range will get the value if
-      * overwrite is TRUE or if the old value is the initial value.
+      * overwrite is true or if the old value is the initial value.
       *
       * Ranges with the lead surrogate flag set will set the alternate
       * lead-surrogate values in the Trie, rather than the code point values.
@@ -731,7 +731,7 @@
      *
      * The compaction
      * - removes blocks that are identical with earlier ones
-     * - overlaps adjacent blocks as much as possible (if overlap==TRUE)
+     * - overlaps adjacent blocks as much as possible (if overlap==true)
      * - moves blocks in steps of the data granularity
      * - moves and overlaps blocks that overlap with multiple values in the overlap region
      *
diff --git a/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java b/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
index 7acb688..822f399 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
@@ -314,7 +314,7 @@
      */
     private static final boolean isgraphPOSIX(int c) {
         /* \p{space}\p{gc=Control} == \p{gc=Z}\p{Control} */
-        /* comparing ==0 returns FALSE for the categories mentioned */
+        /* comparing ==0 returns false for the categories mentioned */
         return (getMask(UCharacter.getType(c))&
                 (GC_CC_MASK|GC_CS_MASK|GC_CN_MASK|GC_Z_MASK))
                ==0;
@@ -824,7 +824,7 @@
     public int getIntPropertyMaxValue(int which) {
         if(which<UProperty.INT_START) {
             if(UProperty.BINARY_START<=which && which<UProperty.BINARY_LIMIT) {
-                return 1;  // maximum TRUE for all binary properties
+                return 1;  // maximum true for all binary properties
             }
         } else if(which<UProperty.INT_LIMIT) {
             return intProps[which-UProperty.INT_START].getMaxValue(which);
diff --git a/android_icu4j/src/main/java/android/icu/impl/Utility.java b/android_icu4j/src/main/java/android/icu/impl/Utility.java
index 110663d..5445e30 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Utility.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Utility.java
@@ -1564,8 +1564,8 @@
      * Escapes one unprintable code point using <backslash>uxxxx notation
      * for U+0000 to U+FFFF and <backslash>Uxxxxxxxx for U+10000 and
      * above.  If the character is printable ASCII, then do nothing
-     * and return FALSE.  Otherwise, append the escaped notation and
-     * return TRUE.
+     * and return false.  Otherwise, append the escaped notation and
+     * return true.
      */
     public static <T extends Appendable> boolean escapeUnprintable(T result, int c) {
         if (isUnprintable(c)) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java
index 0a41f08..3a801ce 100644
--- a/android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java
@@ -12,12 +12,14 @@
 import static android.icu.impl.CharacterIteration.DONE32;
 import static android.icu.impl.CharacterIteration.current32;
 import static android.icu.impl.CharacterIteration.next32;
+import static android.icu.impl.CharacterIteration.previous32;
 
 import java.io.IOException;
 import java.text.CharacterIterator;
 import java.util.HashSet;
 
 import android.icu.impl.Assert;
+import android.icu.impl.ICUConfig;
 import android.icu.impl.ICUData;
 import android.icu.text.Normalizer;
 import android.icu.text.UnicodeSet;
@@ -34,6 +36,8 @@
     private UnicodeSet fClosePunctuationSet;
     private DictionaryMatcher fDictionary = null;
     private HashSet<String> fSkipSet;
+    private MlBreakEngine fMlBreakEngine;
+    private boolean isCj = false;
 
     public CjkBreakEngine(boolean korean) throws IOException {
         fHangulWordSet = new UnicodeSet("[\\uac00-\\ud7a3]");
@@ -50,9 +54,16 @@
         if (korean) {
             setCharacters(fHangulWordSet);
         } else { //Chinese and Japanese
+            isCj = true;
             UnicodeSet cjSet = new UnicodeSet("[[:Han:][:Hiragana:][:Katakana:]\\u30fc\\uff70\\uff9e\\uff9f]");
             setCharacters(cjSet);
-            initializeJapanesePhraseParamater();
+            if (Boolean.parseBoolean(
+                    ICUConfig.get("android.icu.impl.breakiter.useMLPhraseBreaking", "false"))) {
+                fMlBreakEngine = new MlBreakEngine(fDigitOrOpenPunctuationOrAlphabetSet,
+                        fClosePunctuationSet);
+            } else {
+                initializeJapanesePhraseParamater();
+            }
         }
     }
 
@@ -154,6 +165,15 @@
                 charPositions[numCodePts] = index;
             }
         }
+        // Use ML phrase breaking
+        if (Boolean.parseBoolean(
+                ICUConfig.get("android.icu.impl.breakiter.useMLPhraseBreaking", "false"))) {
+            // PhraseBreaking is supported in ja and ko; MlBreakEngine only supports ja.
+            if (isPhraseBreaking && isCj) {
+                return fMlBreakEngine.divideUpRange(inText, startPos, endPos, text,
+                        numCodePts, charPositions, foundBreaks);
+            }
+        }
 
         // From here on out, do the algorithm. Note that our indices
         // refer to indices within the normalized string.
@@ -244,17 +264,18 @@
             t_boundary[numBreaks] = numCodePts;
             numBreaks++;
             int prevIdx = numCodePts;
-            int codeUnitIdx = 0, length = 0;
+            int codeUnitIdx = 0, prevCodeUnitIdx = 0, length = 0;
             for (int i = prev[numCodePts]; i > 0; i = prev[i]) {
                 codeUnitIdx = prenormstr.offsetByCodePoints(0, i);
-                length = prevIdx - i;
+                prevCodeUnitIdx = prenormstr.offsetByCodePoints(0, prevIdx);
+                length =  prevCodeUnitIdx - codeUnitIdx;
                 prevIdx = i;
                 String pattern = getPatternFromText(text, s, codeUnitIdx, length);
                 // Keep the breakpoint if the pattern is not in the fSkipSet and continuous Katakana
                 // characters don't occur.
-                text.setIndex(codeUnitIdx - 1);
+                text.setIndex(codeUnitIdx);
                 if (!fSkipSet.contains(pattern)
-                        && (!isKatakana(current32(text)) || !isKatakana(next32(text)))) {
+                        && (!isKatakana(current32(text)) || !isKatakana(previous32(text)))) {
                     t_boundary[numBreaks] = i;
                     numBreaks++;
                 }
@@ -278,10 +299,11 @@
             // In phrase breaking, there has to be a breakpoint between Cj character and close
             // punctuation.
             // E.g.[携帯電話]正しい選択 -> [携帯▁電話]▁正しい▁選択 -> breakpoint between ] and 正
+            inText.setIndex(pos);
             if (pos > previous) {
                 if (pos != startPos
                         || (isPhraseBreaking && pos > 0
-                        && fClosePunctuationSet.contains(inText.setIndex(pos - 1)))) {
+                        && fClosePunctuationSet.contains(previous32(inText)))) {
                     foundBreaks.push(charPositions[t_boundary[i]] + startPos);
                     correctedNumBreaks++;
                 }
@@ -296,7 +318,9 @@
             // E.g. 乗車率90%程度だろうか -> 乗車▁率▁90%▁程度だろうか -> breakpoint between 率 and 9
             // E.g. しかもロゴがUnicode! -> しかも▁ロゴが▁Unicode!-> breakpoint between が and U
             if (isPhraseBreaking) {
-                if (!fDigitOrOpenPunctuationOrAlphabetSet.contains(inText.setIndex(endPos))) {
+                inText.setIndex(endPos);
+                int current = current32(inText);
+                if (current != DONE32 && !fDigitOrOpenPunctuationOrAlphabetSet.contains(current)) {
                     foundBreaks.pop();
                     correctedNumBreaks--;
                 }
@@ -313,11 +337,11 @@
     private String getPatternFromText(CharacterIterator text, StringBuffer sb, int start,
             int length) {
         sb.setLength(0);
-        if(length > 0) {
+        if (length > 0) {
             text.setIndex(start);
-            sb.appendCodePoint(current32(text));
-            for (int j = 1; j < length; j++) {
-                sb.appendCodePoint(next32(text));
+            sb.append(text.current());
+            for (int i = 1; i < length; i++) {
+                sb.append(text.next());
             }
         }
         return sb.toString();
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java
new file mode 100644
index 0000000..957c8d3
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java
@@ -0,0 +1,287 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.impl.breakiter;
+
+import static android.icu.impl.CharacterIteration.DONE32;
+import static android.icu.impl.CharacterIteration.current32;
+import static android.icu.impl.CharacterIteration.next32;
+import static android.icu.impl.CharacterIteration.previous32;
+
+import android.icu.impl.ICUData;
+import android.icu.text.UnicodeSet;
+import android.icu.util.UResourceBundle;
+import android.icu.util.UResourceBundleIterator;
+
+import java.text.CharacterIterator;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+
+enum ModelIndex {
+    kUWStart(0), kBWStart(6), kTWStart(9);
+    private final int value;
+
+    private ModelIndex(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+}
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class MlBreakEngine {
+    // {UW1, UW2, ... UW6, BW1, ... BW3, TW1, TW2, ... TW4} 6+3+4= 13
+    private static final int MAX_FEATURE = 13;
+    private UnicodeSet fDigitOrOpenPunctuationOrAlphabetSet;
+    private UnicodeSet fClosePunctuationSet;
+    private List<HashMap<String, Integer>> fModel;
+    private int fNegativeSum;
+
+    /**
+     * Constructor for Chinese and Japanese phrase breaking.
+     *
+     * @param digitOrOpenPunctuationOrAlphabetSet An unicode set with the digit and open punctuation
+     *                                            and alphabet.
+     * @param closePunctuationSet                 An unicode set with the close punctuation.
+     */
+    public MlBreakEngine(UnicodeSet digitOrOpenPunctuationOrAlphabetSet,
+            UnicodeSet closePunctuationSet) {
+        fDigitOrOpenPunctuationOrAlphabetSet = digitOrOpenPunctuationOrAlphabetSet;
+        fClosePunctuationSet = closePunctuationSet;
+        fModel = new ArrayList<HashMap<String, Integer>>(MAX_FEATURE);
+        for (int i = 0; i < MAX_FEATURE; i++) {
+            fModel.add(new HashMap<String, Integer>());
+        }
+        fNegativeSum = 0;
+        loadMLModel();
+    }
+
+    /**
+     * Divide up a range of characters handled by this break engine.
+     *
+     * @param inText          An input text.
+     * @param startPos        The start index of the input text.
+     * @param endPos          The end index of the input text.
+     * @param inString        A input string normalized from inText from startPos to endPos
+     * @param codePointLength The number of code points of inString
+     * @param charPositions   A map that transforms inString's code point index to code unit index.
+     * @param foundBreaks     A list to store the breakpoint.
+     * @return The number of breakpoints
+     */
+    public int divideUpRange(CharacterIterator inText, int startPos, int endPos,
+            CharacterIterator inString, int codePointLength, int[] charPositions,
+            DictionaryBreakEngine.DequeI foundBreaks) {
+        if (startPos >= endPos) {
+            return 0;
+        }
+        ArrayList<Integer> boundary = new ArrayList<Integer>(codePointLength);
+        String inputStr = transform(inString);
+        // The ML algorithm groups six char and evaluates whether the 4th char is a breakpoint.
+        // In each iteration, it evaluates the 4th char and then moves forward one char like
+        // sliding window. Initially, the first six values in the indexList are
+        // [-1, -1, 0, 1, 2, 3]. After moving forward, finally the last six values in the indexList
+        // are [length-4, length-3, length-2, length-1, -1, -1]. The "+4" here means four extra
+        // "-1".
+        int indexSize = codePointLength + 4;
+        int indexList[] = new int[indexSize];
+        int numCodeUnits = initIndexList(inString, indexList, codePointLength);
+
+        // Add a break for the start.
+        boundary.add(0, 0);
+
+        for (int idx = 0; idx + 1 < codePointLength; idx++) {
+            evaluateBreakpoint(inputStr, indexList, idx, numCodeUnits, boundary);
+            if (idx + 4 < codePointLength) {
+                indexList[idx + 6] = numCodeUnits;
+                numCodeUnits += Character.charCount(next32(inString));
+            }
+        }
+
+        // Add a break for the end if there is not one there already.
+        if (boundary.get(boundary.size() - 1) != codePointLength) {
+            boundary.add(codePointLength);
+        }
+
+        int correctedNumBreaks = 0;
+        int previous = -1;
+        int numBreaks = boundary.size();
+        for (int i = 0; i < numBreaks; i++) {
+            int pos = charPositions[boundary.get(i)] + startPos;
+            // In phrase breaking, there has to be a breakpoint between Cj character and close
+            // punctuation.
+            // E.g.[携帯電話]正しい選択 -> [携帯▁電話]▁正しい▁選択 -> breakpoint between ] and 正
+            inText.setIndex(pos);
+            if (pos > previous) {
+                if (pos != startPos
+                        || (pos > 0
+                        && fClosePunctuationSet.contains(previous32(inText)))) {
+                    foundBreaks.push(pos);
+                    correctedNumBreaks++;
+                }
+            }
+            previous = pos;
+        }
+
+        if (!foundBreaks.isEmpty() && foundBreaks.peek() == endPos) {
+            // In phrase breaking, there has to be a breakpoint between Cj character and
+            // the number/open punctuation.
+            // E.g. る文字「そうだ、京都」->る▁文字▁「そうだ、▁京都」-> breakpoint between 字 and「
+            // E.g. 乗車率90%程度だろうか -> 乗車▁率▁90%▁程度だろうか -> breakpoint between 率 and 9
+            // E.g. しかもロゴがUnicode! -> しかも▁ロゴが▁Unicode!-> breakpoint between が and U
+            inText.setIndex(endPos);
+            int current = current32(inText);
+            if (current != DONE32 && !fDigitOrOpenPunctuationOrAlphabetSet.contains(current)) {
+                foundBreaks.pop();
+                correctedNumBreaks--;
+            }
+
+        }
+        if (!foundBreaks.isEmpty()) {
+            inText.setIndex(foundBreaks.peek());
+        }
+        return correctedNumBreaks;
+    }
+
+    /**
+     * Transform a CharacterIterator into a String.
+     */
+    private String transform(CharacterIterator inString) {
+        StringBuilder sb = new StringBuilder();
+        inString.setIndex(0);
+        for (char c = inString.first(); c != CharacterIterator.DONE; c = inString.next()) {
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Evaluate whether the breakpointIdx is a potential breakpoint.
+     *
+     * @param inputStr     An input string to be segmented.
+     * @param indexList    A code unit index list of the inputStr.
+     * @param startIdx     The start index of the indexList.
+     * @param numCodeUnits The current code unit boundary of the indexList.
+     * @param boundary     A list including the index of the breakpoint.
+     */
+    private void evaluateBreakpoint(String inputStr, int[] indexList, int startIdx,
+            int numCodeUnits, ArrayList<Integer> boundary) {
+        int start = 0, end = 0;
+        int score = fNegativeSum;
+
+        for (int i = 0; i < 6; i++) {
+            // UW1 ~ UW6
+            start = startIdx + i;
+            if (indexList[start] != -1) {
+                end = (indexList[start + 1] != -1) ? indexList[start + 1] : numCodeUnits;
+                score += fModel.get(ModelIndex.kUWStart.getValue() + i).getOrDefault(
+                        inputStr.substring(indexList[start], end), 0);
+            }
+        }
+        for (int i = 0; i < 3; i++) {
+            // BW1 ~ BW3
+            start = startIdx + i + 1;
+            if (indexList[start] != -1 && indexList[start + 1] != -1) {
+                end = (indexList[start + 2] != -1) ? indexList[start + 2] : numCodeUnits;
+                score += fModel.get(ModelIndex.kBWStart.getValue() + i).getOrDefault(
+                        inputStr.substring(indexList[start], end), 0);
+            }
+        }
+        for (int i = 0; i < 4; i++) {
+            // TW1 ~ TW4
+            start = startIdx + i;
+            if (indexList[start] != -1
+                    && indexList[start + 1] != -1
+                    && indexList[start + 2] != -1) {
+                end = (indexList[start + 3] != -1) ? indexList[start + 3] : numCodeUnits;
+                score += fModel.get(ModelIndex.kTWStart.getValue() + i).getOrDefault(
+                        inputStr.substring(indexList[start], end), 0);
+            }
+        }
+        if (score > 0) {
+            boundary.add(startIdx + 1);
+        }
+    }
+
+    /**
+     * Initialize the index list from the input string.
+     *
+     * @param inString        An input string to be segmented.
+     * @param indexList       A code unit index list of the inString.
+     * @param codePointLength The number of code points of the input string
+     * @return The number of the code units of the first six characters in inString.
+     */
+    private int initIndexList(CharacterIterator inString, int[] indexList, int codePointLength) {
+        int index = 0;
+        inString.setIndex(index);
+        Arrays.fill(indexList, -1);
+        if (codePointLength > 0) {
+            indexList[2] = 0;
+            index += Character.charCount(current32(inString));
+            if (codePointLength > 1) {
+                indexList[3] = index;
+                index += Character.charCount(next32(inString));
+                if (codePointLength > 2) {
+                    indexList[4] = index;
+                    index += Character.charCount(next32(inString));
+                    if (codePointLength > 3) {
+                        indexList[5] = index;
+                        index += Character.charCount(next32(inString));
+                    }
+                }
+            }
+        }
+        return index;
+    }
+
+    /**
+     * Load the machine learning's model file.
+     */
+    private void loadMLModel() {
+        int index = 0;
+        UResourceBundle rb = UResourceBundle.getBundleInstance(ICUData.ICU_BRKITR_BASE_NAME,
+                "jaml");
+        initKeyValue(rb, "UW1Keys", "UW1Values", fModel.get(index++));
+        initKeyValue(rb, "UW2Keys", "UW2Values", fModel.get(index++));
+        initKeyValue(rb, "UW3Keys", "UW3Values", fModel.get(index++));
+        initKeyValue(rb, "UW4Keys", "UW4Values", fModel.get(index++));
+        initKeyValue(rb, "UW5Keys", "UW5Values", fModel.get(index++));
+        initKeyValue(rb, "UW6Keys", "UW6Values", fModel.get(index++));
+        initKeyValue(rb, "BW1Keys", "BW1Values", fModel.get(index++));
+        initKeyValue(rb, "BW2Keys", "BW2Values", fModel.get(index++));
+        initKeyValue(rb, "BW3Keys", "BW3Values", fModel.get(index++));
+        initKeyValue(rb, "TW1Keys", "TW1Values", fModel.get(index++));
+        initKeyValue(rb, "TW2Keys", "TW2Values", fModel.get(index++));
+        initKeyValue(rb, "TW3Keys", "TW3Values", fModel.get(index++));
+        initKeyValue(rb, "TW4Keys", "TW4Values", fModel.get(index++));
+        fNegativeSum /= 2;
+    }
+
+    /**
+     * In the machine learning's model file, specify the name of the key and value to load the
+     * corresponding feature and its score.
+     *
+     * @param rb        A RedouceBundle corresponding to the model file.
+     * @param keyName   The kay name in the model file.
+     * @param valueName The value name in the model file.
+     * @param map       A HashMap to store the pairs of the feature and its score.
+     */
+    private void initKeyValue(UResourceBundle rb, String keyName, String valueName,
+            HashMap<String, Integer> map) {
+        int idx = 0;
+        UResourceBundle keyBundle = rb.get(keyName);
+        UResourceBundle valueBundle = rb.get(valueName);
+        int[] value = valueBundle.getIntVector();
+        UResourceBundleIterator iterator = keyBundle.getIterator();
+        while (iterator.hasNext()) {
+            fNegativeSum -= value[idx];
+            map.put(iterator.nextString(), value[idx++]);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/coll/CollationDataBuilder.java b/android_icu4j/src/main/java/android/icu/impl/coll/CollationDataBuilder.java
index 80a8487..ae1ac74 100644
--- a/android_icu4j/src/main/java/android/icu/impl/coll/CollationDataBuilder.java
+++ b/android_icu4j/src/main/java/android/icu/impl/coll/CollationDataBuilder.java
@@ -55,7 +55,7 @@
         trie = null;
         ce32s = new UVector32();
         ce64s = new UVector64();
-        conditionalCE32s = new ArrayList<ConditionalCE32>();
+        conditionalCE32s = new ArrayList<>();
         modified = false;
         fastLatinEnabled = false;
         fastLatinBuilder = null;
@@ -386,13 +386,25 @@
          * When fetching CEs from the builder, the contexts are built into their runtime form
          * so that the normal collation implementation can process them.
          * The result is cached in the list head. It is reset when the contexts are modified.
+         * All of these builtCE32 are invalidated by clearContexts(),
+         * via incrementing the contextsEra.
          */
         int builtCE32;
         /**
+         * The "era" of building intermediate contexts when the above builtCE32 was set.
+         * When the array of cached, temporary contexts overflows, then clearContexts()
+         * removes them all and invalidates the builtCE32 that used to point to built tries.
+         */
+        int era = -1;
+        /**
          * Index of the next ConditionalCE32.
          * Negative for the end of the list.
          */
         int next;
+        // Note: We could create a separate class for all of the contextual mappings for
+        // a code point, with the builtCE32, the era, and a list of the actual mappings.
+        // The class that represents one mapping would then not need to
+        // store those fields in each element.
     }
 
     protected int getCE32FromOffsetCE32(boolean fromBase, int c, int ce32) {
@@ -416,7 +428,7 @@
         for(int i = 0; i < length; ++i) {
             if(ce32 == ce32s.elementAti(i)) { return i; }
         }
-        ce32s.addElement(ce32);  
+        ce32s.addElement(ce32);
         return length;
     }
 
@@ -990,19 +1002,16 @@
 
     protected void clearContexts() {
         contexts.setLength(0);
-        UnicodeSetIterator iter = new UnicodeSetIterator(contextChars);
-        while(iter.next()) {
-            assert(iter.codepoint != UnicodeSetIterator.IS_STRING);
-            int ce32 = trie.get(iter.codepoint);
-            assert(isBuilderContextCE32(ce32));
-            getConditionalCE32ForCE32(ce32).builtCE32 = Collation.NO_CE32;
-        }
+        // Incrementing the contexts build "era" invalidates all of the builtCE32
+        // from before this clearContexts() call.
+        // Simpler than finding and resetting all of those fields.
+        ++contextsEra;
     }
 
     protected void buildContexts() {
         // Ignore abandoned lists and the cached builtCE32,
         // and build all contexts from scratch.
-        contexts.setLength(0);
+        clearContexts();
         UnicodeSetIterator iter = new UnicodeSetIterator(contextChars);
         while(iter.next()) {
             assert(iter.codepoint != UnicodeSetIterator.IS_STRING);
@@ -1022,8 +1031,12 @@
         assert(!head.hasContext());
         // The list head must be followed by one or more nodes that all do have context.
         assert(head.next >= 0);
-        CharsTrieBuilder prefixBuilder = new CharsTrieBuilder();
-        CharsTrieBuilder contractionBuilder = new CharsTrieBuilder();
+        CharsTrieBuilder prefixBuilder = null;
+        CharsTrieBuilder contractionBuilder = null;
+        // This outer loop goes from each prefix to the next.
+        // For each prefix it finds the one or more same-prefix entries (firstCond..lastCond).
+        // If there are multiple suffixes for the same prefix,
+        // then an inner loop builds a contraction trie for them.
         for(ConditionalCE32 cond = head;; cond = getConditionalCE32(cond.next)) {
             // After the list head, the prefix or suffix can be empty, but not both.
             assert(cond == head || cond.hasContext());
@@ -1032,11 +1045,22 @@
             String prefixString = prefix.toString();
             // Collect all contraction suffixes for one prefix.
             ConditionalCE32 firstCond = cond;
-            ConditionalCE32 lastCond = cond;
-            while(cond.next >= 0 &&
-                    (cond = getConditionalCE32(cond.next)).context.startsWith(prefixString)) {
+            ConditionalCE32 lastCond;
+            do {
                 lastCond = cond;
-            }
+                // Clear the defaultCE32 fields as we go.
+                // They are left over from building a previous version of this list of contexts.
+                //
+                // One of the code paths below may copy a preceding defaultCE32
+                // into its emptySuffixCE32.
+                // If a new suffix has been inserted before what used to be
+                // the firstCond for its prefix, then that previous firstCond could still
+                // contain an outdated defaultCE32 from an earlier buildContext() and
+                // result in an incorrect emptySuffixCE32.
+                // So we reset all defaultCE32 before reading and setting new values.
+                cond.defaultCE32 = Collation.NO_CE32;
+            } while(cond.next >= 0 &&
+                    (cond = getConditionalCE32(cond.next)).context.startsWith(prefixString));
             int ce32;
             int suffixStart = prefixLength + 1;  // == prefix.length()
             if(lastCond.context.length() == suffixStart) {
@@ -1046,7 +1070,11 @@
                 cond = lastCond;
             } else {
                 // Build the contractions trie.
-                contractionBuilder.clear();
+                if(contractionBuilder == null) {
+                    contractionBuilder = new CharsTrieBuilder();
+                } else {
+                    contractionBuilder.clear();
+                }
                 // Entry for an empty suffix, to be stored before the trie.
                 int emptySuffixCE32 = Collation.NO_CE32;  // Will always be set to a real value.
                 int flags = 0;
@@ -1115,6 +1143,9 @@
             } else {
                 prefix.delete(0, 1);  // Remove the length unit.
                 prefix.reverse();
+                if(prefixBuilder == null) {
+                    prefixBuilder = new CharsTrieBuilder();
+                }
                 prefixBuilder.add(prefix, ce32);
                 if(cond.next < 0) { break; }
             }
@@ -1305,7 +1336,7 @@
                 return builder.trie.get(jamo);
             } else {
                 ConditionalCE32 cond = builder.getConditionalCE32ForCE32(ce32);
-                if(cond.builtCE32 == Collation.NO_CE32) {
+                if(cond.builtCE32 == Collation.NO_CE32 || cond.era != builder.contextsEra) {
                     // Build the context-sensitive mappings into their runtime form and cache the result.
                     try {
                         cond.builtCE32 = builder.buildContext(cond);
@@ -1313,6 +1344,7 @@
                         builder.clearContexts();
                         cond.builtCE32 = builder.buildContext(cond);
                     }
+                    cond.era = builder.contextsEra;
                     builderData.contexts = builder.contexts.toString();
                 }
                 return cond.builtCE32;
@@ -1346,6 +1378,13 @@
     protected UnicodeSet contextChars = new UnicodeSet();
     // Serialized UCharsTrie structures for finalized contexts.
     protected StringBuilder contexts = new StringBuilder();
+    /**
+     * The "era" of building intermediate contexts.
+     * When the array of cached, temporary contexts overflows, then clearContexts()
+     * removes them all and invalidates the builtCE32 that used to point to built tries.
+     * See {@link ConditionalCE32#era}.
+     */
+    private int contextsEra = 0;
     protected UnicodeSet unsafeBackwardSet = new UnicodeSet();
     protected boolean modified;
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/coll/CollationFCD.java b/android_icu4j/src/main/java/android/icu/impl/coll/CollationFCD.java
index 533faee..e11b39d 100644
--- a/android_icu4j/src/main/java/android/icu/impl/coll/CollationFCD.java
+++ b/android_icu4j/src/main/java/android/icu/impl/coll/CollationFCD.java
@@ -256,7 +256,7 @@
 0x200ff800,0xfbc00000,0x3eef,0xe000000,0xff000000,0xfffffc00,0xfffffffb,0x10000000,0x1e2000,0x2000,0x40000000,0x602000,0x18000000,0x400,0x7000000,0xf00,
 0x3000000,0x2a00000,0x3c3e0000,0xdf,0x40,0x6800000,0xe0000000,0x300000,0x100000,0x20040000,0x200,0x1800000,0x9fe00001,0xbfff0000,0x7fff,0x10,
 0xff800,0xc00,0xc0040,0x800000,0xfff70000,0x31021fd,0x1fff0000,0x1ffe2,0x38000,0x80000000,0xfc00,0x6000000,0x3ff08000,0xc0000000,0x30000,0x1000,
-0x3ffff,0x3800,0x80000,1,0xc19d0000,2,0x400000,0xc0000fd,0x5108000
+0x3ffff,0x3800,0x80000,1,0xc19d0000,2,0x400000,0xc0000fd,0x7108000
 };
 
     private static final byte[] tcccIndex={
@@ -398,7 +398,7 @@
 0xe0000000,0x300000,0x100000,0x20040000,0x200,0x1800000,0x9fe00001,0xbfff0000,0x7fff,0x10,0xff800,0xc00,0xc0040,0x800000,0xfff70000,0x31021fd,
 0xbffffff,0x3ffffff,0x3f3fffff,0xaaff3f3f,0x3fffffff,0x1fdfffff,0xefcfffde,0x1fdc7fff,0x1fff0000,0x1ffe2,0x800,0xc000000,0x4000,0xe000,0x1210,0x50,
 0x292,0x333e005,0x333,0xf000,0x3c0f,0x38000,0x80000000,0xfc00,0x55555000,0x36db02a5,0x46100000,0x47900000,0x3ff08000,0xc0000000,0x30000,0x1000,
-0x3ffff,0x3800,0x80000,1,0xc19d0000,2,0x400000,0xc0000fd,0x5108000,0x5f7ffc00,0x7fdb
+0x3ffff,0x3800,0x80000,1,0xc19d0000,2,0x400000,0xc0000fd,0x7108000,0x5f7ffc00,0x7fdb
 };
 
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/AffixUtils.java b/android_icu4j/src/main/java/android/icu/impl/number/AffixUtils.java
index 4bacf6d..7f7ffca 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/AffixUtils.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/AffixUtils.java
@@ -279,8 +279,7 @@
         case TYPE_PLUS_SIGN:
             return NumberFormat.Field.SIGN;
         case TYPE_APPROXIMATELY_SIGN:
-            // TODO: Introduce a new field for the approximately sign?
-            return NumberFormat.Field.SIGN;
+            return NumberFormat.Field.APPROXIMATELY_SIGN;
         case TYPE_PERCENT:
             return NumberFormat.Field.PERCENT;
         case TYPE_PERMILLE:
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/CompactData.java b/android_icu4j/src/main/java/android/icu/impl/number/CompactData.java
index 4b33b63..0143cb5 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/CompactData.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/CompactData.java
@@ -39,7 +39,7 @@
     private byte largestMagnitude;
     private boolean isEmpty;
 
-    private static final int COMPACT_MAX_DIGITS = 15;
+    private static final int COMPACT_MAX_DIGITS = 20;
 
     public CompactData() {
         patterns = new String[(CompactData.COMPACT_MAX_DIGITS + 1) * StandardPlural.COUNT];
@@ -190,8 +190,10 @@
                 // Assumes that the keys are always of the form "10000" where the magnitude is the
                 // length of the key minus one. We expect magnitudes to be less than MAX_DIGITS.
                 byte magnitude = (byte) (key.length() - 1);
+                if (magnitude >= COMPACT_MAX_DIGITS) {
+                    continue;
+                }
                 byte multiplier = data.multipliers[magnitude];
-                assert magnitude < COMPACT_MAX_DIGITS;
 
                 // Iterate over the plural variants ("one", "other", etc)
                 UResource.Table pluralVariantsTable = value.getTable();
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java
index b73fc87..2fcb091 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity.java
@@ -233,6 +233,12 @@
     public String toPlainString();
 
     /**
+     * Returns the string using ASCII digits and using exponential notation for non-zero
+     * exponents, following the UTS 35 specification for plural rule samples.
+     */
+    public String toExponentString();
+
+    /**
      * Like clone, but without the restrictions of the Cloneable interface clone.
      *
      * @return A copy of this instance which can be mutated without affecting this instance.
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java
index 046024e..6e24f96 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_AbstractBCD.java
@@ -1124,6 +1124,48 @@
     }
 
     @Override
+    public String toExponentString() {
+        StringBuilder sb = new StringBuilder();
+        toExponentString(sb);
+        return sb.toString();
+    }
+
+    private void toExponentString(StringBuilder result) {
+        assert(!isApproximate);
+        if (isNegative()) {
+            result.append('-');
+        }
+
+        int upper = scale + precision - 1;
+        int lower = scale;
+        if (upper < lReqPos - 1) {
+            upper = lReqPos - 1;
+        }
+        if (lower > rReqPos) {
+            lower = rReqPos;
+        }
+
+        int p = upper;
+        if (p < 0) {
+            result.append('0');
+        }
+        for (; p >= 0; p--) {
+            result.append((char) ('0' + getDigitPos(p - scale)));
+        }
+        if (lower < 0) {
+            result.append('.');
+        }
+        for(; p >= lower; p--) {
+            result.append((char) ('0' + getDigitPos(p - scale)));
+        }
+
+        if (exponent != 0) {
+            result.append('c');
+            result.append(exponent);
+        }
+    }
+
+    @Override
     public boolean equals(Object other) {
         if (this == other) {
             return true;
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java
index e224259..76087ca 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalQuantity_DualStorageBCD.java
@@ -90,6 +90,54 @@
         return new DecimalQuantity_DualStorageBCD(this);
     }
 
+    /**
+     * Returns a DecimalQuantity after parsing the input string.
+     *
+     * @param s The String to parse
+     */
+    public static DecimalQuantity fromExponentString(String num) {
+        if (num.contains("e") || num.contains("c")
+                || num.contains("E") || num.contains("C")) {
+            int ePos = num.lastIndexOf('e');
+            if (ePos < 0) {
+                ePos = num.lastIndexOf('c');
+            }
+            if (ePos < 0) {
+                ePos = num.lastIndexOf('E');
+            }
+            if (ePos < 0) {
+                ePos = num.lastIndexOf('C');
+            }
+            int expNumPos = ePos + 1;
+            String exponentStr = num.substring(expNumPos);
+            int exponent = Integer.parseInt(exponentStr);
+
+            String fractionStr = num.substring(0, ePos);
+            BigDecimal fraction = new BigDecimal(fractionStr);
+
+            DecimalQuantity_DualStorageBCD dq = new DecimalQuantity_DualStorageBCD(fraction);
+            int numFracDigit = getVisibleFractionCount(fractionStr);
+            dq.setMinFraction(numFracDigit);
+            dq.adjustExponent(exponent);
+
+            return dq;
+        } else {
+            int numFracDigit = getVisibleFractionCount(num);
+            DecimalQuantity_DualStorageBCD dq = new DecimalQuantity_DualStorageBCD(new BigDecimal(num));
+            dq.setMinFraction(numFracDigit);
+            return dq;
+        }
+    }
+
+    private static int getVisibleFractionCount(String value) {
+        int decimalPos = value.indexOf('.') + 1;
+        if (decimalPos == 0) {
+            return 0;
+        } else {
+            return value.length() - decimalPos;
+        }
+    }
+
     @Override
     protected byte getDigitPos(int position) {
         if (usingBytes) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java b/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java
index 34f5595..39bb3e9 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/LongNameHandler.java
@@ -297,11 +297,6 @@
             // Continue: fall back to short
         }
 
-        // TODO(ICU-13353): The ICU4J fallback mechanism works differently:
-        // investigate? Without this code, unit tests do fail:
-        key.setLength(0);
-        key.append("unitsShort/");
-        key.append(subKey);
         unitsBundle.getAllItemsWithFallback(key.toString(), sink);
     }
 
@@ -336,6 +331,21 @@
         }
     }
 
+    private static final class AliasSink extends UResource.Sink {
+        String replacement;
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+            UResource.Table aliasTable = value.getTable();
+            for (int i = 0; aliasTable.getKeyAndValue(i, key, value); ++i) {
+                String keyString = key.toString();
+                if (keyString.equals("replacement")) {
+                    this.replacement = value.toString();
+                }
+            }
+        }
+    }
+
     // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed.
     static void getMeasureData(
             ULocale locale,
@@ -353,12 +363,23 @@
         subKey.append(unit.getType());
         subKey.append("/");
 
+        // If the unit is an alias, replace it is identifier with the replacement.
+        String unitSubType = unit.getSubtype();
+        ICUResourceBundle metadataResource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "metadata");
+        AliasSink aliasSink = new AliasSink();
+
+        metadataResource.getAllItemsWithFallbackNoFail("alias/unit/" + unitSubType, aliasSink);
+        if (aliasSink.replacement != null) {
+            unitSubType = aliasSink.replacement;
+        }
+
+
         // Map duration-year-person, duration-week-person, etc. to duration-year, duration-week, ...
         // TODO(ICU-20400): Get duration-*-person data properly with aliases.
-        if (unit.getSubtype() != null && unit.getSubtype().endsWith("-person")) {
-            subKey.append(unit.getSubtype(), 0, unit.getSubtype().length() - 7);
+        if (unitSubType != null && unitSubType.endsWith("-person")) {
+            subKey.append(unitSubType, 0, unitSubType.length() - 7);
         } else {
-            subKey.append(unit.getSubtype());
+            subKey.append(unitSubType);
         }
 
         if (width != UnitWidth.FULL_NAME) {
@@ -457,11 +478,6 @@
             }
         }
 
-        // TODO(icu-units#28): this code is not exercised by unit tests yet.
-        // Also, what's the fallback mechanism mentioned in ICU-13353?
-        key.setLength(0);
-        key.append("unitsShort/compound/");
-        key.append(compoundKey);
         try {
             return resource.getStringWithFallback(key.toString());
         } catch (MissingResourceException e) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/UsagePrefsHandler.java b/android_icu4j/src/main/java/android/icu/impl/number/UsagePrefsHandler.java
index a9a299a..683eaa6 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/UsagePrefsHandler.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/UsagePrefsHandler.java
@@ -24,8 +24,7 @@
         assert parent != null;
 
         this.fParent = parent;
-        this.fUnitsRouter =
-                new UnitsRouter(MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier()), locale.getCountry(), usage);
+        this.fUnitsRouter = new UnitsRouter(MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier()), locale, usage);
     }
 
     /**
diff --git a/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java b/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java
new file mode 100644
index 0000000..6d0918c
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java
@@ -0,0 +1,157 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.impl.personname;
+
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import android.icu.lang.UCharacter;
+import android.icu.text.BreakIterator;
+import android.icu.text.CaseMap;
+import android.icu.text.PersonName;
+import android.icu.text.SimpleFormatter;
+
+/**
+ * Parent class for classes that implement field-modifier behavior.
+ */
+abstract class FieldModifierImpl {
+    public abstract String modifyField(String fieldValue);
+
+    public static FieldModifierImpl forName(PersonName.FieldModifier modifierID, PersonNameFormatterImpl formatterImpl) {
+        switch (modifierID) {
+            case INFORMAL:
+                return NOOP_MODIFIER;
+            case PREFIX:
+                return NULL_MODIFIER;
+            case CORE:
+                return NOOP_MODIFIER;
+            case ALL_CAPS:
+                return new AllCapsModifier(formatterImpl.getLocale());
+            case INITIAL_CAP:
+                return new InitialCapModifier(formatterImpl.getLocale());
+            case INITIAL:
+                return new InitialModifier(formatterImpl.getInitialPattern(), formatterImpl.getInitialSequencePattern());
+            case MONOGRAM:
+                return MONOGRAM_MODIFIER;
+            default:
+                throw new IllegalArgumentException("Invalid modifier ID " + modifierID);
+        }
+    }
+
+    /**
+     * A field modifier that just returns the field value unmodified.  This is used to implement the default
+     * behavior of the "informal" and "core" modifiers ("real" informal or core variants have to be supplied or
+     * calculated by the PersonName object).
+     */
+    private static final FieldModifierImpl NOOP_MODIFIER = new FieldModifierImpl() {
+        @Override
+        public String modifyField(String fieldValue) {
+            return fieldValue;
+        }
+    };
+
+    /**
+     * A field modifier that just returns the empty string.  This is used to implement the default behavior of the
+     * "prefix" modifier ("real" prefix variants have to be supplied to calculated by the PersonName object).
+     */
+    private static final FieldModifierImpl NULL_MODIFIER = new FieldModifierImpl() {
+        @Override
+        public String modifyField(String fieldValue) {
+            return "";
+        }
+    };
+
+    /**
+     * A field modifier that returns the field value converted to ALL CAPS.  This is the default behavior
+     * for the "allCaps" modifier.
+     */
+    private static class AllCapsModifier extends FieldModifierImpl {
+        private final Locale locale;
+
+        public AllCapsModifier(Locale locale) {
+            this.locale = locale;
+        }
+
+        @Override
+        public String modifyField(String fieldValue) {
+            return UCharacter.toUpperCase(locale, fieldValue);
+        }
+    }
+
+    /**
+     * A field modifier that returns the field value with the first letter of each word capitalized.  This is
+     * the default behavior of the "initialCap" modifier.
+     */
+    private static class InitialCapModifier extends FieldModifierImpl {
+        private final Locale locale;
+        private static final CaseMap.Title TO_TITLE_WHOLE_STRING_NO_LOWERCASE = CaseMap.toTitle().wholeString().noLowercase();
+
+        public InitialCapModifier(Locale locale) {
+            this.locale = locale;
+        }
+
+        @Override
+        public String modifyField(String fieldValue) {
+            return TO_TITLE_WHOLE_STRING_NO_LOWERCASE.apply(locale, null, fieldValue);
+        }
+    }
+
+    /**
+     * A field modifier that returns the field value converted into one or more initials.  This is the first grapheme
+     * cluster of each word in the field value, modified using the initialPattern/initial resource value from the
+     * locale data, and strung together using the initialPattern/initialSequence resource value from the locale data.
+     * (In English, these patterns put periods after each initial and connect them with spaces.)
+     * This is default behavior of the "initial" modifier.
+     */
+    private static class InitialModifier extends FieldModifierImpl {
+        private final SimpleFormatter initialFormatter;
+        private final SimpleFormatter initialSequenceFormatter;
+
+        public InitialModifier(String initialPattern, String initialSequencePattern) {
+            this.initialFormatter = SimpleFormatter.compile(initialPattern);
+            this.initialSequenceFormatter = SimpleFormatter.compile(initialSequencePattern);
+        }
+
+        @Override
+        public String modifyField(String fieldValue) {
+            String result = null;
+            StringTokenizer tok = new StringTokenizer(fieldValue, " ");
+            while (tok.hasMoreTokens()) {
+                String curInitial = getFirstGrapheme(tok.nextToken());
+                if (result == null) {
+                    result = initialFormatter.format(curInitial);
+                } else {
+                    result = initialSequenceFormatter.format(result, initialFormatter.format(curInitial));
+                }
+            }
+            return result;
+        }
+    }
+
+    /**
+     * A field modifier that simply returns the first grapheme cluster in the field value.
+     * This is the default implementation of the "monogram" modifier.
+     */
+    private static final FieldModifierImpl MONOGRAM_MODIFIER = new FieldModifierImpl() {
+        @Override
+        public String modifyField(String fieldValue) {
+            return getFirstGrapheme(fieldValue);
+        }
+    };
+
+    /**
+     * A utility function that just returns the first grapheme cluster in the string.
+     */
+    private static String getFirstGrapheme(String s) {
+        // early out if the string is empty to avoid StringIndexOutOfBoundsException
+        if (s.isEmpty()) {
+            return "";
+        }
+
+        // (currently, no locale overrides the grapheme-break rules, so we just use "root" instead of passing in the locale)
+        BreakIterator bi = BreakIterator.getCharacterInstance(Locale.ROOT);
+        bi.setText(s);
+        return s.substring(0, bi.next());
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java
new file mode 100644
index 0000000..98473da
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java
@@ -0,0 +1,330 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.impl.personname;
+
+import static android.icu.util.UResourceBundle.ARRAY;
+import static android.icu.util.UResourceBundle.STRING;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import android.icu.impl.ICUData;
+import android.icu.impl.ICUResourceBundle;
+import android.icu.lang.UScript;
+import android.icu.text.PersonName;
+import android.icu.text.PersonNameFormatter;
+import android.icu.util.ULocale;
+import android.icu.util.UResourceBundle;
+
+/**
+ * Actual implementation class for PersonNameFormatter.
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class PersonNameFormatterImpl {
+    private final Locale locale;
+    private final PersonNamePattern[] gnFirstPatterns;
+    private final PersonNamePattern[] snFirstPatterns;
+    private final Set<String> gnFirstLocales;
+    private final Set<String> snFirstLocales;
+    private final String initialPattern;
+    private final String initialSequencePattern;
+    private final boolean capitalizeSurname;
+    private final String foreignSpaceReplacement;
+    private final boolean formatterLocaleUsesSpaces;
+    private final PersonNameFormatter.Length length;
+    private final PersonNameFormatter.Usage usage;
+    private final PersonNameFormatter.Formality formality;
+    private final Set<PersonNameFormatter.Options> options;
+
+    public PersonNameFormatterImpl(Locale locale,
+                                   PersonNameFormatter.Length length,
+                                   PersonNameFormatter.Usage usage,
+                                   PersonNameFormatter.Formality formality,
+                                   Set<PersonNameFormatter.Options> options) {
+        // null for `options` is the same as the empty set
+        if (options == null) {
+            options = new HashSet<>();
+        }
+
+        // save off our creation parameters (these are only used if we have to create a second formatter)
+        this.length = length;
+        this.usage = usage;
+        this.formality = formality;
+        this.options = options;
+
+        // load simple property values from the resource bundle (or the options set)
+        ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
+        this.locale = locale;
+        this.initialPattern = rb.getStringWithFallback("personNames/initialPattern/initial");
+        this.initialSequencePattern = rb.getStringWithFallback("personNames/initialPattern/initialSequence");
+        this.capitalizeSurname = options.contains(PersonNameFormatter.Options.SURNAME_ALLCAPS);
+        this.foreignSpaceReplacement = rb.getStringWithFallback("personNames/foreignSpaceReplacement");
+        this.formatterLocaleUsesSpaces = !LOCALES_THAT_DONT_USE_SPACES.contains(locale.getLanguage());
+
+        // asjust for combinations of parameters that don't make sense in practice
+        if (usage == PersonNameFormatter.Usage.MONOGRAM) {
+            // we don't support SORTING in conjunction with MONOGRAM; if the caller passes in SORTING, remove it from
+            // the options list
+            options.remove(PersonNameFormatter.Options.SORTING);
+        } else if (options.contains(PersonNameFormatter.Options.SORTING)) {
+            // we only support SORTING in conjunction with REFERRING; if the caller passes in ADDRESSING, treat it
+            // the same as REFERRING
+            usage = PersonNameFormatter.Usage.REFERRING;
+        }
+
+        // load the actual formatting patterns-- since we don't know the name order until formatting time (it can be
+        // different for different names), load patterns for both given-first and surname-first names.  (If the user has
+        // specified SORTING, we don't need to do this-- we just load the "sorting" patterns and ignore the name's order.)
+        final String RESOURCE_PATH_PREFIX = "personNames/namePattern/";
+        String resourceNameBody = length.toString().toLowerCase() + "-" + usage.toString().toLowerCase() + "-"
+                + formality.toString().toLowerCase();
+        if (!options.contains(PersonNameFormatter.Options.SORTING)) {
+            ICUResourceBundle gnFirstResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "givenFirst-" + resourceNameBody);
+            ICUResourceBundle snFirstResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "surnameFirst-" + resourceNameBody);
+
+            gnFirstPatterns = PersonNamePattern.makePatterns(asStringArray(gnFirstResource), this);
+            snFirstPatterns = PersonNamePattern.makePatterns(asStringArray(snFirstResource), this);
+
+            gnFirstLocales = new HashSet<>();
+            Collections.addAll(gnFirstLocales, asStringArray(rb.getWithFallback("personNames/nameOrderLocales/givenFirst")));
+            snFirstLocales = new HashSet<>();
+            Collections.addAll(snFirstLocales, asStringArray(rb.getWithFallback("personNames/nameOrderLocales/surnameFirst")));
+        } else {
+            ICUResourceBundle patternResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "sorting-" + resourceNameBody);
+
+            gnFirstPatterns = PersonNamePattern.makePatterns(asStringArray(patternResource), this);
+            snFirstPatterns = null;
+            gnFirstLocales = null;
+            snFirstLocales = null;
+        }
+    }
+
+    /**
+     * THIS IS A DUMMY CONSTRUCTOR JUST FOR THE USE OF THE UNIT TESTS TO CHECK SOME OF THE INTERNAL IMPLEMENTATION!
+     */
+    public PersonNameFormatterImpl(Locale locale, String[] patterns) {
+        // first, set dummy values for the other fields
+        snFirstPatterns = null;
+        gnFirstLocales = null;
+        snFirstLocales = null;
+        length = PersonNameFormatter.Length.MEDIUM;
+        usage = PersonNameFormatter.Usage.REFERRING;
+        formality = PersonNameFormatter.Formality.FORMAL;
+        options = Collections.emptySet();
+        initialPattern = "{0}.";
+        initialSequencePattern = "{0} {1}";
+        capitalizeSurname = false;
+        foreignSpaceReplacement = " ";
+        formatterLocaleUsesSpaces = true;
+
+        // then, set values for the fields we actually care about
+        this.locale = locale;
+        gnFirstPatterns = PersonNamePattern.makePatterns(patterns, this);
+
+    }
+
+    public String formatToString(PersonName name) {
+        // TODO: Should probably return a FormattedPersonName object
+
+        // if the formatter is for a language that doesn't use spaces between words and the name is from a language
+        // that does, create a formatter for the NAME'S locale and use THAT to format the name
+        Locale nameLocale = getNameLocale(name);
+        boolean nameLocaleUsesSpaces = !LOCALES_THAT_DONT_USE_SPACES.contains(nameLocale.getLanguage());
+        if (!formatterLocaleUsesSpaces && nameLocaleUsesSpaces) {
+            PersonNameFormatterImpl nativeFormatter = new PersonNameFormatterImpl(nameLocale, this.length,
+                    this.usage, this.formality, this.options);
+            String result = nativeFormatter.formatToString(name);
+
+            // BUT, if the name is actually written in the formatter locale's script, replace any spaces in the name
+            // with the foreignSpaceReplacement character
+            if (!foreignSpaceReplacement.equals(" ") && scriptMatchesLocale(result, this.locale)) {
+                result = result.replace(" ", this.foreignSpaceReplacement);
+            }
+            return result;
+        }
+
+        // if we get down to here, we're just doing normal formatting-- if we have both given-first and surname-first
+        // rules, choose which one to use based on the name's locale and preferred field order
+        if (snFirstPatterns == null || nameIsGnFirst(name)) {
+            return getBestPattern(gnFirstPatterns, name).format(name);
+        } else {
+            return getBestPattern(snFirstPatterns, name).format(name);
+        }
+    }
+
+    public Locale getLocale() {
+        return locale;
+    }
+
+    public PersonNameFormatter.Length getLength() { return length; }
+
+    public PersonNameFormatter.Usage getUsage() { return usage; }
+
+    public PersonNameFormatter.Formality getFormality() { return formality; }
+
+    public Set<PersonNameFormatter.Options> getOptions() { return options; }
+
+    public String getInitialPattern() {
+        return initialPattern;
+    }
+
+    public String getInitialSequencePattern() {
+        return initialSequencePattern;
+    }
+
+    public boolean shouldCapitalizeSurname() {
+        return capitalizeSurname;
+    }
+
+    private final Set<String> LOCALES_THAT_DONT_USE_SPACES = new HashSet<>(Arrays.asList("ja", "zh", "th", "yue", "km", "lo"));
+
+    /**
+     * Returns the value of the resource, as a string array.
+     * @param resource An ICUResourceBundle of type STRING or ARRAY.  If ARRAY, this function just returns it
+     *                 as a string array.  If STRING, it returns a one-element array containing that string.
+     * @return The resource's value, as an array of Strings.
+     */
+    private String[] asStringArray(ICUResourceBundle resource) {
+        if (resource.getType() == STRING) {
+            return new String[] { resource.getString() };
+        } else if (resource.getType() == ARRAY){
+            return resource.getStringArray();
+        } else {
+            throw new IllegalStateException("Unsupported resource type " + resource.getType());
+        }
+    }
+
+    /**
+     * Returns the field order to use when formatting this name, taking into account the name's preferredOrder
+     * field, as well as the name and formatter's respective locales.
+     * @param name The name to be formatted.
+     * @return If true, use given-first order to format the name; if false, use surname-first order.
+     */
+    private boolean nameIsGnFirst(PersonName name) {
+        // the name can declare its order-- check that first (it overrides any locale-based calculation)
+        Set<PersonName.FieldModifier> modifiers = new HashSet<>();
+        String preferredOrder = name.getFieldValue(PersonName.NameField.PREFERRED_ORDER, modifiers);
+        if (preferredOrder != null) {
+            if (preferredOrder.equals("givenFirst")) {
+                return true;
+            } else if (preferredOrder.equals("surnameFirst")) {
+                return false;
+            } else {
+                throw new IllegalArgumentException("Illegal preferredOrder value " + preferredOrder);
+            }
+        }
+
+        String localeStr = getNameLocale(name).toString();
+        do {
+            if (gnFirstLocales.contains(localeStr)) {
+                return true;
+            } else if (snFirstLocales.contains(localeStr)) {
+                return false;
+            }
+
+            int lastUnderbarPos = localeStr.lastIndexOf("_");
+            if (lastUnderbarPos >= 0) {
+                localeStr = localeStr.substring(0, lastUnderbarPos);
+            } else {
+                localeStr = "root";
+            }
+        } while (!localeStr.equals("root"));
+
+        // should never get here-- "root" should always be in one of the locales
+        return true;
+    }
+
+    private PersonNamePattern getBestPattern(PersonNamePattern[] patterns, PersonName name) {
+        // early out if there's only one pattern
+        if (patterns.length == 1) {
+            return patterns[0];
+        } else {
+            // if there's more than one pattern, return the one that contains the greatest number of fields that
+            // actually have values in `name`.  If there's a tie, return the pattern that contains the lowest number
+            // of fields that DON'T have values in `name`.
+            int maxPopulatedFields = 0;
+            int minEmptyFields = Integer.MAX_VALUE;
+            PersonNamePattern bestPattern = null;
+
+            for (PersonNamePattern pattern : patterns) {
+                int populatedFields = pattern.numPopulatedFields(name);
+                int emptyFields = pattern.numEmptyFields(name);
+                if (populatedFields > maxPopulatedFields) {
+                    maxPopulatedFields = populatedFields;
+                    minEmptyFields = emptyFields;
+                    bestPattern = pattern;
+                } else if (populatedFields == maxPopulatedFields && emptyFields < minEmptyFields) {
+                    minEmptyFields = emptyFields;
+                    bestPattern = pattern;
+                }
+            }
+            return bestPattern;
+        }
+    }
+
+    /**
+     * Internal function to figure out the name's locale when the name doesn't specify it.
+     * (Note that this code assumes that if the locale is specified, it includes a language
+     * code.)
+     * @param name The name for which we need the locale
+     * @return The name's (real or guessed) locale.
+     */
+    private Locale getNameLocale(PersonName name) {
+        // if the name specifies its locale, we can just return it
+        Locale nameLocale = name.getNameLocale();
+        if (nameLocale == null) {
+            // if not, we look at the characters in the name.  If their script matches the default script for the formatter's
+            // locale, we use the formatter's locale as the name's locale
+            int formatterScript = UScript.getCodeFromName(ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript());
+            String givenName = name.getFieldValue(PersonName.NameField.GIVEN, new HashSet<PersonName.FieldModifier>());
+            int nameScript = UScript.INVALID_CODE;
+            for (int i = 0; nameScript == UScript.INVALID_CODE && i < givenName.length(); i++) {
+                // the script of the name is the script of the first character in the name whose script isn't
+                // COMMON or INHERITED
+                int script = UScript.getScript(givenName.charAt(i));
+                if (script != UScript.COMMON && script != UScript.INHERITED) {
+                    nameScript = script;
+                }
+            }
+            if (formatterScript == nameScript) {
+                nameLocale = this.locale;
+            } else {
+                // if the name's script is different from the formatter's script, we use addLikelySubtags() to find the
+                // default language for the name's script and use THAT as the name's locale
+                nameLocale = new Locale(ULocale.addLikelySubtags(new ULocale("und_" + UScript.getShortName(nameScript))).getLanguage());
+            }
+            // TODO: This algorithm has a few deficiencies: First, it assumes the script of the string is the script of the first
+            // character in the string that's not COMMON or INHERITED.  This won't work well for some languages, such as Japanese,
+            // that use multiple scripts.  Doing better would require adding a new getScript(String) method on UScript, which
+            // might be something we want.  Second, we only look at the given-name field.  This field should always be populated,
+            // but if it isn't, we're stuck.  Looking at all the fields requires API on PersonName that we don't need anywhere
+            // else.
+        }
+        return nameLocale;
+    }
+
+    /**
+     * Returns true if the script of `s` is one of the default scripts for `locale`.
+     * This function only checks the script of the first character whose script isn't "common,"
+     * so it probably won't work right on mixed-script strings.
+     */
+    private boolean scriptMatchesLocale(String s, Locale locale) {
+        int[] localeScripts = UScript.getCode(locale);
+        int stringScript = UScript.COMMON;
+        for (int i = 0; stringScript == UScript.COMMON && i < s.length(); i++) {
+            char c = s.charAt(i);
+            stringScript = UScript.getScript(c);
+        }
+
+        for (int localeScript : localeScripts) {
+            if (localeScript == stringScript) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java
new file mode 100644
index 0000000..d612454
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java
@@ -0,0 +1,276 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.impl.personname;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import android.icu.text.PersonName;
+
+/**
+ * A single name formatting pattern, corresponding to a single namePattern element in CLDR.
+ */
+class PersonNamePattern {
+    private String patternText; // for debugging
+    private Element[] patternElements;
+
+    public static PersonNamePattern[] makePatterns(String[] patternText, PersonNameFormatterImpl formatterImpl) {
+        PersonNamePattern[] result = new PersonNamePattern[patternText.length];
+        for (int i = 0; i < patternText.length; i++) {
+            result[i] = new PersonNamePattern(patternText[i], formatterImpl);
+        }
+        return result;
+    }
+
+    private PersonNamePattern(String patternText, PersonNameFormatterImpl formatterImpl) {
+        this.patternText = patternText;
+
+        List<Element> elements = new ArrayList<>();
+        boolean inField = false;
+        boolean inEscape = false;
+        StringBuilder workingString = new StringBuilder();
+        for (int i = 0; i < patternText.length(); i++) {
+            char c = patternText.charAt(i);
+
+            if (inEscape) {
+                workingString.append(c);
+                inEscape = false;
+            } else {
+                switch (c) {
+                    case '\\':
+                        inEscape = true;
+                        break;
+                    case '{':
+                        if (!inField) {
+                            if (workingString.length() > 0) {
+                                elements.add(new LiteralText(workingString.toString()));
+                                workingString = new StringBuilder();
+                            }
+                            inField = true;
+                        } else {
+                            throw new IllegalArgumentException("Nested braces are not allowed in name patterns");
+                        }
+                        break;
+                    case '}':
+                        if (inField) {
+                            if (workingString.length() > 0) {
+                                elements.add(new NameFieldImpl(workingString.toString(), formatterImpl));
+                                workingString = new StringBuilder();
+                            } else {
+                                throw new IllegalArgumentException("No field name inside braces");
+                            }
+                            inField = false;
+                        } else {
+                            throw new IllegalArgumentException("Unmatched closing brace in literal text");
+                        }
+                        break;
+                    default:
+                        workingString.append(c);
+                }
+            }
+        }
+        if (workingString.length() > 0) {
+            elements.add(new LiteralText(workingString.toString()));
+        }
+        this.patternElements = elements.toArray(new Element[0]);
+    }
+
+    public String format(PersonName name) {
+        StringBuilder result = new StringBuilder();
+        boolean seenLeadingField = false;
+        boolean seenEmptyLeadingField = false;
+        boolean seenEmptyField = false;
+        StringBuilder textBefore = new StringBuilder();
+        StringBuilder textAfter = new StringBuilder();
+
+        // the logic below attempts to implement the following algorithm:
+        // - If one or more fields at the beginning of the name are empty, also skip all literal text
+        //   from the beginning of the name up to the first populated field.
+        // - If one or more fields at the end of the name are empty, also skip all literal text from
+        //   the last populated field to the end of the name.
+        // - If one or more contiguous fields in the middle of the name are empty, skip the literal text
+        //   between them, omit characters from the literal text on either side of the empty fields up to
+        //   the first space on either side, and make sure that the resulting literal text doesn't end up
+        //   with two spaces in a row.
+        for (Element element : patternElements) {
+            if (element.isLiteral()) {
+                if (seenEmptyLeadingField) {
+                    // do nothing; throw away the literal text
+                } else if (seenEmptyField) {
+                    textAfter.append(element.format(name));
+                } else {
+                    textBefore.append(element.format(name));
+                }
+            } else {
+                String fieldText = element.format(name);
+                if (fieldText == null || fieldText.isEmpty()) {
+                    if (!seenLeadingField) {
+                        seenEmptyLeadingField = true;
+                        textBefore.setLength(0);
+                    } else {
+                        seenEmptyField = true;
+                        textAfter.setLength(0);
+                    }
+                } else {
+                    seenLeadingField = true;
+                    seenEmptyLeadingField = false;
+                    if (seenEmptyField) {
+                        result.append(coalesce(textBefore, textAfter));
+                        result.append(fieldText);
+                        seenEmptyField = false;
+                    } else {
+                        result.append(textBefore);
+                        textBefore.setLength(0);
+                        result.append(element.format(name));
+                    }
+                }
+            }
+        }
+        if (!seenEmptyField) {
+            result.append(textBefore);
+        }
+        return result.toString();
+    }
+
+    public int numPopulatedFields(PersonName name) {
+        int result = 0;
+        for (Element element : patternElements) {
+            result += element.isPopulated(name) ? 1 : 0;
+        }
+        return result;
+    }
+
+    public int numEmptyFields(PersonName name) {
+        int result = 0;
+        for (Element element : patternElements) {
+            result += element.isPopulated(name) ? 0 : 1;
+        }
+        return result;
+    }
+
+    /**
+     * Stitches together the literal text on either side of an omitted field by deleting any
+     * non-whitespace characters immediately neighboring the omitted field and coalescing any
+     * adjacent spaces at the join point down to one.
+     * @param s1 The literal text before the omitted field.
+     * @param s2 The literal text after the omitted field.
+     */
+    private String coalesce(StringBuilder s1, StringBuilder s2) {
+        // get the range of non-whitespace characters at the beginning of s1
+        int p1 = 0;
+        while (p1 < s1.length() && !Character.isWhitespace(s1.charAt(p1))) {
+            ++p1;
+        }
+
+        // get the range of non-whitespace characters at the end of s2
+        int p2 = s2.length() - 1;
+        while (p2 >= 0 && !Character.isWhitespace(s2.charAt(p2))) {
+            --p2;
+        }
+
+        // also include one whitespace character from s1 or, if there aren't
+        // any, one whitespace character from s2
+        if (p1 < s1.length()) {
+            ++p1;
+        } else if (p2 >= 0) {
+            --p2;
+        }
+
+        // concatenate those two ranges to get the coalesced literal text
+        String result = s1.substring(0, p1) + s2.substring(p2 + 1);
+
+        // clear out s1 and s2 (done here to improve readability in format() above))
+        s1.setLength(0);
+        s2.setLength(0);
+
+        return result;
+    }
+
+    /**
+     * A single element in a NamePattern.  This is either a name field or a range of literal text.
+     */
+    private interface Element {
+        boolean isLiteral();
+        String format(PersonName name);
+        boolean isPopulated(PersonName name);
+    }
+
+    /**
+     * Literal text from a name pattern.
+     */
+    private static class LiteralText implements Element {
+        private String text;
+
+        public LiteralText(String text) {
+            this.text = text;
+        }
+
+        public boolean isLiteral() {
+            return true;
+        }
+
+        public String format(PersonName name) {
+            return text;
+        }
+
+        public boolean isPopulated(PersonName name) {
+            return false;
+        }
+    }
+
+    /**
+     * An actual name field in a NamePattern (i.e., the stuff represented in the pattern by text
+     * in braces).  This class actually handles fetching the value for the field out of a
+     * PersonName object and applying any modifiers to it.
+     */
+    private static class NameFieldImpl implements Element {
+        private PersonName.NameField fieldID;
+        private Map<PersonName.FieldModifier, FieldModifierImpl> modifiers;
+
+        public NameFieldImpl(String fieldNameAndModifiers, PersonNameFormatterImpl formatterImpl) {
+            List<PersonName.FieldModifier> modifierIDs = new ArrayList<>();
+            StringTokenizer tok = new StringTokenizer(fieldNameAndModifiers, "-");
+
+            this.fieldID = PersonName.NameField.forString(tok.nextToken());
+            while (tok.hasMoreTokens()) {
+                modifierIDs.add(PersonName.FieldModifier.forString(tok.nextToken()));
+            }
+            if (this.fieldID == PersonName.NameField.SURNAME && formatterImpl.shouldCapitalizeSurname()) {
+                modifierIDs.add(PersonName.FieldModifier.ALL_CAPS);
+            }
+
+            this.modifiers = new HashMap<>();
+            for (PersonName.FieldModifier modifierID : modifierIDs) {
+                this.modifiers.put(modifierID, FieldModifierImpl.forName(modifierID, formatterImpl));
+            }
+        }
+
+        public boolean isLiteral() {
+            return false;
+        }
+
+        public String format(PersonName name) {
+            Set<PersonName.FieldModifier> modifierIDs = new HashSet<>(modifiers.keySet());
+            String result = name.getFieldValue(fieldID, modifierIDs);
+            if (result != null) {
+                for (PersonName.FieldModifier modifierID : modifierIDs) {
+                    result = modifiers.get(modifierID).modifyField(result);
+                }
+            }
+            return result;
+        }
+
+        public boolean isPopulated(PersonName name) {
+            // just check whether the unmodified field contains a value
+            Set<PersonName.FieldModifier> modifierIDs = new HashSet<>();
+            String fieldValue = name.getFieldValue(fieldID, modifierIDs);
+            return fieldValue != null && !fieldValue.isEmpty();
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/ComplexUnitsConverter.java b/android_icu4j/src/main/java/android/icu/impl/units/ComplexUnitsConverter.java
index 2d8bcf1..4e614ac 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/ComplexUnitsConverter.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/ComplexUnitsConverter.java
@@ -26,12 +26,15 @@
 public class ComplexUnitsConverter {
     public static final BigDecimal EPSILON = BigDecimal.valueOf(Math.ulp(1.0));
     public static final BigDecimal EPSILON_MULTIPLIER = BigDecimal.valueOf(1).add(EPSILON);
-    private ArrayList<UnitsConverter> unitsConverters_;
+
+    // TODO(ICU-21937): Make it private after submitting the public units conversion API.
+    public ArrayList<UnitsConverter> unitsConverters_;
     /**
      * Individual units of mixed units, sorted big to small, with indices
      * indicating the requested output mixed unit order.
      */
-    private List<MeasureUnitImpl.MeasureUnitImplWithIndex> units_;
+    // TODO(ICU-21937): Make it private after submitting the public units conversion API.
+    public List<MeasureUnitImpl.MeasureUnitImplWithIndex> units_;
     private MeasureUnitImpl inputUnit_;
 
     /**
@@ -170,7 +173,7 @@
      */
     public ComplexConverterResult convert(BigDecimal quantity, Precision rounder) {
         BigInteger sign = BigInteger.ONE;
-        if (quantity.compareTo(BigDecimal.ZERO) < 0) {
+        if (quantity.compareTo(BigDecimal.ZERO) < 0 && unitsConverters_.size() > 1) {
             quantity = quantity.abs();
             sign = sign.negate();
         }
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/MeasureUnitImpl.java b/android_icu4j/src/main/java/android/icu/impl/units/MeasureUnitImpl.java
index 824539e..1277b1b 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/MeasureUnitImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/MeasureUnitImpl.java
@@ -434,7 +434,7 @@
         private int fIndex = 0;
         // Set to true when we've seen a "-per-" or a "per-", after which all units
         // are in the denominator. Until we find an "-and-", at which point the
-        // identifier is invalid pending TODO(CLDR-13700).
+        // identifier is invalid pending TODO(CLDR-13701).
         private boolean fAfterPer = false;
         // If an "-and-" was parsed prior to finding the "single
         //     * unit", sawAnd is set to true. If not, it is left as is.
@@ -568,7 +568,7 @@
          * <p>
          *
          * @throws IllegalArgumentException if we parse both compound units and "-and-", since mixed
-         *                                  compound units are not yet supported - TODO(CLDR-13700).
+         *                                  compound units are not yet supported - TODO(CLDR-13701).
          */
         private SingleUnitImpl nextSingleUnit() {
             SingleUnitImpl result = new SingleUnitImpl();
@@ -604,7 +604,7 @@
                     case PER:
                         if (fSawAnd) {
                             throw new IllegalArgumentException("Mixed compound units not yet supported");
-                            // TODO(CLDR-13700).
+                            // TODO(CLDR-13701).
                         }
 
                         fAfterPer = true;
@@ -619,7 +619,7 @@
 
                     case AND:
                         if (fAfterPer) {
-                            // not yet supported, TODO(CLDR-13700).
+                            // not yet supported, TODO(CLDR-13701).
                             throw new IllegalArgumentException("Can't start with \"-and-\", and mixed compound units");
                         }
                         fSawAnd = true;
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitPreferences.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitPreferences.java
index b6ef278..fd15712 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/UnitPreferences.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/UnitPreferences.java
@@ -5,17 +5,31 @@
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 
 import android.icu.impl.ICUData;
 import android.icu.impl.ICUResourceBundle;
 import android.icu.impl.UResource;
+import android.icu.util.ULocale;
 import android.icu.util.UResourceBundle;
 
 /**
  * @hide Only a subset of ICU is exposed in Android
  */
 public class UnitPreferences {
+    private static final Map<String, String> measurementSystem;
+
+    static {
+        Map<String, String> tempMS = new HashMap<>();
+        tempMS.put("metric", "001");
+        tempMS.put("ussystem", "US");
+        tempMS.put("uksystem", "GB");
+        measurementSystem = Collections.unmodifiableMap(tempMS);
+    }
+
 
     private HashMap<String, HashMap<String, UnitPreference[]>> mapToUnitPreferences = new HashMap<>();
 
@@ -60,7 +74,53 @@
         return result.toArray(new String[0]);
     }
 
-    public UnitPreference[] getPreferencesFor(String category, String usage, String region) {
+    public UnitPreference[] getPreferencesFor(String category, String usage, ULocale locale, UnitsData data) {
+        // TODO: remove this condition when all the categories are allowed.
+        // WARNING: when this is removed please make sure to keep the "fahrenhe" => "fahrenheit" mapping
+        if ("temperature".equals(category)) {
+            String localeUnit = locale.getKeywordValue("mu");
+            // The value for -u-mu- is `fahrenhe`, but CLDR and everything else uses `fahrenheit`
+            if ("fahrenhe".equals(localeUnit)) {
+                localeUnit = "fahrenheit";
+            }
+            String localeUnitCategory;
+            try {
+                localeUnitCategory = localeUnit == null ? null : data.getCategory(MeasureUnitImpl.forIdentifier(localeUnit));
+            } catch (Exception e) {
+                localeUnitCategory = null;
+            }
+
+            if (localeUnitCategory != null && category.equals(localeUnitCategory)) {
+                UnitPreference[] preferences = {new UnitPreference(localeUnit, null, null)};
+                return preferences;
+            }
+        }
+
+        String region = locale.getCountry();
+
+        // Check the locale system tag, e.g `ms=metric`.
+        String localeSystem = locale.getKeywordValue("measure");
+        boolean isLocaleSystem = false;
+        if (measurementSystem.containsKey(localeSystem)) {
+            isLocaleSystem = true;
+            region = measurementSystem.get(localeSystem);
+        }
+
+        // Check the region tag, e.g. `rg=uszzz`.
+        if (!isLocaleSystem) {
+            String localeRegion = locale.getKeywordValue("rg");
+            if (localeRegion != null && localeRegion.length() >= 3) {
+                if (localeRegion.equals("default")) {
+                    region = localeRegion;
+                } else if (Character.isDigit(localeRegion.charAt(0))) {
+                    region = localeRegion.substring(0, 3); // e.g. 001
+                } else {
+                    // Capitalize the first two character of the region, e.g. ukzzzz or usca
+                    region = localeRegion.substring(0, 2).toUpperCase(Locale.ROOT);
+                }
+            }
+        }
+
         String[] subUsages = getAllUsages(usage);
         UnitPreference[] result = null;
         for (String subUsage :
@@ -68,6 +128,7 @@
             result = getUnitPreferences(category, subUsage, region);
             if (result != null) break;
         }
+
         // TODO: if a category is missing, we get an assertion failure, or we
         // return null, causing a NullPointerException. In C++, we return an
         // U_MISSING_RESOURCE_ERROR error.
@@ -108,8 +169,8 @@
 
         public UnitPreference(String unit, String geq, String skeleton) {
             this.unit = unit;
-            this.geq = new BigDecimal(geq);
-            this.skeleton = skeleton;
+            this.geq = geq == null ? BigDecimal.valueOf( Double.MIN_VALUE) /* -inf */ :  new BigDecimal(geq);
+            this.skeleton = skeleton == null? "" : skeleton;
         }
 
         public String getUnit() {
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
index 91c3968..8aea577 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
@@ -119,10 +119,8 @@
         if (this.reciprocal) {
             // We should see no offsets for reciprocal conversions - they don't make sense:
             assert offset == BigDecimal.ZERO;
-            if (result == BigDecimal.ZERO) {
-                // TODO: demonstrate the resulting behaviour in tests... and
-                // figure out desired behaviour. (Theoretical result should be
-                // infinity, not 0, but BigDecimal does not support infinity.)
+            if (result.compareTo(BigDecimal.ZERO) == 0) {
+                // TODO(ICU-21988): determine desirable behaviour
                 return BigDecimal.ZERO;
             }
             result = BigDecimal.ONE.divide(result, DECIMAL128);
@@ -135,10 +133,8 @@
         if (this.reciprocal) {
             // We should see no offsets for reciprocal conversions - they don't make sense:
             assert offset == BigDecimal.ZERO;
-            if (result == BigDecimal.ZERO) {
-                // TODO: demonstrate the resulting behaviour in tests... and
-                // figure out desired behaviour. (Theoretical result should be
-                // infinity, not 0, but BigDecimal does not support infinity.)
+            if (result.compareTo(BigDecimal.ZERO) == 0) {
+                // TODO(ICU-21988): determine desirable behaviour
                 return BigDecimal.ZERO;
             }
             result = BigDecimal.ONE.divide(result, DECIMAL128);
@@ -201,6 +197,12 @@
         private int exponentGlucoseMolarMass = 0;
         /** Exponent for the item per mole conversion rate constant */
         private int exponentItemPerMole = 0;
+        /** Exponent for the meters per AU conversion rate constant */
+        private int exponentMetersPerAU = 0;
+        /** Exponent for the sec per julian year conversion rate constant */
+        private int exponentSecPerJulianYear = 0;
+        /** Exponent for the speed of light meters per second" conversion rate constant */
+        private int exponentSpeedOfLightMetersPerSecond = 0;
 
         /**
          * Creates Empty Factor
@@ -254,6 +256,9 @@
             result.exponentLbToKg = this.exponentLbToKg;
             result.exponentGlucoseMolarMass = this.exponentGlucoseMolarMass;
             result.exponentItemPerMole = this.exponentItemPerMole;
+            result.exponentMetersPerAU = this.exponentMetersPerAU;
+            result.exponentSecPerJulianYear = this.exponentSecPerJulianYear;
+            result.exponentSpeedOfLightMetersPerSecond = this.exponentSpeedOfLightMetersPerSecond;
 
             return result;
         }
@@ -281,6 +286,9 @@
             resultCollector.multiply(new BigDecimal("0.45359237"), this.exponentLbToKg);
             resultCollector.multiply(new BigDecimal("180.1557"), this.exponentGlucoseMolarMass);
             resultCollector.multiply(new BigDecimal("6.02214076E+23"), this.exponentItemPerMole);
+            resultCollector.multiply(new BigDecimal("149597870700"), this.exponentMetersPerAU);
+            resultCollector.multiply(new BigDecimal("31557600"), this.exponentSecPerJulianYear);
+            resultCollector.multiply(new BigDecimal("299792458"), this.exponentSpeedOfLightMetersPerSecond);
 
             return resultCollector.factorNum.divide(resultCollector.factorDen, DECIMAL128);
         }
@@ -337,6 +345,10 @@
             result.exponentLbToKg = this.exponentLbToKg * power;
             result.exponentGlucoseMolarMass = this.exponentGlucoseMolarMass * power;
             result.exponentItemPerMole = this.exponentItemPerMole * power;
+            result.exponentMetersPerAU = this.exponentMetersPerAU * power;
+            result.exponentSecPerJulianYear = this.exponentSecPerJulianYear * power;
+            result.exponentSpeedOfLightMetersPerSecond =
+                this.exponentSpeedOfLightMetersPerSecond * power;
 
             return result;
         }
@@ -355,6 +367,10 @@
             result.exponentGlucoseMolarMass =
                 this.exponentGlucoseMolarMass - other.exponentGlucoseMolarMass;
             result.exponentItemPerMole = this.exponentItemPerMole - other.exponentItemPerMole;
+            result.exponentMetersPerAU = this.exponentMetersPerAU - other.exponentMetersPerAU;
+            result.exponentSecPerJulianYear = this.exponentSecPerJulianYear - other.exponentSecPerJulianYear;
+            result.exponentSpeedOfLightMetersPerSecond =
+                this.exponentSpeedOfLightMetersPerSecond - other.exponentSpeedOfLightMetersPerSecond;
 
             return result;
         }
@@ -373,6 +389,10 @@
             result.exponentGlucoseMolarMass =
                 this.exponentGlucoseMolarMass + other.exponentGlucoseMolarMass;
             result.exponentItemPerMole = this.exponentItemPerMole + other.exponentItemPerMole;
+            result.exponentMetersPerAU = this.exponentMetersPerAU + other.exponentMetersPerAU;
+            result.exponentSecPerJulianYear = this.exponentSecPerJulianYear + other.exponentSecPerJulianYear;
+            result.exponentSpeedOfLightMetersPerSecond =
+                this.exponentSpeedOfLightMetersPerSecond + other.exponentSpeedOfLightMetersPerSecond;
 
             return result;
         }
@@ -416,9 +436,15 @@
                 this.exponentGlucoseMolarMass += power;
             } else if ("item_per_mole".equals(entity)) {
                 this.exponentItemPerMole += power;
+            } else if ("meters_per_AU".equals(entity)) {
+                this.exponentMetersPerAU += power;
             } else if ("PI".equals(entity)) {
                 this.exponentPi += power;
-            } else {
+             } else if ("sec_per_julian_year".equals(entity)) {
+                this.exponentSecPerJulianYear += power;
+            } else if ("speed_of_light_meters_per_second".equals(entity)) {
+                this.exponentSpeedOfLightMetersPerSecond += power;
+           } else {
                 BigDecimal decimalEntity = new BigDecimal(entity).pow(power, DECIMAL128);
                 this.factorNum = this.factorNum.multiply(decimalEntity);
             }
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitsData.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitsData.java
index 373be1c..a57bc55 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/UnitsData.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/UnitsData.java
@@ -12,6 +12,7 @@
 import android.icu.impl.ICUResourceBundle;
 import android.icu.impl.IllegalIcuArgumentException;
 import android.icu.impl.UResource;
+import android.icu.util.ULocale;
 import android.icu.util.UResourceBundle;
 
 /**
@@ -111,8 +112,8 @@
         return Categories.indexToCategory[index];
     }
 
-    public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, String region) {
-        return this.unitPreferences.getPreferencesFor(category, usage, region);
+    public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, ULocale locale) {
+        return this.unitPreferences.getPreferencesFor(category, usage, locale, this);
     }
 
     /**
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitsRouter.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitsRouter.java
index d1909fa..383642e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/UnitsRouter.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/UnitsRouter.java
@@ -11,6 +11,7 @@
 import android.icu.impl.number.MicroProps;
 import android.icu.number.Precision;
 import android.icu.util.MeasureUnit;
+import android.icu.util.ULocale;
 
 /**
  * `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to
@@ -23,7 +24,7 @@
  * `foot+inch`, otherwise, the output will be in `inch`.
  * <p>
  * NOTE:
- * the output units and the their limits MUST BE in order, for example, if the output units, from the
+ * the output units and their limits MUST BE in order, for example, if the output units, from the
  * previous example, are the following:
  * {`inch`     , limit: no value (-inf)}
  * {`foot+inch`, limit: 3.0}
@@ -48,17 +49,17 @@
     private ArrayList<MeasureUnit> outputUnits_ = new ArrayList<>();
     private ArrayList<ConverterPreference> converterPreferences_ = new ArrayList<>();
 
-    public UnitsRouter(String inputUnitIdentifier, String region, String usage) {
-        this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), region, usage);
+    public UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage) {
+        this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), locale, usage);
     }
 
-    public UnitsRouter(MeasureUnitImpl inputUnit, String region, String usage) {
+    public UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage) {
         // TODO: do we want to pass in ConversionRates and UnitPreferences instead?
         // of loading in each UnitsRouter instance? (Or make global?)
         UnitsData data = new UnitsData();
 
         String category = data.getCategory(inputUnit);
-        UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, region);
+        UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, locale);
 
         for (int i = 0; i < unitPreferences.length; ++i) {
             UnitPreferences.UnitPreference preference = unitPreferences[i];
diff --git a/android_icu4j/src/main/java/android/icu/lang/UCharacter.java b/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
index da7dda7..f2da2c6 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
@@ -1101,6 +1101,23 @@
         /***/
         public static final int ZNAMENNY_MUSICAL_NOTATION_ID = 320; /*[1CF00]*/
 
+        // New blocks in Unicode 15.0
+
+        /***/
+        public static final int ARABIC_EXTENDED_C_ID = 321; /*[10EC0]*/
+        /***/
+        public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_H_ID = 322; /*[31350]*/
+        /***/
+        public static final int CYRILLIC_EXTENDED_D_ID = 323; /*[1E030]*/
+        /***/
+        public static final int DEVANAGARI_EXTENDED_A_ID = 324; /*[11B00]*/
+        /***/
+        public static final int KAKTOVIK_NUMERALS_ID = 325; /*[1D2C0]*/
+        /***/
+        public static final int KAWI_ID = 326; /*[11F00]*/
+        /***/
+        public static final int NAG_MUNDARI_ID = 327; /*[1E4D0]*/
+
         /**
          * One more than the highest normal UnicodeBlock value.
          * The highest value is available via UCharacter.getIntPropertyMaxValue(UProperty.BLOCK).
@@ -1109,7 +1126,7 @@
          * @hide unsupported on Android
          */
         @Deprecated
-        public static final int COUNT = 321;
+        public static final int COUNT = 328;
 
         // blocks objects ---------------------------------------------------
 
@@ -2342,6 +2359,30 @@
                 new UnicodeBlock("ZNAMENNY_MUSICAL_NOTATION",
                         ZNAMENNY_MUSICAL_NOTATION_ID); /*[1CF00]*/
 
+        // New blocks in Unicode 15.0
+
+        /***/
+        public static final UnicodeBlock ARABIC_EXTENDED_C =
+                new UnicodeBlock("ARABIC_EXTENDED_C", ARABIC_EXTENDED_C_ID); /*[10EC0]*/
+        /***/
+        public static final UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_H =
+                new UnicodeBlock("CJK_UNIFIED_IDEOGRAPHS_EXTENSION_H",
+                        CJK_UNIFIED_IDEOGRAPHS_EXTENSION_H_ID); /*[31350]*/
+        /***/
+        public static final UnicodeBlock CYRILLIC_EXTENDED_D =
+                new UnicodeBlock("CYRILLIC_EXTENDED_D", CYRILLIC_EXTENDED_D_ID); /*[1E030]*/
+        /***/
+        public static final UnicodeBlock DEVANAGARI_EXTENDED_A =
+                new UnicodeBlock("DEVANAGARI_EXTENDED_A", DEVANAGARI_EXTENDED_A_ID); /*[11B00]*/
+        /***/
+        public static final UnicodeBlock KAKTOVIK_NUMERALS =
+                new UnicodeBlock("KAKTOVIK_NUMERALS", KAKTOVIK_NUMERALS_ID); /*[1D2C0]*/
+        /***/
+        public static final UnicodeBlock KAWI = new UnicodeBlock("KAWI", KAWI_ID); /*[11F00]*/
+        /***/
+        public static final UnicodeBlock NAG_MUNDARI =
+                new UnicodeBlock("NAG_MUNDARI", NAG_MUNDARI_ID); /*[1E4D0]*/
+
         /**
          */
         public static final UnicodeBlock INVALID_CODE
@@ -5329,8 +5370,6 @@
      *         does not have data for the property at all.
      *
      * @see android.icu.lang.UProperty
-     * @see CharacterProperties#getBinaryPropertySet(int)
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static boolean hasBinaryProperty(CharSequence s, int property) {
         int length = s.length();
diff --git a/android_icu4j/src/main/java/android/icu/lang/UProperty.java b/android_icu4j/src/main/java/android/icu/lang/UProperty.java
index 504d8d6..f0057c3 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UProperty.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UProperty.java
@@ -495,50 +495,36 @@
     /**
      * Binary property of strings Basic_Emoji.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int BASIC_EMOJI=65;
     /**
      * Binary property of strings Emoji_Keycap_Sequence.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int EMOJI_KEYCAP_SEQUENCE=66;
     /**
      * Binary property of strings RGI_Emoji_Modifier_Sequence.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int RGI_EMOJI_MODIFIER_SEQUENCE=67;
     /**
      * Binary property of strings RGI_Emoji_Flag_Sequence.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int RGI_EMOJI_FLAG_SEQUENCE=68;
     /**
      * Binary property of strings RGI_Emoji_Tag_Sequence.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int RGI_EMOJI_TAG_SEQUENCE=69;
     /**
      * Binary property of strings RGI_Emoji_ZWJ_Sequence.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int RGI_EMOJI_ZWJ_SEQUENCE=70;
     /**
      * Binary property of strings RGI_Emoji.
      * See https://www.unicode.org/reports/tr51/#Emoji_Sets
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final int RGI_EMOJI=71;
 
diff --git a/android_icu4j/src/main/java/android/icu/lang/UScript.java b/android_icu4j/src/main/java/android/icu/lang/UScript.java
index 2d95fab..1a98ba2 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UScript.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UScript.java
@@ -875,6 +875,11 @@
     /***/
     public static final int VITHKUQI = 197; /* Vith */
 
+    /***/
+    public static final int KAWI = 198; /* Kawi */
+    /***/
+    public static final int NAG_MUNDARI = 199; /* Nagm */
+
     /**
      * One more than the highest normal UScript code.
      * The highest value is available via UCharacter.getIntPropertyMaxValue(UProperty.SCRIPT).
@@ -883,7 +888,7 @@
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int CODE_LIMIT   = 198;
+    public static final int CODE_LIMIT   = 200;
 
     private static int[] getCodesFromLocale(ULocale locale) {
         // Multi-script languages, equivalent to the LocaleScript data
@@ -1354,6 +1359,8 @@
             0x16ABC | EXCLUSION,  // Tnsa
             0x1E290 | EXCLUSION,  // Toto
             0x10582 | EXCLUSION | CASED,  // Vith
+            0x11F1B | EXCLUSION | LB_LETTERS,  // Kawi
+            0x1E4E6 | EXCLUSION,  // Nagm
             // End copy-paste from parsescriptmetadata.py
         };
 
diff --git a/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java
new file mode 100644
index 0000000..e01f72c
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java
@@ -0,0 +1,108 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import android.icu.impl.locale.AsciiUtil;
+import android.icu.text.DateFormat;
+import android.icu.text.SimpleDateFormat;
+
+/**
+ * Creates a {@link Formatter} doing formatting of date / time, similar to
+ * <code>{exp, date}</code> and <code>{exp, time}</code> in {@link android.icu.text.MessageFormat}.
+ */
+class DateTimeFormatterFactory implements FormatterFactory {
+
+    private static int stringToStyle(String option) {
+        switch (AsciiUtil.toUpperString(option)) {
+            case "FULL": return DateFormat.FULL;
+            case "LONG": return DateFormat.LONG;
+            case "MEDIUM": return DateFormat.MEDIUM;
+            case "SHORT": return DateFormat.SHORT;
+            case "": // intentional fall-through
+            case "DEFAULT": return DateFormat.DEFAULT;
+            default: throw new IllegalArgumentException("Invalid datetime style: " + option);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws IllegalArgumentException when something goes wrong
+     *         (for example conflicting options, invalid option values, etc.)
+     */
+    @Override
+    public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
+        DateFormat df;
+
+        // TODO: how to handle conflicts. What if we have both skeleton and style, or pattern?
+        Object opt = fixedOptions.get("skeleton");
+        if (opt != null) {
+            String skeleton = Objects.toString(opt);
+            df = DateFormat.getInstanceForSkeleton(skeleton, locale);
+            return new DateTimeFormatter(df);
+        }
+
+        opt = fixedOptions.get("pattern");
+        if (opt != null) {
+            String pattern = Objects.toString(opt);
+            SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
+            return new DateTimeFormatter(sf);
+        }
+
+        int dateStyle = DateFormat.NONE;
+        opt = fixedOptions.get("datestyle");
+        if (opt != null) {
+            dateStyle = stringToStyle(Objects.toString(opt, ""));
+        }
+
+        int timeStyle = DateFormat.NONE;
+        opt = fixedOptions.get("timestyle");
+        if (opt != null) {
+            timeStyle = stringToStyle(Objects.toString(opt, ""));
+        }
+
+        if (dateStyle == DateFormat.NONE && timeStyle == DateFormat.NONE) {
+            // Match the MessageFormat behavior
+            dateStyle = DateFormat.SHORT;
+            timeStyle = DateFormat.SHORT;
+        }
+        df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
+
+        return new DateTimeFormatter(df);
+    }
+
+    private static class DateTimeFormatter implements Formatter {
+        private final DateFormat icuFormatter;
+
+        private DateTimeFormatter(DateFormat df) {
+            this.icuFormatter = df;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
+            // TODO: use a special type to indicate function without input argument.
+            if (toFormat == null) {
+                throw new IllegalArgumentException("The date to format can't be null");
+            }
+            String result = icuFormatter.format(toFormat);
+            return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
+            return format(toFormat, variableOptions).toString();
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java b/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java
new file mode 100644
index 0000000..960723a
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java
@@ -0,0 +1,122 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.text.AttributedCharacterIterator;
+
+import android.icu.text.ConstrainedFieldPosition;
+import android.icu.text.FormattedValue;
+
+/**
+ * Not yet implemented: The result of a message formatting operation.
+ *
+ * <p>This contains information about where the various fields and placeholders
+ * ended up in the final result.</p>
+ * <p>This class allows the result to be exported in several data types,
+ * including a {@link String}, {@link AttributedCharacterIterator}, more (TBD).</p>
+ *
+ * @deprecated This API is for ICU internal use only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class FormattedMessage implements FormattedValue {
+
+    /**
+     * Not yet implemented.
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public FormattedMessage() {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public int length() {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public char charAt(int index) {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public <A extends Appendable> A appendTo(A appendable) {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public boolean nextPosition(ConstrainedFieldPosition cfpos) {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public AttributedCharacterIterator toCharacterIterator() {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java b/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java
new file mode 100644
index 0000000..16bd52d
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java
@@ -0,0 +1,81 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import android.icu.text.FormattedValue;
+
+/**
+ * An immutable, richer formatting result, encapsulating a {@link FormattedValue},
+ * the original value to format, and we are considering adding some more info.
+ * Very preliminary.
+ *
+ * @deprecated This API is for ICU internal use only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class FormattedPlaceholder {
+    private final FormattedValue formattedValue;
+    private final Object inputValue;
+
+    /**
+     * Constructor creating the {@code FormattedPlaceholder}.
+     *
+     * @param inputValue the original value to be formatted.
+     * @param formattedValue the result of formatting the placeholder.
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public FormattedPlaceholder(Object inputValue, FormattedValue formattedValue) {
+        if (formattedValue == null) {
+            throw new IllegalAccessError("Should not try to wrap a null formatted value");
+        }
+        this.inputValue = inputValue;
+        this.formattedValue = formattedValue;
+    }
+
+    /**
+     * Retrieve the original input value that was formatted.
+     *
+     * @return the original value to be formatted.
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Object getInput() {
+        return inputValue;
+    }
+
+    /**
+     * Retrieve the formatted value.
+     *
+     * @return the result of formatting the placeholder.
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public FormattedValue getFormattedValue() {
+        return formattedValue;
+    }
+
+    /**
+     * Returns a string representation of the object.
+     * It can be null, which is unusual, and we plan to change that.
+     *
+     * @return a string representation of the object.
+     *
+     * @deprecated This API is for ICU internal use only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public String toString() {
+        return formattedValue.toString();
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/Formatter.java b/android_icu4j/src/main/java/android/icu/message2/Formatter.java
new file mode 100644
index 0000000..0eeac56
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Formatter.java
@@ -0,0 +1,46 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Map;
+
+/**
+ * The interface that must be implemented by all formatters
+ * that can be used from {@link MessageFormatter}.
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public interface Formatter {
+    /**
+     * A method that takes the object to format and returns
+     * the i18n-aware string representation.
+     *
+     * @param toFormat the object to format.
+     * @param variableOptions options that are not know at build time.
+     * @return the formatted string.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    String formatToString(Object toFormat, Map<String, Object> variableOptions);
+
+    /**
+     * A method that takes the object to format and returns
+     * the i18n-aware formatted placeholder.
+     *
+     * @param toFormat the object to format.
+     * @param variableOptions options that are not know at build time.
+     * @return the formatted placeholder.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions);
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java
new file mode 100644
index 0000000..6b89c31
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java
@@ -0,0 +1,35 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The interface that must be implemented for each formatting function name
+ * that can be used from {@link MessageFormatter}.
+ *
+ * <p>We use it to create and cache various formatters with various options.</p>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public interface FormatterFactory {
+    /**
+     * The method that is called to create a formatter.
+     *
+     * @param locale the locale to use for formatting.
+     * @param fixedOptions the options to use for formatting. The keys and values are function dependent.
+     * @return the formatter.
+     * @throws IllegalArgumentException
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions);
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java
new file mode 100644
index 0000000..6798b98
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java
@@ -0,0 +1,40 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Creates a {@link Formatter} that simply returns the String non-i18n aware representation of an object.
+ */
+class IdentityFormatterFactory implements FormatterFactory {
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
+        return new IdentityFormatterImpl();
+    }
+
+    private static class IdentityFormatterImpl implements Formatter {
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
+            return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(Objects.toString(toFormat)));
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
+            return format(toFormat, variableOptions).toString();
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MessageFormatter.java b/android_icu4j/src/main/java/android/icu/message2/MessageFormatter.java
new file mode 100644
index 0000000..e8e187a
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MessageFormatter.java
@@ -0,0 +1,341 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * {@code MessageFormatter} is the next iteration of {@link android.icu.text.MessageFormat}.
+ *
+ * <p>This new version builds on what we learned from using {@code MessageFormat} for 20 years
+ * in various environments, either exposed "as is" or as a base for other public APIs.</p>
+ *
+ * <p>It is more modular, easier to backport, and provides extension points to add new
+ * formatters and selectors without having to modify the specification.</p>
+ *
+ * <p>We will be able to add formatters for intervals, relative times, lists, measurement units,
+ * people names, and more, and support custom formatters implemented by developers
+ * outside of ICU itself, for company or even product specific needs.</p>
+ *
+ * <p>MessageFormat 2 will support more complex grammatical features, such as gender, inflections,
+ * and tagging parts of the message for style changes or speech.</p>
+ *
+ * <p>The reasoning for this effort is shared in the
+ * <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/main/docs/why_mf_next.md">“Why
+ * MessageFormat needs a successor”</a> document.</p>
+ *
+ * <p>The “MessageFormat 2” project, which develops the new data model, semantics, and syntax,
+ * is hosted on <a target="github" href="https://github.com/unicode-org/message-format-wg">GitHub</a>.</p>
+ *
+ * <p>The current specification for the syntax and data model can be found
+ * <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/main/spec/syntax.md">here</a>.</p>
+ *
+ * <p>This tech preview implements enough of the {@code MessageFormat} functions to be useful,
+ * but the final set of functions and the parameters accepted by those functions is not yet finalized.</p>
+ *
+ * <p>These are the functions interpreted right now:</p>
+ *
+ * <table border="1">
+ * <tr>
+ *   <td rowspan="4">{@code datetime}</td>
+ *   <td>Similar to the ICU {@code "date"} and {@code "time"}.</td>
+ * </tr>
+ *
+ *   <tr><td>{@code datestyle} and {@code timestyle}<br>
+ *   Similar to {@code argStyle : short | medium | long | full}.<br>
+ *   Same values are accepted, but we can use both in one placeholder,
+ *   for example <code>{$due :datetime datestyle=full timestyle=long}</code>.
+ *   </td></tr>
+ *
+ *   <tr><td>{@code pattern}<br>
+ *   Similar to {@code argStyle = argStyleText}.<br>
+ *   This is bad i18n practice, and will probably be dropped.<br>
+ *   This is included just to support migration to MessageFormat 2.
+ *   </td></tr>
+ *
+ *   <tr><td>{@code skeleton}<br>
+ *   Same as {@code argStyle = argSkeletonText}.<br>
+ *   These are the date/time skeletons as supported by {@link android.icu.text.SimpleDateFormat}.
+ *   </td></tr>
+ *
+ * <tr>
+ *   <td rowspan="4">{@code number}</td>
+ *   <td>Similar to the ICU "number".</td>
+ * </tr>
+ *
+ *   <tr><td>{@code skeleton}<br>
+ *   These are the number skeletons as supported by {@link android.icu.number.NumberFormatter}.</td></tr>
+ *
+ *   <tr><td>{@code minimumFractionDigits}<br>
+ *   Only implemented to be able to pass the unit tests from the ECMA tech preview implementation,
+ *   which prefers options bags to skeletons.<br>
+ *   TBD if the final {@number} function will support skeletons, option backs, or both.</td></tr>
+ *
+ *   <tr><td>{@code offset}<br>
+ *   Used to support plural with an offset.</td></tr>
+ *
+ * <tr><td >{@code identity}</td><td>Returns the direct string value of the argument (calling {@code toString()}).</td></tr>
+ *
+ * <tr>
+ *   <td rowspan="3">{@code plural}</td>
+ *   <td>Similar to the ICU {@code "plural"}.</td>
+ * </tr>
+ *
+ *   <tr><td>{@code skeleton}<br>
+ *   These are the number skeletons as supported by {@link android.icu.number.NumberFormatter}.<br>
+ *   Can also be indirect, from a local variable of type {@code number} (recommended).</td></tr>
+ *
+ *   <tr><td>{@code offset}<br>
+ *   Used to support plural with an offset.<br>
+ *   Can also be indirect, from a local variable of type {@code number} (recommended).</td></tr>
+ *
+ * <tr>
+ *   <td>{@code selectordinal}</td>
+ *   <td>Similar to the ICU {@code "selectordinal"}.<br>
+ * For now it accepts the same parameters as "plural", although there is no use case for them.<br>
+ * TBD if this will be merged into "plural" (with some {@code kind} option) or not.</td></tr>
+ *
+ * <tr><td>{@code select}</td><td>Literal match, same as the ICU4 {@code "select"}.</td></tr>
+ * </table>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class MessageFormatter {
+    private final Locale locale;
+    private final String pattern;
+    private final Mf2FunctionRegistry functionRegistry;
+    private final Mf2DataModel dataModel;
+    private final Mf2DataModelFormatter modelFormatter;
+
+    private MessageFormatter(Builder builder) {
+        this.locale = builder.locale;
+        this.functionRegistry = builder.functionRegistry;
+        if ((builder.pattern == null && builder.dataModel == null)
+                || (builder.pattern != null && builder.dataModel != null)) {
+            throw new IllegalArgumentException("You need to set either a pattern, or a dataModel, but not both.");
+        }
+
+        if (builder.dataModel != null) {
+            this.dataModel = builder.dataModel;
+            this.pattern = Mf2Serializer.dataModelToString(this.dataModel);
+        } else {
+            this.pattern = builder.pattern;
+            Mf2Serializer tree = new Mf2Serializer();
+            Mf2Parser parser = new Mf2Parser(pattern, tree);
+            try {
+                parser.parse_Message();
+                dataModel = tree.build();
+            } catch (Mf2Parser.ParseException pe) {
+                throw new IllegalArgumentException(
+                        "Parse error:\n"
+                        + "Message: <<" + pattern + ">>\n"
+                        + "Error:" + parser.getErrorMessage(pe) + "\n");
+            }
+        }
+        modelFormatter = new Mf2DataModelFormatter(dataModel, locale, functionRegistry);
+    }
+
+    /**
+     * Creates a builder.
+     *
+     * @return the Builder.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Get the locale to use for all the formatting and selections in
+     * the current {@code MessageFormatter}.
+     *
+     * @return the locale.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Locale getLocale() {
+        return locale;
+    }
+
+    /**
+     * Get the pattern (the serialized message in MessageFormat 2 syntax) of
+     * the current {@code MessageFormatter}.
+     *
+     * <p>If the {@code MessageFormatter} was created from an {@link Mf2DataModel}
+     * the this string is generated from that model.</p>
+     *
+     * @return the pattern.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String getPattern() {
+        return pattern;
+    }
+
+    /**
+     * Give public access to the message data model.
+     *
+     * <p>This data model is similar to the functionality we have today
+     * in {@link android.icu.text.MessagePatternUtil} maybe even a bit more higher level.</p>
+     *
+     * <p>We can also imagine a model where one parses the string syntax, takes the data model,
+     * modifies it, and then uses that modified model to create a {@code MessageFormatter}.</p>
+     *
+     * @return the data model.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Mf2DataModel getDataModel() {
+        return dataModel;
+    }
+
+    /**
+     * Formats a map of objects by iterating over the MessageFormat's pattern,
+     * with the plain text “as is” and the arguments replaced by the formatted objects.
+     *
+     * @param arguments a map of objects to be formatted and substituted.
+     * @return the string representing the message with parameters replaced.
+     *
+     * @throws IllegalArgumentException when something goes wrong
+     *         (for example wrong argument type, or null arguments, etc.)
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String formatToString(Map<String, Object> arguments) {
+        return modelFormatter.format(arguments);
+    }
+
+    /**
+     * Not yet implemented: formats a map of objects by iterating over the MessageFormat's
+     * pattern, with the plain text “as is” and the arguments replaced by the formatted objects.
+     *
+     * @param arguments a map of objects to be formatted and substituted.
+     * @return the {@link FormattedMessage} class representing the message with parameters replaced.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public FormattedMessage format(Map<String, Object> arguments) {
+        throw new RuntimeException("Not yet implemented.");
+    }
+
+    /**
+     * A {@code Builder} used to build instances of {@link MessageFormatter}.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Builder {
+        private Locale locale = Locale.getDefault(Locale.Category.FORMAT);
+        private String pattern = null;
+        private Mf2FunctionRegistry functionRegistry = Mf2FunctionRegistry.builder().build();
+        private Mf2DataModel dataModel = null;
+
+        // Prevent direct creation
+        private Builder() {
+        }
+
+        /**
+         * Sets the locale to use for all formatting and selection operations.
+         *
+         * @param locale the locale to set.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setLocale(Locale locale) {
+            this.locale = locale;
+            return this;
+        }
+
+        /**
+         * Sets the pattern (in MessageFormat 2 syntax) used to create the message.<br>
+         * It conflicts with the data model, so it will reset it (the last call on setter wins).
+         *
+         * @param pattern the pattern to set.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setPattern(String pattern) {
+            this.pattern = pattern;
+            this.dataModel = null;
+            return this;
+        }
+
+        /**
+         * Sets an instance of {@link Mf2FunctionRegistry} that should register any
+         * custom functions used by the message.
+         *
+         * <p>There is no need to do this in order to use standard functions
+         * (for example date / time / number formatting, plural / ordinal / literal selection).<br>
+         * The exact set of standard functions, with the types they format and the options
+         * they accept is still TBD.</p>
+         *
+         * @param functionRegistry the function registry to set.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setFunctionRegistry(Mf2FunctionRegistry functionRegistry) {
+            this.functionRegistry = functionRegistry;
+            return this;
+        }
+
+        /**
+         * Sets the data model used to create the message.<br>
+         * It conflicts with the pattern, so it will reset it (the last call on setter wins).
+         *
+         * @param dataModel the pattern to set.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setDataModel(Mf2DataModel dataModel) {
+            this.dataModel = dataModel;
+            this.pattern = null;
+            return this;
+        }
+
+        /**
+         * Builds an instance of {@link MessageFormatter}.
+         *
+         * @return the {@link MessageFormatter} created.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public MessageFormatter build() {
+            return new MessageFormatter(this);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/Mf2DataModel.java b/android_icu4j/src/main/java/android/icu/message2/Mf2DataModel.java
new file mode 100644
index 0000000..0dd5e4b
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Mf2DataModel.java
@@ -0,0 +1,884 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.StringJoiner;
+
+/**
+ * This maps closely to the official specification.
+ * Since it is not final, we will not add javadoc everywhere.
+ *
+ * <p>See <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/main/spec/syntax.md">the
+ * description of the syntax with examples and use cases</a> and the corresponding
+ * <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/main/spec/message.ebnf">EBNF</a>.</p>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+@SuppressWarnings("javadoc")
+public class Mf2DataModel {
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class SelectorKeys {
+        private final List<String> keys;
+
+        private SelectorKeys(Builder builder) {
+            keys = new ArrayList<>();
+            keys.addAll(builder.keys);
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public List<String> getKeys() {
+            return Collections.unmodifiableList(keys);
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            StringJoiner result = new StringJoiner(" ");
+            for (String key : keys) {
+                result.add(key);
+            }
+            return result.toString();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide Only a subset of ICU is exposed in Android
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static class Builder {
+            private final List<String> keys = new ArrayList<>();
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder add(String key) {
+                keys.add(key);
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder addAll(Collection<String> otherKeys) {
+                this.keys.addAll(otherKeys);
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public SelectorKeys build() {
+                return new SelectorKeys(this);
+            }
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Pattern {
+        private final List<Part> parts;
+
+        private Pattern(Builder builder) {
+            parts = new ArrayList<>();
+            parts.addAll(builder.parts);
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public List<Part> getParts() {
+            return parts;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            StringBuilder result = new StringBuilder();
+            result.append("{");
+            for (Part part : parts) {
+                result.append(part);
+            }
+            result.append("}");
+            return result.toString();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide Only a subset of ICU is exposed in Android
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static class Builder {
+            private final List<Part> parts = new ArrayList<>();
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder add(Part part) {
+                parts.add(part);
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder addAll(Collection<Part> otherParts) {
+                parts.addAll(otherParts);
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Pattern build() {
+                return new Pattern(this);
+            }
+
+        }
+    }
+
+    /**
+     * No functional role, this is only to be able to say that a message is a sequence of Part(s),
+     * and that plain text {@link Text} and {@link Expression} are Part(s).
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface Part {
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Text implements Part {
+        private final String value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        private Text(Builder builder) {
+            this(builder.value);
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Text(String value) {
+            this.value = value;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public String getValue() {
+            return value;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            return value;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide Only a subset of ICU is exposed in Android
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static class Builder {
+            private String value;
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder setValue(String value) {
+                this.value = value;
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Text build() {
+                return new Text(this);
+            }
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Expression implements Part {
+        private final Value operand; // Literal | Variable
+        private final String functionName;
+        private final Map<String, Value> options;
+        Formatter formatter = null;
+
+        private Expression(Builder builder) {
+            this.operand = builder.operand;
+            this.functionName = builder.functionName;
+            this.options = builder.options;
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Value getOperand() {
+            return operand;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public String getFunctionName() {
+            return functionName;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Map<String, Value> getOptions() {
+            return options;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            StringBuilder result = new StringBuilder();
+            result.append("{");
+            if (operand != null) {
+                result.append(operand);
+            }
+            if (functionName != null) {
+                result.append(" :").append(functionName);
+            }
+            for (Entry<String, Value> option : options.entrySet()) {
+                result.append(" ").append(option.getKey()).append("=").append(option.getValue());
+            }
+            result.append("}");
+            return result.toString();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide Only a subset of ICU is exposed in Android
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static class Builder {
+            private Value operand = null;
+            private String functionName = null;
+            private final OrderedMap<String, Value> options = new OrderedMap<>();
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder setOperand(Value operand) {
+                this.operand = operand;
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder setFunctionName(String functionName) {
+                this.functionName = functionName;
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder addOption(String key, Value value) {
+                options.put(key, value);
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder addOptions(Map<String, Value> otherOptions) {
+                options.putAll(otherOptions);
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Expression build() {
+                return new Expression(this);
+            }
+        }
+    }
+
+//    public static class Placeholder extends Expression implements Part {
+//        public Placeholder(Builder builder) {
+//            super(builder);
+//        }
+//    }
+
+    /**
+     * A Value can be either a Literal, or a Variable, but not both.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Value {
+        private final String literal;
+        private final String variableName;
+
+        private Value(Builder builder) {
+            this.literal = builder.literal;
+            this.variableName = builder.variableName;
+//            this(builder.literal, builder.variableName);
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public String getLiteral() {
+            return literal;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public String getVariableName() {
+            return variableName;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public boolean isLiteral() {
+            return literal != null;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public boolean isVariable() {
+            return variableName != null;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            return isLiteral() ? "(" + literal + ")" : "$" + variableName;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide Only a subset of ICU is exposed in Android
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static class Builder {
+            private String literal;
+            private String variableName;
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder setLiteral(String literal) {
+                this.literal = literal;
+                this.variableName = null;
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder setVariableName(String variableName) {
+                this.variableName = variableName;
+                this.literal = null;
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Value build() {
+                return new Value(this);
+            }
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Variable {
+        private final String name;
+
+        private Variable(Builder builder) {
+            this.name = builder.name;
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide Only a subset of ICU is exposed in Android
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static class Builder {
+            private String name;
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Builder setName(String name) {
+                this.name = name;
+                return this;
+            }
+
+            /**
+             * @deprecated This API is for technology preview only.
+             * @hide draft / provisional / internal are hidden on Android
+             */
+            @Deprecated
+            public Variable build() {
+                return new Variable(this);
+            }
+        }
+    }
+
+    /**
+     * This is only to not force LinkedHashMap on the public API.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class OrderedMap<K, V> extends LinkedHashMap<K, V> {
+        private static final long serialVersionUID = -7049361727790825496L;
+
+        /**
+         * {@inheritDoc}
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public OrderedMap() {
+            super();
+        }
+    }
+
+    private final OrderedMap<String, Expression> localVariables;
+    private final List<Expression> selectors;
+    private final OrderedMap<SelectorKeys, Pattern> variants;
+    private final Pattern pattern;
+
+    private Mf2DataModel(Builder builder) {
+        this.localVariables = builder.localVariables;
+        this.selectors = builder.selectors;
+        this.variants = builder.variants;
+        this.pattern = builder.pattern;
+    }
+
+    /**
+     * Creates a builder.
+     *
+     * @return the Builder.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public OrderedMap<String, Expression> getLocalVariables() {
+        return localVariables;
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public List<Expression>  getSelectors() {
+        return selectors;
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public OrderedMap<SelectorKeys, Pattern>  getVariants() {
+        return variants;
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        for (Entry<String, Expression> lv : localVariables.entrySet()) {
+            result.append("let $").append(lv.getKey());
+            result.append(" = ");
+            result.append(lv.getValue());
+            result.append("\n");
+        }
+        if (!selectors.isEmpty()) {
+            result.append("match");
+            for (Expression e : this.selectors) {
+                result.append(" ").append(e);
+            }
+            result.append("\n");
+            for (Entry<SelectorKeys, Pattern> variant : variants.entrySet()) {
+                result.append("  when ").append(variant.getKey());
+                result.append(" ");
+                result.append(variant.getValue());
+                result.append("\n");
+            }
+        } else {
+            result.append(pattern);
+        }
+        return result.toString();
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Builder {
+        private final OrderedMap<String, Expression> localVariables = new OrderedMap<>(); // declaration*
+        private final List<Expression> selectors = new ArrayList<>();
+        private final OrderedMap<SelectorKeys, Pattern> variants = new OrderedMap<>();
+        private Pattern pattern = Pattern.builder().build();
+
+        // Prevent direct creation
+        private Builder() {
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addLocalVariable(String variableName, Expression expression) {
+            this.localVariables.put(variableName, expression);
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addLocalVariables(OrderedMap<String, Expression> otherLocalVariables) {
+            this.localVariables.putAll(otherLocalVariables);
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addSelector(Expression otherSelector) {
+            this.selectors.add(otherSelector);
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addSelectors(List<Expression> otherSelectors) {
+            this.selectors.addAll(otherSelectors);
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addVariant(SelectorKeys keys, Pattern newPattern) {
+            this.variants.put(keys, newPattern);
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addVariants(OrderedMap<SelectorKeys, Pattern> otherVariants) {
+            this.variants.putAll(otherVariants);
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setPattern(Pattern pattern) {
+            this.pattern = pattern;
+            return this;
+        }
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Mf2DataModel build() {
+            return new Mf2DataModel(this);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/Mf2DataModelFormatter.java b/android_icu4j/src/main/java/android/icu/message2/Mf2DataModelFormatter.java
new file mode 100644
index 0000000..6ff0f1e
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Mf2DataModelFormatter.java
@@ -0,0 +1,281 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import android.icu.message2.Mf2DataModel.Expression;
+import android.icu.message2.Mf2DataModel.Part;
+import android.icu.message2.Mf2DataModel.Pattern;
+import android.icu.message2.Mf2DataModel.SelectorKeys;
+import android.icu.message2.Mf2DataModel.Text;
+import android.icu.message2.Mf2DataModel.Value;
+import android.icu.util.Calendar;
+import android.icu.util.CurrencyAmount;
+
+/**
+ * Takes an {@link Mf2DataModel} and formats it to a {@link String}
+ * (and later on we will also implement formatting to a {@code FormattedMessage}).
+ */
+// TODO: move this in the MessageFormatter
+class Mf2DataModelFormatter {
+    private final Locale locale;
+    private final Mf2DataModel dm;
+
+    final Mf2FunctionRegistry standardFunctions;
+    final Mf2FunctionRegistry customFunctions;
+    private static final Mf2FunctionRegistry EMPTY_REGISTY = Mf2FunctionRegistry.builder().build();
+
+    Mf2DataModelFormatter(Mf2DataModel dm, Locale locale, Mf2FunctionRegistry customFunctionRegistry) {
+        this.locale = locale;
+        this.dm = dm;
+        this.customFunctions = customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;
+
+        standardFunctions = Mf2FunctionRegistry.builder()
+                // Date/time formatting
+                .setFormatter("datetime", new DateTimeFormatterFactory())
+                .setDefaultFormatterNameForType(Date.class, "datetime")
+                .setDefaultFormatterNameForType(Calendar.class, "datetime")
+
+                // Number formatting
+                .setFormatter("number", new NumberFormatterFactory())
+                .setDefaultFormatterNameForType(Integer.class, "number")
+                .setDefaultFormatterNameForType(Double.class, "number")
+                .setDefaultFormatterNameForType(Number.class, "number")
+                .setDefaultFormatterNameForType(CurrencyAmount.class, "number")
+
+                // Format that returns "to string"
+                .setFormatter("identity", new IdentityFormatterFactory())
+                .setDefaultFormatterNameForType(String.class, "identity")
+                .setDefaultFormatterNameForType(CharSequence.class, "identity")
+
+                // Register the standard selectors
+                .setSelector("plural", new PluralSelectorFactory("cardinal"))
+                .setSelector("selectordinal", new PluralSelectorFactory("ordinal"))
+                .setSelector("select", new TextSelectorFactory())
+                .setSelector("gender", new TextSelectorFactory())
+
+                .build();
+    }
+
+    private static Map<String, Object> mf2OptToFixedOptions(Map<String, Value> options) {
+        Map<String, Object> result = new HashMap<>();
+        for (Entry<String, Value> option : options.entrySet()) {
+            Value value = option.getValue();
+            if (value.isLiteral()) {
+                result.put(option.getKey(), value.getLiteral());
+            }
+        }
+        return result;
+    }
+
+    private Map<String, Object> mf2OptToVariableOptions(Map<String, Value> options, Map<String, Object> arguments) {
+        Map<String, Object> result = new HashMap<>();
+        for (Entry<String, Value> option : options.entrySet()) {
+            Value value = option.getValue();
+            if (value.isVariable()) {
+                result.put(option.getKey(), variableToObjectEx(value, arguments));
+            }
+        }
+        return result;
+    }
+
+    FormatterFactory getFormattingFunctionFactoryByName(Object toFormat, String functionName) {
+        // Get a function name from the type of the object to format
+        if (functionName == null || functionName.isEmpty()) {
+            if (toFormat == null) {
+                // The object to format is null, and no function provided.
+                return null;
+            }
+            Class<?> clazz = toFormat.getClass();
+            functionName = standardFunctions.getDefaultFormatterNameForType(clazz);
+            if (functionName == null) {
+                functionName = customFunctions.getDefaultFormatterNameForType(clazz);
+            }
+            if (functionName == null) {
+                throw new IllegalArgumentException("Object to format without a function, and unknown type: "
+                        + toFormat.getClass().getName());
+            }
+        }
+
+        FormatterFactory func = standardFunctions.getFormatter(functionName);
+        if (func == null) {
+            func = customFunctions.getFormatter(functionName);
+            if (func == null) {
+                throw new IllegalArgumentException("Can't find an implementation for function: '"
+                        + functionName + "'");
+            }
+        }
+        return func;
+    }
+
+    String format(Map<String, Object> arguments) {
+        List<Expression> selectors = dm.getSelectors();
+        Pattern patternToRender = selectors.isEmpty()
+                ? dm.getPattern()
+                : findBestMatchingPattern(selectors, arguments);
+
+        StringBuilder result = new StringBuilder();
+        for (Part part : patternToRender.getParts()) {
+            if (part instanceof Text) {
+                result.append(part);
+            } else if (part instanceof Expression) { // Placeholder is an Expression
+                FormattedPlaceholder fp = formatPlaceholder((Expression) part, arguments, false);
+                result.append(fp.toString());
+            } else {
+                throw new IllegalArgumentException("Unknown part type: " + part);
+            }
+        }
+        return result.toString();
+    }
+
+    private Pattern findBestMatchingPattern(List<Expression> selectors, Map<String, Object> arguments) {
+        Pattern patternToRender = null;
+
+        // Collect all the selector functions in an array, to reuse
+        List<Selector> selectorFunctions = new ArrayList<>(selectors.size());
+        for (Expression selector : selectors) {
+            String functionName = selector.getFunctionName();
+            SelectorFactory funcFactory = standardFunctions.getSelector(functionName);
+            if (funcFactory == null) {
+                funcFactory = customFunctions.getSelector(functionName);
+            }
+            if (funcFactory != null) {
+                Map<String, Object> opt = mf2OptToFixedOptions(selector.getOptions());
+                selectorFunctions.add(funcFactory.createSelector(locale, opt));
+            } else {
+                throw new IllegalArgumentException("Unknown selector type: " + functionName);
+            }
+        }
+        // This should not be possible, we added one function for each selector, or we have thrown an exception.
+        // But just in case someone removes the throw above?
+        if (selectorFunctions.size() != selectors.size()) {
+            throw new IllegalArgumentException("Something went wrong, not enough selector functions, "
+                    + selectorFunctions.size() + " vs. " + selectors.size());
+        }
+
+        // Iterate "vertically", through all variants
+        for (Entry<SelectorKeys, Pattern> variant : dm.getVariants().entrySet()) {
+            int maxCount = selectors.size();
+            List<String> keysToCheck = variant.getKey().getKeys();
+            if (selectors.size() != keysToCheck.size()) {
+                throw new IllegalArgumentException("Mismatch between the number of selectors and the number of keys: "
+                        + selectors.size() + " vs. " + keysToCheck.size());
+            }
+            boolean matches = true;
+            // Iterate "horizontally", through all matching functions and keys
+            for (int i = 0; i < maxCount; i++) {
+                Expression selector = selectors.get(i);
+                String valToCheck = keysToCheck.get(i);
+                Selector func = selectorFunctions.get(i);
+                Map<String, Object> options = mf2OptToVariableOptions(selector.getOptions(), arguments);
+                if (!func.matches(variableToObjectEx(selector.getOperand(), arguments), valToCheck, options)) {
+                    matches = false;
+                    break;
+                }
+            }
+            if (matches) {
+                patternToRender = variant.getValue();
+                break;
+            }
+        }
+
+        // TODO: check that there was an entry with all the keys set to `*`
+        // And should do that only once, when building the data model.
+        if (patternToRender == null) {
+            // If there was a case with all entries in the keys `*` this should not happen
+            throw new IllegalArgumentException("The selection went wrong, cannot select any option.");
+        }
+
+        return patternToRender;
+    }
+
+    /*
+     * Pass a level to prevent local variables calling each-other recursively:
+     *
+     * <code><pre>
+     * let $l1 = {$l4 :number}
+     * let $l2 = {$l1 :number}
+     * let $l3 = {$l2 :number}
+     * let $l4 = {$l3 :number}
+     * </pre></code>
+     *
+     * We can keep track of the calls (complicated and expensive).
+     * Or we can forbid the use of variables before they are declared, but that is not in the spec (yet?).
+     */
+    private Object variableToObjectEx(Value value, Map<String, Object> arguments) {
+        if (value == null) { // function only
+            return null;
+        }
+        // We have an operand. Can be literal, local var, or argument.
+        if (value.isLiteral()) {
+            return value.getLiteral();
+        } else if (value.isVariable()) {
+            String varName = value.getVariableName();
+            Expression localPh = dm.getLocalVariables().get(varName);
+            if (localPh != null) {
+                return formatPlaceholder(localPh, arguments, false);
+            }
+            return arguments.get(varName);
+        } else {
+            throw new IllegalArgumentException("Invalid operand type " + value);
+        }
+    }
+
+    private FormattedPlaceholder formatPlaceholder(Expression ph, Map<String, Object> arguments, boolean localExpression) {
+        Object toFormat;
+        Value operand = ph.getOperand();
+        if (operand == null) { // function only, "...{:currentOs option=value}..."
+            toFormat = null;
+        } else {
+            // We have an operand. Can be literal, local var, or argument.
+            if (operand.isLiteral()) { // "...{(1234.56) :number}..."
+                // If it is a literal, return the string itself
+                toFormat = operand.getLiteral();
+            } else if (operand.isVariable()) {
+                String varName = operand.getVariableName();
+                if (!localExpression) {
+                    Expression localPh = dm.getLocalVariables().get(varName);
+                    if (localPh != null) {
+                        // If it is a local variable, we need to format that (recursive)
+                        // TODO: See if there is any danger to eval the local variables only once
+                        // (on demand in case the local var is not used, for example in a select)
+                        return formatPlaceholder(localPh, arguments, true);
+                    }
+                }
+                // Return the object in the argument bag.
+                toFormat = arguments.get(varName);
+                // toFormat might still be null here.
+            } else {
+                throw new IllegalArgumentException("Invalid operand type " + ph.getOperand());
+            }
+        }
+
+        if (ph.formatter == null) {
+            FormatterFactory funcFactory = getFormattingFunctionFactoryByName(toFormat, ph.getFunctionName());
+            if (funcFactory != null) {
+                Map<String, Object> fixedOptions = mf2OptToFixedOptions(ph.getOptions());
+                Formatter ff = funcFactory.createFormatter(locale, fixedOptions);
+                ph.formatter = ff;
+            }
+        }
+        if (ph.formatter != null) {
+            Map<String, Object> variableOptions = mf2OptToVariableOptions(ph.getOptions(), arguments);
+            try {
+                return ph.formatter.format(toFormat, variableOptions);
+            } catch (IllegalArgumentException e) {
+                // Fall-through to the name of the placeholder without replacement.
+            }
+        }
+
+        return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue("{" + ph.getOperand() + "}"));
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/Mf2FunctionRegistry.java b/android_icu4j/src/main/java/android/icu/message2/Mf2FunctionRegistry.java
new file mode 100644
index 0000000..702b545
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Mf2FunctionRegistry.java
@@ -0,0 +1,350 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class is used to register mappings between various function
+ * names and the factories that can create those functions.
+ *
+ * <p>For example to add formatting for a {@code Person} object one would need to:</p>
+ * <ul>
+ *   <li>write a function (class, lambda, etc.) that does the formatting proper
+ *     (implementing {@link Formatter})</li>
+ *   <li>write a factory that creates such a function
+ *     (implementing {@link FormatterFactory})</li>
+ *  <li>add a mapping from the function name as used in the syntax
+ *    (for example {@code "person"}) to the factory</li>
+ *  <li>optionally add a mapping from the class to format ({@code ...Person.class}) to
+ *     the formatter name ({@code "person"}), so that one can use a placeholder in the message
+ *     without specifying a function (for example {@code "... {$me} ..."} instead of
+ *     {@code "... {$me :person} ..."}, if the class of {@code $me} is an {@code instanceof Person}).
+ *     </li>
+ * </ul>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class Mf2FunctionRegistry {
+    private final Map<String, FormatterFactory> formattersMap;
+    private final Map<String, SelectorFactory> selectorsMap;
+    private final Map<Class<?>, String> classToFormatter;
+
+    private Mf2FunctionRegistry(Builder builder) {
+        this.formattersMap = new HashMap<>(builder.formattersMap);
+        this.selectorsMap = new HashMap<>(builder.selectorsMap);
+        this.classToFormatter = new HashMap<>(builder.classToFormatter);
+    }
+
+    /**
+     * Creates a builder.
+     *
+     * @return the Builder.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns the formatter factory used to create the formatter for function
+     * named {@code name}.
+     *
+     * <p>Note: function name here means the name used to refer to the function in the
+     * MessageFormat 2 syntax, for example {@code "... {$exp :datetime} ..."}<br>
+     * The function name here is {@code "datetime"}, and does not have to correspond to the
+     * name of the methods / classes used to implement the functionality.</p>
+     *
+     * <p>For example one might write a {@code PersonFormatterFactory} returning a {@code PersonFormatter},
+     * and map that to the MessageFormat function named {@code "person"}.<br>
+     * The only name visible to the users of MessageFormat syntax will be {@code "person"}.</p>
+     *
+     * @param formatterName the function name.
+     * @return the factory creating formatters for {@code name}. Returns {@code null} if none is registered.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public FormatterFactory getFormatter(String formatterName) {
+        return formattersMap.get(formatterName);
+    }
+
+    /**
+     * Get all know names that have a mappings from name to {@link FormatterFactory}.
+     *
+     * @return a set of all the known formatter names.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Set<String> getFormatterNames() {
+        return formattersMap.keySet();
+    }
+
+    /**
+     * Returns the name of the formatter used to format an object of type {@code clazz}.
+     *
+     * @param clazz the class of the object to format.
+     * @return the name of the formatter class, if registered. Returns {@code null} otherwise.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String getDefaultFormatterNameForType(Class<?> clazz) {
+        // Search for the class "as is", to save time.
+        // If we don't find it then we iterate the registered classes and check
+        // if the class is an instanceof the ones registered.
+        // For example a BuddhistCalendar when we only registered Calendar
+        String result = classToFormatter.get(clazz);
+        if (result != null) {
+            return result;
+        }
+        // We didn't find the class registered explicitly "as is"
+        for (Map.Entry<Class<?>, String> e : classToFormatter.entrySet()) {
+            if (e.getKey().isAssignableFrom(clazz)) {
+                return e.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get all know classes that have a mappings from class to function name.
+     *
+     * @return a set of all the known classes that have mapping to function names.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Set<Class<?>> getDefaultFormatterTypes() {
+        return classToFormatter.keySet();
+    }
+
+    /**
+     * Returns the selector factory used to create the selector for function
+     * named {@code name}.
+     *
+     * <p>Note: the same comments about naming as the ones on {@code getFormatter} apply.</p>
+     *
+     * @param selectorName the selector name.
+     * @return the factory creating selectors for {@code name}. Returns {@code null} if none is registered.
+     * @see #getFormatter(String)
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public SelectorFactory getSelector(String selectorName) {
+        return selectorsMap.get(selectorName);
+    }
+
+    /**
+     * Get all know names that have a mappings from name to {@link SelectorFactory}.
+     *
+     * @return a set of all the known selector names.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Set<String> getSelectorNames() {
+        return selectorsMap.keySet();
+    }
+
+    /**
+     * A {@code Builder} used to build instances of {@link Mf2FunctionRegistry}.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Builder {
+        private final Map<String, FormatterFactory> formattersMap = new HashMap<>();
+        private final Map<String, SelectorFactory> selectorsMap = new HashMap<>();
+        private final Map<Class<?>, String> classToFormatter = new HashMap<>();
+
+        // Prevent direct creation
+        private Builder() {
+        }
+
+        /**
+         * Adds all the mapping from another registry to this one.
+         *
+         * @param functionRegistry the registry to copy from.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addAll(Mf2FunctionRegistry functionRegistry) {
+            formattersMap.putAll(functionRegistry.formattersMap);
+            selectorsMap.putAll(functionRegistry.selectorsMap);
+            classToFormatter.putAll(functionRegistry.classToFormatter);
+            return this;
+        }
+
+        /**
+         * Adds a mapping from a formatter name to a {@link FormatterFactory}
+         *
+         * @param formatterName the function name (as used in the MessageFormat 2 syntax).
+         * @param formatterFactory the factory that handles the name.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setFormatter(String formatterName, FormatterFactory formatterFactory) {
+            formattersMap.put(formatterName, formatterFactory);
+            return this;
+        }
+
+        /**
+         * Remove the formatter associated with the name.
+         *
+         * @param formatterName the name of the formatter to remove.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder removeFormatter(String formatterName) {
+            formattersMap.remove(formatterName);
+            return this;
+        }
+
+        /**
+         * Remove all the formatter mappings.
+         *
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder clearFormatters() {
+            formattersMap.clear();
+            return this;
+        }
+
+        /**
+         * Adds a mapping from a type to format to a {@link FormatterFactory} formatter name.
+         *
+         * @param clazz the class of the type to format.
+         * @param formatterName the formatter name (as used in the MessageFormat 2 syntax).
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setDefaultFormatterNameForType(Class<?> clazz, String formatterName) {
+            classToFormatter.put(clazz, formatterName);
+            return this;
+        }
+
+        /**
+         * Remove the function name associated with the class.
+         *
+         * @param clazz the class to remove the mapping for.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder removeDefaultFormatterNameForType(Class<?> clazz) {
+            classToFormatter.remove(clazz);
+            return this;
+        }
+
+        /**
+         * Remove all the class to formatter-names mappings.
+         *
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder clearDefaultFormatterNames() {
+            classToFormatter.clear();
+            return this;
+        }
+
+        /**
+         * Adds a mapping from a selector name to a {@link SelectorFactory}
+         *
+         * @param selectorName the function name (as used in the MessageFormat 2 syntax).
+         * @param selectorFactory the factory that handles the name.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setSelector(String selectorName, SelectorFactory selectorFactory) {
+            selectorsMap.put(selectorName, selectorFactory);
+            return this;
+        }
+
+        /**
+         * Remove the selector associated with the name.
+         *
+         * @param selectorName the name of the selector to remove.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder removeSelector(String selectorName) {
+            selectorsMap.remove(selectorName);
+            return this;
+        }
+
+        /**
+         * Remove all the selector mappings.
+         *
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder clearSelectors() {
+            selectorsMap.clear();
+            return this;
+        }
+
+        /**
+         * Builds an instance of {@link Mf2FunctionRegistry}.
+         *
+         * @return the function registry created.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Mf2FunctionRegistry build() {
+            return new Mf2FunctionRegistry(this);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/Mf2Parser.java b/android_icu4j/src/main/java/android/icu/message2/Mf2Parser.java
new file mode 100644
index 0000000..5445212
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Mf2Parser.java
@@ -0,0 +1,770 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Arrays;
+
+/**
+ * Class generated from EBNF.
+ */
+@SuppressWarnings("all") // Disable all warnings in the generated file
+class Mf2Parser
+{
+  static class ParseException extends RuntimeException
+  {
+    private static final long serialVersionUID = 1L;
+    private int begin, end, offending, expected, state;
+
+    public ParseException(int b, int e, int s, int o, int x)
+    {
+      begin = b;
+      end = e;
+      state = s;
+      offending = o;
+      expected = x;
+    }
+
+    @Override
+    public String getMessage()
+    {
+      return offending < 0
+           ? "lexical analysis failed"
+           : "syntax error";
+    }
+
+    public void serialize(EventHandler eventHandler)
+    {
+    }
+
+    public int getBegin() {return begin;}
+    public int getEnd() {return end;}
+    public int getState() {return state;}
+    public int getOffending() {return offending;}
+    public int getExpected() {return expected;}
+    public boolean isAmbiguousInput() {return false;}
+  }
+
+  /**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public interface EventHandler
+  {
+    public void reset(CharSequence string);
+    public void startNonterminal(String name, int begin);
+    public void endNonterminal(String name, int end);
+    public void terminal(String name, int begin, int end);
+    public void whitespace(int begin, int end);
+  }
+
+  /**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public static class TopDownTreeBuilder implements EventHandler
+  {
+    private CharSequence input = null;
+    public Nonterminal[] stack = new Nonterminal[64];
+    private int top = -1;
+
+    @Override
+    public void reset(CharSequence input)
+    {
+      this.input = input;
+      top = -1;
+    }
+
+    @Override
+    public void startNonterminal(String name, int begin)
+    {
+      Nonterminal nonterminal = new Nonterminal(name, begin, begin, new Symbol[0]);
+      if (top >= 0) addChild(nonterminal);
+      if (++top >= stack.length) stack = Arrays.copyOf(stack, stack.length << 1);
+      stack[top] = nonterminal;
+    }
+
+    @Override
+    public void endNonterminal(String name, int end)
+    {
+      stack[top].end = end;
+      if (top > 0) --top;
+    }
+
+    @Override
+    public void terminal(String name, int begin, int end)
+    {
+      addChild(new Terminal(name, begin, end));
+    }
+
+    @Override
+    public void whitespace(int begin, int end)
+    {
+    }
+
+    private void addChild(Symbol s)
+    {
+      Nonterminal current = stack[top];
+      current.children = Arrays.copyOf(current.children, current.children.length + 1);
+      current.children[current.children.length - 1] = s;
+    }
+
+    public void serialize(EventHandler e)
+    {
+      e.reset(input);
+      stack[0].send(e);
+    }
+  }
+
+  /**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public static abstract class Symbol
+  {
+    public String name;
+    public int begin;
+    public int end;
+
+    protected Symbol(String name, int begin, int end)
+    {
+      this.name = name;
+      this.begin = begin;
+      this.end = end;
+    }
+
+    public abstract void send(EventHandler e);
+  }
+
+  /**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public static class Terminal extends Symbol
+  {
+    public Terminal(String name, int begin, int end)
+    {
+      super(name, begin, end);
+    }
+
+    @Override
+    public void send(EventHandler e)
+    {
+      e.terminal(name, begin, end);
+    }
+  }
+
+  /**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public static class Nonterminal extends Symbol
+  {
+    public Symbol[] children;
+
+    public Nonterminal(String name, int begin, int end, Symbol[] children)
+    {
+      super(name, begin, end);
+      this.children = children;
+    }
+
+    @Override
+    public void send(EventHandler e)
+    {
+      e.startNonterminal(name, begin);
+      int pos = begin;
+      for (Symbol c : children)
+      {
+        if (pos < c.begin) e.whitespace(pos, c.begin);
+        c.send(e);
+        pos = c.end;
+      }
+      if (pos < end) e.whitespace(pos, end);
+      e.endNonterminal(name, end);
+    }
+  }
+
+  public Mf2Parser(CharSequence string, EventHandler t)
+  {
+    initialize(string, t);
+  }
+
+  public void initialize(CharSequence source, EventHandler parsingEventHandler)
+  {
+    eventHandler = parsingEventHandler;
+    input = source;
+    size = source.length();
+    reset(0, 0, 0);
+  }
+
+  public CharSequence getInput()
+  {
+    return input;
+  }
+
+  public int getTokenOffset()
+  {
+    return b0;
+  }
+
+  public int getTokenEnd()
+  {
+    return e0;
+  }
+
+  public final void reset(int l, int b, int e)
+  {
+            b0 = b; e0 = b;
+    l1 = l; b1 = b; e1 = e;
+    end = e;
+    eventHandler.reset(input);
+  }
+
+  public void reset()
+  {
+    reset(0, 0, 0);
+  }
+
+  public static String getOffendingToken(ParseException e)
+  {
+    return e.getOffending() < 0 ? null : TOKEN[e.getOffending()];
+  }
+
+  public static String[] getExpectedTokenSet(ParseException e)
+  {
+    String[] expected;
+    if (e.getExpected() >= 0)
+    {
+      expected = new String[]{TOKEN[e.getExpected()]};
+    }
+    else
+    {
+      expected = getTokenSet(- e.getState());
+    }
+    return expected;
+  }
+
+  public String getErrorMessage(ParseException e)
+  {
+    String message = e.getMessage();
+    String[] tokenSet = getExpectedTokenSet(e);
+    String found = getOffendingToken(e);
+    int size = e.getEnd() - e.getBegin();
+    message += (found == null ? "" : ", found " + found)
+            + "\nwhile expecting "
+            + (tokenSet.length == 1 ? tokenSet[0] : java.util.Arrays.toString(tokenSet))
+            + "\n"
+            + (size == 0 || found != null ? "" : "after successfully scanning " + size + " characters beginning ");
+    String prefix = input.subSequence(0, e.getBegin()).toString();
+    int line = prefix.replaceAll("[^\n]", "").length() + 1;
+    int column = prefix.length() - prefix.lastIndexOf('\n');
+    return message
+         + "at line " + line + ", column " + column + ":\n..."
+         + input.subSequence(e.getBegin(), Math.min(input.length(), e.getBegin() + 64))
+         + "...";
+  }
+
+  public void parse_Message()
+  {
+    eventHandler.startNonterminal("Message", e0);
+    for (;;)
+    {
+      lookahead1W(12);              // WhiteSpace | 'let' | 'match' | '{'
+      if (l1 != 13)                 // 'let'
+      {
+        break;
+      }
+      whitespace();
+      parse_Declaration();
+    }
+    switch (l1)
+    {
+    case 16:                        // '{'
+      whitespace();
+      parse_Pattern();
+      break;
+    default:
+      whitespace();
+      parse_Selector();
+      for (;;)
+      {
+        whitespace();
+        parse_Variant();
+        lookahead1W(4);             // END | WhiteSpace | 'when'
+        if (l1 != 15)               // 'when'
+        {
+          break;
+        }
+      }
+    }
+    eventHandler.endNonterminal("Message", e0);
+  }
+
+  private void parse_Declaration()
+  {
+    eventHandler.startNonterminal("Declaration", e0);
+    consume(13);                    // 'let'
+    lookahead1W(0);                 // WhiteSpace | Variable
+    consume(4);                     // Variable
+    lookahead1W(1);                 // WhiteSpace | '='
+    consume(12);                    // '='
+    lookahead1W(2);                 // WhiteSpace | '{'
+    consume(16);                    // '{'
+    lookahead1W(9);                 // WhiteSpace | Variable | Function | Literal
+    whitespace();
+    parse_Expression();
+    consume(17);                    // '}'
+    eventHandler.endNonterminal("Declaration", e0);
+  }
+
+  private void parse_Selector()
+  {
+    eventHandler.startNonterminal("Selector", e0);
+    consume(14);                    // 'match'
+    for (;;)
+    {
+      lookahead1W(2);               // WhiteSpace | '{'
+      consume(16);                  // '{'
+      lookahead1W(9);               // WhiteSpace | Variable | Function | Literal
+      whitespace();
+      parse_Expression();
+      consume(17);                  // '}'
+      lookahead1W(7);               // WhiteSpace | 'when' | '{'
+      if (l1 != 16)                 // '{'
+      {
+        break;
+      }
+    }
+    eventHandler.endNonterminal("Selector", e0);
+  }
+
+  private void parse_Variant()
+  {
+    eventHandler.startNonterminal("Variant", e0);
+    consume(15);                    // 'when'
+    for (;;)
+    {
+      lookahead1W(11);              // WhiteSpace | Nmtoken | Literal | '*'
+      whitespace();
+      parse_VariantKey();
+      lookahead1W(13);              // WhiteSpace | Nmtoken | Literal | '*' | '{'
+      if (l1 == 16)                 // '{'
+      {
+        break;
+      }
+    }
+    whitespace();
+    parse_Pattern();
+    eventHandler.endNonterminal("Variant", e0);
+  }
+
+  private void parse_VariantKey()
+  {
+    eventHandler.startNonterminal("VariantKey", e0);
+    switch (l1)
+    {
+    case 10:                        // Literal
+      consume(10);                  // Literal
+      break;
+    case 9:                         // Nmtoken
+      consume(9);                   // Nmtoken
+      break;
+    default:
+      consume(11);                  // '*'
+    }
+    eventHandler.endNonterminal("VariantKey", e0);
+  }
+
+  private void parse_Pattern()
+  {
+    eventHandler.startNonterminal("Pattern", e0);
+    consume(16);                    // '{'
+    for (;;)
+    {
+      lookahead1(8);                // Text | '{' | '}'
+      if (l1 == 17)                 // '}'
+      {
+        break;
+      }
+      switch (l1)
+      {
+      case 3:                       // Text
+        consume(3);                 // Text
+        break;
+      default:
+        parse_Placeholder();
+      }
+    }
+    consume(17);                    // '}'
+    eventHandler.endNonterminal("Pattern", e0);
+  }
+
+  private void parse_Placeholder()
+  {
+    eventHandler.startNonterminal("Placeholder", e0);
+    consume(16);                    // '{'
+    lookahead1W(14);                // WhiteSpace | Variable | Function | MarkupStart | MarkupEnd | Literal | '}'
+    if (l1 != 17)                   // '}'
+    {
+      switch (l1)
+      {
+      case 6:                       // MarkupStart
+        whitespace();
+        parse_Markup();
+        break;
+      case 7:                       // MarkupEnd
+        consume(7);                 // MarkupEnd
+        break;
+      default:
+        whitespace();
+        parse_Expression();
+      }
+    }
+    lookahead1W(3);                 // WhiteSpace | '}'
+    consume(17);                    // '}'
+    eventHandler.endNonterminal("Placeholder", e0);
+  }
+
+  private void parse_Expression()
+  {
+    eventHandler.startNonterminal("Expression", e0);
+    switch (l1)
+    {
+    case 5:                         // Function
+      parse_Annotation();
+      break;
+    default:
+      parse_Operand();
+      lookahead1W(5);               // WhiteSpace | Function | '}'
+      if (l1 == 5)                  // Function
+      {
+        whitespace();
+        parse_Annotation();
+      }
+    }
+    eventHandler.endNonterminal("Expression", e0);
+  }
+
+  private void parse_Operand()
+  {
+    eventHandler.startNonterminal("Operand", e0);
+    switch (l1)
+    {
+    case 10:                        // Literal
+      consume(10);                  // Literal
+      break;
+    default:
+      consume(4);                   // Variable
+    }
+    eventHandler.endNonterminal("Operand", e0);
+  }
+
+  private void parse_Annotation()
+  {
+    eventHandler.startNonterminal("Annotation", e0);
+    consume(5);                     // Function
+    for (;;)
+    {
+      lookahead1W(6);               // WhiteSpace | Name | '}'
+      if (l1 != 8)                  // Name
+      {
+        break;
+      }
+      whitespace();
+      parse_Option();
+    }
+    eventHandler.endNonterminal("Annotation", e0);
+  }
+
+  private void parse_Option()
+  {
+    eventHandler.startNonterminal("Option", e0);
+    consume(8);                     // Name
+    lookahead1W(1);                 // WhiteSpace | '='
+    consume(12);                    // '='
+    lookahead1W(10);                // WhiteSpace | Variable | Nmtoken | Literal
+    switch (l1)
+    {
+    case 10:                        // Literal
+      consume(10);                  // Literal
+      break;
+    case 9:                         // Nmtoken
+      consume(9);                   // Nmtoken
+      break;
+    default:
+      consume(4);                   // Variable
+    }
+    eventHandler.endNonterminal("Option", e0);
+  }
+
+  private void parse_Markup()
+  {
+    eventHandler.startNonterminal("Markup", e0);
+    consume(6);                     // MarkupStart
+    for (;;)
+    {
+      lookahead1W(6);               // WhiteSpace | Name | '}'
+      if (l1 != 8)                  // Name
+      {
+        break;
+      }
+      whitespace();
+      parse_Option();
+    }
+    eventHandler.endNonterminal("Markup", e0);
+  }
+
+  private void consume(int t)
+  {
+    if (l1 == t)
+    {
+      whitespace();
+      eventHandler.terminal(TOKEN[l1], b1, e1);
+      b0 = b1; e0 = e1; l1 = 0;
+    }
+    else
+    {
+      error(b1, e1, 0, l1, t);
+    }
+  }
+
+  private void whitespace()
+  {
+    if (e0 != b1)
+    {
+      eventHandler.whitespace(e0, b1);
+      e0 = b1;
+    }
+  }
+
+  private int matchW(int tokenSetId)
+  {
+    int code;
+    for (;;)
+    {
+      code = match(tokenSetId);
+      if (code != 2)                // WhiteSpace
+      {
+        break;
+      }
+    }
+    return code;
+  }
+
+  private void lookahead1W(int tokenSetId)
+  {
+    if (l1 == 0)
+    {
+      l1 = matchW(tokenSetId);
+      b1 = begin;
+      e1 = end;
+    }
+  }
+
+  private void lookahead1(int tokenSetId)
+  {
+    if (l1 == 0)
+    {
+      l1 = match(tokenSetId);
+      b1 = begin;
+      e1 = end;
+    }
+  }
+
+  private int error(int b, int e, int s, int l, int t)
+  {
+    throw new ParseException(b, e, s, l, t);
+  }
+
+  private int     b0, e0;
+  private int l1, b1, e1;
+  private EventHandler eventHandler = null;
+  private CharSequence input = null;
+  private int size = 0;
+  private int begin = 0;
+  private int end = 0;
+
+  private int match(int tokenSetId)
+  {
+    begin = end;
+    int current = end;
+    int result = INITIAL[tokenSetId];
+    int state = 0;
+
+    for (int code = result & 63; code != 0; )
+    {
+      int charclass;
+      int c0 = current < size ? input.charAt(current) : 0;
+      ++current;
+      if (c0 < 0x80)
+      {
+        charclass = MAP0[c0];
+      }
+      else if (c0 < 0xd800)
+      {
+        int c1 = c0 >> 4;
+        charclass = MAP1[(c0 & 15) + MAP1[(c1 & 31) + MAP1[c1 >> 5]]];
+      }
+      else
+      {
+        if (c0 < 0xdc00)
+        {
+          int c1 = current < size ? input.charAt(current) : 0;
+          if (c1 >= 0xdc00 && c1 < 0xe000)
+          {
+            ++current;
+            c0 = ((c0 & 0x3ff) << 10) + (c1 & 0x3ff) + 0x10000;
+          }
+        }
+
+        int lo = 0, hi = 6;
+        for (int m = 3; ; m = (hi + lo) >> 1)
+        {
+          if (MAP2[m] > c0) {hi = m - 1;}
+          else if (MAP2[7 + m] < c0) {lo = m + 1;}
+          else {charclass = MAP2[14 + m]; break;}
+          if (lo > hi) {charclass = 0; break;}
+        }
+      }
+
+      state = code;
+      int i0 = (charclass << 6) + code - 1;
+      code = TRANSITION[(i0 & 7) + TRANSITION[i0 >> 3]];
+
+      if (code > 63)
+      {
+        result = code;
+        code &= 63;
+        end = current;
+      }
+    }
+
+    result >>= 6;
+    if (result == 0)
+    {
+      end = current - 1;
+      int c1 = end < size ? input.charAt(end) : 0;
+      if (c1 >= 0xdc00 && c1 < 0xe000)
+      {
+        --end;
+      }
+      return error(begin, end, state, -1, -1);
+    }
+
+    if (end > size) end = size;
+    return (result & 31) - 1;
+  }
+
+  private static String[] getTokenSet(int tokenSetId)
+  {
+    java.util.ArrayList<String> expected = new java.util.ArrayList<>();
+    int s = tokenSetId < 0 ? - tokenSetId : INITIAL[tokenSetId] & 63;
+    for (int i = 0; i < 18; i += 32)
+    {
+      int j = i;
+      int i0 = (i >> 5) * 38 + s - 1;
+      int f = EXPECTED[i0];
+      for ( ; f != 0; f >>>= 1, ++j)
+      {
+        if ((f & 1) != 0)
+        {
+          expected.add(TOKEN[j]);
+        }
+      }
+    }
+    return expected.toArray(new String[]{});
+  }
+
+  private static final int[] MAP0 =
+  {
+    /*   0 */ 24, 24, 24, 24, 24, 24, 24, 24, 24, 1, 1, 24, 24, 1, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+    /*  27 */ 24, 24, 24, 24, 24, 1, 24, 24, 24, 2, 24, 24, 24, 3, 4, 5, 6, 24, 7, 8, 24, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+    /*  58 */ 9, 24, 24, 10, 24, 24, 24, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+    /*  85 */ 11, 11, 11, 11, 11, 11, 24, 12, 24, 24, 11, 24, 13, 11, 14, 11, 15, 11, 11, 16, 11, 11, 11, 17, 18, 19,
+    /* 111 */ 11, 11, 11, 11, 11, 20, 11, 11, 21, 11, 11, 11, 22, 24, 23, 24, 24
+  };
+
+  private static final int[] MAP1 =
+  {
+    /*   0 */ 108, 124, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 156, 181, 181, 181, 181,
+    /*  21 */ 181, 214, 215, 213, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+    /*  42 */ 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+    /*  63 */ 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+    /*  84 */ 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+    /* 105 */ 214, 214, 214, 383, 330, 396, 353, 291, 262, 247, 308, 330, 330, 330, 322, 292, 284, 292, 284, 292, 292,
+    /* 126 */ 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 347, 347, 347, 347, 347, 347, 347,
+    /* 147 */ 277, 292, 292, 292, 292, 292, 292, 292, 292, 369, 330, 330, 331, 329, 330, 330, 292, 292, 292, 292, 292,
+    /* 168 */ 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 330, 330, 330, 330, 330, 330, 330, 330,
+    /* 189 */ 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330,
+    /* 210 */ 330, 330, 330, 291, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292,
+    /* 231 */ 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 330, 24, 13, 11, 14, 11, 15,
+    /* 253 */ 11, 11, 16, 11, 11, 11, 17, 18, 19, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 24, 12, 24, 24, 11, 11,
+    /* 279 */ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 24, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+    /* 305 */ 11, 11, 11, 11, 11, 11, 11, 20, 11, 11, 21, 11, 11, 11, 22, 24, 23, 24, 24, 24, 24, 24, 24, 24, 8, 24, 24,
+    /* 332 */ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+    /* 363 */ 9, 24, 24, 10, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 11, 11, 24, 24, 24, 24, 24, 24, 24,
+    /* 390 */ 24, 24, 1, 1, 24, 24, 1, 24, 24, 24, 2, 24, 24, 24, 3, 4, 5, 6, 24, 7, 8, 24
+  };
+
+  private static final int[] MAP2 =
+  {
+    /*  0 */ 55296, 63744, 64976, 65008, 65534, 65536, 983040, 63743, 64975, 65007, 65533, 65535, 983039, 1114111, 24,
+    /* 15 */ 11, 24, 11, 24, 11, 24
+  };
+
+  private static final int[] INITIAL =
+  {
+    /*  0 */ 1, 2, 3, 4, 133, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
+  };
+
+  private static final int[] TRANSITION =
+  {
+    /*   0 */ 237, 237, 237, 237, 237, 237, 237, 237, 200, 208, 455, 237, 237, 237, 237, 237, 236, 230, 455, 237, 237,
+    /*  21 */ 237, 237, 237, 237, 245, 376, 382, 237, 237, 237, 237, 237, 380, 314, 382, 237, 237, 237, 237, 237, 263,
+    /*  42 */ 455, 237, 237, 237, 237, 237, 237, 295, 455, 237, 237, 237, 237, 237, 237, 322, 287, 281, 252, 237, 237,
+    /*  63 */ 237, 237, 344, 287, 281, 252, 237, 237, 237, 255, 358, 455, 237, 237, 237, 237, 237, 417, 380, 455, 237,
+    /*  84 */ 237, 237, 237, 237, 419, 390, 215, 329, 252, 237, 237, 237, 237, 398, 275, 382, 237, 237, 237, 237, 419,
+    /* 105 */ 390, 215, 410, 252, 237, 237, 237, 419, 390, 215, 329, 309, 237, 237, 237, 419, 390, 222, 365, 252, 237,
+    /* 126 */ 237, 237, 419, 390, 427, 329, 302, 237, 237, 237, 419, 435, 215, 329, 252, 237, 237, 237, 419, 443, 215,
+    /* 147 */ 329, 252, 237, 237, 237, 419, 390, 215, 329, 372, 237, 237, 237, 419, 390, 215, 336, 451, 237, 237, 237,
+    /* 168 */ 402, 390, 215, 329, 252, 237, 237, 237, 350, 463, 269, 237, 237, 237, 237, 237, 474, 471, 269, 237, 237,
+    /* 189 */ 237, 237, 237, 237, 380, 455, 237, 237, 237, 237, 237, 192, 192, 192, 192, 192, 192, 192, 192, 277, 192,
+    /* 210 */ 192, 192, 192, 192, 192, 0, 414, 595, 0, 277, 22, 663, 0, 414, 595, 0, 277, 22, 663, 32, 277, 16, 16, 0,
+    /* 234 */ 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 277, 22, 22, 22, 0, 22, 22, 0, 482, 547, 0, 0, 0, 0, 0, 18, 0, 0, 277,
+    /* 264 */ 0, 0, 768, 0, 768, 0, 0, 0, 277, 0, 22, 0, 0, 0, 277, 20, 31, 0, 0, 0, 348, 0, 414, 0, 0, 595, 0, 277, 22,
+    /* 293 */ 663, 0, 277, 0, 0, 0, 0, 0, 26, 0, 482, 547, 0, 0, 960, 0, 0, 482, 547, 0, 38, 0, 0, 0, 0, 277, 704, 0, 0,
+    /* 322 */ 277, 0, 663, 663, 0, 663, 27, 0, 482, 547, 348, 0, 414, 0, 0, 482, 547, 348, 0, 414, 0, 896, 277, 0, 663,
+    /* 347 */ 663, 0, 663, 0, 0, 1088, 0, 0, 0, 0, 1088, 277, 18, 0, 0, 0, 0, 18, 0, 482, 547, 348, 36, 414, 0, 0, 482,
+    /* 374 */ 547, 1024, 0, 0, 0, 0, 277, 0, 0, 0, 0, 0, 0, 0, 22, 0, 277, 0, 663, 663, 0, 663, 0, 348, 20, 0, 0, 0, 0,
+    /* 403 */ 0, 0, 0, 17, 0, 595, 17, 33, 482, 547, 348, 0, 414, 0, 0, 832, 0, 0, 0, 0, 0, 0, 595, 0, 29, 414, 595, 0,
+    /* 431 */ 277, 22, 663, 0, 277, 0, 663, 663, 24, 663, 0, 348, 277, 0, 663, 663, 25, 663, 0, 348, 37, 482, 547, 0, 0,
+    /* 456 */ 0, 0, 0, 277, 22, 0, 0, 1088, 0, 0, 0, 1088, 1088, 0, 0, 1152, 0, 0, 0, 0, 0, 1152, 0, 1152, 1152, 0
+  };
+
+  private static final int[] EXPECTED =
+  {
+    /*  0 */ 20, 4100, 65540, 131076, 32772, 131108, 131332, 98308, 196616, 1076, 1556, 3588, 90116, 69124, 132340, 16,
+    /* 16 */ 32768, 32, 256, 8, 8, 1024, 512, 8192, 16384, 64, 128, 16, 32768, 32, 1024, 8192, 16384, 64, 128, 32768,
+    /* 36 */ 16384, 16384
+  };
+
+  private static final String[] TOKEN =
+  {
+    "(0)",
+    "END",
+    "WhiteSpace",
+    "Text",
+    "Variable",
+    "Function",
+    "MarkupStart",
+    "MarkupEnd",
+    "Name",
+    "Nmtoken",
+    "Literal",
+    "'*'",
+    "'='",
+    "'let'",
+    "'match'",
+    "'when'",
+    "'{'",
+    "'}'"
+  };
+}
+
+// End
diff --git a/android_icu4j/src/main/java/android/icu/message2/Mf2Serializer.java b/android_icu4j/src/main/java/android/icu/message2/Mf2Serializer.java
new file mode 100644
index 0000000..64ae13e
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Mf2Serializer.java
@@ -0,0 +1,501 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.icu.message2.Mf2DataModel.Expression;
+import android.icu.message2.Mf2DataModel.Pattern;
+import android.icu.message2.Mf2DataModel.SelectorKeys;
+import android.icu.message2.Mf2DataModel.Text;
+import android.icu.message2.Mf2DataModel.Value;
+import android.icu.message2.Mf2Parser.EventHandler;
+import android.icu.message2.Mf2Serializer.Token.Type;
+
+// TODO: find a better name for this class
+class Mf2Serializer implements EventHandler {
+    private String input;
+    private final List<Token> tokens = new ArrayList<>();
+
+    static class Token {
+        final String name;
+        final int begin;
+        final int end;
+        final Kind kind;
+        private final Type type;
+        private final String input;
+
+        enum Kind {
+            TERMINAL,
+            NONTERMINAL_START,
+            NONTERMINAL_END
+        }
+
+        enum Type {
+            MESSAGE,
+            PATTERN,
+            TEXT,
+            PLACEHOLDER,
+            EXPRESSION,
+            OPERAND,
+            VARIABLE,
+            IGNORE,
+            FUNCTION,
+            OPTION,
+            NAME,
+            NMTOKEN,
+            LITERAL,
+            SELECTOR,
+            VARIANT,
+            DECLARATION, VARIANTKEY, DEFAULT,
+        }
+
+        Token(Kind kind, String name, int begin, int end, String input) {
+            this.kind = kind;
+            this.name = name;
+            this.begin = begin;
+            this.end = end;
+            this.input = input;
+            switch (name) {
+                case "Message": type = Type.MESSAGE; break;
+                case "Pattern": type = Type.PATTERN; break;
+                case "Text": type = Type.TEXT; break;
+                case "Placeholder": type = Type.PLACEHOLDER; break;
+                case "Expression": type = Type.EXPRESSION; break;
+                case "Operand": type = Type.OPERAND; break;
+                case "Variable": type = Type.VARIABLE; break;
+                case "Function": type = Type.FUNCTION; break;
+                case "Option": type = Type.OPTION; break;
+                case "Annotation": type = Type.IGNORE; break;
+                case "Name": type = Type.NAME; break;
+                case "Nmtoken": type = Type.NMTOKEN; break;
+                case "Literal": type = Type.LITERAL; break;
+                case "Selector": type = Type.SELECTOR; break;
+                case "Variant": type = Type.VARIANT; break;
+                case "VariantKey": type = Type.VARIANTKEY; break;
+                case "Declaration": type = Type.DECLARATION; break;
+
+                case "Markup": type = Type.IGNORE; break;
+                case "MarkupStart": type = Type.IGNORE; break;
+                case "MarkupEnd": type = Type.IGNORE; break;
+
+                case "'['": type = Type.IGNORE; break;
+                case "']'": type = Type.IGNORE; break;
+                case "'{'": type = Type.IGNORE; break;
+                case "'}'": type = Type.IGNORE; break;
+                case "'='": type = Type.IGNORE; break;
+                case "'match'": type = Type.IGNORE; break;
+                case "'when'": type = Type.IGNORE; break;
+                case "'let'": type = Type.IGNORE; break;
+                case "'*'": type = Type.DEFAULT; break;
+                default:
+                    throw new IllegalArgumentException("Parse error: Unknown token \"" + name + "\"");
+            }
+        }
+
+        boolean isStart() {
+            return Kind.NONTERMINAL_START.equals(kind);
+        }
+
+        boolean isEnd() {
+            return Kind.NONTERMINAL_END.equals(kind);
+        }
+
+        boolean isTerminal() {
+            return Kind.TERMINAL.equals(kind);
+        }
+
+        @Override
+        public String toString() {
+            int from = begin == -1 ? 0 : begin;
+            String strval = end == -1 ? input.substring(from) : input.substring(from, end);
+            return String.format("Token(\"%s\", [%d, %d], %s) // \"%s\"", name, begin, end, kind, strval);
+        }
+    }
+
+    Mf2Serializer() {}
+
+    @Override
+    public void reset(CharSequence input) {
+        this.input = input.toString();
+        tokens.clear();
+    }
+
+    @Override
+    public void startNonterminal(String name, int begin) {
+        tokens.add(new Token(Token.Kind.NONTERMINAL_START, name, begin, -1, input));
+    }
+
+    @Override
+    public void endNonterminal(String name, int end) {
+        tokens.add(new Token(Token.Kind.NONTERMINAL_END, name, -1, end, input));
+    }
+
+    @Override
+    public void terminal(String name, int begin, int end) {
+        tokens.add(new Token(Token.Kind.TERMINAL, name, begin, end, input));
+    }
+
+    @Override
+    public void whitespace(int begin, int end) {
+    }
+
+    Mf2DataModel build() {
+        if (!tokens.isEmpty()) {
+            Token firstToken = tokens.get(0);
+            if (Type.MESSAGE.equals(firstToken.type) && firstToken.isStart()) {
+                return parseMessage();
+            }
+        }
+        return null;
+    }
+
+    private Mf2DataModel parseMessage() {
+        Mf2DataModel.Builder result = Mf2DataModel.builder();
+
+        for (int i = 0; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case MESSAGE:
+                    if (token.isStart() && i == 0) {
+                        // all good
+                    } else if (token.isEnd() && i == tokens.size() - 1) {
+                        // We check if this last token is at the end of the input
+                        if (token.end != input.length()) {
+                            String leftover = input.substring(token.end)
+                                    .replace("\n", "")
+                                    .replace("\r", "")
+                                    .replace(" ", "")
+                                    .replace("\t", "")
+                                    ;
+                            if (!leftover.isEmpty()) {
+                                throw new IllegalArgumentException("Parse error: Content detected after the end of the message: '"
+                                        + input.substring(token.end) + "'");
+                            }
+                        }
+                        return result.build();
+                    } else {
+                        // End of message, we ignore the rest
+                        throw new IllegalArgumentException("Parse error: Extra tokens at the end of the message");
+                    }
+                    break;
+                case PATTERN:
+                    ParseResult<Pattern> patternResult = parsePattern(i);
+                    i = patternResult.skipLen;
+                    result.setPattern(patternResult.resultValue);
+                    break;
+                case DECLARATION:
+                    Declaration declaration = new Declaration();
+                    i = parseDeclaration(i, declaration);
+                    result.addLocalVariable(declaration.variableName, declaration.expr);
+                    break;
+                case SELECTOR:
+                    ParseResult<List<Expression>> selectorResult = parseSelector(i);
+                    result.addSelectors(selectorResult.resultValue);
+                    i = selectorResult.skipLen;
+                    break;
+                case VARIANT:
+                    ParseResult<Variant> variantResult = parseVariant(i);
+                    i = variantResult.skipLen;
+                    Variant variant = variantResult.resultValue;
+                    result.addVariant(variant.getSelectorKeys(), variant.getPattern());
+                    break;
+                case IGNORE:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parseMessage UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing MessageFormatter");
+    }
+
+    private ParseResult<Variant> parseVariant(int startToken) {
+        Variant.Builder result = Variant.builder();
+
+        for (int i = startToken; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case VARIANT:
+                    if (token.isStart()) { // all good
+                    } else if (token.isEnd()) {
+                        return new ParseResult<>(i, result.build());
+                    }
+                    break;
+                case LITERAL:
+                    result.addSelectorKey(input.substring(token.begin + 1, token.end - 1));
+                    break;
+                case NMTOKEN:
+                    result.addSelectorKey(input.substring(token.begin, token.end));
+                    break;
+                case DEFAULT:
+                    result.addSelectorKey("*");
+                    break;
+                case PATTERN:
+                    ParseResult<Pattern> patternResult = parsePattern(i);
+                    i = patternResult.skipLen;
+                    result.setPattern(patternResult.resultValue);
+                    break;
+                case VARIANTKEY:
+//                    variant.variantKey = new VariantKey(input.substring(token.begin, token.end));
+                    break;
+                case IGNORE:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parseVariant UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing Variant");
+    }
+
+    private ParseResult<List<Expression>> parseSelector(int startToken) {
+        List<Expression> result = new ArrayList<>();
+
+        for (int i = startToken; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case SELECTOR:
+                    if (token.isStart()) { // all good, do nothing
+                    } else if (token.isEnd()) {
+                        return new ParseResult<>(i, result);
+                    }
+                    break;
+                case EXPRESSION:
+                    ParseResult<Expression> exprResult = parseExpression(i);
+                    i = exprResult.skipLen;
+                    result.add(exprResult.resultValue);
+                    break;
+                case IGNORE:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parseSelector UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing selectors");
+    }
+
+    private int parseDeclaration(int startToken, Declaration declaration) {
+        for (int i = startToken; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case DECLARATION:
+                    if (token.isStart()) { // all good
+                    } else if (token.isEnd()) {
+                        return i;
+                    }
+                    break;
+                case VARIABLE:
+                    declaration.variableName = input.substring(token.begin + 1, token.end);
+                    break;
+                case EXPRESSION:
+                    ParseResult<Expression> exprResult = parseExpression(i);
+                    i = exprResult.skipLen;
+                    declaration.expr = exprResult.resultValue;
+                    break;
+                case IGNORE:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parseDeclaration UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing Declaration");
+    }
+
+    private ParseResult<Pattern> parsePattern(int startToken) {
+        Pattern.Builder result = Pattern.builder();
+
+        for (int i = startToken; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case TEXT:
+                    Text text = new Text(input.substring(token.begin, token.end));
+                    result.add(text);
+                    break;
+                case PLACEHOLDER:
+                    break;
+                case EXPRESSION:
+                    ParseResult<Expression> exprResult = parseExpression(i);
+                    i = exprResult.skipLen;
+                    result.add(exprResult.resultValue);
+                    break;
+                case VARIABLE:
+                case IGNORE:
+                    break;
+                case PATTERN:
+                    if (token.isStart() && i == startToken) { // all good, do nothing
+                    } else if (token.isEnd()) {
+                        return new ParseResult<>(i, result.build());
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parsePattern UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing Pattern");
+    }
+
+    static class Option {
+        String name;
+        Value value;
+    }
+
+    static class Declaration {
+        String variableName;
+        Expression expr;
+    }
+
+    static class Variant {
+        private final SelectorKeys selectorKeys;
+        private final Pattern pattern;
+
+        private Variant(Builder builder) {
+            this.selectorKeys = builder.selectorKeys.build();
+            this.pattern = builder.pattern;
+        }
+
+        /**
+         * Creates a builder.
+         *
+         * @return the Builder.
+         */
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        public SelectorKeys getSelectorKeys() {
+            return selectorKeys;
+        }
+
+        public Pattern getPattern() {
+            return pattern;
+        }
+
+        /**
+         * @hide Only a subset of ICU is exposed in Android
+         */
+        public static class Builder {
+            private final SelectorKeys.Builder selectorKeys = SelectorKeys.builder();
+            private Pattern pattern = Pattern.builder().build();
+
+            // Prevent direct creation
+            private Builder() {
+            }
+
+            public Builder setSelectorKeys(SelectorKeys selectorKeys) {
+                this.selectorKeys.addAll(selectorKeys.getKeys());
+                return this;
+            }
+
+            public Builder addSelectorKey(String selectorKey) {
+                this.selectorKeys.add(selectorKey);
+                return this;
+            }
+
+            public Builder setPattern(Pattern pattern) {
+                this.pattern = pattern;
+                return this;
+            }
+
+            public Variant build() {
+                return new Variant(this);
+            }
+        }
+    }
+
+    static class ParseResult<T> {
+        final int skipLen;
+        final T resultValue;
+
+        public ParseResult(int skipLen, T resultValue) {
+            this.skipLen = skipLen;
+            this.resultValue = resultValue;
+        }
+    }
+
+    private ParseResult<Expression> parseExpression(int startToken) {
+        Expression.Builder result = Expression.builder();
+
+        for (int i = startToken; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case EXPRESSION: // intentional fall-through
+                case PLACEHOLDER:
+                    if (token.isStart() && i == startToken) {
+                        // all good
+                    } else if (token.isEnd()) {
+                        return new ParseResult<>(i, result.build());
+                    }
+                    break;
+                case FUNCTION:
+                    result.setFunctionName(input.substring(token.begin + 1, token.end));
+                    break;
+                case LITERAL:
+                    result.setOperand(Value.builder()
+                            .setLiteral(input.substring(token.begin + 1, token.end - 1))
+                            .build());
+                    break;
+                case VARIABLE:
+                    result.setOperand(Value.builder()
+                            .setVariableName(input.substring(token.begin + 1, token.end))
+                            .build());
+                    break;
+                case OPTION:
+                    Option option = new Option();
+                    i = parseOptions(i, option);
+                    result.addOption(option.name, option.value);
+                    break;
+                case OPERAND:
+                    break;
+                case IGNORE:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parseExpression UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing Expression");
+    }
+
+    private int parseOptions(int startToken, Option option) {
+        for (int i = startToken; i < tokens.size(); i++) {
+            Token token = tokens.get(i);
+            switch (token.type) {
+                case OPTION:
+                    if (token.isStart() && i == startToken) {
+                        // all good
+                    } else if (token.isEnd()) {
+                        return i;
+                    }
+                    break;
+                case NAME:
+                    option.name = input.substring(token.begin, token.end);
+                    break;
+                case LITERAL:
+                    option.value = Value.builder()
+                            .setLiteral(input.substring(token.begin + 1, token.end - 1))
+                            .build();
+                    break;
+                case NMTOKEN:
+                    option.value = Value.builder()
+                            .setLiteral(input.substring(token.begin, token.end))
+                            .build();
+                    break;
+                case VARIABLE:
+                    option.value = Value.builder()
+                            .setVariableName(input.substring(token.begin + 1, token.end))
+                            .build();
+                    break;
+                case IGNORE:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Parse error: parseOptions UNEXPECTED TOKEN: '" + token + "'");
+            }
+        }
+        throw new IllegalArgumentException("Parse error: Error parsing Option");
+    }
+
+    static String dataModelToString(Mf2DataModel dataModel) {
+        return dataModel.toString();
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/NumberFormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/NumberFormatterFactory.java
new file mode 100644
index 0000000..f10cc18
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/NumberFormatterFactory.java
@@ -0,0 +1,133 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import android.icu.math.BigDecimal;
+import android.icu.number.LocalizedNumberFormatter;
+import android.icu.number.NumberFormatter;
+import android.icu.number.Precision;
+import android.icu.number.UnlocalizedNumberFormatter;
+import android.icu.text.FormattedValue;
+import android.icu.util.CurrencyAmount;
+
+
+/**
+ * Creates a {@link Formatter} doing numeric formatting, similar to <code>{exp, number}</code>
+ * in {@link android.icu.text.MessageFormat}.
+ */
+class NumberFormatterFactory implements FormatterFactory {
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
+        return new NumberFormatterImpl(locale, fixedOptions);
+    }
+
+    static class NumberFormatterImpl implements Formatter {
+        private final Locale locale;
+        private final Map<String, Object> fixedOptions;
+        private final LocalizedNumberFormatter icuFormatter;
+        final boolean advanced;
+
+        private static LocalizedNumberFormatter formatterForOptions(Locale locale, Map<String, Object> fixedOptions) {
+            UnlocalizedNumberFormatter nf;
+            String skeleton = OptUtils.getString(fixedOptions, "skeleton");
+            if (skeleton != null) {
+                nf = NumberFormatter.forSkeleton(skeleton);
+            } else {
+                nf = NumberFormatter.with();
+                Integer minFractionDigits = OptUtils.getInteger(fixedOptions, "minimumFractionDigits");
+                if (minFractionDigits != null) {
+                    nf = nf.precision(Precision.minFraction(minFractionDigits));
+                }
+            }
+            return nf.locale(locale);
+        }
+
+        NumberFormatterImpl(Locale locale, Map<String, Object> fixedOptions) {
+            this.locale = locale;
+            this.fixedOptions = new HashMap<>(fixedOptions);
+            String skeleton = OptUtils.getString(fixedOptions, "skeleton");
+            boolean fancy = skeleton != null;
+            this.icuFormatter = formatterForOptions(locale, fixedOptions);
+            this.advanced = fancy;
+        }
+
+        LocalizedNumberFormatter getIcuFormatter() {
+            return icuFormatter;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
+            return format(toFormat, variableOptions).toString();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
+            LocalizedNumberFormatter realFormatter;
+            if (variableOptions.isEmpty()) {
+                realFormatter = this.icuFormatter;
+            } else {
+                Map<String, Object> mergedOptions = new HashMap<>(fixedOptions);
+                mergedOptions.putAll(variableOptions);
+                // This is really wasteful, as we don't use the existing
+                // formatter if even one option is variable.
+                // We can optimize, but for now will have to do.
+                realFormatter = formatterForOptions(locale, mergedOptions);
+            }
+
+            Integer offset = OptUtils.getInteger(variableOptions, "offset");
+            if (offset == null && fixedOptions != null) {
+                offset = OptUtils.getInteger(fixedOptions, "offset");
+            }
+            if (offset == null) {
+                offset = 0;
+            }
+
+            FormattedValue result = null;
+            if (toFormat == null) {
+                // This is also what MessageFormat does.
+                throw new NullPointerException("Argument to format can't be null");
+            } else if (toFormat instanceof Double) {
+                result = realFormatter.format((double) toFormat - offset);
+            } else if (toFormat instanceof Long) {
+                result = realFormatter.format((long) toFormat - offset);
+            } else if (toFormat instanceof Integer) {
+                result = realFormatter.format((int) toFormat - offset);
+            } else if (toFormat instanceof BigDecimal) {
+                BigDecimal bd = (BigDecimal) toFormat;
+                result = realFormatter.format(bd.subtract(BigDecimal.valueOf(offset)));
+            } else if (toFormat instanceof Number) {
+                result = realFormatter.format(((Number) toFormat).doubleValue() - offset);
+            } else if (toFormat instanceof CurrencyAmount) {
+                result = realFormatter.format((CurrencyAmount) toFormat);
+            } else {
+                // The behavior is not in the spec, will be in the registry.
+                // We can return "NaN", or try to parse the string as a number
+                String strValue = Objects.toString(toFormat);
+                Number nrValue = OptUtils.asNumber(strValue);
+                if (nrValue != null) {
+                    result = realFormatter.format(nrValue.doubleValue() - offset);
+                } else {
+                    result = new PlainStringFormattedValue("NaN");
+                }
+            }
+            return new FormattedPlaceholder(toFormat, result);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/OptUtils.java b/android_icu4j/src/main/java/android/icu/message2/OptUtils.java
new file mode 100644
index 0000000..b7ece56
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/OptUtils.java
@@ -0,0 +1,53 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Map;
+
+class OptUtils {
+    private OptUtils() {}
+
+    static Number asNumber(Object value) {
+        if (value instanceof Number) {
+            return (Number) value;
+        }
+        if (value instanceof CharSequence) {
+            String strValue = value.toString();
+            try {
+                return Double.parseDouble(strValue);
+            } catch (NumberFormatException e) {
+            }
+            try {
+                return Integer.decode(strValue);
+            } catch (NumberFormatException e) {
+            }
+        }
+        return null;
+    }
+
+    static Integer getInteger(Map<String, Object> options, String key) {
+        Object value = options.get(key);
+        if (value == null) {
+            return null;
+        }
+        Number nrValue = asNumber(value);
+        if (nrValue != null) {
+            return nrValue.intValue();
+        }
+        return null;
+    }
+
+    static String getString(Map<String, Object> options, String key) {
+        Object value = options.get(key);
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof CharSequence) {
+            return value.toString();
+        }
+        return null;
+    }
+
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/PlainStringFormattedValue.java b/android_icu4j/src/main/java/android/icu/message2/PlainStringFormattedValue.java
new file mode 100644
index 0000000..388647b
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/PlainStringFormattedValue.java
@@ -0,0 +1,134 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.text.AttributedCharacterIterator;
+
+import android.icu.text.ConstrainedFieldPosition;
+import android.icu.text.FormattedValue;
+
+/**
+ * Very-very rough implementation of FormattedValue, packaging a string.
+ * Expect it to change.
+ *
+ * @deprecated This API is for unit testing only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class PlainStringFormattedValue implements FormattedValue {
+    private final String value;
+
+    /**
+     * Constructor, taking the string to store.
+     *
+     * @param value the string value to store
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public PlainStringFormattedValue(String value) {
+        if (value == null) {
+            throw new IllegalAccessError("Should not try to wrap a null in a formatted value");
+        }
+        this.value = value;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public int length() {
+        return value == null ? 0 : value.length();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public char charAt(int index) {
+        return value.charAt(index);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return value.subSequence(start, end);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public <A extends Appendable> A appendTo(A appendable) {
+        try {
+            appendable.append(value);
+        } catch (IOException e) {
+            throw new UncheckedIOException("problem appending", e);
+        }
+        return appendable;
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public boolean nextPosition(ConstrainedFieldPosition cfpos) {
+        throw new RuntimeException("nextPosition not yet implemented");
+    }
+
+    /**
+     * Not yet implemented.
+     *
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public AttributedCharacterIterator toCharacterIterator() {
+        throw new RuntimeException("toCharacterIterator not yet implemented");
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    @Override
+    public String toString() {
+        return value;
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/PluralSelectorFactory.java b/android_icu4j/src/main/java/android/icu/message2/PluralSelectorFactory.java
new file mode 100644
index 0000000..b941709
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/PluralSelectorFactory.java
@@ -0,0 +1,111 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+import android.icu.number.FormattedNumber;
+import android.icu.text.FormattedValue;
+import android.icu.text.PluralRules;
+import android.icu.text.PluralRules.PluralType;
+
+/**
+ * Creates a {@link Selector} doing plural selection, similar to <code>{exp, plural}</code>
+ * in {@link android.icu.text.MessageFormat}.
+ */
+class PluralSelectorFactory implements SelectorFactory {
+    private final PluralType pluralType;
+
+    /**
+     * Creates a {@code PluralSelectorFactory} of the desired type.
+     *
+     * @param type the kind of plural selection we want
+     */
+    // TODO: Use an enum
+    PluralSelectorFactory(String type) {
+        switch (type) {
+            case "ordinal":
+                pluralType = PluralType.ORDINAL;
+                break;
+            case "cardinal": // intentional fallthrough
+            default:
+                pluralType = PluralType.CARDINAL;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Selector createSelector(Locale locale, Map<String, Object> fixedOptions) {
+        PluralRules rules = PluralRules.forLocale(locale, pluralType);
+        return new PluralSelectorImpl(rules, fixedOptions);
+    }
+
+    private static class PluralSelectorImpl implements Selector {
+        private final PluralRules rules;
+        private Map<String, Object> fixedOptions;
+
+        private PluralSelectorImpl(PluralRules rules, Map<String, Object> fixedOptions) {
+            this.rules = rules;
+            this.fixedOptions = fixedOptions;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean matches(Object value, String key, Map<String, Object> variableOptions) {
+            if (value == null) {
+                return false;
+            }
+            if ("*".equals(key)) {
+                return true;
+            }
+
+            Integer offset = OptUtils.getInteger(variableOptions, "offset");
+            if (offset == null && fixedOptions != null) {
+                offset = OptUtils.getInteger(fixedOptions, "offset");
+            }
+            if (offset == null) {
+                offset = 0;
+            }
+
+            double valToCheck = Double.MIN_VALUE;
+            FormattedValue formattedValToCheck = null;
+            if (value instanceof FormattedPlaceholder) {
+                FormattedPlaceholder fph = (FormattedPlaceholder) value;
+                value = fph.getInput();
+                formattedValToCheck = fph.getFormattedValue();
+            }
+
+            if (value instanceof Double) {
+                valToCheck = (double) value;
+            } else if (value instanceof Integer) {
+                valToCheck = (Integer) value;
+            } else {
+                return false;
+            }
+
+            // If there is nothing "tricky" about the formatter part we compare values directly.
+            // Right now ICU4J checks if the formatter is a DecimalFormt, which also feels "hacky".
+            // We need something better.
+            if (!fixedOptions.containsKey("skeleton") && !variableOptions.containsKey("skeleton")) {
+                try { // for match exact.
+                    if (Double.parseDouble(key) == valToCheck) {
+                        return true;
+                    }
+                } catch (NumberFormatException e) {
+                }
+            }
+
+            String match = formattedValToCheck instanceof FormattedNumber
+                    ? rules.select((FormattedNumber) formattedValToCheck)
+                    : rules.select(valToCheck - offset);
+            return match.equals(key);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/Selector.java b/android_icu4j/src/main/java/android/icu/message2/Selector.java
new file mode 100644
index 0000000..895c2d7
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/Selector.java
@@ -0,0 +1,39 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Map;
+
+/**
+ * The interface that must be implemented by all selectors
+ * that can be used from {@link MessageFormatter}.
+ *
+ * <p>Selectors are used to choose between different message variants,
+ * similar to <code>plural</code>, <code>selectordinal</code>,
+ * and <code>select</code> in {@link android.icu.text.MessageFormat}.</p>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public interface Selector {
+    /**
+     * A method that is invoked for the object to match and each key.
+     *
+     * <p>For example an English plural {@code matches} would return {@code true}
+     * for {@code matches(1, "1")}, {@code matches(1, "one")}, and {@code matches(1, "*")}.</p>
+     *
+     * @param value the value to select on.
+     * @param key the key to test for matching.
+     * @param variableOptions options that are not know at build time.
+     * @return the formatted string.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    boolean matches(Object value, String key, Map<String, Object> variableOptions);
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/SelectorFactory.java b/android_icu4j/src/main/java/android/icu/message2/SelectorFactory.java
new file mode 100644
index 0000000..2b9ead3
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/SelectorFactory.java
@@ -0,0 +1,34 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The interface that must be implemented for each selection function
+ * that can be used from {@link MessageFormatter}.
+ *
+ * <p>The we use it to create and cache various selectors with various options.</p>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public interface SelectorFactory {
+    /**
+     * The method that is called to create a selector.
+     *
+     * @param locale the locale to use for selection.
+     * @param fixedOptions the options to use for selection. The keys and values are function dependent.
+     * @return The Selector.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    Selector createSelector(Locale locale, Map<String, Object> fixedOptions);
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/TextSelectorFactory.java b/android_icu4j/src/main/java/android/icu/message2/TextSelectorFactory.java
new file mode 100644
index 0000000..f293a12
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/TextSelectorFactory.java
@@ -0,0 +1,37 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+
+/**
+ * Creates a {@link Selector} doing literal selection, similar to <code>{exp, select}</code>
+ * in {@link android.icu.text.MessageFormat}.
+ */
+class TextSelectorFactory implements SelectorFactory {
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Selector createSelector(Locale locale, Map<String, Object> fixedOptions) {
+        return new TextSelector();
+    }
+
+    private static class TextSelector implements Selector {
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean matches(Object value, String key, Map<String, Object> variableOptions) {
+            if ("*".equals(key)) {
+                return true;
+            }
+            return key.equals(value);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java b/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java
index 0f10b8a..7855bbf 100644
--- a/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java
+++ b/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java
@@ -11,6 +11,7 @@
 import android.icu.impl.Utility;
 import android.icu.impl.number.DecimalQuantity;
 import android.icu.text.ConstrainedFieldPosition;
+import android.icu.text.DisplayOptions;
 import android.icu.text.FormattedValue;
 import android.icu.text.PluralRules.IFixedDecimal;
 import android.icu.util.MeasureUnit;
@@ -121,6 +122,17 @@
     }
 
     /**
+     * Gets the noun class of the formatted output. Returns `UNDEFINED` when the noun class is not
+     * supported yet.
+     *
+     * @return NounClass
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public DisplayOptions.NounClass getNounClass() {
+        return DisplayOptions.NounClass.fromIdentifier(this.gender);
+    }
+
+    /**
      * The gender of the formatted output.
      *
      * @deprecated This API is for technology preview only.
diff --git a/android_icu4j/src/main/java/android/icu/number/FractionPrecision.java b/android_icu4j/src/main/java/android/icu/number/FractionPrecision.java
index ced2a35..3f6966a 100644
--- a/android_icu4j/src/main/java/android/icu/number/FractionPrecision.java
+++ b/android_icu4j/src/main/java/android/icu/number/FractionPrecision.java
@@ -30,8 +30,6 @@
      * @param priority
      *            How to disambiguate between fraction digits and significant digits.
      * @return A precision for chaining or passing to the NumberFormatter precision() setter.
-     *
-     * @hide draft / provisional / internal are hidden on Android
      */
     public Precision withSignificantDigits(
             int minSignificantDigits,
@@ -41,7 +39,7 @@
                 maxSignificantDigits >= minSignificantDigits &&
                 maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
             return constructFractionSignificant(
-                this, minSignificantDigits, maxSignificantDigits, priority);
+                this, minSignificantDigits, maxSignificantDigits, priority, false);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
@@ -73,7 +71,7 @@
     public Precision withMinDigits(int minSignificantDigits) {
         if (minSignificantDigits >= 1 && minSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
             return constructFractionSignificant(
-                this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED);
+                this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED, true);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
@@ -105,7 +103,7 @@
     public Precision withMaxDigits(int maxSignificantDigits) {
         if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
             return constructFractionSignificant(
-                this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT);
+                this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT, true);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java b/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java
index 101b06e..05174d7 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java
@@ -109,22 +109,15 @@
      *
      * <p>Here, RELAXED favors Max-Fraction and STRICT favors Max-Significant. Note that this larger
      * number caused the two modes to favor the opposite result.
-     *
-     * @hide Only a subset of ICU is exposed in Android
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static enum RoundingPriority {
         /**
          * Favor greater precision by relaxing one of the rounding constraints.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         RELAXED,
 
         /**
          * Favor adherence to all rounding constraints by producing lower precision.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         STRICT,
     }
@@ -415,15 +408,11 @@
 
         /**
          * Same as AUTO, but do not show the sign on negative zero.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         NEGATIVE,
 
         /**
          * Same as ACCOUNTING, but do not show the sign on negative zero.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         ACCOUNTING_NEGATIVE,
     }
@@ -463,22 +452,15 @@
      * <li>AUTO: 0.90, 1.00, 1.10
      * <li>HIDE_IF_WHOLE: 0.90, 1, 1.10
      * </ul>
-     *
-     * @hide Only a subset of ICU is exposed in Android
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static enum TrailingZeroDisplay {
         /**
          * Display trailing zeros according to the settings for minimum fraction and significant digits.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         AUTO,
 
         /**
          * Same as AUTO, but hide trailing zeros after the decimal separator if they are all zero.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         HIDE_IF_WHOLE,
     }
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java b/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java
index 698d42a..b37e16e 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java
@@ -364,7 +364,9 @@
         // The default middle modifier is weak (thus the false argument).
         MutablePatternModifier patternMod = new MutablePatternModifier(false);
         AffixPatternProvider affixProvider =
-            (macros.affixProvider != null)
+            (macros.affixProvider != null && (
+                    // For more information on this condition, see ICU-22073
+                    !isCompactNotation || isCurrency == macros.affixProvider.hasCurrencySign()))
                 ? macros.affixProvider
                 : patternInfo;
         patternMod.setPatternInfo(affixProvider, null);
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberFormatterSettings.java b/android_icu4j/src/main/java/android/icu/number/NumberFormatterSettings.java
index 2efa445..76946f6 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberFormatterSettings.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatterSettings.java
@@ -12,6 +12,8 @@
 import android.icu.number.NumberFormatter.SignDisplay;
 import android.icu.number.NumberFormatter.UnitWidth;
 import android.icu.text.DecimalFormatSymbols;
+import android.icu.text.DisplayOptions;
+import android.icu.text.DisplayOptions.GrammaticalCase;
 import android.icu.text.NumberingSystem;
 import android.icu.util.Currency;
 import android.icu.util.Measure;
@@ -529,6 +531,22 @@
     }
 
     /**
+     * Specifies the {@code DisplayOptions}. For example, {@code GrammaticalCase} specifies
+     * the desired case for a unit formatter's output (e.g. accusative, dative, genitive).
+     *
+     * @return The fluent chain.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public T displayOptions(DisplayOptions displayOptions) {
+        // `displayCase` does not recognise the `undefined`
+        if (displayOptions.getGrammaticalCase() == GrammaticalCase.UNDEFINED) {
+            return create(KEY_UNIT_DISPLAY_CASE, null);
+        }
+
+        return create(KEY_UNIT_DISPLAY_CASE, displayOptions.getGrammaticalCase().getIdentifier());
+    }
+
+    /**
      * Specifies the desired case for a unit formatter's output (e.g.
      * accusative, dative, genitive).
      *
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java b/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java
index 8ba138b..5fd805e 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java
@@ -302,8 +302,6 @@
             } else {
                 macros.notation = Notation.compactShort();
             }
-            // Do not forward the affix provider.
-            macros.affixProvider = null;
         }
 
         /////////////////
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatter.java b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatter.java
index 9618b5a..1d986fa 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatter.java
@@ -145,7 +145,6 @@
      * Class for span fields in FormattedNumberRange.
      *
      * @hide Only a subset of ICU is exposed in Android
-     * @hide draft / provisional / internal are hidden on Android
      */
     public static final class SpanField extends UFormat.SpanField {
         private static final long serialVersionUID = 8750397196515368729L;
@@ -155,8 +154,6 @@
          *
          * Instances of NUMBER_RANGE_SPAN should have an associated value, the index within the input
          * list that is represented by the span.
-         *
-         * @hide draft / provisional / internal are hidden on Android
          */
         public static final SpanField NUMBER_RANGE_SPAN = new SpanField("number-range-span");
 
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterImpl.java b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterImpl.java
index bff637e..b7ae9a8 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterImpl.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterImpl.java
@@ -158,7 +158,7 @@
                 : NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY;
 
         String nsName = formatterImpl1.getRawMicroProps().nsName;
-        if (nsName == null || !nsName.equals(formatterImpl2.getRawMicroProps().nsName)) {
+        if (nsName == null || (!fSameFormatters && !nsName.equals(formatterImpl2.getRawMicroProps().nsName))) {
             throw new IllegalArgumentException("Both formatters must have same numbering system");
         }
         getNumberRangeData(macros.loc, nsName, this);
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java b/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java
index 6356939..5532eb7 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java
@@ -1336,8 +1336,9 @@
                 // @, @@, @@@
                 maxSig = minSig;
             }
-            RoundingPriority priority;
+            FractionPrecision oldRounder = (FractionPrecision) macros.precision;
             if (offset < segment.length()) {
+                RoundingPriority priority;
                 if (maxSig == -1) {
                     throw new SkeletonSyntaxException(
                         "Invalid digits option: Wildcard character not allowed with the priority annotation", segment);
@@ -1356,21 +1357,18 @@
                     throw new SkeletonSyntaxException(
                         "Invalid digits option for fraction rounder", segment);
                 }
+                macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
             } else if (maxSig == -1) {
                 // withMinDigits
-                maxSig = minSig;
-                minSig = 1;
-                priority = RoundingPriority.RELAXED;
+                macros.precision = oldRounder.withMinDigits(minSig);
             } else if (minSig == 1) {
                 // withMaxDigits
-                priority = RoundingPriority.STRICT;
+                macros.precision = oldRounder.withMaxDigits(maxSig);
             } else {
                 throw new SkeletonSyntaxException(
                     "Invalid digits option: Priority annotation required", segment);
             }
 
-            FractionPrecision oldRounder = (FractionPrecision) macros.precision;
-            macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
             return true;
         }
 
@@ -1578,11 +1576,19 @@
                 Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
                 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
                 sb.append('/');
-                BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
-                if (impl.priority == RoundingPriority.RELAXED) {
-                    sb.append('r');
+                if (impl.retain) {
+                    if (impl.priority == RoundingPriority.RELAXED) {
+                        BlueprintHelpers.generateDigitsStem(impl.maxSig, -1, sb);
+                    } else {
+                        BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
+                    }
                 } else {
-                    sb.append('s');
+                    BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
+                    if (impl.priority == RoundingPriority.RELAXED) {
+                        sb.append('r');
+                    } else {
+                        sb.append('s');
+                    }
                 }
             } else if (macros.precision instanceof Precision.IncrementRounderImpl) {
                 Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
diff --git a/android_icu4j/src/main/java/android/icu/number/Precision.java b/android_icu4j/src/main/java/android/icu/number/Precision.java
index c892ad8..7d139df 100644
--- a/android_icu4j/src/main/java/android/icu/number/Precision.java
+++ b/android_icu4j/src/main/java/android/icu/number/Precision.java
@@ -332,7 +332,6 @@
      * when the number is an integer, use HIDE_IF_WHOLE.
      *
      * @param trailingZeroDisplay Option to configure the display of trailing zeros.
-     * @hide draft / provisional / internal are hidden on Android
      */
     public Precision trailingZeroDisplay(TrailingZeroDisplay trailingZeroDisplay) {
         Precision result = this.createCopy();
@@ -360,6 +359,16 @@
     abstract Precision createCopy();
 
     /**
+     * Call this function to copy the fields from the Precision base class.
+     *
+     * Note: It would be nice if this returned the copy, but most impls return the child class, not Precision.
+     */
+    /* package-private */ void createCopyHelper(Precision copy) {
+        copy.mathContext = mathContext;
+        copy.trailingZeroDisplay = trailingZeroDisplay;
+    }
+
+    /**
      * @deprecated ICU 60 This API is ICU internal only.
      * @hide draft / provisional / internal are hidden on Android
      */
@@ -387,7 +396,8 @@
     static final SignificantRounderImpl FIXED_SIG_3 = new SignificantRounderImpl(3, 3);
     static final SignificantRounderImpl RANGE_SIG_2_3 = new SignificantRounderImpl(2, 3);
 
-    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED);
+    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED,
+            false);
 
     static final IncrementFiveRounderImpl NICKEL = new IncrementFiveRounderImpl(new BigDecimal("0.05"), 2, 2);
 
@@ -423,16 +433,16 @@
         }
     }
 
-    static Precision constructFractionSignificant(
-            FractionPrecision base_, int minSig, int maxSig, RoundingPriority priority) {
+    static Precision constructFractionSignificant(FractionPrecision base_, int minSig, int maxSig,
+            RoundingPriority priority, boolean retain) {
         assert base_ instanceof FractionRounderImpl;
         FractionRounderImpl base = (FractionRounderImpl) base_;
         Precision returnValue;
-        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 &&
-                priority == RoundingPriority.RELAXED) {
+        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 && priority == RoundingPriority.RELAXED
+                && !retain) {
             returnValue = COMPACT_STRATEGY;
         } else {
-            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority);
+            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority, retain);
         }
         return returnValue.withMode(base.mathContext);
     }
@@ -590,7 +600,7 @@
         @Override
         BogusRounder createCopy() {
             BogusRounder copy = new BogusRounder();
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
 
@@ -603,7 +613,7 @@
         @Deprecated
         public Precision into(Precision precision) {
             Precision copy = precision.createCopy();
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -622,7 +632,7 @@
         @Override
         InfiniteRounderImpl createCopy() {
             InfiniteRounderImpl copy = new InfiniteRounderImpl();
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -645,7 +655,7 @@
         @Override
         FractionRounderImpl createCopy() {
             FractionRounderImpl copy = new FractionRounderImpl(minFrac, maxFrac);
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -681,7 +691,7 @@
         @Override
         SignificantRounderImpl createCopy() {
             SignificantRounderImpl copy = new SignificantRounderImpl(minSig, maxSig);
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -692,13 +702,16 @@
         final int minSig;
         final int maxSig;
         final RoundingPriority priority;
+        final boolean retain;
 
-        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority) {
+        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority,
+                boolean retain) {
             this.minFrac = minFrac;
             this.maxFrac = maxFrac;
             this.minSig = minSig;
             this.maxSig = maxSig;
             this.priority = priority;
+            this.retain = retain;
         }
 
         @Override
@@ -711,18 +724,42 @@
             } else {
                 roundingMag = Math.max(roundingMag1, roundingMag2);
             }
-            value.roundToMagnitude(roundingMag, mathContext);
+            if (!value.isZeroish()) {
+                int upperMag = value.getMagnitude();
+                value.roundToMagnitude(roundingMag, mathContext);
+                if (!value.isZeroish() && value.getMagnitude() != upperMag && roundingMag1 == roundingMag2) {
+                    // roundingMag2 needs to be the magnitude after rounding
+                    roundingMag2 += 1;
+                }
+            }
 
             int displayMag1 = getDisplayMagnitudeFraction(minFrac);
             int displayMag2 = getDisplayMagnitudeSignificant(value, minSig);
-            int displayMag = Math.min(displayMag1, displayMag2);
+            int displayMag;
+            if (retain) {
+                // withMinDigits + withMaxDigits
+                displayMag = Math.min(displayMag1, displayMag2);
+            } else if (priority == RoundingPriority.RELAXED) {
+                if (roundingMag2 <= roundingMag1) {
+                    displayMag = displayMag2;
+                } else {
+                    displayMag = displayMag1;
+                }
+            } else {
+                assert(priority == RoundingPriority.STRICT);
+                if (roundingMag2 <= roundingMag1) {
+                    displayMag = displayMag1;
+                } else {
+                    displayMag = displayMag2;
+                }
+            }
             setResolvedMinFraction(value, Math.max(0, -displayMag));
         }
 
         @Override
         FracSigRounderImpl createCopy() {
-            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority);
-            copy.mathContext = mathContext;
+            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority, retain);
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -740,13 +777,13 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToIncrement(increment, mathContext);
-            setResolvedMinFraction(value, increment.scale());
+            setResolvedMinFraction(value, Math.max(0, increment.scale()));
         }
 
         @Override
         IncrementRounderImpl createCopy() {
             IncrementRounderImpl copy = new IncrementRounderImpl(increment);
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -775,7 +812,7 @@
         @Override
         IncrementOneRounderImpl createCopy() {
             IncrementOneRounderImpl copy = new IncrementOneRounderImpl(increment, minFrac, maxFrac);
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -802,7 +839,7 @@
         @Override
         IncrementFiveRounderImpl createCopy() {
             IncrementFiveRounderImpl copy = new IncrementFiveRounderImpl(increment, minFrac, maxFrac);
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
@@ -823,7 +860,7 @@
         @Override
         CurrencyRounderImpl createCopy() {
             CurrencyRounderImpl copy = new CurrencyRounderImpl(usage);
-            copy.mathContext = mathContext;
+            createCopyHelper(copy);
             return copy;
         }
     }
diff --git a/android_icu4j/src/main/java/android/icu/platform/AndroidDataFiles.java b/android_icu4j/src/main/java/android/icu/platform/AndroidDataFiles.java
index 73e2d50..74ab82f 100644
--- a/android_icu4j/src/main/java/android/icu/platform/AndroidDataFiles.java
+++ b/android_icu4j/src/main/java/android/icu/platform/AndroidDataFiles.java
@@ -37,7 +37,6 @@
     public static final String ANDROID_ROOT_ENV = "ANDROID_ROOT";
     public static final String ANDROID_I18N_ROOT_ENV = "ANDROID_I18N_ROOT";
     public static final String ANDROID_TZDATA_ROOT_ENV = "ANDROID_TZDATA_ROOT";
-    public static final String ANDROID_DATA_ENV = "ANDROID_DATA";
 
     // VisibleForTesting
     public static String getTimeZoneModuleIcuFile(String fileName) {
@@ -63,15 +62,7 @@
         // Note: This logic below should match the logic in IcuRegistration.cpp in external/icu/
         // to ensure consistent behavior between ICU4C and ICU4J.
 
-        // ICU should first look in ANDROID_DATA. This is used for (optional) time zone data
-        // delivered by APK (https://source.android.com/devices/tech/config/timezone-rules)
-        String dataIcuDataPath =
-                getEnvironmentPath(ANDROID_DATA_ENV, "/misc/zoneinfo/current/icu/");
-        if (dataIcuDataPath != null) {
-            paths.add(dataIcuDataPath);
-        }
-
-        // ICU should then look for a mounted time zone module file in /apex. This is used for
+        // ICU should look for a mounted time zone module file in /apex. This is used for
         // (optional) time zone data that can be updated with an APEX file.
         String timeZoneModuleIcuDataPath = getTimeZoneModuleIcuFile("");
         paths.add(timeZoneModuleIcuDataPath);
@@ -83,16 +74,4 @@
 
         return String.join(":", paths);
     }
-
-    /**
-     * Creates a path by combining the value of an environment variable with a relative path.
-     * Returns {@code null} if the environment variable is not set.
-     */
-    private static String getEnvironmentPath(String environmentVariable, String path) {
-        String variable = System.getenv(environmentVariable);
-        if (variable == null) {
-            return null;
-        }
-        return variable + path;
-    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/text/AnyTransliterator.java b/android_icu4j/src/main/java/android/icu/text/AnyTransliterator.java
index 934b03b..abde1d3 100644
--- a/android_icu4j/src/main/java/android/icu/text/AnyTransliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/AnyTransliterator.java
@@ -53,6 +53,12 @@
     static final String LATIN_PIVOT = "-Latin;Latin-";
 
     /**
+     * Special code for handling width characters
+     */
+    private static final Transliterator WIDTH_FIX =
+            Transliterator.getInstance("[[:dt=Nar:][:dt=Wide:]] nfkd");
+
+    /**
      * Cache mapping UScriptCode values to Transliterator*.
      */
     private ConcurrentHashMap<Integer, Transliterator> cache;
@@ -68,11 +74,6 @@
     private int targetScript;
 
     /**
-     * Special code for handling width characters
-     */
-    private Transliterator widthFix = Transliterator.getInstance("[[:dt=Nar:][:dt=Wide:]] nfkd");
-
-    /**
      * Implements {@link Transliterator#handleTransliterate}.
      */
     @Override
@@ -176,7 +177,7 @@
             if (isWide(targetScript)) {
                 return null;
             } else {
-                return widthFix;
+                return WIDTH_FIX;
             }
         }
 
@@ -201,7 +202,7 @@
             if (t != null) {
                 if (!isWide(targetScript)) {
                     List<Transliterator> v = new ArrayList<Transliterator>();
-                    v.add(widthFix);
+                    v.add(WIDTH_FIX);
                     v.add(t);
                     t = new CompoundTransliterator(v);
                 }
@@ -210,7 +211,7 @@
                     t = prevCachedT;
                 }
             } else if (!isWide(targetScript)) {
-                return widthFix;
+                return WIDTH_FIX;
             }
         }
 
@@ -342,7 +343,7 @@
 
 
         /**
-         * Returns TRUE if there are any more runs.  TRUE is always
+         * Returns true if there are any more runs.  true is always
          * returned at least once.  Upon return, the caller should
          * examine scriptCode, start, and limit.
          */
@@ -385,7 +386,7 @@
                 ++limit;
             }
 
-            // Return TRUE even if the entire text is COMMON / INHERITED, in
+            // Return true even if the entire text is COMMON / INHERITED, in
             // which case scriptCode will be UScript.INVALID_CODE.
             return true;
         }
@@ -408,7 +409,7 @@
         if (filter != null && filter instanceof UnicodeSet) {
             filter = new UnicodeSet((UnicodeSet)filter);
         }
-        return new AnyTransliterator(getID(), filter, target, targetScript, widthFix, cache);
+        return new AnyTransliterator(getID(), filter, target, targetScript, WIDTH_FIX, cache);
     }
 
     /* (non-Javadoc)
diff --git a/android_icu4j/src/main/java/android/icu/text/BidiLine.java b/android_icu4j/src/main/java/android/icu/text/BidiLine.java
index ef95239..4f3fbe8 100644
--- a/android_icu4j/src/main/java/android/icu/text/BidiLine.java
+++ b/android_icu4j/src/main/java/android/icu/text/BidiLine.java
@@ -94,7 +94,7 @@
            are already set to paragraph level.
            Setting trailingWSStart to pBidi->length will avoid changing the
            level of B chars from 0 to paraLevel in getLevels when
-           orderParagraphsLTR==TRUE
+           orderParagraphsLTR==true
         */
         if (dirProps[start - 1] == Bidi.B) {
             bidi.trailingWSStart = start;   /* currently == bidi.length */
diff --git a/android_icu4j/src/main/java/android/icu/text/BreakIteratorFactory.java b/android_icu4j/src/main/java/android/icu/text/BreakIteratorFactory.java
index e8dfb42..1922926 100644
--- a/android_icu4j/src/main/java/android/icu/text/BreakIteratorFactory.java
+++ b/android_icu4j/src/main/java/android/icu/text/BreakIteratorFactory.java
@@ -137,7 +137,8 @@
                 typeKeyExt = "_" + keyValue;
             }
             String language = locale.getLanguage();
-            if (language != null && language.equals("ja")) {
+            // lw=phrase is only supported in Japanese and Korean
+            if (language != null && (language.equals("ja") || language.equals("ko"))) {
                 keyValue = locale.getKeywordValue("lw");
                 if (keyValue != null && keyValue.equals("phrase")) {
                     typeKeyExt += "_" + keyValue;
diff --git a/android_icu4j/src/main/java/android/icu/text/CompoundTransliterator.java b/android_icu4j/src/main/java/android/icu/text/CompoundTransliterator.java
index fb85f8b..c9677f0 100644
--- a/android_icu4j/src/main/java/android/icu/text/CompoundTransliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/CompoundTransliterator.java
@@ -139,7 +139,7 @@
      * @param splitTrans a transliterator to be inserted
      * before the entry at offset idSplitPoint in the id string.  May be
      * NULL to insert no entry.
-     * @param fixReverseID if TRUE, then reconstruct the ID of reverse
+     * @param fixReverseID if true, then reconstruct the ID of reverse
      * entries by calling getID() of component entries.  Some constructors
      * do not require this because they apply a facade ID anyway.
      */
@@ -174,7 +174,7 @@
      * is, it should be in the FORWARD order; if direction is REVERSE then
      * the list order will be reversed.
      * @param direction either FORWARD or REVERSE
-     * @param fixReverseID if TRUE, then reconstruct the ID of reverse
+     * @param fixReverseID if true, then reconstruct the ID of reverse
      * entries by calling getID() of component entries.  Some constructors
      * do not require this because they apply a facade ID anyway.
      */
@@ -258,7 +258,7 @@
      * Override Transliterator:
      * Create a rule string that can be passed to createFromRules()
      * to recreate this transliterator.
-     * @param escapeUnprintable if TRUE then convert unprintable
+     * @param escapeUnprintable if true then convert unprintable
      * character to their hex escape representations, \\uxxxx or
      * \\Uxxxxxxxx.  Unprintable characters are those other than
      * U+000A, U+0020..U+007E.
diff --git a/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java b/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java
index cb513a9..8fa483b 100644
--- a/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java
+++ b/android_icu4j/src/main/java/android/icu/text/ConstrainedFieldPosition.java
@@ -197,7 +197,7 @@
      * Gets the field for the current position.
      *
      * The return value is well-defined and non-null only after
-     * FormattedValue#nextPosition returns TRUE.
+     * FormattedValue#nextPosition returns true.
      *
      * @return The field saved in the instance. See above for null conditions.
      */
@@ -208,7 +208,7 @@
     /**
      * Gets the INCLUSIVE start index for the current position.
      *
-     * The return value is well-defined only after FormattedValue#nextPosition returns TRUE.
+     * The return value is well-defined only after FormattedValue#nextPosition returns true.
      *
      * @return The start index saved in the instance.
      */
@@ -219,7 +219,7 @@
     /**
      * Gets the EXCLUSIVE end index stored for the current position.
      *
-     * The return value is well-defined only after FormattedValue#nextPosition returns TRUE.
+     * The return value is well-defined only after FormattedValue#nextPosition returns true.
      *
      * @return The end index saved in the instance.
      */
@@ -230,7 +230,7 @@
     /**
      * Gets the value associated with the current field position. The field value is often not set.
      *
-     * The return value is well-defined only after FormattedValue#nextPosition returns TRUE.
+     * The return value is well-defined only after FormattedValue#nextPosition returns true.
      *
      * @return The value for the current position. Might be null.
      */
diff --git a/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java b/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java
index 9b693af..8948b79 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateIntervalFormat.java
@@ -207,11 +207,12 @@
  *   // and parses into
  *   DateInterval dtInterval = new DateInterval(1000*3600*24L, 1000*3600*24*2L);
  *   DateIntervalFormat dtIntervalFmt = DateIntervalFormat.getInstance(
- *                   YEAR_MONTH_DAY, Locale("en", "GB", ""));
- *   StringBuffer str = new StringBuffer("");
- *   FieldPosition pos = new FieldPosition(0);
+ *           DateFormat.YEAR_MONTH_DAY, new Locale("en", "GB", ""));
+ *   StringBuffer result = new StringBuffer("");
+ *   FieldPosition pos = new FieldPosition(-1);
  *   // formatting
- *   dtIntervalFmt.format(dtInterval, dateIntervalString, pos);
+ *   dtIntervalFmt.format(dtInterval, result, pos);
+ *   assertEquals("interval", "1–2 January 1970", result.toString());
  *
  * </pre>
  *
@@ -1588,18 +1589,27 @@
         StringBuilder result = new StringBuilder(skeleton);
 
         char hourMetachar = '\0';
-        int metacharStart = 0;
-        int metacharCount = 0;
+        char dayPeriodChar = '\0';
+        int hourFieldStart = 0;
+        int hourFieldLength = 0;
+        int dayPeriodStart = 0;
+        int dayPeriodLength = 0;
         for (int i = 0; i < result.length(); i++) {
             char c = result.charAt(i);
-            if (c == 'j' || c == 'J' || c == 'C') {
+            if (c == 'j' || c == 'J' || c == 'C' || c == 'h' || c == 'H' || c == 'k' || c == 'K') {
                 if (hourMetachar == '\0') {
                     hourMetachar = c;
-                    metacharStart = i;
+                    hourFieldStart = i;
                 }
-                ++metacharCount;
+                ++hourFieldLength;
+            } else if (c == 'a' || c == 'b' || c == 'B') {
+                if (dayPeriodChar == '\0') {
+                    dayPeriodChar = c;
+                    dayPeriodStart = i;
+                }
+                ++dayPeriodLength;
             } else {
-                if (hourMetachar != '\0') {
+                if (hourMetachar != '\0' && dayPeriodChar != '\0') {
                     break;
                 }
             }
@@ -1607,7 +1617,6 @@
 
         if (hourMetachar != '\0') {
             char hourChar = 'H';
-            char dayPeriodChar = 'a';
 
             DateTimePatternGenerator dtptng = DateTimePatternGenerator.getInstance(locale);
             String convertedPattern = dtptng.getBestPattern(String.valueOf(hourMetachar));
@@ -1635,34 +1644,30 @@
                 dayPeriodChar = 'b';
             } else if (convertedPattern.indexOf('B') != -1) {
                 dayPeriodChar = 'B';
+            } else if (dayPeriodChar == '\0') {
+                dayPeriodChar = 'a';
             }
 
-            if (hourChar == 'H' || hourChar == 'k') {
-                result.replace(metacharStart, metacharStart + metacharCount, String.valueOf(hourChar));
-            } else {
-                StringBuilder hourAndDayPeriod = new StringBuilder();
-                hourAndDayPeriod.append(hourChar);
-                switch (metacharCount) {
-                    case 1:
-                    case 2:
-                    default:
-                        hourAndDayPeriod.append(dayPeriodChar);
-                        break;
-                    case 3:
-                    case 4:
-                        for (int i = 0; i < 4; i++) {
-                            hourAndDayPeriod.append(dayPeriodChar);
-                        }
-                        break;
-                    case 5:
-                    case 6:
-                        for (int i = 0; i < 5; i++) {
-                            hourAndDayPeriod.append(dayPeriodChar);
-                        }
-                        break;
+            StringBuilder hourAndDayPeriod = new StringBuilder();
+            hourAndDayPeriod.append(hourChar);
+            if (hourChar != 'H' && hourChar != 'k') {
+                int newDayPeriodLength = 0;
+                if (dayPeriodLength >= 5 || hourFieldLength >= 5) {
+                    newDayPeriodLength = 5;
+                } else if (dayPeriodLength >= 3 || hourFieldLength >= 3) {
+                    newDayPeriodLength = 3;
+                } else {
+                    newDayPeriodLength = 1;
                 }
-                result.replace(metacharStart, metacharStart + metacharCount, hourAndDayPeriod.toString());
+                for (int i = 0; i < newDayPeriodLength; i++) {
+                    hourAndDayPeriod.append(dayPeriodChar);
+                }
             }
+            result.replace(hourFieldStart, hourFieldStart + hourFieldLength, hourAndDayPeriod.toString());
+            if (dayPeriodStart > hourFieldStart) {
+                dayPeriodStart += hourAndDayPeriod.length() - hourFieldLength;
+            }
+            result.delete(dayPeriodStart, dayPeriodStart + dayPeriodLength);
         }
         return result.toString();
     }
@@ -2095,8 +2100,16 @@
         if (suppressDayPeriodField) {
             if (bestMatchIntervalPattern.indexOf(" a") != -1) {
                 bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, " a", "");
+            } else if (bestMatchIntervalPattern.indexOf("\u00A0a") != -1) {
+                bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "\u00A0a", "");
+            } else if (bestMatchIntervalPattern.indexOf("\u202Fa") != -1) {
+                bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "\u202Fa", "");
             } else if (bestMatchIntervalPattern.indexOf("a ") != -1) {
                 bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a ", "");
+            } else if (bestMatchIntervalPattern.indexOf("a\u00A0") != -1) {
+                bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a\u00A0", "");
+            } else if (bestMatchIntervalPattern.indexOf("a\u202F") != -1) {
+                bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a\u202F", "");
             }
             bestMatchIntervalPattern = findReplaceInPattern(bestMatchIntervalPattern, "a", "");
         }
diff --git a/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java b/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java
index e5bbb8f..4069b5b 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateIntervalInfo.java
@@ -906,8 +906,8 @@
      * Get default order -- whether the first date in pattern is later date
      *                      or not.
      *
-     * return default date ordering in interval pattern. TRUE if the first date
-     *        in pattern is later date, FALSE otherwise.
+     * return default date ordering in interval pattern. true if the first date
+     *        in pattern is later date, false otherwise.
      */
     public boolean getDefaultOrder()
     {
diff --git a/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java b/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java
index 3bd036f..1c51b1e 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java
@@ -309,8 +309,11 @@
     }
 
     private void setDateTimeFromCalendar(ULocale uLocale) {
-        String dateTimeFormat = Calendar.getDateTimePattern(Calendar.getInstance(uLocale), uLocale, DateFormat.MEDIUM);
-        setDateTimeFormat(dateTimeFormat);
+        Calendar cal = Calendar.getInstance(uLocale);
+        for (int style = DateFormat.FULL; style <= DateFormat.SHORT; style++) {
+            String dateTimeFormat = Calendar.getDateAtTimePattern(cal, uLocale, style);
+            setDateTimeFormat(style, dateTimeFormat);
+        }
     }
 
     private void setDecimalSymbols(ULocale uLocale) {
@@ -351,6 +354,7 @@
 
         String language = uLocale.getLanguage();
         String country = uLocale.getCountry();
+        
         if (language.isEmpty() || country.isEmpty()) {
             // Note: addLikelySubtags is documented not to throw in Java,
             // unlike in C++.
@@ -359,6 +363,15 @@
             country = max.getCountry();
         }
 
+        String regionOverride = uLocale.getKeywordValue("rg");
+        if (regionOverride != null && !regionOverride.isEmpty()) {
+            // chop off any subdivision codes that may have been included
+            if (regionOverride.length() > 2) {
+                regionOverride = regionOverride.substring(0, 2);
+            }
+            country = regionOverride;
+        }
+        
         if (language.isEmpty()) {
             // Unexpected, but fail gracefully
             language = "und";
@@ -676,8 +689,26 @@
 
         if (datePattern == null) return timePattern == null ? "" : timePattern;
         if (timePattern == null) return datePattern;
+        // determine which dateTimeFormat to use
+        String canonicalSkeleton = current.toCanonicalString(); // month fields use M, weekday fields use E
+        int style = DateFormat.SHORT;
+        int monthFieldLen = 0;
+        int monthFieldOffset = canonicalSkeleton.indexOf('M');
+        if (monthFieldOffset >= 0) {
+            monthFieldLen = 1 + canonicalSkeleton.lastIndexOf('M') - monthFieldOffset;
+        }
+        if (monthFieldLen == 4) {
+            if (canonicalSkeleton.indexOf('E') >= 0) {
+                style = DateFormat.FULL;
+            } else {
+                style = DateFormat.LONG;
+            }
+        } else if (monthFieldLen == 3) {
+            style = DateFormat.MEDIUM;
+        }
+        // and now use it to compose date and time
         return SimpleFormatterImpl.formatRawPattern(
-                getDateTimeFormat(), 2, 2, timePattern, datePattern);
+                getDateTimeFormat(style), 2, 2, timePattern, datePattern);
     }
 
     /*
@@ -1012,21 +1043,77 @@
      * for those two skeletons, so the result is put together with this pattern,
      * resulting in "d-MMM h:mm".
      *
+     * There are four DateTimeFormats in a DateTimePatternGenerator object,
+     * corresponding to date styles DateFormat.FULL..DateFormat.SHORT. This method sets
+     * all of them to the specified pattern. To set them individually, see
+     * setDateTimeFormat(int style, ...).
+     *
      * @param dateTimeFormat message format pattern, where {1} will be replaced by the date
      *            pattern and {0} will be replaced by the time pattern.
      */
     public void setDateTimeFormat(String dateTimeFormat) {
         checkFrozen();
-        this.dateTimeFormat = dateTimeFormat;
+        for (int style = DateFormat.FULL; style <= DateFormat.SHORT; style++) {
+            setDateTimeFormat(style, dateTimeFormat);
+        }
     }
 
     /**
      * Getter corresponding to setDateTimeFormat.
      *
+     * There are four DateTimeFormats in a DateTimePatternGenerator object,
+     * corresponding to date styles DateFormat.FULL..DateFormat.SHORT. This method gets
+     * the style for DateFormat.MEDIUM (the default). To get them individually, see
+     * getDateTimeFormat(int style).
+     *
      * @return pattern
      */
     public String getDateTimeFormat() {
-        return dateTimeFormat;
+        return getDateTimeFormat(DateFormat.MEDIUM);
+    }
+
+    /**
+     * dateTimeFormats are message patterns used to compose combinations of date
+     * and time patterns. There are four length styles, corresponding to the
+     * inferred style of the date pattern:
+     *  - DateFormat.FULL (for date pattern with weekday and long month), else
+     *  - DateFormat.LONG (for a date pattern with long month), else
+     *  - DateFormat.MEDIUM (for a date pattern with abbreviated month), else
+     *  - DateFormat.SHORT (for any other date pattern).
+     * For details on dateTimeFormats, see
+     * https://www.unicode.org/reports/tr35/tr35-dates.html#dateTimeFormats.
+     * The default pattern in the root locale for all styles is "{1} {0}".
+     *
+     * @param style
+     *              one of DateFormat.FULL..DateFormat.SHORT. An exception will
+     *              be thrown if out of range.
+     * @param dateTimeFormat
+     *              the new dateTimeFormat to set for the specified style
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public void setDateTimeFormat(int style, String dateTimeFormat) {
+        if (style < DateFormat.FULL || style > DateFormat.SHORT) {
+            throw new IllegalArgumentException("Illegal style here: " + style);
+        }
+        checkFrozen();
+        this.dateTimeFormats[style] = dateTimeFormat;
+    }
+
+    /**
+     * Getter corresponding to setDateTimeFormat.
+     *
+     * @param style
+     *              one of DateFormat.FULL..DateFormat.SHORT. An exception will
+     *              be thrown if out of range.
+     * @return
+     *              the current dateTimeFormat for the specified style.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public String getDateTimeFormat(int style) {
+        if (style < DateFormat.FULL || style > DateFormat.SHORT) {
+            throw new IllegalArgumentException("Illegal style here: " + style);
+        }
+        return dateTimeFormats[style];
     }
 
     /**
@@ -1405,7 +1492,7 @@
      * the pattern map from parent locales.
      *
      * @param key of the availableFormatMask in CLDR
-     * @return TRUE if the corresponding slot of CLDR_AVAIL_FORMAT_KEY[]
+     * @return true if the corresponding slot of CLDR_AVAIL_FORMAT_KEY[]
      * has been added to DateTimePatternGenerator.
      */
     private boolean isAvailableFormatSet(String key) {
@@ -1450,6 +1537,7 @@
             DateTimePatternGenerator result = (DateTimePatternGenerator) (super.clone());
             result.skeleton2pattern = (TreeMap<DateTimeMatcher, PatternWithSkeletonFlag>) skeleton2pattern.clone();
             result.basePattern_pattern = (TreeMap<String, PatternWithSkeletonFlag>) basePattern_pattern.clone();
+            result.dateTimeFormats = dateTimeFormats.clone();
             result.appendItemFormats = appendItemFormats.clone();
             result.fieldDisplayNames = fieldDisplayNames.clone();
             result.current = new DateTimeMatcher();
@@ -1970,7 +2058,14 @@
     private TreeMap<DateTimeMatcher, PatternWithSkeletonFlag> skeleton2pattern = new TreeMap<>(); // items are in priority order
     private TreeMap<String, PatternWithSkeletonFlag> basePattern_pattern = new TreeMap<>(); // items are in priority order
     private String decimal = "?";
-    private String dateTimeFormat = "{1} {0}";
+    // For the following, need fallback patterns in case an empty instance 
+    // of DateTimePatterngenerator is used for formatting.
+    private String[] dateTimeFormats = {
+        "{1} {0}",
+        "{1} {0}",
+        "{1} {0}",
+        "{1} {0}"
+    };
     private String[] appendItemFormats = new String[TYPE_LIMIT];
     private String[][] fieldDisplayNames = new String[TYPE_LIMIT][DisplayWidth.COUNT];
     private char defaultHourFormatChar = 'H';
@@ -2709,8 +2804,8 @@
                     char ch1 = original.getFieldChar(field);
                     char ch2 = value.charAt(0);
                     if ( allowDuplicateFields ||
-                            (ch1 == 'r' && ch2 == 'U') ||
-                            (ch1 == 'U' && ch2 == 'r') ) {
+                            (ch1 == 'r' && (ch2 == 'U' || ch2 == 'y')) ||
+                            ((ch1 == 'U' || ch1 == 'y') && ch2 == 'r') ) {
                         continue;
                     }
                     throw new IllegalArgumentException("Conflicting fields:\t"
diff --git a/android_icu4j/src/main/java/android/icu/text/DisplayOptions.java b/android_icu4j/src/main/java/android/icu/text/DisplayOptions.java
new file mode 100644
index 0000000..3f890e8
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/text/DisplayOptions.java
@@ -0,0 +1,751 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package android.icu.text;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents all the display options that are supported by CLDR such as grammatical case, noun
+ * class, ... etc. It currently supports enums, but may be extended in the future to have other
+ * types of data. It replaces a DisplayContext[] as a method parameter.
+ * <p>
+ * NOTE: This class is Immutable, and uses a Builder interface.
+ * <p>For example:
+ * {@code DisplayOptions x =
+ *                DisplayOptions.builder()
+ *                             .setNounClass(NounClass.DATIVE)
+ *                             .setPluralCategory(PluralCategory.FEW)
+ *                             .build();
+ *                             }
+ *
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+public final class DisplayOptions {
+    private final GrammaticalCase grammaticalCase;
+    private final NounClass nounClass;
+    private final PluralCategory pluralCategory;
+    private final Capitalization capitalization;
+    private final NameStyle nameStyle;
+    private final DisplayLength displayLength;
+    private final SubstituteHandling substituteHandling;
+
+    private DisplayOptions(Builder builder) {
+        this.grammaticalCase = builder.grammaticalCase;
+        this.nounClass = builder.nounClass;
+        this.pluralCategory = builder.pluralCategory;
+        this.capitalization = builder.capitalization;
+        this.nameStyle = builder.nameStyle;
+        this.displayLength = builder.displayLength;
+        this.substituteHandling = builder.substituteHandling;
+    }
+
+    /**
+     * Creates a builder with the {@code UNDEFINED} value for all the parameters.
+     *
+     * @return Builder
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Creates a builder with the same parameters from this object.
+     *
+     * @return Builder
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public Builder copyToBuilder() {
+        return new Builder(this);
+    }
+
+    /**
+     * Gets the grammatical case.
+     *
+     * @return GrammaticalCase
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public GrammaticalCase getGrammaticalCase() {
+        return this.grammaticalCase;
+    }
+
+    /**
+     * Gets the noun class.
+     *
+     * @return NounClass
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public NounClass getNounClass() {
+        return this.nounClass;
+    }
+
+    /**
+     * Gets the plural category.
+     *
+     * @return PluralCategory
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public PluralCategory getPluralCategory() {
+        return this.pluralCategory;
+    }
+
+    /**
+     * Gets the capitalization.
+     *
+     * @return Capitalization
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public Capitalization getCapitalization() {
+        return this.capitalization;
+    }
+
+    /**
+     * Gets the name style.
+     *
+     * @return NameStyle
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public NameStyle getNameStyle() {
+        return this.nameStyle;
+    }
+
+    /**
+     * Gets the display length.
+     *
+     * @return DisplayLength
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public DisplayLength getDisplayLength() {
+        return this.displayLength;
+    }
+
+    /**
+     * Gets the substitute handling.
+     *
+     * @return SubstituteHandling
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public SubstituteHandling getSubstituteHandling() {
+        return this.substituteHandling;
+    }
+
+    /**
+     * Responsible for building {@code DisplayOptions}.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static class Builder {
+        private GrammaticalCase grammaticalCase;
+        private NounClass nounClass;
+        private PluralCategory pluralCategory;
+        private Capitalization capitalization;
+        private NameStyle nameStyle;
+        private DisplayLength displayLength;
+        private SubstituteHandling substituteHandling;
+
+        /**
+         * Creates a {@code DisplayOptions.Builder} with the default values.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        private Builder() {
+            this.grammaticalCase = GrammaticalCase.UNDEFINED;
+            this.nounClass = NounClass.UNDEFINED;
+            this.pluralCategory = PluralCategory.UNDEFINED;
+            this.capitalization = Capitalization.UNDEFINED;
+            this.nameStyle = NameStyle.UNDEFINED;
+            this.displayLength = DisplayLength.UNDEFINED;
+            this.substituteHandling = SubstituteHandling.UNDEFINED;
+        }
+
+        /**
+         * Creates a {@code Builder} with all the information from a {@code DisplayOptions}.
+         *
+         * @param displayOptions Options to be copied.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        private Builder(DisplayOptions displayOptions) {
+            this.grammaticalCase = displayOptions.grammaticalCase;
+            this.nounClass = displayOptions.nounClass;
+            this.pluralCategory = displayOptions.pluralCategory;
+            this.capitalization = displayOptions.capitalization;
+            this.nameStyle = displayOptions.nameStyle;
+            this.displayLength = displayOptions.displayLength;
+            this.substituteHandling = displayOptions.substituteHandling;
+        }
+
+        /**
+         * Sets the grammatical case.
+         *
+         * @param grammaticalCase The grammatical case.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setGrammaticalCase(GrammaticalCase grammaticalCase) {
+            this.grammaticalCase = grammaticalCase;
+            return this;
+        }
+
+        /**
+         * Sets the noun class.
+         *
+         * @param nounClass The noun class.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setNounClass(NounClass nounClass) {
+            this.nounClass = nounClass;
+            return this;
+        }
+
+        /**
+         * Sets the plural category.
+         *
+         * @param pluralCategory The plural category.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setPluralCategory(PluralCategory pluralCategory) {
+            this.pluralCategory = pluralCategory;
+            return this;
+        }
+
+        /**
+         * Sets the capitalization.
+         *
+         * @param capitalization The capitalization.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setCapitalization(Capitalization capitalization) {
+            this.capitalization = capitalization;
+            return this;
+        }
+
+        /**
+         * Sets the name style.
+         *
+         * @param nameStyle The name style.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setNameStyle(NameStyle nameStyle) {
+            this.nameStyle = nameStyle;
+            return this;
+        }
+
+        /**
+         * Sets the display length.
+         *
+         * @param displayLength The display length.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setDisplayLength(DisplayLength displayLength) {
+            this.displayLength = displayLength;
+            return this;
+        }
+
+        /**
+         * Sets the substitute handling.
+         *
+         * @param substituteHandling The substitute handling.
+         * @return Builder
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public Builder setSubstituteHandling(SubstituteHandling substituteHandling) {
+            this.substituteHandling = substituteHandling;
+            return this;
+        }
+
+        /**
+         * Builds the display options.
+         *
+         * @return DisplayOptions
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public DisplayOptions build() {
+            DisplayOptions displayOptions = new DisplayOptions(this);
+            return displayOptions;
+        }
+    }
+
+    /**
+     * Represents all the grammatical noun classes that are supported by CLDR.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum NounClass {
+        /**
+         * A possible setting for NounClass. The noun class context to be used is unknown (this is the
+         * default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED("undefined"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        OTHER("other"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        NEUTER("neuter"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        FEMININE("feminine"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        MASCULINE("masculine"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        ANIMATE("animate"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        INANIMATE("inanimate"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        PERSONAL("personal"),
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        COMMON("common");
+
+        private final String identifier;
+
+        private NounClass(String identifier) {
+            this.identifier = identifier;
+        }
+
+        /**
+         * Unmodifiable List of all noun classes constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<NounClass> VALUES =
+                Collections.unmodifiableList(Arrays.asList(NounClass.values()));
+
+        /**
+         * @return the lowercase CLDR keyword string for the noun class.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public final String getIdentifier() {
+            return this.identifier;
+        }
+
+        /**
+         * @param identifier in lower case such as "feminine" or "masculine"
+         * @return the plural category corresponding to the identifier, or {@code UNDEFINED}
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final NounClass fromIdentifier(String identifier) {
+            if (identifier == null) {
+                return NounClass.UNDEFINED;
+            }
+
+            for (NounClass nounClass : VALUES) {
+                if (identifier.equals(nounClass.getIdentifier())) {
+                    return nounClass;
+                }
+            }
+
+            return NounClass.UNDEFINED;
+        }
+    }
+
+    /**
+     * Represents all the name styles.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum NameStyle {
+        /**
+         * A possible setting for NameStyle. The NameStyle context to be used is unknown (this is the
+         * default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED,
+        /**
+         * Use standard names when generating a locale name, e.g. en_GB displays as 'English (United
+         * Kingdom)'.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        STANDARD_NAMES,
+
+        /**
+         * Use dialect names, when generating a locale name, e.g. en_GB displays as 'British English'.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        DIALECT_NAMES;
+
+        /**
+         * Unmodifiable List of all name styles constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<NameStyle> VALUES =
+                Collections.unmodifiableList(Arrays.asList(NameStyle.values()));
+    }
+
+    /**
+     * Represents all the substitute handlings.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum SubstituteHandling {
+        /**
+         * A possible setting for SubstituteHandling. The SubstituteHandling context to be used is
+         * unknown (this is the default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED,
+        /**
+         * Returns a fallback value (e.g., the input code) when no data is available. This is the
+         * default behaviour.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        SUBSTITUTE,
+
+        /**
+         * Returns a null value when no data is available.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        NO_SUBSTITUTE;
+
+        /**
+         * Unmodifiable List of all substitute handlings constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<SubstituteHandling> VALUES =
+                Collections.unmodifiableList(Arrays.asList(SubstituteHandling.values()));
+    }
+
+    /**
+     * Represents all the display lengths.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum DisplayLength {
+        /**
+         * A possible setting for DisplayLength. The DisplayLength context to be used is unknown (this
+         * is the default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED,
+        /**
+         * Uses full names when generating a locale name, e.g. "United States" for US.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        LENGTH_FULL,
+
+        /**
+         * Use short names when generating a locale name, e.g. "U.S." for US.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        LENGTH_SHORT;
+
+        /**
+         * Unmodifiable List of all display lengths constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<DisplayLength> VALUES =
+                Collections.unmodifiableList(Arrays.asList(DisplayLength.values()));
+    }
+
+    /**
+     * Represents all the capitalization options.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum Capitalization {
+        /**
+         * A possible setting for Capitalization. The capitalization context to be used is unknown (this
+         * is the default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED,
+
+        /**
+         * The capitalization context if a date, date symbol or display name is to be formatted with
+         * capitalization appropriate for the beginning of a sentence.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        BEGINNING_OF_SENTENCE,
+
+        /**
+         * The capitalization context if a date, date symbol or display name is to be formatted with
+         * capitalization appropriate for the middle of a sentence.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        MIDDLE_OF_SENTENCE,
+
+        /**
+         * The capitalization context if a date, date symbol or display name is to be formatted with
+         * capitalization appropriate for stand-alone usage such as an isolated name on a calendar
+         * page.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        STANDALONE,
+
+        /**
+         * The capitalization context if a date, date symbol or display name is to be formatted with
+         * capitalization appropriate for a user-interface list or menu item.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UI_LIST_OR_MENU;
+
+        /**
+         * Unmodifiable List of all the capitalizations constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<Capitalization> VALUES =
+                Collections.unmodifiableList(Arrays.asList(Capitalization.values()));
+    }
+
+    /**
+     * Standard CLDR plural category constants. See http://www.unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum PluralCategory {
+        /**
+         * A possible setting for PluralCategory. The plural category context to be used is unknown
+         * (this is the default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED("undefined"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        ZERO("zero"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        ONE("one"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        TWO("two"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        FEW("few"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        MANY("many"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        OTHER("other");
+
+        private final String identifier;
+
+        private PluralCategory(String identifier) {
+            this.identifier = identifier;
+        }
+
+        /**
+         * Unmodifiable List of all plural categories constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<PluralCategory> VALUES =
+                Collections.unmodifiableList(Arrays.asList(PluralCategory.values()));
+
+        /**
+         * @return the lowercase CLDR keyword string for the plural category
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public final String getIdentifier() {
+            return this.identifier;
+        }
+
+        /**
+         * @param identifier in lower case such as "few" or "other"
+         * @return the plural category corresponding to the identifier, or {@code UNDEFINED}
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final PluralCategory fromIdentifier(String identifier) {
+            if (identifier == null) {
+                return PluralCategory.UNDEFINED;
+            }
+
+            for (PluralCategory pluralCategory : VALUES) {
+                if (identifier.equals(pluralCategory.getIdentifier())) {
+                    return pluralCategory;
+                }
+            }
+
+            return PluralCategory.UNDEFINED;
+        }
+    }
+
+    /**
+     * Represents all the grammatical cases that are supported by CLDR.
+     *
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum GrammaticalCase {
+        /**
+         * A possible setting for GrammaticalCase. The grammatical case context to be used is unknown
+         * (this is the default value).
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        UNDEFINED("undefined"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        ABLATIVE("ablative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        ACCUSATIVE("accusative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        COMITATIVE("comitative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        DATIVE("dative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        ERGATIVE("ergative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        GENITIVE("genitive"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        INSTRUMENTAL("instrumental"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        LOCATIVE("locative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        LOCATIVE_COPULATIVE("locative_copulative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        NOMINATIVE("nominative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        OBLIQUE("oblique"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        PREPOSITIONAL("prepositional"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        SOCIATIVE("sociative"),
+
+        /**
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        VOCATIVE("vocative");
+
+        private final String identifier;
+
+        private GrammaticalCase(String identifier) {
+            this.identifier = identifier;
+        }
+
+        /**
+         * Unmodifiable List of all grammatical cases constants. List version of {@link #values()}.
+         *
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final List<GrammaticalCase> VALUES =
+                Collections.unmodifiableList(Arrays.asList(GrammaticalCase.values()));
+
+        /**
+         * @return the lowercase CLDR keyword string for the grammatical case.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public final String getIdentifier() {
+            return this.identifier;
+        }
+
+        /**
+         * @param identifier in lower case such as "dative" or "nominative"
+         * @return the plural category corresponding to the identifier, or {@code UNDEFINED}
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final GrammaticalCase fromIdentifier(String identifier) {
+            if (identifier == null) {
+                return GrammaticalCase.UNDEFINED;
+            }
+
+            for (GrammaticalCase grammaticalCase : VALUES) {
+                if (identifier.equals(grammaticalCase.getIdentifier())) {
+                    return grammaticalCase;
+                }
+            }
+
+            return GrammaticalCase.UNDEFINED;
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/text/FormattedValue.java b/android_icu4j/src/main/java/android/icu/text/FormattedValue.java
index 2b9f3d4..a7c682e 100644
--- a/android_icu4j/src/main/java/android/icu/text/FormattedValue.java
+++ b/android_icu4j/src/main/java/android/icu/text/FormattedValue.java
@@ -43,7 +43,7 @@
      * To loop over all field positions:
      *
      * <pre>
-     *     ConstrainableFieldPosition cfpos = new ConstrainableFieldPosition();
+     *     ConstrainedFieldPosition cfpos = new ConstrainedFieldPosition();
      *     while (fmtval.nextPosition(cfpos)) {
      *         // handle the field position; get information from cfpos
      *     }
diff --git a/android_icu4j/src/main/java/android/icu/text/NumberFormat.java b/android_icu4j/src/main/java/android/icu/text/NumberFormat.java
index 9fb8c24..c03bc08 100644
--- a/android_icu4j/src/main/java/android/icu/text/NumberFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/NumberFormat.java
@@ -1866,6 +1866,12 @@
         public static final Field COMPACT = new Field("compact");
 
         /**
+         * Approximately sign. In ICU 70, this was categorized under the generic SIGN field.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        public static final Field APPROXIMATELY_SIGN = new Field("approximately sign");
+
+        /**
          * Constructs a new instance of NumberFormat.Field with the given field
          * name.
          */
diff --git a/android_icu4j/src/main/java/android/icu/text/PersonName.java b/android_icu4j/src/main/java/android/icu/text/PersonName.java
new file mode 100644
index 0000000..634abf4
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/text/PersonName.java
@@ -0,0 +1,272 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.text;
+
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * An object used to provide name data to the PersonNameFormatter for formatting.
+ * Clients can implement this interface to talk directly to some other subsystem
+ * that actually contains the name data (instead of having to copy it into a separate
+ * object just for formatting) or to override the default modifier behavior described
+ * above.  A concrete SimplePersonName object that does store the field values directly
+ * is provided.
+ *
+ * @see SimplePersonName
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public interface PersonName {
+    //==============================================================================
+    // Identifiers used to request field values from the PersonName object
+
+    /**
+     * Identifiers for the name fields supported by the PersonName object.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    enum NameField {
+        /**
+         * Contains titles and other words that precede the actual name, such as "Mr."
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        PREFIX("prefix"),
+
+        /**
+         * The given name.  May contain more than one token.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        GIVEN("given"),
+
+        /**
+         * Additional given names.  (In English, this is usually the "middle name" and
+         * may contain more than one word.)
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        GIVEN2("given2"),
+
+        /**
+         * The surname.  In Spanish, this is the patronymic surname.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        SURNAME("surname"),
+
+        /**
+         * Additional surnames.  This is only used in a few languages, such as Spanish,
+         * where it is the matronymic surname.  (In most languages, multiple surnames all
+         * just go in the SURNAME field.)
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        SURNAME2("surname2"),
+
+        /**
+         * Generational and professional qualifiers that generally follow the actual name,
+         * such as "Jr." or "M.D."
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        SUFFIX("suffix"),
+
+        /**
+         * The preferred field order for the name.  PersonName objects generally shouldn't provide
+         * this field, allowing the PersonNameFormatter to deduce the proper field order based on
+         * the locales of the name of the formatter.  But this can be used to force a particular
+         * field order, generally in cases where the deduction logic in PersonNameFormatter would
+         * guess wrong.  When used, the only valid values are "givenFirst" and "surnameFirst".
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        PREFERRED_ORDER("preferredOrder");
+
+        private final String name;
+
+        private NameField(String name) {
+            this.name = name;
+        }
+
+        /**
+         * Returns the NameField's display name.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            return name;
+        }
+
+        /**
+         * Returns the appropriate NameField for its display name.
+         * @deprecated This API is for ICU internal use only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static NameField forString(String name) {
+            for (NameField field : values()) {
+                if (field.name.equals(name)) {
+                    return field;
+                }
+            }
+            throw new IllegalArgumentException("Invalid field name " + name);
+        }
+    }
+
+    /**
+     * Identifiers for the name field modifiers supported by the PersonName and PersonNameFormatter objects.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    enum FieldModifier {
+        /**
+         * Requests an "informal" variant of the field, generally a nickname of some type:
+         * if "given" is "James", "given-informal" might be "Jimmy".  Only applied to the "given"
+         * field.  If the PersonName object doesn't apply this modifier, PersonNameFormatter just
+         * uses the unmodified version of "given".
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        INFORMAL("informal"),
+
+        /**
+         * If the field contains a main word with one or more separate prefixes, such as
+         * "van den Hul", this requests just the prefixes ("van den").  Only applied to the "surname"
+         * field.  If the PersonName object doesn't apply this modifier, PersonNameFormatter
+         * assumes there are no prefixes.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        PREFIX("prefix"),
+
+        /**
+         * If the field contains a main word with one or more separate prefixes, such as
+         * "van den Hul", this requests just the main word ("Hul").  Only applied to the "surname"
+         * field.  If the implementing class doesn't apply this modifier, PersonNameFormatter
+         * assumes the entire "surname" field is the "core".
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        CORE("core"),
+
+        /**
+         * Requests an initial for the specified field.  PersonNameFormatter will do
+         * this algorithmically, but a PersonName object can apply this modifier itself if it wants
+         * different initial-generation logic (or stores the initial separately).
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        INITIAL("initial"),
+
+        /**
+         * Requests an initial for the specified field, suitable for use in a monogram
+         * (this usually differs from "initial" in that "initial" often adds a period and "monogram"
+         * never does).  PersonNameFormatter will do this algorithmically, but a PersonName object can
+         * apply this modifier itself if it wants different monogram-generation logic.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        MONOGRAM("monogram"),
+
+        /**
+         * Requests the field value converted to ALL CAPS.  PersonName objects
+         * generally won't need to handle this modifier themselves.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        ALL_CAPS("allCaps"),
+
+        /**
+         * Requests the field value with the first grapheme of each word converted to titlecase.
+         * A PersonName object might handle this modifier itself to capitalize words more
+         * selectively.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        INITIAL_CAP("initialCap");
+
+        private final String name;
+
+        private FieldModifier(String name) {
+            this.name = name;
+        }
+
+        /**
+         * Returns the FieldModifier's display name.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        public String toString() {
+            return name;
+        }
+
+        /**
+         * Returns the appropriate fieldModifier for its display name.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public static FieldModifier forString(String name) {
+            for (FieldModifier modifier : values()) {
+                if (modifier.name.equals(name)) {
+                    return modifier;
+                }
+            }
+            throw new IllegalArgumentException("Invalid modifier name " + name);
+        }
+    }
+
+    //==============================================================================
+    // Public API on PersonName
+    /**
+     * Returns the locale of the name-- that is, the language or country of origin for the person being named.
+     * An implementing class is allowed to return null here to indicate the name's locale is unknown.
+     *
+     * @return The name's locale, or null if it's not known.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Locale getNameLocale();
+
+    /**
+     * Returns one field of the name, possibly in a modified form.
+     *
+     * @param identifier The identifier of the requested field.
+     * @param modifiers  An **IN/OUT** parameter that specifies modifiers to apply to the basic field value.
+     *                   An implementing class can choose to handle or ignore any modifiers; it should modify
+     *                   the passed-in Set so that on exit, it contains only the requested modifiers that it
+     *                   DIDN'T handle.  This parameter may not be null, and must either be mutable or empty.
+     * @return The value of the requested field, optionally modified by some or all of the requested modifiers, or
+     * null if the requested field isn't present in the name.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String getFieldValue(NameField identifier, Set<FieldModifier> modifiers);
+}
diff --git a/android_icu4j/src/main/java/android/icu/text/PersonNameFormatter.java b/android_icu4j/src/main/java/android/icu/text/PersonNameFormatter.java
new file mode 100644
index 0000000..034deee
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/text/PersonNameFormatter.java
@@ -0,0 +1,370 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.text;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import android.icu.impl.personname.PersonNameFormatterImpl;
+
+/**
+ * A class for formatting names of people.  Takes raw name data for a person and renders it into a string according to
+ * the caller's specifications, taking into account how people's names are rendered in the caller's locale.
+ *
+ * The Length, Usage, and Formality options can be used to get a wide variety of results.  In English, they would
+ * produce results along these lines:
+ * <table border="1">
+ *     <tr>
+ *         <th rowspan="2">
+ *         </th>
+ *         <th colspan="2">
+ *             REFERRING
+ *         </th>
+ *         <th colspan="2">
+ *             ADDRESSING
+ *         </th>
+ *         <th colspan="2">
+ *             MONOGRAM
+ *         </th>
+ *     </tr>
+ *     <tr>
+ *         <th>FORMAL</th>
+ *         <th>INFORMAL</th>
+ *         <th>FORMAL</th>
+ *         <th>INFORMAL</th>
+ *         <th>FORMAL</th>
+ *         <th>INFORMAL</th>
+ *     </tr>
+ *     <tr>
+ *         <th>LONG</th>
+ *         <td>James Earl Carter Jr.</td>
+ *         <td>Jimmy Carter</td>
+ *         <td>Mr. Carter</td>
+ *         <td>Jimmy</td>
+ *         <td>JEC</td>
+ *         <td>JC</td>
+ *     </tr>
+ *     <tr>
+ *         <th>MEDIUM</th>
+ *         <td>James E. Carter Jr.</td>
+ *         <td>Jimmy Carter</td>
+ *         <td>Mr. Carter</td>
+ *         <td>Jimmy</td>
+ *         <td>C</td>
+ *         <td>J</td>
+ *     </tr>
+ *     <tr>
+ *         <th>SHORT</th>
+ *         <td>J. E. Carter</td>
+ *         <td>Jimmy Carter</td>
+ *         <td>Mr. Carter</td>
+ *         <td>Jimmy</td>
+ *         <td>C</td>
+ *         <td>J</td>
+ *     </tr>
+ * </table>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class PersonNameFormatter {
+    //==============================================================================
+    // Parameters that control formatting behavior
+
+    /**
+     * Specifies the desired length of the formatted name.
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public enum Length {
+        /**
+         * The longest name length.  Generally uses most of the fields in the name object.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        LONG,
+
+        /**
+         * The most typical name length.  Generally includes the given name and surname, but generally
+         * not most of the other fields.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        MEDIUM,
+
+        /**
+         * A shortened name.  Skips most fields and may abbreviate some name fields to just their initials.
+         * When Formality is INFORMAL, may only include one field.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        SHORT
+    }
+
+    /**
+     * Specifies the intended usage of the formatted name.
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public enum Usage {
+        /**
+         * Used for when the name is going to be used to address the user directly: "Turn left here, John."
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        ADDRESSING,
+
+        /**
+         * Used in general cases, when the name is used to refer to somebody else.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        REFERRING,
+
+        /**
+         * Used to generate monograms, short 1 to 3-character versions of the name suitable for use in things
+         * like chat avatars.  In English, this is usually the person's initials, but this isn't true in all
+         * languages.  When the caller specifies Usage.MONOGRAM, the Length parameter can be used to get different
+         * lengths of monograms: Length.SHORT is generally a single letter; Length.LONG may be as many as three or four.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        MONOGRAM
+    }
+
+    /**
+     * Specifies the intended formality of the formatted name.
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public enum Formality {
+        /**
+         * The more formal version of the name.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        FORMAL,
+
+        /**
+         * The more informal version of the name.  In English, this might omit fields or use the "informal" variant
+         * of the given name.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        INFORMAL
+    }
+
+    /**
+     * Additional options to customize the behavior of the formatter.
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public enum Options {
+        /**
+         * Causes the formatter to generate results suitable for inclusion in a sorted list.  For GN-first languages,
+         * this generally means moving the surname to the beginning of the string, with a comma between it and
+         * the rest of the name: e.g., "Carter, James E. Jr.".
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        SORTING,
+
+        /**
+         * Requests that the surname in the formatted result be rendered in ALL CAPS.  This is often done with
+         * Japanese names to highlight which name is the surname.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        SURNAME_ALLCAPS
+    }
+
+    private final PersonNameFormatterImpl impl;
+
+    //==============================================================================
+    // Builder for PersonNameFormatter
+
+    /**
+     * A utility class that can be used to construct a PersonNameFormatter.
+     * Use PersonNameFormatter.builder() to get a new instance.
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Builder {
+        /**
+         * Sets the locale for the formatter to be constructed.
+         * @param locale The new formatter locale.  May not be null.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setLocale(Locale locale) {
+            if (locale != null) {
+                this.locale = locale;
+            }
+            return this;
+        }
+
+        /**
+         * Sets the name length for the formatter to be constructed.
+         * @param length The new name length.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setLength(Length length) {
+            this.length = length;
+            return this;
+        }
+
+        /**
+         * Sets the name usage for the formatter to be constructed.
+         * @param usage The new name length.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setUsage(Usage usage) {
+            this.usage = usage;
+            return this;
+        }
+
+        /**
+         * Sets the name formality for the formatter to be constructed.
+         * @param formality The new name length.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setFormality(Formality formality) {
+            this.formality = formality;
+            return this;
+        }
+
+        /**
+         * Sets the options set for the formatter to be constructed.  The Set passed in
+         * here replaces the entire options set the builder already has (if one has
+         * already been set); this method doesn't modify the builder's options set.
+         * @param options The new options set.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setOptions(Set<Options> options) {
+            this.options = options;
+            return this;
+        }
+
+        /**
+         * Returns a new PersonNameFormatter with the values that were passed to this builder.
+         * This method doesn't freeze or delete the builder; you can call build() more than once
+         * (presumably after calling the other methods to change the parameter) to create more
+         * than one PersonNameFormatter; you don't need a new Builder for each PersonNameFormatter.
+         * @return A new PersonNameFormatter.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public PersonNameFormatter build() {
+            return new PersonNameFormatter(locale, length, usage, formality, options);
+        }
+
+        private Builder() {
+       }
+
+        private Locale locale = Locale.getDefault();
+        private Length length = Length.MEDIUM;
+        private Usage usage = Usage.REFERRING;
+        private Formality formality = Formality.FORMAL;
+        private Set<Options> options = new HashSet<>();
+    }
+
+    //==============================================================================
+    // Public API on PersonNameFormatter
+
+    /**
+     * Returns a Builder object that can be used to construct a new PersonNameFormatter.
+     * @return A new Builder.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns a Builder object whose fields match those used to construct this formatter,
+     * allowing a new formatter to be created based on this one.
+     * @return A new Builder that can be used to create a new formatter based on this formatter.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Builder toBuilder() {
+        Builder builder = builder();
+        builder.setLocale(impl.getLocale());
+        builder.setLength(impl.getLength());
+        builder.setUsage(impl.getUsage());
+        builder.setFormality(impl.getFormality());
+        builder.setOptions(impl.getOptions());
+        return builder;
+    }
+
+    /**
+     * Formats a name.
+     * @param name A PersonName object that supplies individual field values (optionally, with modifiers applied)
+     *             to the formatter for formatting.
+     * @return The name, formatted according to the locale and other parameters passed to the formatter's constructor.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String formatToString(PersonName name) {
+        // TODO: Add a format() method that returns a FormattedPersonName object that descends from FormattedValue.
+        return impl.formatToString(name);
+    }
+
+    //==============================================================================
+    // Internal implementation
+    private PersonNameFormatter(Locale locale, Length length, Usage usage, Formality formality, Set<Options> options) {
+        this.impl = new PersonNameFormatterImpl(locale, length, usage, formality, options);
+    }
+
+    /**
+     * @deprecated This API is for unit testing only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public PersonNameFormatter(Locale locale, String[] patterns) {
+        this.impl = new PersonNameFormatterImpl(locale, patterns);
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/text/PluralRules.java b/android_icu4j/src/main/java/android/icu/text/PluralRules.java
index dce91c1..c7bab06 100644
--- a/android_icu4j/src/main/java/android/icu/text/PluralRules.java
+++ b/android_icu4j/src/main/java/android/icu/text/PluralRules.java
@@ -16,21 +16,22 @@
 import java.io.ObjectOutputStream;
 import java.io.ObjectStreamException;
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.regex.Pattern;
 
 import android.icu.impl.PluralRulesLoader;
 import android.icu.impl.StandardPlural;
+import android.icu.impl.number.DecimalQuantity;
+import android.icu.impl.number.DecimalQuantity_DualStorageBCD;
 import android.icu.impl.number.range.StandardPluralRanges;
 import android.icu.number.FormattedNumber;
 import android.icu.number.FormattedNumberRange;
@@ -334,6 +335,16 @@
     public static final double NO_UNIQUE_VALUE = -0.00123456777;
 
     /**
+     * Value returned by {@link #getUniqueKeywordDecimalQuantityValue} when there is no
+     * unique value to return.
+     * @deprecated This API is ICU internal only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static final DecimalQuantity NO_UNIQUE_VALUE_DECIMAL_QUANTITY =
+        new DecimalQuantity_DualStorageBCD(-0.00123456777);
+
+    /**
      * Type of plurals and PluralRules.
      */
     public enum PluralType {
@@ -908,51 +919,6 @@
         }
 
         /**
-         * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        @Deprecated
-        public FixedDecimal (String n) {
-            // Ugly, but for samples we don't care.
-            this(parseDecimalSampleRangeNumString(n));
-        }
-
-        /**
-         * @deprecated This API is ICU internal only
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        @Deprecated
-        private static FixedDecimal parseDecimalSampleRangeNumString(String num) {
-            if (num.contains("e") || num.contains("c")) {
-                int ePos = num.lastIndexOf('e');
-                if (ePos < 0) {
-                    ePos = num.lastIndexOf('c');
-                }
-                int expNumPos = ePos + 1;
-                String exponentStr = num.substring(expNumPos);
-                int exponent = Integer.parseInt(exponentStr);
-                String fractionStr = num.substring(0, ePos);
-                return FixedDecimal.createWithExponent(
-                        Double.parseDouble(fractionStr),
-                        getVisibleFractionCount(fractionStr),
-                        exponent);
-            } else {
-                return new FixedDecimal(Double.parseDouble(num), getVisibleFractionCount(num));
-            }
-        }
-
-        private static int getVisibleFractionCount(String value) {
-            value = value.trim();
-            int decimalPos = value.indexOf('.') + 1;
-            if (decimalPos == 0) {
-                return 0;
-            } else {
-                return value.length() - decimalPos;
-            }
-        }
-
-        /**
          * {@inheritDoc}
          *
          * @deprecated This API is ICU internal only.
@@ -1121,20 +1087,6 @@
             return (isNegative ? -source : source) * Math.pow(10, exponent);
         }
 
-        /**
-         * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        @Deprecated
-        public long getShiftedValue() {
-            if (exponent != 0 && visibleDecimalDigitCount == 0 && decimalDigits == 0) {
-                // Need to take exponent into account if we have it
-                return (long)(source * Math.pow(10, exponent));
-            }
-            return integerValue * baseFactor + decimalDigits;
-        }
-
         private void writeObject(
                 ObjectOutputStream out)
                         throws IOException {
@@ -1195,36 +1147,33 @@
     }
 
     /**
-     * A range of NumberInfo that includes all values with the same visibleFractionDigitCount.
+     * A range of DecimalQuantity representing PluralRules samples that includes
+     * all values with the same visibleFractionDigitCount.
      * @deprecated This API is ICU internal only.
      * @hide Only a subset of ICU is exposed in Android
-     * @hide original deprecated declaration
      * @hide draft / provisional / internal are hidden on Android
      */
     @Deprecated
-    public static class FixedDecimalRange {
+    public static class DecimalQuantitySamplesRange {
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public final FixedDecimal start;
+        public final DecimalQuantity start;
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public final FixedDecimal end;
+        public final DecimalQuantity end;
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public FixedDecimalRange(FixedDecimal start, FixedDecimal end) {
-            if (start.visibleDecimalDigitCount != end.visibleDecimalDigitCount) {
+        public DecimalQuantitySamplesRange(DecimalQuantity start, DecimalQuantity end) {
+            if (start.getPluralOperand(Operand.v)!= end.getPluralOperand(Operand.v)) {
                 throw new IllegalArgumentException("Ranges must have the same number of visible decimals: " + start + "~" + end);
             }
             this.start = start;
@@ -1232,42 +1181,38 @@
         }
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
         @Override
         public String toString() {
-            return start + (end == start ? "" : "~" + end);
+            return start.toExponentString() + (end == start ? "" : "~" + end.toExponentString());
         }
     }
 
     /**
-     * A list of NumberInfo that includes all values with the same visibleFractionDigitCount.
+     * A list of DecimalQuantity representing PluralRules that includes all
+     * values with the same visibleFractionDigitCount.
      * @deprecated This API is ICU internal only.
      * @hide Only a subset of ICU is exposed in Android
-     * @hide original deprecated declaration
      * @hide draft / provisional / internal are hidden on Android
      */
     @Deprecated
-    public static class FixedDecimalSamples {
+    public static class DecimalQuantitySamples {
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
         public final SampleType sampleType;
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public final Set<FixedDecimalRange> samples;
+        public final Set<DecimalQuantitySamplesRange> samples;
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
@@ -1277,7 +1222,7 @@
          * @param sampleType
          * @param samples
          */
-        private FixedDecimalSamples(SampleType sampleType, Set<FixedDecimalRange> samples, boolean bounded) {
+        private DecimalQuantitySamples(SampleType sampleType, Set<DecimalQuantitySamplesRange> samples, boolean bounded) {
             super();
             this.sampleType = sampleType;
             this.samples = samples;
@@ -1286,11 +1231,11 @@
         /*
          * Parse a list of the form described in CLDR. The source must be trimmed.
          */
-        static FixedDecimalSamples parse(String source) {
+        static DecimalQuantitySamples parse(String source) {
             SampleType sampleType2;
             boolean bounded2 = true;
             boolean haveBound = false;
-            Set<FixedDecimalRange> samples2 = new LinkedHashSet<>();
+            Set<DecimalQuantitySamplesRange> samples2 = new LinkedHashSet<>();
 
             if (source.startsWith("integer")) {
                 sampleType2 = SampleType.INTEGER;
@@ -1301,7 +1246,7 @@
             }
             source = source.substring(7).trim(); // remove both
 
-            for (String range : COMMA_SEPARATED.split(source)) {
+            for (String range : COMMA_SEPARATED.split(source, 0)) {
                 if (range.equals("…") || range.equals("...")) {
                     bounded2 = false;
                     haveBound = true;
@@ -1310,54 +1255,107 @@
                 if (haveBound) {
                     throw new IllegalArgumentException("Can only have … at the end of samples: " + range);
                 }
-                String[] rangeParts = TILDE_SEPARATED.split(range);
+                String[] rangeParts = TILDE_SEPARATED.split(range, 0);
                 switch (rangeParts.length) {
                 case 1:
-                    FixedDecimal sample = new FixedDecimal(rangeParts[0]);
+                    DecimalQuantity sample =
+                        DecimalQuantity_DualStorageBCD.fromExponentString(rangeParts[0]);
                     checkDecimal(sampleType2, sample);
-                    samples2.add(new FixedDecimalRange(sample, sample));
+                    samples2.add(new DecimalQuantitySamplesRange(sample, sample));
                     break;
                 case 2:
-                    FixedDecimal start = new FixedDecimal(rangeParts[0]);
-                    FixedDecimal end = new FixedDecimal(rangeParts[1]);
+                    DecimalQuantity start =
+                            DecimalQuantity_DualStorageBCD.fromExponentString(rangeParts[0]);
+                    DecimalQuantity end =
+                            DecimalQuantity_DualStorageBCD.fromExponentString(rangeParts[1]);
                     checkDecimal(sampleType2, start);
                     checkDecimal(sampleType2, end);
-                    samples2.add(new FixedDecimalRange(start, end));
+                    samples2.add(new DecimalQuantitySamplesRange(start, end));
                     break;
                 default: throw new IllegalArgumentException("Ill-formed number range: " + range);
                 }
             }
-            return new FixedDecimalSamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2);
+            return new DecimalQuantitySamples(sampleType2, Collections.unmodifiableSet(samples2), bounded2);
         }
 
-        private static void checkDecimal(SampleType sampleType2, FixedDecimal sample) {
-            if ((sampleType2 == SampleType.INTEGER) != (sample.getVisibleDecimalDigitCount() == 0)) {
+        private static void checkDecimal(SampleType sampleType2, DecimalQuantity sample) {
+            // TODO(CLDR-15452): Remove the need for the fallback check for exponent notation integers classified
+            // as "@decimal" type samples, if/when changes are made to
+            // resolve https://unicode-org.atlassian.net/browse/CLDR-15452
+            if ((sampleType2 == SampleType.INTEGER && sample.getPluralOperand(Operand.v) != 0)
+                    || (sampleType2 == SampleType.DECIMAL && sample.getPluralOperand(Operand.v) == 0
+                        && sample.getPluralOperand(Operand.e) == 0)) {
                 throw new IllegalArgumentException("Ill-formed number range: " + sample);
             }
         }
 
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public Set<Double> addSamples(Set<Double> result) {
-            for (FixedDecimalRange item : samples) {
-                // we have to convert to longs so we don't get strange double issues
-                long startDouble = item.start.getShiftedValue();
-                long endDouble = item.end.getShiftedValue();
-
-                for (long d = startDouble; d <= endDouble; d += 1) {
-                    result.add(d/(double)item.start.baseFactor);
-                }
-            }
+        public Collection<Double> addSamples(Collection<Double> result) {
+            addSamples(result, null);
             return result;
         }
 
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Collection<DecimalQuantity> addDecimalQuantitySamples(Collection<DecimalQuantity> result) {
+            addSamples(null, result);
+            return result;
+        }
+
+        /**
+         * @deprecated This API is ICU internal only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public void addSamples(Collection<Double> doubleResult, Collection<DecimalQuantity> dqResult) {
+            if ((doubleResult == null && dqResult == null)
+                    || (doubleResult != null && dqResult != null)) {
+                return;
+            }
+            boolean isDouble = doubleResult != null;
+            for (DecimalQuantitySamplesRange range : samples) {
+                DecimalQuantity start = range.start;
+                DecimalQuantity end = range.end;
+                int lowerDispMag = start.getLowerDisplayMagnitude();
+                int exponent = start.getExponent();
+                int incrementScale = lowerDispMag + exponent;
+                BigDecimal incrementBd = BigDecimal.ONE.movePointRight(incrementScale);
+
+                for (DecimalQuantity dq = start.createCopy(); dq.toDouble() <= end.toDouble(); ) {
+                    if (isDouble) {
+                        double dblValue = dq.toDouble();
+                        // Hack Alert: don't return any decimal samples with integer values that
+                        //    originated from a format with trailing decimals.
+                        //    This API is returning doubles, which can't distinguish having displayed
+                        //    zeros to the right of the decimal.
+                        //    This results in test failures with values mapping back to a different keyword.
+                        if (!(dblValue == Math.floor(dblValue)) && dq.getPluralOperand(Operand.v) > 0) {
+                            doubleResult.add(dblValue);
+                        }
+                    } else {
+                        dqResult.add(dq);
+                    }
+
+                    // Increment dq for next iteration
+                    java.math.BigDecimal dqBd = dq.toBigDecimal();
+                    java.math.BigDecimal newDqBd = dqBd.add(incrementBd);
+                    dq = new DecimalQuantity_DualStorageBCD(newDqBd);
+                    dq.setMinFraction(-lowerDispMag);
+                    dq.adjustMagnitude(-exponent);
+                    dq.adjustExponent(exponent);
+                }
+            }
+        }
+
+        /**
+         * @deprecated This API is ICU internal only.
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
@@ -1365,7 +1363,7 @@
         public String toString() {
             StringBuilder b = new StringBuilder("@").append(sampleType.toString().toLowerCase(Locale.ENGLISH));
             boolean first = true;
-            for (FixedDecimalRange item : samples) {
+            for (DecimalQuantitySamplesRange item : samples) {
                 if (first) {
                     first = false;
                 } else {
@@ -1381,24 +1379,22 @@
 
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public Set<FixedDecimalRange> getSamples() {
+        public Set<DecimalQuantitySamplesRange> getSamples() {
             return samples;
         }
 
         /**
          * @deprecated This API is ICU internal only.
-         * @hide original deprecated declaration
          * @hide draft / provisional / internal are hidden on Android
          */
         @Deprecated
-        public void getStartEndSamples(Set<FixedDecimal> target) {
-            for (FixedDecimalRange item : samples) {
-                target.add(item.start);
-                target.add(item.end);
+        public void getStartEndSamples(Set<DecimalQuantity> target) {
+            for (DecimalQuantitySamplesRange range : samples) {
+                target.add(range.start);
+                target.add(range.end);
             }
         }
     }
@@ -1479,10 +1475,10 @@
             throws ParseException {
 
         Constraint result = null;
-        String[] or_together = OR_SEPARATED.split(description);
+        String[] or_together = OR_SEPARATED.split(description, 0);
         for (int i = 0; i < or_together.length; ++i) {
             Constraint andConstraint = null;
-            String[] and_together = AND_SEPARATED.split(or_together[i]);
+            String[] and_together = AND_SEPARATED.split(or_together[i], 0);
             for (int j = 0; j < and_together.length; ++j) {
                 Constraint newConstraint = NO_CONSTRAINT;
 
@@ -1677,21 +1673,21 @@
         }
 
         description = description.substring(x+1).trim();
-        String[] constraintOrSamples = AT_SEPARATED.split(description);
+        String[] constraintOrSamples = AT_SEPARATED.split(description, 0);
         boolean sampleFailure = false;
-        FixedDecimalSamples integerSamples = null, decimalSamples = null;
+        DecimalQuantitySamples integerSamples = null, decimalSamples = null;
         switch (constraintOrSamples.length) {
         case 1: break;
         case 2:
-            integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]);
+            integerSamples = DecimalQuantitySamples.parse(constraintOrSamples[1]);
             if (integerSamples.sampleType == SampleType.DECIMAL) {
                 decimalSamples = integerSamples;
                 integerSamples = null;
             }
             break;
         case 3:
-            integerSamples = FixedDecimalSamples.parse(constraintOrSamples[1]);
-            decimalSamples = FixedDecimalSamples.parse(constraintOrSamples[2]);
+            integerSamples = DecimalQuantitySamples.parse(constraintOrSamples[1]);
+            decimalSamples = DecimalQuantitySamples.parse(constraintOrSamples[2]);
             if (integerSamples.sampleType != SampleType.INTEGER || decimalSamples.sampleType != SampleType.DECIMAL) {
                 throw new IllegalArgumentException("Must have @integer then @decimal in " + description);
             }
@@ -1731,7 +1727,7 @@
         if (description.endsWith(";")) {
             description = description.substring(0,description.length()-1);
         }
-        String[] rules = SEMI_SEPARATED.split(description);
+        String[] rules = SEMI_SEPARATED.split(description, 0);
         for (int i = 0; i < rules.length; ++i) {
             Rule rule = parseRule(rules[i].trim());
             result.hasExplicitBoundingInfo |= rule.integerSamples != null || rule.decimalSamples != null;
@@ -1926,10 +1922,10 @@
         private static final long serialVersionUID = 1;
         private final String keyword;
         private final Constraint constraint;
-        private final FixedDecimalSamples integerSamples;
-        private final FixedDecimalSamples decimalSamples;
+        private final DecimalQuantitySamples integerSamples;
+        private final DecimalQuantitySamples decimalSamples;
 
-        public Rule(String keyword, Constraint constraint, FixedDecimalSamples integerSamples, FixedDecimalSamples decimalSamples) {
+        public Rule(String keyword, Constraint constraint, DecimalQuantitySamples integerSamples, DecimalQuantitySamples decimalSamples) {
             this.keyword = keyword;
             this.constraint = constraint;
             this.integerSamples = integerSamples;
@@ -2040,7 +2036,7 @@
 
         public boolean isLimited(String keyword, SampleType sampleType) {
             if (hasExplicitBoundingInfo) {
-                FixedDecimalSamples mySamples = getDecimalSamples(keyword, sampleType);
+                DecimalQuantitySamples mySamples = getDecimalSamples(keyword, sampleType);
                 return mySamples == null ? true : mySamples.bounded;
             }
 
@@ -2092,7 +2088,7 @@
             return false;
         }
 
-        public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) {
+        public DecimalQuantitySamples getDecimalSamples(String keyword, SampleType sampleType) {
             for (Rule rule : rules) {
                 if (rule.getKeyword().equals(keyword)) {
                     return sampleType == SampleType.INTEGER ? rule.integerSamples : rule.decimalSamples;
@@ -2346,11 +2342,29 @@
      * @return The unique value for the keyword, or NO_UNIQUE_VALUE.
      */
     public double getUniqueKeywordValue(String keyword) {
-        Collection<Double> values = getAllKeywordValues(keyword);
+        DecimalQuantity uniqValDq = getUniqueKeywordDecimalQuantityValue(keyword);
+        if (uniqValDq.equals(NO_UNIQUE_VALUE_DECIMAL_QUANTITY)) {
+            return NO_UNIQUE_VALUE;
+        } else {
+            return uniqValDq.toDouble();
+        }
+    }
+
+    /**
+     * Returns the unique value that this keyword matches, or {@link #NO_UNIQUE_VALUE}
+     * if the keyword matches multiple values or is not defined for this PluralRules.
+     *
+     * @param keyword the keyword to check for a unique value
+     * @deprecated This API is ICU internal only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public DecimalQuantity getUniqueKeywordDecimalQuantityValue(String keyword) {
+        Collection<DecimalQuantity> values = getAllKeywordDecimalQuantityValues(keyword);
         if (values != null && values.size() == 1) {
             return values.iterator().next();
         }
-        return NO_UNIQUE_VALUE;
+        return NO_UNIQUE_VALUE_DECIMAL_QUANTITY;
     }
 
     /**
@@ -2362,6 +2376,31 @@
      * is immutable. It will be empty if the keyword is not defined.
      */
     public Collection<Double> getAllKeywordValues(String keyword) {
+        Collection<DecimalQuantity> samples = getAllKeywordDecimalQuantityValues(keyword);
+        if (samples == null) {
+            return null;
+        } else {
+            Collection<Double> result = new LinkedHashSet<>();
+            for (DecimalQuantity dq : samples) {
+                result.add(dq.toDouble());
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Returns all the values that trigger this keyword, or null if the number of such
+     * values is unlimited.
+     *
+     * @param keyword the keyword
+     * @return the values that trigger this keyword, or null.  The returned collection
+     * is immutable. It will be empty if the keyword is not defined.
+     *
+     * @deprecated This API is ICU internal only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Collection<DecimalQuantity> getAllKeywordDecimalQuantityValues(String keyword) {
         return getAllKeywordValues(keyword, SampleType.INTEGER);
     }
 
@@ -2379,12 +2418,11 @@
      * @hide draft / provisional / internal are hidden on Android
      */
     @Deprecated
-    public Collection<Double> getAllKeywordValues(String keyword, SampleType type) {
+    public Collection<DecimalQuantity> getAllKeywordValues(String keyword, SampleType type) {
         if (!isLimited(keyword, type)) {
             return null;
         }
-        Collection<Double> samples = getSamples(keyword, type);
-        return samples == null ? null : Collections.unmodifiableCollection(samples);
+        return getDecimalQuantitySamples(keyword, type);
     }
 
     /**
@@ -2401,6 +2439,22 @@
     }
 
     /**
+     * Returns a list of integer values for which select() would return that keyword,
+     * or null if the keyword is not defined. The returned collection is unmodifiable.
+     * The returned list is not complete, and there might be additional values that
+     * would return the keyword.
+     *
+     * @param keyword the keyword to test
+     * @return a list of values matching the keyword.
+     * @deprecated ICU internal only
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+  public Collection<DecimalQuantity> getDecimalQuantitySamples(String keyword) {
+        return getDecimalQuantitySamples(keyword, SampleType.INTEGER);
+    }
+
+    /**
      * Returns a list of values for which select() would return that keyword,
      * or null if the keyword is not defined.
      * The returned collection is unmodifiable.
@@ -2417,15 +2471,43 @@
      */
     @Deprecated
     public Collection<Double> getSamples(String keyword, SampleType sampleType) {
+        Collection<DecimalQuantity> samples = getDecimalQuantitySamples(keyword, sampleType);
+        if (samples == null) {
+            return null;
+        } else {
+            Collection<Double> result = new LinkedHashSet<>();
+            for (DecimalQuantity dq: samples) {
+                result.add(dq.toDouble());
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Returns a list of values for which select() would return that keyword,
+     * or null if the keyword is not defined.
+     * The returned collection is unmodifiable.
+     * The returned list is not complete, and there might be additional values that
+     * would return the keyword. The keyword might be defined, and yet have an empty set of samples,
+     * IF there are samples for the other sampleType.
+     *
+     * @param keyword the keyword to test
+     * @param sampleType the type of samples requested, INTEGER or DECIMAL
+     * @return a list of values matching the keyword.
+     * @deprecated ICU internal only
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Collection<DecimalQuantity> getDecimalQuantitySamples(String keyword, SampleType sampleType) {
         if (!keywords.contains(keyword)) {
             return null;
         }
-        Set<Double> result = new TreeSet<>();
+        Set<DecimalQuantity> result = new LinkedHashSet<>();
 
         if (rules.hasExplicitBoundingInfo) {
-            FixedDecimalSamples samples = rules.getDecimalSamples(keyword, sampleType);
+            DecimalQuantitySamples samples = rules.getDecimalSamples(keyword, sampleType);
             return samples == null ? Collections.unmodifiableSet(result)
-                    : Collections.unmodifiableSet(samples.addSamples(result));
+                    : Collections.unmodifiableCollection(samples.addDecimalQuantitySamples(result));
         }
 
         // hack in case the rule is created without explicit samples
@@ -2434,31 +2516,31 @@
         switch (sampleType) {
         case INTEGER:
             for (int i = 0; i < 200; ++i) {
-                if (!addSample(keyword, i, maxCount, result)) {
+                if (!addSample(keyword, new DecimalQuantity_DualStorageBCD(i), maxCount, result)) {
                     break;
                 }
             }
-            addSample(keyword, 1000000, maxCount, result); // hack for Welsh
+            addSample(keyword, new DecimalQuantity_DualStorageBCD(1000000), maxCount, result); // hack for Welsh
             break;
         case DECIMAL:
             for (int i = 0; i < 2000; ++i) {
-                if (!addSample(keyword, new FixedDecimal(i/10d, 1), maxCount, result)) {
+                DecimalQuantity_DualStorageBCD nextSample = new DecimalQuantity_DualStorageBCD(i);
+                nextSample.adjustMagnitude(-1);
+                if (!addSample(keyword, nextSample, maxCount, result)) {
                     break;
                 }
             }
-            addSample(keyword, new FixedDecimal(1000000d, 1), maxCount, result); // hack for Welsh
+            addSample(keyword, DecimalQuantity_DualStorageBCD.fromExponentString("1000000.0"), maxCount, result); // hack for Welsh
             break;
         }
+
         return result.size() == 0 ? null : Collections.unmodifiableSet(result);
     }
 
-    /**
-     * @hide original deprecated declaration
-     */
-    private boolean addSample(String keyword, Number sample, int maxCount, Set<Double> result) {
-        String selectedKeyword = sample instanceof FixedDecimal ? select((FixedDecimal)sample) : select(sample.doubleValue());
+    private boolean addSample(String keyword, DecimalQuantity sample, int maxCount, Set<DecimalQuantity> result) {
+        String selectedKeyword = select(sample);
         if (selectedKeyword.equals(keyword)) {
-            result.add(sample.doubleValue());
+            result.add(sample);
             if (--maxCount < 0) {
                 return false;
             }
@@ -2481,7 +2563,7 @@
      * @hide draft / provisional / internal are hidden on Android
      */
     @Deprecated
-    public FixedDecimalSamples getDecimalSamples(String keyword, SampleType sampleType) {
+    public DecimalQuantitySamples getDecimalSamples(String keyword, SampleType sampleType) {
         return rules.getDecimalSamples(keyword, sampleType);
     }
 
@@ -2588,14 +2670,14 @@
      *            the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before
      *            checking against the keyword values.
      * @param explicits
-     *            a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null.
+     *            a set of {@code DecimalQuantity}s that are used explicitly (eg [=0], "[=1]"). May be empty or null.
      * @param uniqueValue
      *            If non null, set to the unique value.
      * @return the KeywordStatus
      * @hide draft / provisional / internal are hidden on Android
      */
-    public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits,
-            Output<Double> uniqueValue) {
+    public KeywordStatus getKeywordStatus(String keyword, int offset, Set<DecimalQuantity> explicits,
+            Output<DecimalQuantity> uniqueValue) {
         return getKeywordStatus(keyword, offset, explicits, uniqueValue, SampleType.INTEGER);
     }
     /**
@@ -2607,19 +2689,18 @@
      *            the offset used, or 0.0d if not. Internally, the offset is subtracted from each explicit value before
      *            checking against the keyword values.
      * @param explicits
-     *            a set of Doubles that are used explicitly (eg [=0], "[=1]"). May be empty or null.
+     *            a set of {@code DecimalQuantity}s that are used explicitly (eg [=0], "[=1]"). May be empty or null.
      * @param sampleType
      *            request KeywordStatus relative to INTEGER or DECIMAL values
      * @param uniqueValue
      *            If non null, set to the unique value.
      * @return the KeywordStatus
      * @deprecated This API is ICU internal only.
-     * @hide original deprecated declaration
      * @hide draft / provisional / internal are hidden on Android
      */
     @Deprecated
-    public KeywordStatus getKeywordStatus(String keyword, int offset, Set<Double> explicits,
-            Output<Double> uniqueValue, SampleType sampleType) {
+    public KeywordStatus getKeywordStatus(String keyword, int offset,
+            Set<DecimalQuantity> explicits, Output<DecimalQuantity> uniqueValue, SampleType sampleType) {
         if (uniqueValue != null) {
             uniqueValue.value = null;
         }
@@ -2632,7 +2713,7 @@
             return KeywordStatus.UNBOUNDED;
         }
 
-        Collection<Double> values = getSamples(keyword, sampleType);
+        Collection<DecimalQuantity> values = getDecimalQuantitySamples(keyword, sampleType);
 
         int originalSize = values.size();
 
@@ -2654,9 +2735,12 @@
 
         // Compute if the quick test is insufficient.
 
-        HashSet<Double> subtractedSet = new HashSet<>(values);
-        for (Double explicit : explicits) {
-            subtractedSet.remove(explicit - offset);
+        ArrayList<DecimalQuantity> subtractedSet = new ArrayList<>(values);
+        for (DecimalQuantity explicit : explicits) {
+            BigDecimal explicitBd = explicit.toBigDecimal();
+            BigDecimal valToRemoveBd = explicitBd.subtract(new BigDecimal(offset));
+            DecimalQuantity_DualStorageBCD valToRemove = new DecimalQuantity_DualStorageBCD(valToRemoveBd);
+            subtractedSet.remove(valToRemove);
         }
         if (subtractedSet.size() == 0) {
             return KeywordStatus.SUPPRESSED;
diff --git a/android_icu4j/src/main/java/android/icu/text/PluralSamples.java b/android_icu4j/src/main/java/android/icu/text/PluralSamples.java
deleted file mode 100644
index 42ca5b9..0000000
--- a/android_icu4j/src/main/java/android/icu/text/PluralSamples.java
+++ /dev/null
@@ -1,333 +0,0 @@
-/* GENERATED SOURCE. DO NOT MODIFY. */
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
-/*
- *******************************************************************************
- * Copyright (C) 2013-2015, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
-package android.icu.text;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeSet;
-
-import android.icu.text.PluralRules.FixedDecimal;
-import android.icu.text.PluralRules.KeywordStatus;
-import android.icu.util.Output;
-
-/**
- * @author markdavis
- * Refactor samples as first step to moving into CLDR
- * 
- * @deprecated This API is ICU internal only.
- * @hide Only a subset of ICU is exposed in Android
- * @hide draft / provisional / internal are hidden on Android
- */
-@Deprecated
-public class PluralSamples {
-
-    private PluralRules pluralRules;
-    private final Map<String, List<Double>> _keySamplesMap;
-
-    /**
-     * @deprecated This API is ICU internal only.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    @Deprecated
-    public final Map<String, Boolean> _keyLimitedMap;
-    private final Map<String, Set<FixedDecimal>> _keyFractionSamplesMap;
-    private final Set<FixedDecimal> _fractionSamples;
-
-    /**
-     * @deprecated This API is ICU internal only.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    @Deprecated
-    public PluralSamples(PluralRules pluralRules) {
-        this.pluralRules = pluralRules;
-        Set<String> keywords = pluralRules.getKeywords();
-        // ensure both _keySamplesMap and _keyLimitedMap are initialized.
-        // If this were allowed to vary on a per-call basis, we'd have to recheck and
-        // possibly rebuild the samples cache.  Doesn't seem worth it.
-        // This 'max samples' value only applies to keywords that are unlimited, for
-        // other keywords all the matching values are returned.  This might be a lot.
-        final int MAX_SAMPLES = 3;
-
-        Map<String, Boolean> temp = new HashMap<String, Boolean>();
-        for (String k : keywords) {
-            temp.put(k, pluralRules.isLimited(k));
-        }
-        _keyLimitedMap = temp;
-
-        Map<String, List<Double>> sampleMap = new HashMap<String, List<Double>>();
-        int keywordsRemaining = keywords.size();
-
-        int limit = 128; // Math.max(5, getRepeatLimit() * MAX_SAMPLES) * 2;
-
-        for (int i = 0; keywordsRemaining > 0 && i < limit; ++i) {
-            keywordsRemaining = addSimpleSamples(pluralRules, MAX_SAMPLES, sampleMap, keywordsRemaining, i / 2.0);
-        }
-        // Hack for Celtic
-        keywordsRemaining = addSimpleSamples(pluralRules, MAX_SAMPLES, sampleMap, keywordsRemaining, 1000000);
-
-
-        // collect explicit samples
-        Map<String, Set<FixedDecimal>> sampleFractionMap = new HashMap<String, Set<FixedDecimal>>();
-        Set<FixedDecimal> mentioned = new TreeSet<FixedDecimal>();
-        // make sure that there is at least one 'other' value
-        Map<String, Set<FixedDecimal>> foundKeywords = new HashMap<String, Set<FixedDecimal>>();
-        for (FixedDecimal s : mentioned) {
-            String keyword = pluralRules.select(s);
-            addRelation(foundKeywords, keyword, s);
-        }
-        main:
-            if (foundKeywords.size() != keywords.size()) {
-                for (int i = 1; i < 1000; ++i) {
-                    boolean done = addIfNotPresent(i, mentioned, foundKeywords);
-                    if (done) break main;
-                }
-                // if we are not done, try tenths
-                for (int i = 10; i < 1000; ++i) {
-                    boolean done = addIfNotPresent(i/10d, mentioned, foundKeywords);
-                    if (done) break main;
-                }
-                System.out.println("Failed to find sample for each keyword: " + foundKeywords + "\n\t" + pluralRules + "\n\t" + mentioned);
-            }
-        mentioned.add(new FixedDecimal(0)); // always there
-        mentioned.add(new FixedDecimal(1)); // always there
-        mentioned.add(new FixedDecimal(2)); // always there
-        mentioned.add(new FixedDecimal(0.1,1)); // always there
-        mentioned.add(new FixedDecimal(1.99,2)); // always there
-        mentioned.addAll(fractions(mentioned));
-        for (FixedDecimal s : mentioned) {
-            String keyword = pluralRules.select(s);
-            Set<FixedDecimal> list = sampleFractionMap.get(keyword);
-            if (list == null) {
-                list = new LinkedHashSet<FixedDecimal>(); // will be sorted because the iteration is
-                sampleFractionMap.put(keyword, list);
-            }
-            list.add(s);
-        }
-
-        if (keywordsRemaining > 0) {
-            for (String k : keywords) {
-                if (!sampleMap.containsKey(k)) {
-                    sampleMap.put(k, Collections.<Double>emptyList());
-                }
-                if (!sampleFractionMap.containsKey(k)) {
-                    sampleFractionMap.put(k, Collections.<FixedDecimal>emptySet());
-                }
-            }
-        }
-
-        // Make lists immutable so we can return them directly
-        for (Entry<String, List<Double>> entry : sampleMap.entrySet()) {
-            sampleMap.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
-        }
-        for (Entry<String, Set<FixedDecimal>> entry : sampleFractionMap.entrySet()) {
-            sampleFractionMap.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue()));
-        }
-        _keySamplesMap = sampleMap;
-        _keyFractionSamplesMap = sampleFractionMap;
-        _fractionSamples = Collections.unmodifiableSet(mentioned);
-    }
-
-    private int addSimpleSamples(PluralRules pluralRules, final int MAX_SAMPLES, Map<String, List<Double>> sampleMap,
-            int keywordsRemaining, double val) {
-        String keyword = pluralRules.select(val);
-        boolean keyIsLimited = _keyLimitedMap.get(keyword);
-
-        List<Double> list = sampleMap.get(keyword);
-        if (list == null) {
-            list = new ArrayList<Double>(MAX_SAMPLES);
-            sampleMap.put(keyword, list);
-        } else if (!keyIsLimited && list.size() == MAX_SAMPLES) {
-            return keywordsRemaining;
-        }
-        list.add(Double.valueOf(val));
-
-        if (!keyIsLimited && list.size() == MAX_SAMPLES) {
-            --keywordsRemaining;
-        }
-        return keywordsRemaining;
-    }
-
-    private void addRelation(Map<String, Set<FixedDecimal>> foundKeywords, String keyword, FixedDecimal s) {
-        Set<FixedDecimal> set = foundKeywords.get(keyword);
-        if (set == null) {
-            foundKeywords.put(keyword, set = new HashSet<FixedDecimal>());
-        }
-        set.add(s);
-    }
-
-    private boolean addIfNotPresent(double d, Set<FixedDecimal> mentioned, Map<String, Set<FixedDecimal>> foundKeywords) {
-        FixedDecimal numberInfo = new FixedDecimal(d);
-        String keyword = pluralRules.select(numberInfo);
-        if (!foundKeywords.containsKey(keyword) || keyword.equals("other")) {
-            addRelation(foundKeywords, keyword, numberInfo);
-            mentioned.add(numberInfo);
-            if (keyword.equals("other")) {
-                if (foundKeywords.get("other").size() > 1) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private static final int[] TENS = {1, 10, 100, 1000, 10000, 100000, 1000000};
-
-    private static final int LIMIT_FRACTION_SAMPLES = 3;
-
-
-    private Set<FixedDecimal> fractions(Set<FixedDecimal> original) {
-        Set<FixedDecimal> toAddTo = new HashSet<FixedDecimal>();
-
-        Set<Integer> result = new HashSet<Integer>();
-        for (FixedDecimal base1 : original) {
-            result.add((int)base1.integerValue);
-        }
-        List<Integer> ints = new ArrayList<Integer>(result);
-        Set<String> keywords = new HashSet<String>();
-
-        for (int j = 0; j < ints.size(); ++j) {
-            Integer base = ints.get(j);
-            String keyword = pluralRules.select(base);
-            if (keywords.contains(keyword)) {
-                continue;
-            }
-            keywords.add(keyword);
-            toAddTo.add(new FixedDecimal(base,1)); // add .0
-            toAddTo.add(new FixedDecimal(base,2)); // add .00
-            Integer fract = getDifferentCategory(ints, keyword);
-            if (fract >= TENS[LIMIT_FRACTION_SAMPLES-1]) { // make sure that we always get the value
-                toAddTo.add(new FixedDecimal(base + "." + fract));
-            } else {
-                for (int visibleFractions = 1; visibleFractions < LIMIT_FRACTION_SAMPLES; ++visibleFractions) {
-                    for (int i = 1; i <= visibleFractions; ++i) {
-                        // with visible fractions = 3, and fract = 1, then we should get x.10, 0.01
-                        // with visible fractions = 3, and fract = 15, then we should get x.15, x.15
-                        if (fract >= TENS[i]) {
-                            continue;
-                        }
-                        toAddTo.add(new FixedDecimal(base + fract/(double)TENS[i], visibleFractions));
-                    }
-                }
-            }
-        }
-        return toAddTo;
-    }
-
-    private Integer getDifferentCategory(List<Integer> ints, String keyword) {
-        for (int i = ints.size() - 1; i >= 0; --i) {
-            Integer other = ints.get(i);
-            String keywordOther = pluralRules.select(other);
-            if (!keywordOther.equals(keyword)) {
-                return other;
-            }
-        }
-        return 37;
-    }
-
-    /**
-     * @deprecated This API is ICU internal only.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    @Deprecated
-    public KeywordStatus getStatus(String keyword, int offset, Set<Double> explicits, Output<Double> uniqueValue) {
-        if (uniqueValue != null) {
-            uniqueValue.value = null;
-        }
-
-        if (!pluralRules.getKeywords().contains(keyword)) {
-            return KeywordStatus.INVALID;
-        }
-        Collection<Double> values = pluralRules.getAllKeywordValues(keyword);
-        if (values == null) {
-            return KeywordStatus.UNBOUNDED;
-        }
-        int originalSize = values.size();
-
-        if (explicits == null) {
-            explicits = Collections.emptySet();
-        }
-
-        // Quick check on whether there are multiple elements
-
-        if (originalSize > explicits.size()) {
-            if (originalSize == 1) {
-                if (uniqueValue != null) {
-                    uniqueValue.value = values.iterator().next();
-                }
-                return KeywordStatus.UNIQUE;
-            }
-            return KeywordStatus.BOUNDED;
-        }
-
-        // Compute if the quick test is insufficient.
-
-        HashSet<Double> subtractedSet = new HashSet<Double>(values);
-        for (Double explicit : explicits) {
-            subtractedSet.remove(explicit - offset);
-        }
-        if (subtractedSet.size() == 0) {
-            return KeywordStatus.SUPPRESSED;
-        }
-
-        if (uniqueValue != null && subtractedSet.size() == 1) {
-            uniqueValue.value = subtractedSet.iterator().next();
-        }
-
-        return originalSize == 1 ? KeywordStatus.UNIQUE : KeywordStatus.BOUNDED;
-    }
-
-    Map<String, List<Double>> getKeySamplesMap() {
-        return _keySamplesMap;
-    }
-
-    Map<String, Set<FixedDecimal>> getKeyFractionSamplesMap() {
-        return _keyFractionSamplesMap;
-    }
-
-    Set<FixedDecimal> getFractionSamples() {
-        return _fractionSamples;
-    }
-
-    /**
-     * Returns all the values that trigger this keyword, or null if the number of such
-     * values is unlimited.
-     *
-     * @param keyword the keyword
-     * @return the values that trigger this keyword, or null.  The returned collection
-     * is immutable. It will be empty if the keyword is not defined.
-     */
-
-    Collection<Double> getAllKeywordValues(String keyword) {
-        // HACK for now
-        if (!pluralRules.getKeywords().contains(keyword)) {
-            return Collections.<Double>emptyList();
-        }
-        Collection<Double> result = getKeySamplesMap().get(keyword);
-
-        // We depend on MAX_SAMPLES here.  It's possible for a conjunction
-        // of unlimited rules that 'looks' unlimited to return a limited
-        // number of values.  There's no bounds to this limited number, in
-        // general, because you can construct arbitrarily complex rules.  Since
-        // we always generate 3 samples if a rule is really unlimited, that's
-        // where we put the cutoff.
-        if (result.size() > 2 && !_keyLimitedMap.get(keyword)) {
-            return null;
-        }
-        return result;
-    }
-}
diff --git a/android_icu4j/src/main/java/android/icu/text/RBBINode.java b/android_icu4j/src/main/java/android/icu/text/RBBINode.java
index 9c05bf2..0729c68 100644
--- a/android_icu4j/src/main/java/android/icu/text/RBBINode.java
+++ b/android_icu4j/src/main/java/android/icu/text/RBBINode.java
@@ -91,7 +91,7 @@
                                         //   corresponds to columns in the final
                                         //   state transition table.
 
-    boolean      fLookAheadEnd;        // For endMark nodes, set TRUE if
+    boolean      fLookAheadEnd;        // For endMark nodes, set true if
                                        //   marking the end of a look-ahead rule.
 
     boolean      fRuleRoot;             // True if this node is the root of a rule.
diff --git a/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java b/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java
index 80ed906..76dae00 100644
--- a/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/text/RelativeDateTimeFormatter.java
@@ -1420,28 +1420,9 @@
             this.ulocale = ulocale;
         }
 
-        private String getDateTimePattern(ICUResourceBundle r) {
-            String calType = r.getStringWithFallback("calendar/default");
-            if (calType == null || calType.equals("")) {
-                calType = "gregorian";
-            }
-            String resourcePath = "calendar/" + calType + "/DateTimePatterns";
-            ICUResourceBundle patternsRb = r.findWithFallback(resourcePath);
-            if (patternsRb == null && calType.equals("gregorian")) {
-                // Try with gregorian.
-                patternsRb = r.findWithFallback("calendar/gregorian/DateTimePatterns");
-            }
-            if (patternsRb == null || patternsRb.getSize() < 9) {
-                // Undefined or too few elements.
-                return "{1} {0}";
-            } else {
-                int elementType = patternsRb.get(8).getType();
-                if (elementType == UResourceBundle.ARRAY) {
-                    return patternsRb.get(8).getString(0);
-                } else {
-                    return patternsRb.getString(8);
-                }
-            }
+        private String getDateTimePattern() {
+            Calendar cal = Calendar.getInstance(ulocale);
+            return Calendar.getDateAtTimePattern(cal, ulocale, DateFormat.MEDIUM);
         }
 
         public RelativeDateTimeFormatterData load() {
@@ -1469,7 +1450,7 @@
 
             return new RelativeDateTimeFormatterData(
                     sink.qualitativeUnitMap, sink.styleRelUnitPatterns,
-                    getDateTimePattern(r));
+                    getDateTimePattern());
         }
     }
 
diff --git a/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java b/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java
index 2a91f3c..566c1e7 100644
--- a/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/RuleBasedBreakIterator.java
@@ -1415,42 +1415,89 @@
     boolean populateNear(int position) {
         assert(position < fBoundaries[fStartBufIdx] || position > fBoundaries[fEndBufIdx]);
 
-        // Find a boundary somewhere in the vicinity of the requested position.
-        // Depending on the safe rules and the text data, it could be either before, at, or after
-        // the requested position.
-
+        // Add boundaries to the cache near the specified position.
+        // The given position need not be a boundary itself.
+        // The input position must be within the range of the text, and
+        // on a code point boundary.
+        // If the requested position is a break boundary, leave the iteration
+        // position on it.
+        // If the requested position is not a boundary, leave the iteration
+        // position on the preceding boundary and include both the
+        // preceding and following boundaries in the cache.
+        // Additional boundaries, either preceding or following, may be added
+        // to the cache as a side effect.
 
         // If the requested position is not near already cached positions, clear the existing cache,
         // find a near-by boundary and begin new cache contents there.
 
-        if ((position < fBoundaries[fStartBufIdx] - 15) || position > (fBoundaries[fEndBufIdx] + 15)) {
-            int aBoundary = fText.getBeginIndex();
-            int ruleStatusIndex = 0;
-            if (position > aBoundary + 20) {
-                int backupPos = handleSafePrevious(position);
-                if (backupPos > aBoundary) {
-                    // Advance to the boundary following the backup position.
-                    // There is a complication: the safe reverse rules identify pairs of code points
-                    // that are safe. If advancing from the safe point moves forwards by less than
-                    // two code points, we need to advance one more time to ensure that the boundary
-                    // is good, including a correct rules status value.
-                    //
-                    fPosition = backupPos;
+        // Threshold for a text position to be considered near to existing cache contents.
+        // TODO: See issue ICU-22024 "perf tuning of Cache needed."
+        //       This value is subject to change. See the ticket for more details.
+        final int CACHE_NEAR = 15;
+
+        int startOfText = fText.getBeginIndex();
+        int aBoundary = -1;
+        int ruleStatusIndex = 0;
+        boolean retainCache = false;
+        if ((position > fBoundaries[fStartBufIdx] - CACHE_NEAR) && position < (fBoundaries[fEndBufIdx] + CACHE_NEAR)) {
+            // Requested position is near the existing cache. Retain it.
+            retainCache = true;
+        } else if (position <= startOfText + CACHE_NEAR) {
+            // Requested position is near the start of the text. Fill cache from start, skipping
+            // the need to find a safe point.
+            retainCache = false;
+            aBoundary = startOfText;
+        } else {
+            // Requested position is not near the existing cache.
+            // Find a safe point to refill the cache from.
+            int backupPos = handleSafePrevious(position);
+
+            if (fBoundaries[fEndBufIdx] < position && fBoundaries[fEndBufIdx] >= (backupPos - CACHE_NEAR)) {
+                // The requested position is beyond the end of the existing cache, but the
+                // reverse rules produced a position near or before the cached region.
+                // Retain the existing cache, and fill from the end of it.
+                retainCache = true;
+            } else if (backupPos < startOfText + CACHE_NEAR) {
+                // The safe reverse rules moved us to near the start of text.
+                // Take that (index 0) as the backup boundary, avoiding the complication
+                // (in the following block) of moving forward from the safe point to a known boundary.
+                //
+                // Retain the cache if it begins not too far from the requested position.
+                aBoundary = startOfText;
+                retainCache = (fBoundaries[fStartBufIdx] <= (position + CACHE_NEAR));
+            } else {
+                // The safe reverse rules produced a position that is neither near the existing
+                // cache, nor near the start of text.
+                // Advance to the boundary following.
+                // There is a complication: the safe reverse rules identify pairs of code points
+                // that are safe. If advancing from the safe point moves forwards by less than
+                // two code points, we need to advance one more time to ensure that the boundary
+                // is good, including a correct rules status value.
+                //
+                retainCache = false;
+                fPosition = backupPos;
+                aBoundary = handleNext();
+                if (aBoundary == backupPos + 1 ||
+                        (aBoundary == backupPos + 2 &&
+                        Character.isHighSurrogate(fText.setIndex(backupPos)) &&
+                        Character.isLowSurrogate(fText.next()))) {
+                    // The initial handleNext() only advanced by a single code point. Go again.
+                    // Safe rules identify safe pairs.
                     aBoundary = handleNext();
-                    if (aBoundary == backupPos + 1 ||
-                            (aBoundary == backupPos + 2 &&
-                            Character.isHighSurrogate(fText.setIndex(backupPos)) &&
-                            Character.isLowSurrogate(fText.next()))) {
-                        // The initial handleNext() only advanced by a single code point. Go again.
-                        // Safe rules identify safe pairs.
-                        aBoundary = handleNext();
-                    }
+                }
+                if (aBoundary == BreakIterator.DONE) {
+                    aBoundary = fText.getEndIndex();
                 }
                 ruleStatusIndex = fRuleStatusIndex;
             }
+        }
+
+        if (!retainCache) {
+            assert(aBoundary != -1);
             reset(aBoundary, ruleStatusIndex);               // Reset cache to hold aBoundary as a single starting point.
         }
 
+
         // Fill in boundaries between existing cache content and the new requested position.
 
         if (fBoundaries[fEndBufIdx] < position) {
diff --git a/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java b/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java
index edcf60e..152b8be 100644
--- a/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/RuleBasedTransliterator.java
@@ -185,7 +185,7 @@
      * Return a representation of this transliterator as source rules.
      * These rules will produce an equivalent transliterator if used
      * to construct a new transliterator.
-     * @param escapeUnprintable if TRUE then convert unprintable
+     * @param escapeUnprintable if true then convert unprintable
      * character to their hex escape representations, \\uxxxx or
      * \\Uxxxxxxxx.  Unprintable characters are those other than
      * U+000A, U+0020..U+007E.
diff --git a/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java b/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java
index bf0f3a7..934ffe0 100644
--- a/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/SimpleDateFormat.java
@@ -1228,11 +1228,7 @@
                 if (patternsRb == null || patternsRb.getSize() < 9) {
                     cachedDefaultPattern = FALLBACKPATTERN;
                 } else {
-                    int defaultIndex = 8;
-                    if (patternsRb.getSize() >= 13) {
-                        defaultIndex += (SHORT + 1);
-                    }
-                    String basePattern = patternsRb.getString(defaultIndex);
+                    String basePattern = Calendar.getDateAtTimePattern(cal, cachedDefaultLocale, SHORT);
 
                     cachedDefaultPattern = SimpleFormatterImpl.formatRawPattern(
                             basePattern, 2, 2,
@@ -2520,7 +2516,7 @@
                                 while (idx < plen) {
 
                                     char pch = patl.charAt(idx);
-                                    if (PatternProps.isWhiteSpace(pch))
+                                    if (PatternProps.isWhiteSpace(pch) || UCharacter.isUWhiteSpace(pch))
                                         idx++;
                                     else
                                         break;
@@ -2827,16 +2823,18 @@
         while (idx < plen && pos < tlen) {
             char pch = patternLiteral.charAt(idx);
             char ich = text.charAt(pos);
-            if (PatternProps.isWhiteSpace(pch)
-                && PatternProps.isWhiteSpace(ich)) {
+            if ((PatternProps.isWhiteSpace(pch) || UCharacter.isUWhiteSpace(pch))
+                && (PatternProps.isWhiteSpace(ich) || UCharacter.isUWhiteSpace(ich))) {
                 // White space characters found in both patten and input.
                 // Skip contiguous white spaces.
                 while ((idx + 1) < plen &&
-                        PatternProps.isWhiteSpace(patternLiteral.charAt(idx + 1))) {
+                        (PatternProps.isWhiteSpace(patternLiteral.charAt(idx + 1)) ||
+                        UCharacter.isUWhiteSpace(patternLiteral.charAt(idx + 1)))) {
                      ++idx;
                 }
                 while ((pos + 1) < tlen &&
-                        PatternProps.isWhiteSpace(text.charAt(pos + 1))) {
+                        (PatternProps.isWhiteSpace(text.charAt(pos + 1)) ||
+                        UCharacter.isUWhiteSpace(text.charAt(pos + 1)))) {
                      ++pos;
                 }
             } else if (pch != ich) {
@@ -2893,6 +2891,68 @@
     static final UnicodeSet DATE_PATTERN_TYPE = new UnicodeSet("[GyYuUQqMLlwWd]").freeze();
 
     /**
+     * Attempt to match the text at a given position against two arrays of
+     * month symbol strings.  Since multiple strings in the array may match (for
+     * example, if the array contains "a", "ab", and "abc", all will match
+     * the input string "abcd") the longest match is returned.  As a side
+     * effect, the given field of <code>cal</code> is set to the index
+     * of the best match, if there is one.
+     * @param text the time text being parsed.
+     * @param start where to start parsing.
+     * @param wideData the string array of wide month symbols
+     * @param shortData the string array of short month symbols
+     * @param cal
+     * @return the new start position if matching succeeded; a negative
+     * number indicating matching failure, otherwise.  As a side effect,
+     * sets the <code>cal</code> field <code>field</code> to the index
+     * of the best match, if matching succeeded.
+     * @deprecated This API is ICU internal only.
+     * Does not handle monthPattern.
+     * field is always Calendar.MONTH
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    private int matchAlphaMonthStrings(String text, int start, String[] wideData, String[] shortData, Calendar cal)
+    {
+        int i;
+        int bestMatchLength = 0, bestMatch = -1;
+
+        for (i = 0; i<wideData.length; ++i)
+            {
+                int length = wideData[i].length();
+                int matchLength = 0;
+                // Always compare if we have no match yet; otherwise only compare
+                // against potentially better matches (longer strings).
+                if (length > bestMatchLength &&
+                    (matchLength = regionMatchesWithOptionalDot(text, start, wideData[i], length)) >= 0)
+                    {
+                        bestMatch = i;
+                        bestMatchLength = matchLength;
+                    }
+            }
+        for (i = 0; i<shortData.length; ++i)
+            {
+                int length = shortData[i].length();
+                int matchLength = 0;
+                // Always compare if we have no match yet; otherwise only compare
+                // against potentially better matches (longer strings).
+                if (length > bestMatchLength &&
+                    (matchLength = regionMatchesWithOptionalDot(text, start, shortData[i], length)) >= 0)
+                    {
+                        bestMatch = i;
+                        bestMatchLength = matchLength;
+                    }
+            }
+        if (bestMatch >= 0)
+            {
+                cal.set(Calendar.MONTH, bestMatch);
+                return start + bestMatchLength;
+            }
+        return ~start;
+    }
+
+
+    /**
      * Attempt to match the text at a given position against an array of
      * strings.  Since multiple strings in the array may match (for
      * example, if the array contains "a", "ab", and "abc", all will match
@@ -3174,7 +3234,9 @@
                 return ~start;
             }
             int c = UTF16.charAt(text, start);
-            if (!UCharacter.isUWhiteSpace(c) || !PatternProps.isWhiteSpace(c)) {
+            // Changed the following from || to &&, as in ICU4C; needed to skip NBSP, NNBSP.
+            // Only UWhiteSpace includes \u00A0\u202F\u2009\u3000...; only PatternProps.isWhiteSpace includes \u200E\u200F
+            if (!UCharacter.isUWhiteSpace(c) && !PatternProps.isWhiteSpace(c)) {
                 break;
             }
             start += UTF16.getCharCount(c);
@@ -3337,14 +3399,22 @@
                     boolean haveMonthPat = (formatData.leapMonthPatterns != null && formatData.leapMonthPatterns.length >= DateFormatSymbols.DT_MONTH_PATTERN_COUNT);
                     // Try count == 4 first:, unless we're strict
                     int newStart = 0;
+                    if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH)&& count>=3 && count <=4 && !haveMonthPat) {
+                        newStart = (patternCharIndex == 2)?
+                            matchAlphaMonthStrings(text, start, formatData.months, formatData.shortMonths, cal):
+                            matchAlphaMonthStrings(text, start, formatData.standaloneMonths, formatData.standaloneShortMonths, cal);
+                        if (newStart > 0) {
+                            return newStart;
+                        }
+                    }
                     if(getBooleanAttribute(DateFormat.BooleanAttribute.PARSE_MULTIPLE_PATTERNS_FOR_MATCH) || count == 4) {
                         newStart = (patternCharIndex == 2)?
                             matchString(text, start, Calendar.MONTH, formatData.months,
                                     (haveMonthPat)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_FORMAT_WIDE]: null, cal):
                             matchString(text, start, Calendar.MONTH, formatData.standaloneMonths,
                                     (haveMonthPat)? formatData.leapMonthPatterns[DateFormatSymbols.DT_LEAP_MONTH_PATTERN_STANDALONE_WIDE]: null, cal);
-                    if (newStart > 0) {
-                        return newStart;
+                        if (newStart > 0) {
+                            return newStart;
                         }
                     }
                     // count == 4 failed, now try count == 3
@@ -3769,6 +3839,9 @@
                 } else {
                     number = parseInt(text, pos, allowNegative,currentNumberFormat);
                 }
+                if (!isLenient() && pos.getIndex() < start + count) {
+                    return -start;
+                }
                 if (number != null) {
                     if (patternCharIndex != DateFormat.RELATED_YEAR) {
                         cal.set(field, number.intValue());
diff --git a/android_icu4j/src/main/java/android/icu/text/SimplePersonName.java b/android_icu4j/src/main/java/android/icu/text/SimplePersonName.java
new file mode 100644
index 0000000..5c2cfd9
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/text/SimplePersonName.java
@@ -0,0 +1,228 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.text;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+/**
+ * A concrete implementation of PersonNameFormatter.PersonName that simply stores the field
+ * values in a Map.
+ *
+ * A caller can store both raw field values (such as "given") and modified field values (such as "given-informal")
+ * in a SimplePersonName.  But beyond storing and returning modified field values provided to it by the caller,
+ * SimplePersonName relies on the PersonNameFormatter's default handling of field modifiers.
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class SimplePersonName implements PersonName {
+    /**
+     * A utility class for constructing a SimplePersonName.  Use SimplePersonName.builder()
+     * to get a new Builder instance.
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Builder {
+        /**
+         * Set the locale for the new name object.
+         * @param locale The locale for the new name object.  Can be null, which indicates the
+         *               name's locale is unknown.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setLocale(Locale locale) {
+            this.locale = locale;
+            return this;
+        }
+
+        /**
+         * Sets the value for one field (with optional modifiers) in the new name object.
+         * @param field A NameField object specifying the field to set.
+         * @param modifiers A collection of FieldModifier objects for any modifiers that apply
+         *                  to this field value.  May be null, which is the same as the empty set.
+         * @param value The value for this field.
+         * @return This builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addField(NameField field,
+                                Collection<FieldModifier> modifiers,
+                                String value) {
+            // generate the modifiers' internal names, and sort them alphabetically
+            Set<String> modifierNames = new TreeSet<>();
+            if (modifiers != null) {
+                for (FieldModifier modifier : modifiers) {
+                    modifierNames.add(modifier.toString());
+                }
+            }
+
+            // construct the modified field name, which is the field name, with all the modifier names attached in
+            // alphabetical order, delimited by hyphens
+            StringBuilder fieldName = new StringBuilder();
+            fieldName.append(field.toString());
+            for (String modifierName : modifierNames) {
+                fieldName.append("-");
+                fieldName.append(modifierName);
+            }
+
+            fieldValues.put(fieldName.toString(), value);
+            return this;
+        }
+
+        /**
+         * Returns a SimplePersonName with the field values and name locale that were passed to this builder.
+         * @return A SimplePersonName with the field values and name locale that were passed to this builder.
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public SimplePersonName build() {
+            // special-case code for the "surname" field -- if it isn't specified, but "surname-prefix" and
+            // "surname-core" both are, let "surname" be the other two fields joined with a space
+            if (fieldValues.get("surname") == null) {
+                String surnamePrefix = fieldValues.get("surname-prefix");
+                String surnameCore = fieldValues.get("surname-core");
+                if (surnamePrefix != null && surnameCore != null) {
+                    fieldValues.put("surname", surnamePrefix + " " + surnameCore);
+                }
+            }
+
+            return new SimplePersonName(locale, fieldValues);
+        }
+
+        private Builder() {
+            locale = null;
+            fieldValues = new HashMap<>();
+        }
+
+        private Locale locale;
+        private Map<String, String> fieldValues;
+    }
+
+    /**
+     * Returns a Builder object that can be used to construct a new SimplePersonName object.
+     * @return A Builder object that can be used to construct a new SimplePersonName object.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Internal constructor used by the Builder object.
+     */
+    private SimplePersonName(Locale nameLocale, Map<String, String> fieldValues) {
+        this.nameLocale = nameLocale;
+        this.fieldValues = new HashMap<>(fieldValues);
+    }
+
+    /**
+     * Returns the locale of the name-- that is, the language or country of origin for the person being named.
+     * @return The name's locale, or null if it's unknown.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Override
+    @Deprecated
+    public Locale getNameLocale() {
+        return nameLocale;
+    }
+
+    /**
+     * Returns one field of the name, possibly in a modified form.  This class can store modified versions of fields,
+     * provided at construction time, and this function will return them.  Otherwise, it ignores modifiers and
+     * relies on PersonNameFormat's default modifier handling.
+     * @param nameField The identifier of the requested field.
+     * @param modifiers An **IN/OUT** parameter that specifies modifiers to apply to the basic field value.
+     *                  On return, this list will contain any modifiers that this object didn't handle.  This class
+     *                  will always return this set unmodified, unless a modified version of the requested field
+     *                  was provided at construction time.
+     * @return The value of the requested field, optionally modified by some or all of the requested modifiers, or
+     * null if the requested field isn't present in the name.
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Override
+    @Deprecated
+    public String getFieldValue(NameField nameField, Set<FieldModifier> modifiers) {
+        // first look for the fully modified name in the internal table
+        String fieldName = nameField.toString();
+        String result = fieldValues.get(makeModifiedFieldName(nameField, modifiers));
+        if (result != null) {
+            modifiers.clear();
+            return result;
+        }
+
+        // if we don't find it, check the fully unmodified name.  If it's not there, nothing else will be
+        result = fieldValues.get(fieldName);
+        if (result == null) {
+            return null;
+        } else if (modifiers.size() == 1) {
+            // and if it IS there and there's only one modifier, we're done
+            return result;
+        }
+
+        // but if there are two or more modifiers, then we have to go through the whole list of fields and look for the best match
+        String winningKey = fieldName;
+        int winningScore = 0;
+        for (String key : fieldValues.keySet()) {
+            if (key.startsWith(fieldName)) {
+                Set<FieldModifier> keyModifiers = makeModifiersFromName(key);
+                if (modifiers.containsAll(keyModifiers)) {
+                    if (keyModifiers.size() > winningScore || (keyModifiers.size() == winningScore && key.compareTo(winningKey) < 0)) {
+                        winningKey = key;
+                        winningScore = keyModifiers.size();
+                    }
+                }
+            }
+        }
+        result = fieldValues.get(winningKey);
+        modifiers.removeAll(makeModifiersFromName(winningKey));
+        return result;
+    }
+
+    private static String makeModifiedFieldName(NameField fieldName,
+                                                Collection<FieldModifier> modifiers) {
+        StringBuilder result = new StringBuilder();
+        result.append(fieldName);
+
+        TreeSet<String> sortedModifierNames = new TreeSet<>();
+        for (FieldModifier modifier : modifiers) {
+            sortedModifierNames.add(modifier.toString());
+        }
+        for (String modifierName : sortedModifierNames) {
+            result.append("-");
+            result.append(modifierName);
+        }
+        return result.toString();
+    }
+
+    private static Set<FieldModifier> makeModifiersFromName(String modifiedName) {
+        StringTokenizer tok = new StringTokenizer(modifiedName, "-");
+        Set<FieldModifier> result = new HashSet<>();
+        String fieldName = tok.nextToken(); // throw away the field name
+        while (tok.hasMoreTokens()) {
+            result.add(PersonName.FieldModifier.forString(tok.nextToken()));
+        }
+        return result;
+    }
+
+    private final Locale nameLocale;
+    private final Map<String, String> fieldValues;
+}
\ No newline at end of file
diff --git a/android_icu4j/src/main/java/android/icu/text/SpoofChecker.java b/android_icu4j/src/main/java/android/icu/text/SpoofChecker.java
index b0859c5..701a94f 100644
--- a/android_icu4j/src/main/java/android/icu/text/SpoofChecker.java
+++ b/android_icu4j/src/main/java/android/icu/text/SpoofChecker.java
@@ -260,8 +260,8 @@
      * Security Profile constant from UTS 39 for use in {@link SpoofChecker.Builder#setAllowedChars}.
      */
     public static final UnicodeSet INCLUSION = new UnicodeSet(
-            "['\\-.\\:\\u00B7\\u0375\\u058A\\u05F3\\u05F4\\u06FD\\u06FE\\u0F0B\\u200C"
-            + "\\u200D\\u2010\\u2019\\u2027\\u30A0\\u30FB]"
+            "['\\-.\\:\\u00B7\\u0375\\u058A\\u05F3\\u05F4\\u06FD\\u06FE\\u0F0B\\u2010"
+            + "\\u2019\\u2027\\u30A0\\u30FB]"
             ).freeze();
     // Note: data from IdentifierStatus.txt & IdentifierType.txt
     // There is tooling to generate this constant in the unicodetools project:
@@ -303,14 +303,14 @@
             + "\\u0C56\\u0C5D\\u0C60\\u0C61\\u0C66-\\u0C6F\\u0C80\\u0C82\\u0C83\\u0C85-"
             + "\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBC-"
             + "\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0CDD\\u0CE0-\\u0CE3"
-            + "\\u0CE6-\\u0CEF\\u0CF1\\u0CF2\\u0D00\\u0D02\\u0D03\\u0D05-\\u0D0C\\u0D0E-"
+            + "\\u0CE6-\\u0CEF\\u0CF1-\\u0CF3\\u0D00\\u0D02\\u0D03\\u0D05-\\u0D0C\\u0D0E-"
             + "\\u0D10\\u0D12-\\u0D3A\\u0D3D-\\u0D43\\u0D46-\\u0D48\\u0D4A-\\u0D4E\\u0D54-"
             + "\\u0D57\\u0D60\\u0D61\\u0D66-\\u0D6F\\u0D7A-\\u0D7F\\u0D82\\u0D83\\u0D85-"
             + "\\u0D8E\\u0D91-\\u0D96\\u0D9A-\\u0DA5\\u0DA7-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD"
             + "\\u0DC0-\\u0DC6\\u0DCA\\u0DCF-\\u0DD4\\u0DD6\\u0DD8-\\u0DDE\\u0DF2\\u0E01-"
             + "\\u0E32\\u0E34-\\u0E3A\\u0E40-\\u0E4E\\u0E50-\\u0E59\\u0E81\\u0E82\\u0E84"
             + "\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB2\\u0EB4-\\u0EBD\\u0EC0-"
-            + "\\u0EC4\\u0EC6\\u0EC8-\\u0ECD\\u0ED0-\\u0ED9\\u0EDE\\u0EDF\\u0F00\\u0F20-"
+            + "\\u0EC4\\u0EC6\\u0EC8-\\u0ECE\\u0ED0-\\u0ED9\\u0EDE\\u0EDF\\u0F00\\u0F20-"
             + "\\u0F29\\u0F35\\u0F37\\u0F3E-\\u0F42\\u0F44-\\u0F47\\u0F49-\\u0F4C\\u0F4E-"
             + "\\u0F51\\u0F53-\\u0F56\\u0F58-\\u0F5B\\u0F5D-\\u0F68\\u0F6A-\\u0F6C\\u0F71"
             + "\\u0F72\\u0F74\\u0F7A-\\u0F80\\u0F82-\\u0F84\\u0F86-\\u0F92\\u0F94-\\u0F97"
@@ -331,17 +331,18 @@
             + "\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u3005-"
             + "\\u3007\\u3041-\\u3096\\u3099\\u309A\\u309D\\u309E\\u30A1-\\u30FA\\u30FC-"
             + "\\u30FE\\u3105-\\u312D\\u312F\\u31A0-\\u31BF\\u3400-\\u4DBF\\u4E00-\\u9FFF"
-            + "\\uA67F\\uA717-\\uA71F\\uA788\\uA78D\\uA792\\uA793\\uA7AA\\uA7AE\\uA7B8"
-            + "\\uA7B9\\uA7C0-\\uA7CA\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7D9\\uA9E7-\\uA9FE"
-            + "\\uAA60-\\uAA76\\uAA7A-\\uAA7F\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16"
-            + "\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB66\\uAB67\\uAC00-\\uD7A3\\uFA0E\\uFA0F"
-            + "\\uFA11\\uFA13\\uFA14\\uFA1F\\uFA21\\uFA23\\uFA24\\uFA27-\\uFA29\\U00011301"
-            + "\\U00011303\\U0001133B\\U0001133C\\U00016FF0\\U00016FF1\\U0001B11F-"
-            + "\\U0001B122\\U0001B150-\\U0001B152\\U0001B164-\\U0001B167\\U0001DF00-"
-            + "\\U0001DF1E\\U0001E7E0-\\U0001E7E6\\U0001E7E8-\\U0001E7EB\\U0001E7ED"
-            + "\\U0001E7EE\\U0001E7F0-\\U0001E7FE\\U00020000-\\U0002A6DF\\U0002A700-"
-            + "\\U0002B738\\U0002B740-\\U0002B81D\\U0002B820-\\U0002CEA1\\U0002CEB0-"
-            + "\\U0002EBE0\\U00030000-\\U0003134A]"
+            + "\\uA67F\\uA717-\\uA71F\\uA788\\uA78D\\uA792\\uA793\\uA7AA\\uA7C0-\\uA7CA"
+            + "\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7D9\\uA9E7-\\uA9FE\\uAA60-\\uAA76\\uAA7A-"
+            + "\\uAA7F\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-"
+            + "\\uAB2E\\uAB66\\uAB67\\uAC00-\\uD7A3\\uFA0E\\uFA0F\\uFA11\\uFA13\\uFA14"
+            + "\\uFA1F\\uFA21\\uFA23\\uFA24\\uFA27-\\uFA29\\U00011301\\U00011303"
+            + "\\U0001133B\\U0001133C\\U00016FF0\\U00016FF1\\U0001B11F-\\U0001B122"
+            + "\\U0001B132\\U0001B150-\\U0001B152\\U0001B155\\U0001B164-\\U0001B167"
+            + "\\U0001DF00-\\U0001DF1E\\U0001DF25-\\U0001DF2A\\U0001E08F\\U0001E7E0-"
+            + "\\U0001E7E6\\U0001E7E8-\\U0001E7EB\\U0001E7ED\\U0001E7EE\\U0001E7F0-"
+            + "\\U0001E7FE\\U00020000-\\U0002A6DF\\U0002A700-\\U0002B739\\U0002B740-"
+            + "\\U0002B81D\\U0002B820-\\U0002CEA1\\U0002CEB0-\\U0002EBE0\\U00030000-"
+            + "\\U0003134A\\U00031350-\\U000323AF]"
             ).freeze();
     // Note: data from IdentifierStatus.txt & IdentifierType.txt
     // There is tooling to generate this constant in the unicodetools project:
diff --git a/android_icu4j/src/main/java/android/icu/text/StringSearch.java b/android_icu4j/src/main/java/android/icu/text/StringSearch.java
index cd3f8d8..68522d3 100644
--- a/android_icu4j/src/main/java/android/icu/text/StringSearch.java
+++ b/android_icu4j/src/main/java/android/icu/text/StringSearch.java
@@ -850,7 +850,7 @@
      * Checks for identical match
      * @param start offset of possible match
      * @param end offset of possible match
-     * @return TRUE if identical match is found
+     * @return true if identical match is found
      */
     private boolean checkIdentical(int start, int end) {
         if (strength_ != Collator.IDENTICAL) {
@@ -898,7 +898,7 @@
     }
 
     /*
-     * Returns TRUE if index is on a break boundary. If the UStringSearch
+     * Returns true if index is on a break boundary. If the UStringSearch
      * has an external break iterator, test using that, otherwise test
      * using the internal character break iterator.
      */
diff --git a/android_icu4j/src/main/java/android/icu/text/TransliterationRule.java b/android_icu4j/src/main/java/android/icu/text/TransliterationRule.java
index da8b112..6a8d112 100644
--- a/android_icu4j/src/main/java/android/icu/text/TransliterationRule.java
+++ b/android_icu4j/src/main/java/android/icu/text/TransliterationRule.java
@@ -361,11 +361,11 @@
      *
      * @param text the text
      * @param pos the position indices
-     * @param incremental if TRUE, test for partial matches that may
+     * @param incremental if true, test for partial matches that may
      * be completed by additional text inserted at pos.limit.
      * @return one of <code>U_MISMATCH</code>,
      * <code>U_PARTIAL_MATCH</code>, or <code>U_MATCH</code>.  If
-     * incremental is FALSE then U_PARTIAL_MATCH will not be returned.
+     * incremental is false then U_PARTIAL_MATCH will not be returned.
      */
     public int matchAndReplace(Replaceable text,
                                Transliterator.Position pos,
diff --git a/android_icu4j/src/main/java/android/icu/text/TransliterationRuleSet.java b/android_icu4j/src/main/java/android/icu/text/TransliterationRuleSet.java
index c2a0309..1001791 100644
--- a/android_icu4j/src/main/java/android/icu/text/TransliterationRuleSet.java
+++ b/android_icu4j/src/main/java/android/icu/text/TransliterationRuleSet.java
@@ -177,14 +177,14 @@
 
     /**
      * Transliterate the given text with the given UTransPosition
-     * indices.  Return TRUE if the transliteration should continue
-     * or FALSE if it should halt (because of a U_PARTIAL_MATCH match).
-     * Note that FALSE is only ever returned if isIncremental is TRUE.
+     * indices.  Return true if the transliteration should continue
+     * or false if it should halt (because of a U_PARTIAL_MATCH match).
+     * Note that false is only ever returned if isIncremental is true.
      * @param text the text to be transliterated
      * @param pos the position indices, which will be updated
-     * @param incremental if TRUE, assume new text may be inserted
-     * at index.limit, and return FALSE if there is a partial match.
-     * @return TRUE unless a U_PARTIAL_MATCH has been obtained,
+     * @param incremental if true, assume new text may be inserted
+     * at index.limit, and return false if there is a partial match.
+     * @return true unless a U_PARTIAL_MATCH has been obtained,
      * indicating that transliteration should stop until more text
      * arrives.
      */
diff --git a/android_icu4j/src/main/java/android/icu/text/Transliterator.java b/android_icu4j/src/main/java/android/icu/text/Transliterator.java
index d9dd4d5..77b9fa0 100644
--- a/android_icu4j/src/main/java/android/icu/text/Transliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/Transliterator.java
@@ -975,14 +975,14 @@
      * Top-level transliteration method, handling filtering, incremental and
      * non-incremental transliteration, and rollback.  All transliteration
      * public API methods eventually call this method with a rollback argument
-     * of TRUE.  Other entities may call this method but rollback should be
-     * FALSE.
+     * of true.  Other entities may call this method but rollback should be
+     * false.
      *
      * <p>If this transliterator has a filter, break up the input text into runs
      * of unfiltered characters.  Pass each run to
      * <subclass>.handleTransliterate().
      *
-     * <p>In incremental mode, if rollback is TRUE, perform a special
+     * <p>In incremental mode, if rollback is true, perform a special
      * incremental procedure in which several passes are made over the input
      * text, adding one character at a time, and committing successful
      * transliterations as they occur.  Unsuccessful transliterations are rolled
@@ -990,12 +990,12 @@
      *
      * @param text the text to be transliterated
      * @param index the position indices
-     * @param incremental if TRUE, then assume more characters may be inserted
+     * @param incremental if true, then assume more characters may be inserted
      * at index.limit, and postpone processing to accommodate future incoming
      * characters
-     * @param rollback if TRUE and if incremental is TRUE, then perform special
+     * @param rollback if true and if incremental is true, then perform special
      * incremental processing, as described above, and undo partial
-     * transliterations where necessary.  If incremental is FALSE then this
+     * transliterations where necessary.  If incremental is false then this
      * parameter is ignored.
      */
     private void filteredTransliterate(Replaceable text,
@@ -1080,7 +1080,7 @@
 
             // Is this run incremental?  If there is additional
             // filtered text (if limit < globalLimit) then we pass in
-            // an incremental value of FALSE to force the subclass to
+            // an incremental value of false to force the subclass to
             // complete the transliteration for this run.
             boolean isIncrementalRun =
                 (index.limit < globalLimit ? false : incremental);
@@ -1307,7 +1307,7 @@
      * another transliterator.
      * @param text the text to be transliterated
      * @param index the position indices
-     * @param incremental if TRUE, then assume more characters may be inserted
+     * @param incremental if true, then assume more characters may be inserted
      * at index.limit, and postpone processing to accommodate future incoming
      * characters
      */
diff --git a/android_icu4j/src/main/java/android/icu/text/TransliteratorRegistry.java b/android_icu4j/src/main/java/android/icu/text/TransliteratorRegistry.java
index 6039bc0..d7b2dbe 100644
--- a/android_icu4j/src/main/java/android/icu/text/TransliteratorRegistry.java
+++ b/android_icu4j/src/main/java/android/icu/text/TransliteratorRegistry.java
@@ -17,11 +17,13 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.MissingResourceException;
 import java.util.ResourceBundle;
+import java.util.Set;
 
 import android.icu.impl.ICUData;
 import android.icu.impl.ICUResourceBundle;
@@ -70,7 +72,7 @@
     /**
      * Vector of public full IDs (CaseInsensitiveString objects).
      */
-    private List<CaseInsensitiveString> availableIDs;
+    private final Set<CaseInsensitiveString> availableIDs;
 
     //----------------------------------------------------------------------
     // class Spec
@@ -97,8 +99,8 @@
         private String spec;       // current spec
         private String n