Skip NumberFormatTest#testMonetarySeparator on U or below am: 6d2364d5ea am: 921400dd8d

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

Change-Id: Iba0d2b9dd647d1d76bc5febba70f949b7f8dcef9
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index a45866e..d7fb36b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -67,7 +67,6 @@
         "-Wno-unused-const-variable",
         "-Wno-unneeded-internal-declaration",
         "-Wno-deprecated-declarations",
-        "-Wno-ignored-attributes",
     ],
     // Upstream requires C++11 as the minimum version.
     cppflags: [
diff --git a/README.version b/README.version
index 39c819c..70eadc7 100644
--- a/README.version
+++ b/README.version
@@ -1,3 +1,3 @@
 URL: https://github.com/unicode-org/icu
-Version: 68.2
+Version: 70.1
 BugComponent: 23970
diff --git a/android_icu4j/Android.bp b/android_icu4j/Android.bp
index 5018ef3..dabd918 100644
--- a/android_icu4j/Android.bp
+++ b/android_icu4j/Android.bp
@@ -129,7 +129,9 @@
     permitted_packages: [
         "android.icu",
         "com.android.icu",
+        "com.android.i18n.system",
         "com.android.i18n.timezone",
+        "com.android.i18n.util",
     ],
     installable: true,
     hostdex: false,
@@ -168,9 +170,9 @@
 java_sdk_library {
     name: "i18n.module.public.api",
     visibility: [
+        "//build/soong/java/core-libraries",
         "//frameworks/base",
         "//frameworks/base/api",
-        "//libcore",
         "//packages/modules/RuntimeI18n/apex",
         // Visibility for prebuilt i18n-module-sdk from the prebuilt of
         // this module.
@@ -189,8 +191,23 @@
             "-Xep:MissingOverride:OFF",
         ],
     },
+
+    public: {
+        enabled: true,
+    },
+    system: {
+        enabled: true,
+    },
+    module_lib: {
+        enabled: true,
+    },
+
     api_dir: "api/public",
     api_only: true,
+
+    // Emit nullability annotations from the source to the stub files.
+    annotations_enabled: true,
+
     sdk_version: "none",
     system_modules: "art-module-public-api-stubs-system-modules",
 
@@ -233,7 +250,6 @@
     droiddoc_options: [
         "--hide-annotation libcore.api.Hide ",
         "--show-single-annotation libcore.api.IntraCoreApi ",
-        "--skip-annotation-instance-methods=false ",
     ],
 
     // Don't copy any output files to the dist.
@@ -329,7 +345,7 @@
         ":android_icu4j_repackaged_src_files",
     ],
     visibility: [
-        "//libcore:__subpackages__",
+        "//build/soong/java/core-libraries",
         "//packages/modules/RuntimeI18n/apex",
         // Visibility for prebuilt i18n-module-sdk from the prebuilt of
         // this module.
@@ -346,7 +362,6 @@
     droiddoc_options: [
         "--hide-annotation libcore.api.Hide ",
         "--show-single-annotation libcore.api.CorePlatformApi ",
-        "--skip-annotation-instance-methods=false ",
     ],
 
     // Don't copy any output files to the dist.
@@ -360,7 +375,7 @@
         ":android_icu4j_repackaged_src_files",
     ],
     visibility: [
-        "//libcore:__subpackages__",
+        "//build/soong/java/core-libraries",
         "//packages/modules/RuntimeI18n/apex",
         // Visibility for prebuilt i18n-module-sdk from the prebuilt of
         // this module.
@@ -377,7 +392,6 @@
     droiddoc_options: [
         "--hide-annotation libcore.api.Hide ",
         "--show-single-annotation libcore.api.CorePlatformApi\\(status=libcore.api.CorePlatformApi.Status.STABLE\\)",
-        "--skip-annotation-instance-methods=false ",
     ],
 
     // Don't copy any output files to the dist.
@@ -417,4 +431,8 @@
     patch_module: "java.base",
     sdk_version: "none",
     system_modules: "art-module-intra-core-api-stubs-system-modules",
+
+    errorprone: {
+        javacflags: ["-Xep:EqualsNull:WARN"],
+    },
 }
diff --git a/android_icu4j/api/legacy_platform/current.txt b/android_icu4j/api/legacy_platform/current.txt
index 692b2b1..337a712 100644
--- a/android_icu4j/api/legacy_platform/current.txt
+++ b/android_icu4j/api/legacy_platform/current.txt
@@ -1,12 +1,4 @@
 // Signature format: 2.0
-package android.icu.impl {
-
-  public class TimeZoneAdapter extends java.util.TimeZone {
-    method public static java.util.TimeZone wrap(android.icu.util.TimeZone);
-  }
-
-}
-
 package android.icu.text {
 
   public class DateFormatSymbols implements java.lang.Cloneable java.io.Serializable {
@@ -26,32 +18,6 @@
 
 }
 
-package android.icu.util {
-
-  public abstract class BasicTimeZone extends android.icu.util.TimeZone {
-    method public abstract android.icu.util.TimeZoneTransition getNextTransition(long, boolean);
-  }
-
-  public class Region implements java.lang.Comparable<android.icu.util.Region> {
-    method public static java.util.Set<android.icu.util.Region> getAvailable(android.icu.util.Region.RegionType);
-  }
-
-  public enum Region.RegionType {
-    enum_constant public static final android.icu.util.Region.RegionType TERRITORY;
-  }
-
-  public abstract class TimeZoneRule implements java.io.Serializable {
-    method public int getDSTSavings();
-  }
-
-  public class TimeZoneTransition {
-    method public android.icu.util.TimeZoneRule getFrom();
-    method public long getTime();
-    method public android.icu.util.TimeZoneRule getTo();
-  }
-
-}
-
 package com.android.i18n.timezone {
 
   public final class CountryTimeZones {
diff --git a/android_icu4j/api/public/current.txt b/android_icu4j/api/public/current.txt
index 5b3fc5c..93473d7 100644
--- a/android_icu4j/api/public/current.txt
+++ b/android_icu4j/api/public/current.txt
@@ -23,6 +23,7 @@
     method public static int getCharFromExtendedName(String);
     method public static int getCharFromName(String);
     method public static int getCharFromNameAlias(String);
+    method public static int getCodePoint(int, int);
     method public static int getCodePoint(char, char);
     method public static int getCodePoint(char);
     method public static int getCombiningClass(int);
@@ -53,6 +54,7 @@
     method public static boolean isBaseForm(int);
     method public static boolean isDefined(int);
     method public static boolean isDigit(int);
+    method public static boolean isHighSurrogate(int);
     method public static boolean isHighSurrogate(char);
     method public static boolean isISOControl(int);
     method public static boolean isIdentifierIgnorable(int);
@@ -62,6 +64,7 @@
     method public static boolean isLegal(String);
     method public static boolean isLetter(int);
     method public static boolean isLetterOrDigit(int);
+    method public static boolean isLowSurrogate(int);
     method public static boolean isLowSurrogate(char);
     method public static boolean isLowerCase(int);
     method public static boolean isMirrored(int);
@@ -69,6 +72,7 @@
     method public static boolean isSpaceChar(int);
     method public static boolean isSupplementary(int);
     method public static boolean isSupplementaryCodePoint(int);
+    method public static boolean isSurrogatePair(int, int);
     method public static boolean isSurrogatePair(char, char);
     method public static boolean isTitleCase(int);
     method public static boolean isUAlphabetic(int);
@@ -84,6 +88,7 @@
     method public static int offsetByCodePoints(char[], int, int, int, int);
     method public static int toChars(int, char[], int);
     method public static char[] toChars(int);
+    method public static int toCodePoint(int, int);
     method public static int toCodePoint(char, char);
     method public static int toLowerCase(int);
     method public static String toLowerCase(String);
@@ -342,6 +347,8 @@
     field public static final int TEH_MARBUTA = 42; // 0x2a
     field public static final int TEH_MARBUTA_GOAL = 14; // 0xe
     field public static final int TETH = 43; // 0x2b
+    field public static final int THIN_YEH = 102; // 0x66
+    field public static final int VERTICAL_TAIL = 103; // 0x67
     field public static final int WAW = 44; // 0x2c
     field public static final int YEH = 45; // 0x2d
     field public static final int YEH_BARREE = 46; // 0x2e
@@ -459,6 +466,8 @@
     field public static final android.icu.lang.UCharacter.UnicodeBlock ARABIC;
     field public static final android.icu.lang.UCharacter.UnicodeBlock ARABIC_EXTENDED_A;
     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 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
@@ -582,6 +591,8 @@
     field public static final int CURRENCY_SYMBOLS_ID = 42; // 0x2a
     field public static final android.icu.lang.UCharacter.UnicodeBlock CYPRIOT_SYLLABARY;
     field public static final int CYPRIOT_SYLLABARY_ID = 123; // 0x7b
+    field public static final android.icu.lang.UCharacter.UnicodeBlock CYPRO_MINOAN;
+    field public static final int CYPRO_MINOAN_ID = 310; // 0x136
     field public static final android.icu.lang.UCharacter.UnicodeBlock CYRILLIC;
     field public static final android.icu.lang.UCharacter.UnicodeBlock CYRILLIC_EXTENDED_A;
     field public static final int CYRILLIC_EXTENDED_A_ID = 158; // 0x9e
@@ -634,6 +645,8 @@
     field public static final android.icu.lang.UCharacter.UnicodeBlock ETHIOPIC_EXTENDED;
     field public static final android.icu.lang.UCharacter.UnicodeBlock ETHIOPIC_EXTENDED_A;
     field public static final int ETHIOPIC_EXTENDED_A_ID = 200; // 0xc8
+    field public static final android.icu.lang.UCharacter.UnicodeBlock ETHIOPIC_EXTENDED_B;
+    field public static final int ETHIOPIC_EXTENDED_B_ID = 311; // 0x137
     field public static final int ETHIOPIC_EXTENDED_ID = 133; // 0x85
     field public static final int ETHIOPIC_ID = 31; // 0x1f
     field public static final android.icu.lang.UCharacter.UnicodeBlock ETHIOPIC_SUPPLEMENT;
@@ -716,6 +729,8 @@
     field public static final int KAITHI_ID = 193; // 0xc1
     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;
+    field public static final int KANA_EXTENDED_B_ID = 312; // 0x138
     field public static final android.icu.lang.UCharacter.UnicodeBlock KANA_SUPPLEMENT;
     field public static final int KANA_SUPPLEMENT_ID = 203; // 0xcb
     field public static final android.icu.lang.UCharacter.UnicodeBlock KANBUN;
@@ -758,6 +773,10 @@
     field public static final int LATIN_EXTENDED_D_ID = 149; // 0x95
     field public static final android.icu.lang.UCharacter.UnicodeBlock LATIN_EXTENDED_E;
     field public static final int LATIN_EXTENDED_E_ID = 231; // 0xe7
+    field public static final android.icu.lang.UCharacter.UnicodeBlock LATIN_EXTENDED_F;
+    field public static final int LATIN_EXTENDED_F_ID = 313; // 0x139
+    field public static final android.icu.lang.UCharacter.UnicodeBlock LATIN_EXTENDED_G;
+    field public static final int LATIN_EXTENDED_G_ID = 314; // 0x13a
     field public static final android.icu.lang.UCharacter.UnicodeBlock LEPCHA;
     field public static final int LEPCHA_ID = 156; // 0x9c
     field public static final android.icu.lang.UCharacter.UnicodeBlock LETTERLIKE_SYMBOLS;
@@ -883,6 +902,8 @@
     field public static final int OLD_SOUTH_ARABIAN_ID = 187; // 0xbb
     field public static final android.icu.lang.UCharacter.UnicodeBlock OLD_TURKIC;
     field public static final int OLD_TURKIC_ID = 191; // 0xbf
+    field public static final android.icu.lang.UCharacter.UnicodeBlock OLD_UYGHUR;
+    field public static final int OLD_UYGHUR_ID = 315; // 0x13b
     field public static final android.icu.lang.UCharacter.UnicodeBlock OL_CHIKI;
     field public static final int OL_CHIKI_ID = 157; // 0x9d
     field public static final android.icu.lang.UCharacter.UnicodeBlock OPTICAL_CHARACTER_RECOGNITION;
@@ -1011,6 +1032,8 @@
     field public static final int TAMIL_ID = 20; // 0x14
     field public static final android.icu.lang.UCharacter.UnicodeBlock TAMIL_SUPPLEMENT;
     field public static final int TAMIL_SUPPLEMENT_ID = 299; // 0x12b
+    field public static final android.icu.lang.UCharacter.UnicodeBlock TANGSA;
+    field public static final int TANGSA_ID = 316; // 0x13c
     field public static final android.icu.lang.UCharacter.UnicodeBlock TANGUT;
     field public static final android.icu.lang.UCharacter.UnicodeBlock TANGUT_COMPONENTS;
     field public static final int TANGUT_COMPONENTS_ID = 273; // 0x111
@@ -1029,12 +1052,16 @@
     field public static final int TIFINAGH_ID = 144; // 0x90
     field public static final android.icu.lang.UCharacter.UnicodeBlock TIRHUTA;
     field public static final int TIRHUTA_ID = 251; // 0xfb
+    field public static final android.icu.lang.UCharacter.UnicodeBlock TOTO;
+    field public static final int TOTO_ID = 317; // 0x13d
     field public static final android.icu.lang.UCharacter.UnicodeBlock TRANSPORT_AND_MAP_SYMBOLS;
     field public static final int TRANSPORT_AND_MAP_SYMBOLS_ID = 207; // 0xcf
     field public static final android.icu.lang.UCharacter.UnicodeBlock UGARITIC;
     field public static final int UGARITIC_ID = 120; // 0x78
     field public static final android.icu.lang.UCharacter.UnicodeBlock UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS;
     field public static final android.icu.lang.UCharacter.UnicodeBlock UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED;
+    field public static final android.icu.lang.UCharacter.UnicodeBlock UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_A;
+    field public static final int UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_A_ID = 318; // 0x13e
     field public static final int UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_ID = 173; // 0xad
     field public static final int UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_ID = 33; // 0x21
     field public static final android.icu.lang.UCharacter.UnicodeBlock VAI;
@@ -1047,6 +1074,8 @@
     field public static final int VEDIC_EXTENSIONS_ID = 175; // 0xaf
     field public static final android.icu.lang.UCharacter.UnicodeBlock VERTICAL_FORMS;
     field public static final int VERTICAL_FORMS_ID = 145; // 0x91
+    field public static final android.icu.lang.UCharacter.UnicodeBlock VITHKUQI;
+    field public static final int VITHKUQI_ID = 319; // 0x13f
     field public static final android.icu.lang.UCharacter.UnicodeBlock WANCHO;
     field public static final int WANCHO_ID = 300; // 0x12c
     field public static final android.icu.lang.UCharacter.UnicodeBlock WARANG_CITI;
@@ -1061,6 +1090,8 @@
     field public static final int YI_SYLLABLES_ID = 72; // 0x48
     field public static final android.icu.lang.UCharacter.UnicodeBlock ZANABAZAR_SQUARE;
     field public static final int ZANABAZAR_SQUARE_ID = 280; // 0x118
+    field public static final android.icu.lang.UCharacter.UnicodeBlock ZNAMENNY_MUSICAL_NOTATION;
+    field public static final int ZNAMENNY_MUSICAL_NOTATION_ID = 320; // 0x140
   }
 
   public static interface UCharacter.VerticalOrientation {
@@ -1355,6 +1386,7 @@
     field public static final int COPTIC = 7; // 0x7
     field public static final int CUNEIFORM = 101; // 0x65
     field public static final int CYPRIOT = 47; // 0x2f
+    field public static final int CYPRO_MINOAN = 193; // 0xc1
     field public static final int CYRILLIC = 8; // 0x8
     field public static final int DEMOTIC_EGYPTIAN = 69; // 0x45
     field public static final int DESERET = 9; // 0x9
@@ -1461,6 +1493,7 @@
     field public static final int OLD_PERSIAN = 61; // 0x3d
     field public static final int OLD_SOGDIAN = 184; // 0xb8
     field public static final int OLD_SOUTH_ARABIAN = 133; // 0x85
+    field public static final int OLD_UYGHUR = 194; // 0xc2
     field public static final int OL_CHIKI = 109; // 0x6d
     field public static final int ORIYA = 31; // 0x1f
     field public static final int ORKHON = 88; // 0x58
@@ -1500,6 +1533,7 @@
     field public static final int TAI_VIET = 127; // 0x7f
     field public static final int TAKRI = 153; // 0x99
     field public static final int TAMIL = 35; // 0x23
+    field public static final int TANGSA = 195; // 0xc3
     field public static final int TANGUT = 154; // 0x9a
     field public static final int TELUGU = 36; // 0x24
     field public static final int TENGWAR = 98; // 0x62
@@ -1508,6 +1542,7 @@
     field public static final int TIBETAN = 39; // 0x27
     field public static final int TIFINAGH = 60; // 0x3c
     field public static final int TIRHUTA = 158; // 0x9e
+    field public static final int TOTO = 196; // 0xc4
     field public static final int TRADITIONAL_HAN = 74; // 0x4a
     field public static final int UCAS = 40; // 0x28
     field public static final int UGARITIC = 53; // 0x35
@@ -1515,6 +1550,7 @@
     field public static final int UNWRITTEN_LANGUAGES = 102; // 0x66
     field public static final int VAI = 99; // 0x63
     field public static final int VISIBLE_SPEECH = 100; // 0x64
+    field public static final int VITHKUQI = 197; // 0xc5
     field public static final int WANCHO = 188; // 0xbc
     field public static final int WARANG_CITI = 146; // 0x92
     field public static final int WESTERN_SYRIAC = 96; // 0x60
@@ -1649,6 +1685,7 @@
   public class FormattedNumber implements android.icu.text.FormattedValue {
     method public <A extends java.lang.Appendable> A appendTo(A);
     method public char charAt(int);
+    method public android.icu.util.MeasureUnit getOutputUnit();
     method public int length();
     method public boolean nextPosition(android.icu.text.ConstrainedFieldPosition);
     method public CharSequence subSequence(int, int);
@@ -1730,11 +1767,13 @@
   }
 
   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;
     enum_constant public static final android.icu.number.NumberFormatter.UnitWidth HIDDEN;
     enum_constant public static final android.icu.number.NumberFormatter.UnitWidth ISO_CODE;
     enum_constant public static final android.icu.number.NumberFormatter.UnitWidth NARROW;
     enum_constant public static final android.icu.number.NumberFormatter.UnitWidth SHORT;
+    enum_constant public static final android.icu.number.NumberFormatter.UnitWidth VARIANT;
   }
 
   public abstract class NumberFormatterSettings<T extends android.icu.number.NumberFormatterSettings<?>> {
@@ -1751,6 +1790,7 @@
     method public T symbols(android.icu.text.NumberingSystem);
     method public T unit(android.icu.util.MeasureUnit);
     method public T unitWidth(android.icu.number.NumberFormatter.UnitWidth);
+    method public T usage(String);
   }
 
   public abstract class NumberRangeFormatter {
@@ -2241,6 +2281,7 @@
     method public static final android.icu.text.DateFormat getDateTimeInstance(android.icu.util.Calendar, int, int);
     method public static final android.icu.text.DateFormat getInstance();
     method public static final android.icu.text.DateFormat getInstance(android.icu.util.Calendar, java.util.Locale);
+    method public static final android.icu.text.DateFormat getInstance(android.icu.util.Calendar, android.icu.util.ULocale);
     method public static final android.icu.text.DateFormat getInstance(android.icu.util.Calendar);
     method public static final android.icu.text.DateFormat getInstanceForSkeleton(String);
     method public static final android.icu.text.DateFormat getInstanceForSkeleton(String, java.util.Locale);
@@ -2407,6 +2448,13 @@
     field public static final android.icu.text.DateFormat.Field YEAR_WOY;
   }
 
+  public enum DateFormat.HourCycle {
+    enum_constant public static final android.icu.text.DateFormat.HourCycle HOUR_CYCLE_11;
+    enum_constant public static final android.icu.text.DateFormat.HourCycle HOUR_CYCLE_12;
+    enum_constant public static final android.icu.text.DateFormat.HourCycle HOUR_CYCLE_23;
+    enum_constant public static final android.icu.text.DateFormat.HourCycle HOUR_CYCLE_24;
+  }
+
   public class DateFormatSymbols implements java.lang.Cloneable java.io.Serializable {
     ctor public DateFormatSymbols();
     ctor public DateFormatSymbols(java.util.Locale);
@@ -2467,6 +2515,7 @@
     method public final StringBuffer format(android.icu.util.Calendar, android.icu.util.Calendar, StringBuffer, java.text.FieldPosition);
     method public android.icu.text.DateIntervalFormat.FormattedDateInterval formatToValue(android.icu.util.DateInterval);
     method public android.icu.text.DateIntervalFormat.FormattedDateInterval formatToValue(android.icu.util.Calendar, android.icu.util.Calendar);
+    method public android.icu.text.DisplayContext getContext(android.icu.text.DisplayContext.Type);
     method public android.icu.text.DateFormat getDateFormat();
     method public android.icu.text.DateIntervalInfo getDateIntervalInfo();
     method public static final android.icu.text.DateIntervalFormat getInstance(String);
@@ -2477,6 +2526,7 @@
     method public static final android.icu.text.DateIntervalFormat getInstance(String, android.icu.util.ULocale, android.icu.text.DateIntervalInfo);
     method public android.icu.util.TimeZone getTimeZone();
     method @Deprecated public Object parseObject(String, java.text.ParsePosition);
+    method public void setContext(android.icu.text.DisplayContext);
     method public void setDateIntervalInfo(android.icu.text.DateIntervalInfo);
     method public void setTimeZone(android.icu.util.TimeZone);
   }
@@ -2525,6 +2575,7 @@
     method public String getBestPattern(String, int);
     method public String getDateTimeFormat();
     method public String getDecimal();
+    method public android.icu.text.DateFormat.HourCycle getDefaultHourCycle();
     method public static android.icu.text.DateTimePatternGenerator getEmptyInstance();
     method public String getFieldDisplayName(int, android.icu.text.DateTimePatternGenerator.DisplayWidth);
     method public static android.icu.text.DateTimePatternGenerator getInstance();
@@ -2652,6 +2703,8 @@
     method public void setSignificantDigitsUsed(boolean);
     method public String toLocalizedPattern();
     method public String toPattern();
+    field public static final int MINIMUM_GROUPING_DIGITS_AUTO = -2; // 0xfffffffe
+    field public static final int MINIMUM_GROUPING_DIGITS_MIN2 = -3; // 0xfffffffd
     field public static final int PAD_AFTER_PREFIX = 1; // 0x1
     field public static final int PAD_AFTER_SUFFIX = 3; // 0x3
     field public static final int PAD_BEFORE_PREFIX = 0; // 0x0
@@ -2666,6 +2719,7 @@
     method public static android.icu.text.DecimalFormatSymbols forNumberingSystem(java.util.Locale, android.icu.text.NumberingSystem);
     method public static android.icu.text.DecimalFormatSymbols forNumberingSystem(android.icu.util.ULocale, android.icu.text.NumberingSystem);
     method public static java.util.Locale[] getAvailableLocales();
+    method public static android.icu.util.ULocale[] getAvailableULocales();
     method public android.icu.util.Currency getCurrency();
     method public String getCurrencySymbol();
     method public char getDecimalSeparator();
@@ -2838,12 +2892,37 @@
   public final class ListFormatter {
     method public String format(java.lang.Object...);
     method public String format(java.util.Collection<?>);
+    method public android.icu.text.ListFormatter.FormattedList formatToValue(java.lang.Object...);
+    method public android.icu.text.ListFormatter.FormattedList formatToValue(java.util.Collection<?>);
+    method public static android.icu.text.ListFormatter getInstance(android.icu.util.ULocale, android.icu.text.ListFormatter.Type, android.icu.text.ListFormatter.Width);
+    method public static android.icu.text.ListFormatter getInstance(java.util.Locale, android.icu.text.ListFormatter.Type, android.icu.text.ListFormatter.Width);
     method public static android.icu.text.ListFormatter getInstance(android.icu.util.ULocale);
     method public static android.icu.text.ListFormatter getInstance(java.util.Locale);
     method public static android.icu.text.ListFormatter getInstance();
     method public String getPatternForNumItems(int);
   }
 
+  public static final class ListFormatter.FormattedList implements android.icu.text.FormattedValue {
+    method public <A extends java.lang.Appendable> A appendTo(A);
+    method public char charAt(int);
+    method public int length();
+    method public boolean nextPosition(android.icu.text.ConstrainedFieldPosition);
+    method public CharSequence subSequence(int, int);
+    method public java.text.AttributedCharacterIterator toCharacterIterator();
+  }
+
+  public enum ListFormatter.Type {
+    enum_constant public static final android.icu.text.ListFormatter.Type AND;
+    enum_constant public static final android.icu.text.ListFormatter.Type OR;
+    enum_constant public static final android.icu.text.ListFormatter.Type UNITS;
+  }
+
+  public enum ListFormatter.Width {
+    enum_constant public static final android.icu.text.ListFormatter.Width NARROW;
+    enum_constant public static final android.icu.text.ListFormatter.Width SHORT;
+    enum_constant public static final android.icu.text.ListFormatter.Width WIDE;
+  }
+
   public abstract class LocaleDisplayNames {
     method public abstract android.icu.text.DisplayContext getContext(android.icu.text.DisplayContext.Type);
     method public abstract android.icu.text.LocaleDisplayNames.DialectHandling getDialectHandling();
@@ -3224,6 +3303,7 @@
     method public static android.icu.text.PluralRules parseDescription(String) throws java.text.ParseException;
     method public String select(double);
     method public String select(android.icu.number.FormattedNumber);
+    method public String select(android.icu.number.FormattedNumberRange);
     field public static final android.icu.text.PluralRules DEFAULT;
     field public static final String KEYWORD_FEW = "few";
     field public static final String KEYWORD_MANY = "many";
@@ -4152,10 +4232,12 @@
     method public String getSymbol(android.icu.util.ULocale);
     method public static boolean isAvailable(String, java.util.Date, java.util.Date);
     method public java.util.Currency toJavaCurrency();
+    field public static final int FORMAL_SYMBOL_NAME = 4; // 0x4
     field public static final int LONG_NAME = 1; // 0x1
     field public static final int NARROW_SYMBOL_NAME = 3; // 0x3
     field public static final int PLURAL_LONG_NAME = 2; // 0x2
     field public static final int SYMBOL_NAME = 0; // 0x0
+    field public static final int VARIANT_SYMBOL_NAME = 5; // 0x5
   }
 
   public enum Currency.CurrencyUsage {
@@ -4394,11 +4476,19 @@
   }
 
   public class MeasureUnit implements java.io.Serializable {
+    method public static android.icu.util.MeasureUnit forIdentifier(String);
     method public static java.util.Set<android.icu.util.MeasureUnit> getAvailable(String);
     method public static java.util.Set<android.icu.util.MeasureUnit> getAvailable();
     method public static java.util.Set<java.lang.String> getAvailableTypes();
+    method public android.icu.util.MeasureUnit.Complexity getComplexity();
+    method public int getDimensionality();
+    method public String getIdentifier();
     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);
     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;
@@ -4410,6 +4500,7 @@
     field public static final android.icu.util.MeasureUnit BUSHEL;
     field public static final android.icu.util.MeasureUnit BYTE;
     field public static final android.icu.util.MeasureUnit CALORIE;
+    field public static final android.icu.util.MeasureUnit CANDELA;
     field public static final android.icu.util.MeasureUnit CARAT;
     field public static final android.icu.util.MeasureUnit CELSIUS;
     field public static final android.icu.util.MeasureUnit CENTILITER;
@@ -4429,6 +4520,7 @@
     field public static final android.icu.util.MeasureUnit DECILITER;
     field public static final android.icu.util.MeasureUnit DECIMETER;
     field public static final android.icu.util.MeasureUnit DEGREE;
+    field public static final android.icu.util.MeasureUnit DOT;
     field public static final android.icu.util.MeasureUnit DOT_PER_CENTIMETER;
     field public static final android.icu.util.MeasureUnit DOT_PER_INCH;
     field public static final android.icu.util.MeasureUnit EM;
@@ -4473,6 +4565,7 @@
     field public static final android.icu.util.MeasureUnit LITER;
     field public static final android.icu.util.MeasureUnit LITER_PER_100KILOMETERS;
     field public static final android.icu.util.MeasureUnit LITER_PER_KILOMETER;
+    field public static final android.icu.util.MeasureUnit LUMEN;
     field public static final android.icu.util.MeasureUnit LUX;
     field public static final android.icu.util.MeasureUnit MEGABIT;
     field public static final android.icu.util.MeasureUnit MEGABYTE;
@@ -4548,6 +4641,12 @@
     field public static final android.icu.util.TimeUnit YEAR;
   }
 
+  public enum MeasureUnit.Complexity {
+    enum_constant public static final android.icu.util.MeasureUnit.Complexity COMPOUND;
+    enum_constant public static final android.icu.util.MeasureUnit.Complexity MIXED;
+    enum_constant public static final android.icu.util.MeasureUnit.Complexity SINGLE;
+  }
+
   public class Output<T> {
     ctor public Output();
     ctor public Output(T);
@@ -4658,6 +4757,7 @@
     method public Object clone();
     method public int compareTo(android.icu.util.ULocale);
     method public static android.icu.util.ULocale createCanonical(String);
+    method public static android.icu.util.ULocale createCanonical(android.icu.util.ULocale);
     method public static android.icu.util.ULocale forLanguageTag(String);
     method public static android.icu.util.ULocale forLocale(java.util.Locale);
     method public static android.icu.util.ULocale[] getAvailableLocales();
@@ -4854,6 +4954,7 @@
     field public static final android.icu.util.VersionInfo UNICODE_12_0;
     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_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/api/public/module-lib-current.txt b/android_icu4j/api/public/module-lib-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/android_icu4j/api/public/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/android_icu4j/api/public/module-lib-removed.txt b/android_icu4j/api/public/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/android_icu4j/api/public/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/android_icu4j/api/public/system-current.txt b/android_icu4j/api/public/system-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/android_icu4j/api/public/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/android_icu4j/api/public/system-removed.txt b/android_icu4j/api/public/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/android_icu4j/api/public/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/CountryTimeZones.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/CountryTimeZones.java
index 2640c9c..0528b10 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/CountryTimeZones.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/CountryTimeZones.java
@@ -366,7 +366,7 @@
      * otherwise an arbitrary match is returned based on the {@link
      * #getEffectiveTimeZoneMappingsAt(long)} ordering.
      *
-     * @param whenMillis the UTC time to match against
+     * @param whenMillis the Unix epoch time to match against
      * @param bias the time zone to prefer, can be {@code null} to indicate there is no preference
      * @param totalOffsetMillis the offset from UTC at {@code whenMillis}
      * @param isDst the Daylight Savings Time state at {@code whenMillis}. {@code true} means DST,
@@ -386,7 +386,7 @@
      * otherwise an arbitrary match is returned based on the {@link
      * #getEffectiveTimeZoneMappingsAt(long)} ordering.
      *
-     * @param whenMillis the UTC time to match against
+     * @param whenMillis the Unix epoch time to match against
      * @param bias the time zone to prefer, can be {@code null} to indicate there is no preference
      * @param totalOffsetMillis the offset from UTC at {@code whenMillis}
      * @return an {@link OffsetResult} with information about a matching zone, or {@code null} if
@@ -457,7 +457,7 @@
      * otherwise an arbitrary match is returned based on the {@link
      * #getEffectiveTimeZoneMappingsAt(long)} ordering.
      *
-     * @param whenMillis the UTC time to match against
+     * @param whenMillis the Unix epoch time to match against
      * @param bias the time zone to prefer, can be {@code null}
      * @param totalOffsetMillis the offset from UTC at {@code whenMillis}
      * @param isDst the Daylight Savings Time state at {@code whenMillis}. {@code true} means DST,
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 ac73b37..5af6d45 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 = 5; // Android S
+    public static final int CURRENT_FORMAT_MAJOR_VERSION = 6; // Android T
 
     /**
      * Returns the major tz data format version supported by this device.
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java
index 92c0c43..0d419a9 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java
@@ -85,8 +85,9 @@
     private final int mEarliestRawOffset;
 
     /**
-     * The times (in seconds) at which the offsets changes for any reason, whether that is a change
-     * in the offset from UTC or a change in the DST.
+     * The times (in Unix epoch time, seconds since 1st Jan 1970 00:00:00 UTC) at which the offsets
+     * changes for any reason, whether that is a change in the offset from UTC or a change in the
+     * DST.
      *
      * <p>These times are pre-calculated externally from a set of rules (both historical and
      * future) and stored in a file from which {@link ZoneInfoData#readTimeZone(String,
@@ -563,21 +564,21 @@
 
     /**
      * Get the raw and DST offsets for the specified time in milliseconds since
-     * 1st Jan 1970 00:00:00.000 UTC.
+     * 1st Jan 1970 00:00:00 UTC.
      *
      * <p>The raw offset, i.e. that part of the total offset which is not due to DST, is stored at
      * index 0 of the {@code offsets} array and the DST offset, i.e. that part of the offset which
      * is due to DST is stored at index 1.
      *
-     * @param utcTimeInMillis the UTC time in milliseconds.
+     * @param unixEpochTimeInMillis the Unix epoch time in milliseconds.
      * @param offsets the array whose length must be greater than or equal to 2.
      * @return the total offset which is the sum of the raw and DST offsets.
      *
      * @hide
      */
     @libcore.api.IntraCoreApi
-    public int getOffsetsByUtcTime(long utcTimeInMillis, @NonNull int[] offsets) {
-        int transitionIndex = findTransitionIndex(roundDownMillisToSeconds(utcTimeInMillis));
+    public int getOffsetsByUtcTime(long unixEpochTimeInMillis, @NonNull int[] offsets) {
+        int transitionIndex = findTransitionIndex(roundDownMillisToSeconds(unixEpochTimeInMillis));
         int totalOffset;
         int rawOffset;
         int dstOffset;
@@ -624,15 +625,15 @@
     }
 
     /**
-     * Returns the offset from UTC in milliseconds at the specified time {@when}.
+     * Returns the offset from UTC in milliseconds at the specified time {@code whenMillis}.
      *
-     * @param when the number of milliseconds since January 1, 1970, 00:00:00 GMT
+     * @param whenMillis the Unix epoch time in milliseconds since 1st Jan 1970, 00:00:00 UTC
      *
      * @hide
      */
     @libcore.api.IntraCoreApi
-    public int getOffset(long when) {
-        int offsetIndex = findOffsetIndexForTimeInMilliseconds(when);
+    public int getOffset(long whenMillis) {
+        int offsetIndex = findOffsetIndexForTimeInMilliseconds(whenMillis);
         if (offsetIndex == -1) {
             // Assume that all times before our first transition correspond to the
             // oldest-known non-daylight offset. The obvious alternative would be to
@@ -643,15 +644,15 @@
     }
 
     /**
-     * Returns whether the given {@code when} is in daylight saving time in this time zone.
+     * Returns whether the given {@code whenMillis} is in daylight saving time in this time zone.
      *
-     * @param when the number of milliseconds since January 1, 1970, 00:00:00 GMT
+     * @param whenMillis the Unix epoch time in milliseconds since 1st Jan 1970, 00:00:00 UTC
      *
      * @hide
      */
     @libcore.api.IntraCoreApi
-    public boolean isInDaylightTime(long when) {
-        int offsetIndex = findOffsetIndexForTimeInMilliseconds(when);
+    public boolean isInDaylightTime(long whenMillis) {
+        int offsetIndex = findOffsetIndexForTimeInMilliseconds(whenMillis);
         if (offsetIndex == -1) {
             // Assume that all times before our first transition are non-daylight.
             // Transition data tends to start with a transition to daylight, so just
@@ -674,15 +675,15 @@
 
     /**
      * Returns the offset of daylight saving in milliseconds in the latest Daylight Savings Time
-     * after the time {@code when}. If no known DST occurs after {@code when}, it returns
-     * {@code null}.
+     * after the time {@code whenMillis}. If no known DST occurs after {@code whenMillis}, it
+     * returns {@code null}.
      *
-     * @param when the number of milliseconds since January 1, 1970, 00:00:00 GMT
+     * @param whenMillis the Unix epoch time in milliseconds since 1st Jan 1970, 00:00:00 UTC
      *
      * @hide
      */
     @libcore.api.IntraCoreApi
-    public @Nullable Integer getLatestDstSavingsMillis(long when) {
+    public @Nullable Integer getLatestDstSavingsMillis(long whenMillis) {
         // Find the latest daylight and standard offsets (if any).
         int lastStdTransitionIndex = -1;
         int lastDstTransitionIndex = -1;
@@ -707,7 +708,7 @@
             // time in milliseconds into seconds in order to compare with transition time this
             // rounds up rather than down. It does that because this is interested in what
             // transitions apply in future
-            long currentUnixTimeSeconds = roundUpMillisToSeconds(when);
+            long currentUnixTimeSeconds = roundUpMillisToSeconds(whenMillis);
 
             // Is this zone observing DST currently or in the future?
             // We don't care if they've historically used it: most places have at least once.
@@ -811,8 +812,9 @@
     }
 
     /**
-     * Returns the times (in seconds) at which the offsets changes for any reason, whether that is a
-     * change in the offset from UTC or a change in the DST.
+     * Returns the Unix epoch times (in seconds since 1st Jan 1970 00:00:00 UTC) at which the
+     * offsets changes for any reason, whether that is a change in the offset from UTC or a change
+     * in the DST.
      *
      * WARNING: This API is exposed only for app compat usage in @link libcore.util.ZoneInfo}.
      *
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/internal/Memory.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/internal/Memory.java
index 846044a..d61b99d 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/internal/Memory.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/internal/Memory.java
@@ -153,12 +153,19 @@
     }
     @FastNative
     private static native short peekShortNative(long address);
+    @FastNative
     public static native void peekByteArray(long address, byte[] dst, int dstOffset, int byteCount);
+    @FastNative
     public static native void peekCharArray(long address, char[] dst, int dstOffset, int charCount, boolean swap);
+    @FastNative
     public static native void peekDoubleArray(long address, double[] dst, int dstOffset, int doubleCount, boolean swap);
+    @FastNative
     public static native void peekFloatArray(long address, float[] dst, int dstOffset, int floatCount, boolean swap);
+    @FastNative
     public static native void peekIntArray(long address, int[] dst, int dstOffset, int intCount, boolean swap);
+    @FastNative
     public static native void peekLongArray(long address, long[] dst, int dstOffset, int longCount, boolean swap);
+    @FastNative
     public static native void peekShortArray(long address, short[] dst, int dstOffset, int shortCount, boolean swap);
     @FastNative
     public static native void pokeByte(long address, byte value);
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/icu/util/ExtendedTimeZone.java b/android_icu4j/libcore_bridge/src/java/com/android/icu/util/ExtendedTimeZone.java
index aaa8563..d0ec593 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/icu/util/ExtendedTimeZone.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/icu/util/ExtendedTimeZone.java
@@ -217,7 +217,7 @@
             AnnualTimeZoneRule firstAnnualRule =
                     (AnnualTimeZoneRule) firstTransitionToAnnualRule.getTo();
             AnnualTimeZoneRule currentTimeZoneRule = firstAnnualRule;
-            long currentUtcTime = firstTransitionToAnnualRule.getTime();
+            long currentUnixEpochTime = firstTransitionToAnnualRule.getTime();
 
             do {
                 annualTimeZoneRules.add(currentTimeZoneRule);
@@ -231,7 +231,7 @@
                 if (!lastStandardOffset.equals(ruleStandardOffset)) {
                     standardOffsetTransitionList.add(
                             ZoneOffsetTransition.of(
-                                    localDateTime(currentUtcTime, lastStandardOffset),
+                                    localDateTime(currentUnixEpochTime, lastStandardOffset),
                                     lastStandardOffset,
                                     ruleStandardOffset
                             ));
@@ -239,7 +239,7 @@
                 }
 
                 int currentYear =
-                        Instant.ofEpochMilli(currentUtcTime).atOffset(lastWallOffset).getYear();
+                        Instant.ofEpochMilli(currentUnixEpochTime).atOffset(lastWallOffset).getYear();
                 ZoneOffsetTransition recurringRuleTransition =
                         createZoneOffsetTransitionRule(
                                 currentTimeZoneRule,
@@ -250,20 +250,20 @@
                 // After introduction of first annual rule wall offset may not change.
                 if (!lastWallOffset.equals(recurringRuleTransition.getOffsetAfter())) {
                     transitionList.add(ZoneOffsetTransition.of(
-                            localDateTime(currentUtcTime, lastWallOffset),
+                            localDateTime(currentUnixEpochTime, lastWallOffset),
                             lastWallOffset,
                             recurringRuleTransition.getOffsetAfter()));
                     lastWallOffset = recurringRuleTransition.getOffsetAfter();
                 }
 
                 TimeZoneTransition nextTransition =
-                        basicTimeZone.getNextTransition(currentUtcTime, false /* inclusive */);
-                currentUtcTime = nextTransition.getTime();
+                        basicTimeZone.getNextTransition(currentUnixEpochTime, false /* inclusive */);
+                currentUnixEpochTime = nextTransition.getTime();
                 currentTimeZoneRule = (AnnualTimeZoneRule) nextTransition.getTo();
 
                 if (currentTimeZoneRule == null) {
                     throw zoneRulesException("No transitions after "
-                            + currentUtcTime + " for a timezone with recurring rules");
+                            + currentUnixEpochTime + " for a timezone with recurring rules");
                 }
             } while (!currentTimeZoneRule.isEquivalentTo(firstAnnualRule));
 
diff --git a/android_icu4j/libcore_bridge/src/native/com_android_i18n_timezone_internal_Memory.cpp b/android_icu4j/libcore_bridge/src/native/com_android_i18n_timezone_internal_Memory.cpp
index f97b332..0027a43 100644
--- a/android_icu4j/libcore_bridge/src/native/com_android_i18n_timezone_internal_Memory.cpp
+++ b/android_icu4j/libcore_bridge/src/native/com_android_i18n_timezone_internal_Memory.cpp
@@ -1,3 +1,4 @@
+
 /*
  * Copyright (C) 2007 The Android Open Source Project
  *
@@ -222,16 +223,16 @@
 
 static JNINativeMethod gMethods[] = {
     FAST_NATIVE_METHOD(Memory, peekByte, "(J)B"),
-    NATIVE_METHOD(Memory, peekByteArray, "(J[BII)V"),
-    NATIVE_METHOD(Memory, peekCharArray, "(J[CIIZ)V"),
-    NATIVE_METHOD(Memory, peekDoubleArray, "(J[DIIZ)V"),
-    NATIVE_METHOD(Memory, peekFloatArray, "(J[FIIZ)V"),
+    FAST_NATIVE_METHOD(Memory, peekByteArray, "(J[BII)V"),
+    FAST_NATIVE_METHOD(Memory, peekCharArray, "(J[CIIZ)V"),
+    FAST_NATIVE_METHOD(Memory, peekDoubleArray, "(J[DIIZ)V"),
+    FAST_NATIVE_METHOD(Memory, peekFloatArray, "(J[FIIZ)V"),
     FAST_NATIVE_METHOD(Memory, peekIntNative, "(J)I"),
-    NATIVE_METHOD(Memory, peekIntArray, "(J[IIIZ)V"),
+    FAST_NATIVE_METHOD(Memory, peekIntArray, "(J[IIIZ)V"),
     FAST_NATIVE_METHOD(Memory, peekLongNative, "(J)J"),
-    NATIVE_METHOD(Memory, peekLongArray, "(J[JIIZ)V"),
+    FAST_NATIVE_METHOD(Memory, peekLongArray, "(J[JIIZ)V"),
     FAST_NATIVE_METHOD(Memory, peekShortNative, "(J)S"),
-    NATIVE_METHOD(Memory, peekShortArray, "(J[SIIZ)V"),
+    FAST_NATIVE_METHOD(Memory, peekShortArray, "(J[SIIZ)V"),
     FAST_NATIVE_METHOD(Memory, pokeByte, "(JB)V"),
     NATIVE_METHOD(Memory, pokeByteArray, "(J[BII)V"),
     NATIVE_METHOD(Memory, pokeCharArray, "(J[CIIZ)V"),
diff --git a/android_icu4j/src/main/java/android/icu/impl/CalendarAstronomer.java b/android_icu4j/src/main/java/android/icu/impl/CalendarAstronomer.java
index 2ea9ebd..088100d 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CalendarAstronomer.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CalendarAstronomer.java
@@ -363,7 +363,7 @@
      */
     public double getGreenwichSidereal() {
         if (siderealTime == INVALID) {
-            // See page 86 of "Practial Astronomy with your Calculator",
+            // See page 86 of "Practical Astronomy with your Calculator",
             // by Peter Duffet-Smith, for details on the algorithm.
 
             double UT = normalize((double)time/HOUR_MS, 24);
@@ -441,7 +441,7 @@
      */
     public final Equatorial eclipticToEquatorial(double eclipLong, double eclipLat)
     {
-        // See page 42 of "Practial Astronomy with your Calculator",
+        // See page 42 of "Practical Astronomy with your Calculator",
         // by Peter Duffet-Smith, for details on the algorithm.
 
         double obliq = eclipticObliquity();
@@ -569,7 +569,7 @@
      */
     public double getSunLongitude()
     {
-        // See page 86 of "Practial Astronomy with your Calculator",
+        // See page 86 of "Practical Astronomy with your Calculator",
         // by Peter Duffet-Smith, for details on the algorithm.
 
         if (sunLongitude == INVALID) {
@@ -585,7 +585,7 @@
      */
     /*public*/ double[] getSunLongitude(double julian)
     {
-        // See page 86 of "Practial Astronomy with your Calculator",
+        // See page 86 of "Practical Astronomy with your Calculator",
         // by Peter Duffet-Smith, for details on the algorithm.
 
         double day = julian - JD_EPOCH;       // Days since epoch
@@ -1007,7 +1007,7 @@
     public Equatorial getMoonPosition()
     {
         //
-        // See page 142 of "Practial Astronomy with your Calculator",
+        // See page 142 of "Practical Astronomy with your Calculator",
         // by Peter Duffet-Smith, for details on the algorithm.
         //
         if (moonPosition == null) {
@@ -1093,7 +1093,7 @@
      * @hide draft / provisional / internal are hidden on Android
      */
     public double getMoonAge() {
-        // See page 147 of "Practial Astronomy with your Calculator",
+        // See page 147 of "Practical Astronomy with your Calculator",
         // by Peter Duffet-Smith, for details on the algorithm.
         //
         // Force the moon's position to be calculated.  We're going to use
@@ -1119,7 +1119,7 @@
      * @hide draft / provisional / internal are hidden on Android
      */
     public double getMoonPhase() {
-        // See page 147 of "Practial Astronomy with your Calculator",
+        // See page 147 of "Practical Astronomy with your Calculator",
         // by Peter Duffet-Smith, for details on the algorithm.
         return 0.5 * (1 - Math.cos(getMoonAge()));
     }
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 137c4b9..7925b3c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
@@ -834,7 +834,7 @@
         private static final int AFTER_VOWEL_WITH_ACCENT = 2;
 
         // Data generated by prototype code, see
-        // http://site.icu-project.org/design/case/greek-upper
+        // https://icu.unicode.org/design/case/greek-upper
         // TODO: Move this data into ucase.icu.
         private static final char[] data0370 = {
             // U+0370..03FF
diff --git a/android_icu4j/src/main/java/android/icu/impl/CharacterIteration.java b/android_icu4j/src/main/java/android/icu/impl/CharacterIteration.java
index 7a52a00..affff65 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CharacterIteration.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CharacterIteration.java
@@ -37,30 +37,30 @@
         //   which leaves it in position for underlying iterator's next() to work.
         int c = ci.current();
         if (c >= UTF16.LEAD_SURROGATE_MIN_VALUE && c<=UTF16.LEAD_SURROGATE_MAX_VALUE) {
-            c = ci.next();   
+            c = ci.next();
             if (c<UTF16.TRAIL_SURROGATE_MIN_VALUE || c>UTF16.TRAIL_SURROGATE_MAX_VALUE) {
-                ci.previous();   
+                ci.previous();
             }
         }
 
         // For BMP chars, this next() is the real deal.
         c = ci.next();
-        
-        // If we might have a lead surrogate, we need to peak ahead to get the trail 
+
+        // If we might have a lead surrogate, we need to peak ahead to get the trail
         //  even though we don't want to really be positioned there.
         if (c >= UTF16.LEAD_SURROGATE_MIN_VALUE) {
-            c = nextTrail32(ci, c);   
+            c = nextTrail32(ci, c);
         }
-        
+
         if (c >= UTF16.SUPPLEMENTARY_MIN_VALUE && c != DONE32) {
-            // We got a supplementary char.  Back the iterator up to the postion
+            // We got a supplementary char.  Back the iterator up to the position
             // of the lead surrogate.
-            ci.previous();   
+            ci.previous();
         }
         return c;
    }
 
-    
+
     // Out-of-line portion of the in-line Next32 code.
     // The call site does an initial ci.next() and calls this function
     //    if the 16 bit value it gets is >= LEAD_SURROGATE_MIN_VALUE.
@@ -85,36 +85,36 @@
         }
         return retVal;
     }
-       
+
     public static int previous32(CharacterIterator ci) {
         if (ci.getIndex() <= ci.getBeginIndex()) {
-            return DONE32;   
+            return DONE32;
         }
         char trail = ci.previous();
         int retVal = trail;
         if (UTF16.isTrailSurrogate(trail) && ci.getIndex()>ci.getBeginIndex()) {
             char lead = ci.previous();
             if (UTF16.isLeadSurrogate(lead)) {
-                retVal = (((int)lead  - UTF16.LEAD_SURROGATE_MIN_VALUE) << 10) +
-                          ((int)trail - UTF16.TRAIL_SURROGATE_MIN_VALUE) +
+                retVal = ((lead  - UTF16.LEAD_SURROGATE_MIN_VALUE) << 10) +
+                          (trail - UTF16.TRAIL_SURROGATE_MIN_VALUE) +
                           UTF16.SUPPLEMENTARY_MIN_VALUE;
             } else {
                 ci.next();
-            }           
+            }
         }
         return retVal;
     }
-   
+
     public static int current32(CharacterIterator ci) {
         char  lead   = ci.current();
         int   retVal = lead;
         if (retVal < UTF16.LEAD_SURROGATE_MIN_VALUE) {
-            return retVal;   
+            return retVal;
         }
         if (UTF16.isLeadSurrogate(lead)) {
-            int  trail = (int)ci.next();
+            int  trail = ci.next();
             ci.previous();
-            if (UTF16.isTrailSurrogate((char)trail)) {
+            if (UTF16.isTrailSurrogate(trail)) {
                 retVal = ((lead  - UTF16.LEAD_SURROGATE_MIN_VALUE) << 10) +
                          (trail - UTF16.TRAIL_SURROGATE_MIN_VALUE) +
                          UTF16.SUPPLEMENTARY_MIN_VALUE;
@@ -122,7 +122,7 @@
          } else {
             if (lead == CharacterIterator.DONE) {
                 if (ci.getIndex() >= ci.getEndIndex())   {
-                    retVal = DONE32;   
+                    retVal = DONE32;
                 }
             }
          }
diff --git a/android_icu4j/src/main/java/android/icu/impl/CharacterPropertiesImpl.java b/android_icu4j/src/main/java/android/icu/impl/CharacterPropertiesImpl.java
index 7a6bd9b..ffbef2c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CharacterPropertiesImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CharacterPropertiesImpl.java
@@ -72,6 +72,10 @@
             case UCharacterProperty.SRC_VO:
                 UCharacterProperty.ulayout_addPropertyStarts(src, incl);
                 break;
+            case UCharacterProperty.SRC_EMOJI: {
+                EmojiProps.INSTANCE.addPropertyStarts(incl);
+                break;
+            }
             default:
                 throw new IllegalStateException("getInclusions(unknown src " + src + ")");
             }
diff --git a/android_icu4j/src/main/java/android/icu/impl/DateNumberFormat.java b/android_icu4j/src/main/java/android/icu/impl/DateNumberFormat.java
index 6bf6589..f160e78 100644
--- a/android_icu4j/src/main/java/android/icu/impl/DateNumberFormat.java
+++ b/android_icu4j/src/main/java/android/icu/impl/DateNumberFormat.java
@@ -144,7 +144,7 @@
     @Override
     public StringBuffer format(double number, StringBuffer toAppendTo,
             FieldPosition pos) {
-        throw new UnsupportedOperationException("StringBuffer format(double, StringBuffer, FieldPostion) is not implemented");
+        throw new UnsupportedOperationException("StringBuffer format(double, StringBuffer, FieldPosition) is not implemented");
     }
 
     @Override
@@ -190,19 +190,19 @@
     @Override
     public StringBuffer format(BigInteger number, StringBuffer toAppendTo,
             FieldPosition pos) {
-        throw new UnsupportedOperationException("StringBuffer format(BigInteger, StringBuffer, FieldPostion) is not implemented");
+        throw new UnsupportedOperationException("StringBuffer format(BigInteger, StringBuffer, FieldPosition) is not implemented");
     }
 
     @Override
     public StringBuffer format(java.math.BigDecimal number, StringBuffer toAppendTo,
             FieldPosition pos) {
-        throw new UnsupportedOperationException("StringBuffer format(BigDecimal, StringBuffer, FieldPostion) is not implemented");
+        throw new UnsupportedOperationException("StringBuffer format(BigDecimal, StringBuffer, FieldPosition) is not implemented");
     }
 
     @Override
     public StringBuffer format(BigDecimal number,
             StringBuffer toAppendTo, FieldPosition pos) {
-        throw new UnsupportedOperationException("StringBuffer format(BigDecimal, StringBuffer, FieldPostion) is not implemented");
+        throw new UnsupportedOperationException("StringBuffer format(BigDecimal, StringBuffer, FieldPosition) is not implemented");
     }
 
     /*
diff --git a/android_icu4j/src/main/java/android/icu/impl/EmojiProps.java b/android_icu4j/src/main/java/android/icu/impl/EmojiProps.java
new file mode 100644
index 0000000..d114201
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/EmojiProps.java
@@ -0,0 +1,201 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2021 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+// emojiprops.h
+// created: 2021sep06 Markus W. Scherer
+
+package android.icu.impl;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import android.icu.lang.UProperty;
+import android.icu.text.UnicodeSet;
+import android.icu.util.BytesTrie;
+import android.icu.util.CharsTrie;
+import android.icu.util.CodePointMap;
+import android.icu.util.CodePointTrie;
+import android.icu.util.ICUUncheckedIOException;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public final class EmojiProps {
+    private static final class IsAcceptable implements ICUBinary.Authenticate {
+        @Override
+        public boolean isDataVersionAcceptable(byte version[]) {
+            return version[0] == 1;
+        }
+    }
+    private static final IsAcceptable IS_ACCEPTABLE = new IsAcceptable();
+    private static final int DATA_FORMAT = 0x456d6f6a;  // "Emoj"
+
+    // Byte offsets from the start of the data, after the generic header,
+    // in ascending order.
+    // UCPTrie=CodePointTrie, follows the indexes
+    private static final int IX_CPTRIE_OFFSET = 0;
+
+    // UCharsTrie=CharsTrie
+    private static final int IX_BASIC_EMOJI_TRIE_OFFSET = 4;
+    //ivate static final int IX_EMOJI_KEYCAP_SEQUENCE_TRIE_OFFSET = 5;
+    //ivate static final int IX_RGI_EMOJI_MODIFIER_SEQUENCE_TRIE_OFFSET = 6;
+    //ivate static final int IX_RGI_EMOJI_FLAG_SEQUENCE_TRIE_OFFSET = 7;
+    //ivate static final int IX_RGI_EMOJI_TAG_SEQUENCE_TRIE_OFFSET = 8;
+    private static final int IX_RGI_EMOJI_ZWJ_SEQUENCE_TRIE_OFFSET = 9;
+
+    // Properties in the code point trie.
+    // https://www.unicode.org/reports/tr51/#Emoji_Properties
+    private static final int BIT_EMOJI = 0;
+    private static final int BIT_EMOJI_PRESENTATION = 1;
+    private static final int BIT_EMOJI_MODIFIER = 2;
+    private static final int BIT_EMOJI_MODIFIER_BASE = 3;
+    private static final int BIT_EMOJI_COMPONENT = 4;
+    private static final int BIT_EXTENDED_PICTOGRAPHIC = 5;
+    // https://www.unicode.org/reports/tr51/#Emoji_Sets
+    private static final int BIT_BASIC_EMOJI = 6;
+
+    public static final EmojiProps INSTANCE = new EmojiProps();
+
+    private CodePointTrie.Fast8 cpTrie = null;
+    private String stringTries[] = new String[6];
+
+    /** Input i: One of the IX_..._TRIE_OFFSET indexes into the data file indexes[] array. */
+    private static int getStringTrieIndex(int i) {
+        return i - IX_BASIC_EMOJI_TRIE_OFFSET;
+    }
+
+    private EmojiProps() {
+        ByteBuffer bytes = ICUBinary.getRequiredData("uemoji.icu");
+        try {
+            ICUBinary.readHeaderAndDataVersion(bytes, DATA_FORMAT, IS_ACCEPTABLE);
+            int startPos = bytes.position();
+
+            int cpTrieOffset = bytes.getInt();  // inIndexes[IX_CPTRIE_OFFSET]
+            int indexesLength = cpTrieOffset / 4;
+            if (indexesLength <= IX_RGI_EMOJI_ZWJ_SEQUENCE_TRIE_OFFSET) {
+                throw new ICUUncheckedIOException(
+                        "Emoji properties data: not enough indexes");
+            }
+
+            int[] inIndexes = new int[indexesLength];
+            inIndexes[0] = cpTrieOffset;
+            for (int i = 1; i < indexesLength; ++i) {
+                inIndexes[i] = bytes.getInt();
+            }
+
+            int i = IX_CPTRIE_OFFSET;
+            int offset = inIndexes[i++];
+            int nextOffset = inIndexes[i];
+            cpTrie = CodePointTrie.Fast8.fromBinary(bytes);
+            int pos = bytes.position() - startPos;
+            assert nextOffset >= pos;
+            ICUBinary.skipBytes(bytes, nextOffset - pos);  // skip padding after trie bytes
+
+            offset = nextOffset;
+            nextOffset = inIndexes[IX_BASIC_EMOJI_TRIE_OFFSET];
+            ICUBinary.skipBytes(bytes, nextOffset - offset);  // skip unknown bytes
+
+            for (i = IX_BASIC_EMOJI_TRIE_OFFSET; i <= IX_RGI_EMOJI_ZWJ_SEQUENCE_TRIE_OFFSET; ++i) {
+                offset = inIndexes[i];
+                nextOffset = inIndexes[i + 1];
+                // Set/leave null if there is no CharsTrie.
+                if (nextOffset > offset) {
+                    stringTries[getStringTrieIndex(i)] =
+                            ICUBinary.getString(bytes, (nextOffset - offset) / 2, 0);
+                }
+            }
+        } catch(IOException e) {
+            throw new ICUUncheckedIOException(e);
+        }
+    }
+
+    public UnicodeSet addPropertyStarts(UnicodeSet set) {
+        // Add the start code point of each same-value range of the trie.
+        CodePointMap.Range range = new CodePointMap.Range();
+        int start = 0;
+        while (cpTrie.getRange(start, null, range)) {
+            set.add(start);
+            start = range.getEnd() + 1;
+        }
+        return set;
+    }
+
+    // Note: REGIONAL_INDICATOR is a single, hardcoded range implemented elsewhere.
+    private static final byte[] bitFlags = {
+        BIT_EMOJI,                  // UCHAR_EMOJI=57
+        BIT_EMOJI_PRESENTATION,     // UCHAR_EMOJI_PRESENTATION=58
+        BIT_EMOJI_MODIFIER,         // UCHAR_EMOJI_MODIFIER=59
+        BIT_EMOJI_MODIFIER_BASE,    // UCHAR_EMOJI_MODIFIER_BASE=60
+        BIT_EMOJI_COMPONENT,        // UCHAR_EMOJI_COMPONENT=61
+        -1,                         // UCHAR_REGIONAL_INDICATOR=62
+        -1,                         // UCHAR_PREPENDED_CONCATENATION_MARK=63
+        BIT_EXTENDED_PICTOGRAPHIC,  // UCHAR_EXTENDED_PICTOGRAPHIC=64
+        BIT_BASIC_EMOJI,            // UCHAR_BASIC_EMOJI=65
+        -1,                         // UCHAR_EMOJI_KEYCAP_SEQUENCE=66
+        -1,                         // UCHAR_RGI_EMOJI_MODIFIER_SEQUENCE=67
+        -1,                         // UCHAR_RGI_EMOJI_FLAG_SEQUENCE=68
+        -1,                         // UCHAR_RGI_EMOJI_TAG_SEQUENCE=69
+        -1,                         // UCHAR_RGI_EMOJI_ZWJ_SEQUENCE=70
+        BIT_BASIC_EMOJI,            // UCHAR_RGI_EMOJI=71
+    };
+
+    public boolean hasBinaryProperty(int c, int which) {
+        if (which < UProperty.EMOJI || UProperty.RGI_EMOJI < which) {
+            return false;
+        }
+        int bit = bitFlags[which - UProperty.EMOJI];
+        if (bit < 0) {
+            return false;  // not a property that we support in this function
+        }
+        int bits = cpTrie.get(c);
+        return ((bits >> bit) & 1) != 0;
+    }
+
+    public boolean hasBinaryProperty(CharSequence s, int which) {
+        int length = s.length();
+        if (length == 0) { return false; }  // empty string
+        // The caller should have delegated single code points to hasBinaryProperty(c, which).
+        if (which < UProperty.BASIC_EMOJI || UProperty.RGI_EMOJI < which) {
+            return false;
+        }
+        int firstProp = which, lastProp = which;
+        if (which == UProperty.RGI_EMOJI) {
+            // RGI_Emoji is the union of the other emoji properties of strings.
+            firstProp = UProperty.BASIC_EMOJI;
+            lastProp = UProperty.RGI_EMOJI_ZWJ_SEQUENCE;
+        }
+        for (int prop = firstProp; prop <= lastProp; ++prop) {
+            String trieUChars = stringTries[prop - UProperty.BASIC_EMOJI];
+            if (trieUChars != null) {
+                CharsTrie trie = new CharsTrie(trieUChars, 0);
+                BytesTrie.Result result = trie.next(s, 0, length);
+                if (result.hasValue()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public void addStrings(int which, UnicodeSet set) {
+        if (which < UProperty.BASIC_EMOJI || UProperty.RGI_EMOJI < which) {
+            return;
+        }
+        int firstProp = which, lastProp = which;
+        if (which == UProperty.RGI_EMOJI) {
+            // RGI_Emoji is the union of the other emoji properties of strings.
+            firstProp = UProperty.BASIC_EMOJI;
+            lastProp = UProperty.RGI_EMOJI_ZWJ_SEQUENCE;
+        }
+        for (int prop = firstProp; prop <= lastProp; ++prop) {
+            String trieUChars = stringTries[prop - UProperty.BASIC_EMOJI];
+            if (trieUChars != null) {
+                CharsTrie trie = new CharsTrie(trieUChars, 0);
+                for (CharsTrie.Entry entry : trie) {
+                    set.add(entry.chars);
+                }
+            }
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/EraRules.java b/android_icu4j/src/main/java/android/icu/impl/EraRules.java
index 3bb2c54..34532f0 100644
--- a/android_icu4j/src/main/java/android/icu/impl/EraRules.java
+++ b/android_icu4j/src/main/java/android/icu/impl/EraRules.java
@@ -56,7 +56,7 @@
             try {
                 eraIdx = Integer.parseInt(eraIdxStr);
             } catch (NumberFormatException e) {
-                throw new ICUException("Invald era rule key:" + eraIdxStr + " in era rule data for " + calType.getId());
+                throw new ICUException("Invalid era rule key:" + eraIdxStr + " in era rule data for " + calType.getId());
             }
             if (eraIdx < 0 || eraIdx >= numEras) {
                 throw new ICUException("Era rule key:" + eraIdxStr + " in era rule data for " + calType.getId()
@@ -64,7 +64,7 @@
             }
             if (isSet(startDates[eraIdx])) {
                 throw new ICUException(
-                        "Dupulicated era rule for rule key:" + eraIdxStr + " in era rule data for " + calType.getId());
+                        "Duplicated era rule for rule key:" + eraIdxStr + " in era rule data for " + calType.getId());
             }
 
             boolean hasName = true;
diff --git a/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java b/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java
index 6c971d2..616101e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java
+++ b/android_icu4j/src/main/java/android/icu/impl/FormattedStringBuilder.java
@@ -27,6 +27,25 @@
  */
 public class FormattedStringBuilder implements CharSequence, Appendable {
 
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public static interface FieldWrapper {
+        java.text.Format.Field unwrap();
+    }
+
+    public static java.text.Format.Field unwrapField(Object field) {
+        if (field == null) {
+            return null;
+        } else if (field instanceof FieldWrapper) {
+            return ((FieldWrapper) field).unwrap();
+        } else if (field instanceof java.text.Format.Field) {
+            return (java.text.Format.Field) field;
+        } else {
+            throw new AssertionError("Not a field: " + field);
+        }
+    }
+
     /** A constant, empty FormattedStringBuilder. Do NOT call mutative operations on this. */
     public static final FormattedStringBuilder EMPTY = new FormattedStringBuilder();
 
@@ -536,10 +555,12 @@
         if (fields.length != length)
             return false;
         for (int i = 0; i < length; i++) {
-            if (this.chars[zero + i] != chars[i])
+            if (this.chars[zero + i] != chars[i]) {
                 return false;
-            if (this.fields[zero + i] != fields[i])
+            }
+            if (unwrapField(this.fields[zero + i]) != unwrapField(fields[i])) {
                 return false;
+            }
         }
         return true;
     }
@@ -553,7 +574,10 @@
         if (length != other.length)
             return false;
         for (int i = 0; i < length; i++) {
-            if (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) {
+            if (charAt(i) != other.charAt(i)) {
+                return false;
+            }
+            if (unwrapField(fieldAt(i)) != unwrapField(other.fieldAt(i))) {
                 return false;
             }
         }
diff --git a/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java b/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java
index c5e8954..bb26a17 100644
--- a/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/FormattedValueStringBuilderImpl.java
@@ -33,10 +33,16 @@
      * Does not currently support nested fields beyond one level.
      * @hide Only a subset of ICU is exposed in Android
      */
-    public static class SpanFieldPlaceholder {
+    public static class SpanFieldPlaceholder implements FormattedStringBuilder.FieldWrapper {
         public UFormat.SpanField spanField;
         public Field normalField;
         public Object value;
+        public int start;
+        public int length;
+
+        public Field unwrap() {
+            return normalField;
+        }
     }
 
     /**
@@ -57,6 +63,29 @@
         return -1;
     }
 
+    /**
+     * Upgrade a range of a string to a span field.
+     *
+     * Similar to appendSpanInfo in ICU4C.
+     */
+    public static void applySpanRange(
+            FormattedStringBuilder self,
+            UFormat.SpanField spanField,
+            Object value,
+            int start,
+            int end) {
+        for (int i = start + self.zero; i < end + self.zero; i++) {
+            Object oldField = self.fields[i];
+            SpanFieldPlaceholder newField = new SpanFieldPlaceholder();
+            newField.spanField = spanField;
+            newField.normalField = (java.text.Format.Field) oldField;
+            newField.value = value;
+            newField.start = start;
+            newField.length = end - start;
+            self.fields[i] = newField;
+        }
+    }
+
     public static boolean nextFieldPosition(FormattedStringBuilder self, FieldPosition fp) {
         java.text.Format.Field rawField = fp.getFieldAttribute();
 
@@ -139,18 +168,23 @@
     public static boolean nextPosition(FormattedStringBuilder self, ConstrainedFieldPosition cfpos, Field numericField) {
         int fieldStart = -1;
         Object currField = null;
+        boolean prevIsSpan = false;
+        if (cfpos.getLimit() > 0) {
+            prevIsSpan = cfpos.getField() instanceof UFormat.SpanField
+                && cfpos.getStart() < cfpos.getLimit();
+        }
+        boolean prevIsNumeric = false;
+        if (numericField != null) {
+            prevIsNumeric = cfpos.getField() == numericField;
+        }
+        boolean prevIsInteger = cfpos.getField() == NumberFormat.Field.INTEGER;
+
         for (int i = self.zero + cfpos.getLimit(); i <= self.zero + self.length; i++) {
             Object _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END;
             // Case 1: currently scanning a field.
             if (currField != null) {
                 if (currField != _field) {
                     int end = i - self.zero;
-                    // Handle span fields; don't trim them
-                    if (currField instanceof SpanFieldPlaceholder) {
-                        boolean handleResult = handleSpan(currField, cfpos, fieldStart, end);
-                        assert handleResult;
-                        return true;
-                    }
                     // Grouping separators can be whitespace; don't throw them out!
                     if (isTrimmable(currField)) {
                         end = trimBack(self, end);
@@ -171,11 +205,30 @@
                 }
                 continue;
             }
+            // Special case: emit normalField if we are pointing at the end of spanField.
+            if (i > self.zero && prevIsSpan) {
+                assert self.fields[i-1] instanceof SpanFieldPlaceholder;
+                SpanFieldPlaceholder ph = (SpanFieldPlaceholder) self.fields[i-1];
+                if (ph.normalField == ListFormatter.Field.ELEMENT) {
+                    // Special handling for ULISTFMT_ELEMENT_FIELD
+                    if (cfpos.matchesField(ListFormatter.Field.ELEMENT, null)) {
+                        fieldStart = i - self.zero - ph.length;
+                        int end = fieldStart + ph.length;
+                        cfpos.setState(ListFormatter.Field.ELEMENT, null, fieldStart, end);
+                        return true;
+                    }
+                } else {
+                    // Re-wind, since there may be multiple fields in the span.
+                    i -= ph.length;
+                    assert i >= self.zero;
+                    _field = ((SpanFieldPlaceholder) self.fields[i]).normalField;
+                }
+            }
             // Special case: coalesce the INTEGER if we are pointing at the end of the INTEGER.
             if (cfpos.matchesField(NumberFormat.Field.INTEGER, null)
                     && i > self.zero
-                    // don't return the same field twice in a row:
-                    && i - self.zero > cfpos.getLimit()
+                    && !prevIsInteger
+                    && !prevIsNumeric
                     && isIntOrGroup(self.fields[i - 1])
                     && !isIntOrGroup(_field)) {
                 int j = i - 1;
@@ -187,46 +240,58 @@
             if (numericField != null
                     && cfpos.matchesField(numericField, null)
                     && i > self.zero
-                    // don't return the same field twice in a row:
-                    && (i - self.zero > cfpos.getLimit() || cfpos.getField() != numericField)
+                    && !prevIsNumeric
                     && isNumericField(self.fields[i - 1])
                     && !isNumericField(_field)) {
+                // Re-wind to the beginning of the field and then emit it
                 int j = i - 1;
                 for (; j >= self.zero && isNumericField(self.fields[j]); j--) {}
                 cfpos.setState(numericField, null, j - self.zero + 1, i - self.zero);
                 return true;
             }
-            // Special case: emit normalField if we are pointing at the end of spanField.
-            if (i > self.zero
-                    && self.fields[i-1] instanceof SpanFieldPlaceholder) {
-                int j = i - 1;
-                for (; j >= self.zero && self.fields[j] == self.fields[i-1]; j--) {}
-                if (handleSpan(self.fields[i-1], cfpos, j - self.zero + 1, i - self.zero)) {
+            // Check for span field
+            SpanFieldPlaceholder ph = null;
+            if (_field instanceof SpanFieldPlaceholder) {
+                ph = (SpanFieldPlaceholder) _field;
+                _field = ph.normalField;
+            }
+            if (ph != null && (ph.start == -1 || ph.start == i - self.zero)) {
+                if (cfpos.matchesField(ph.spanField, ph.value)) {
+                    fieldStart = i - self.zero;
+                    int end = fieldStart + ph.length;
+                    cfpos.setState(ph.spanField, ph.value, fieldStart, end);
                     return true;
+                } else if (ph.normalField == ListFormatter.Field.ELEMENT) {
+                    // Special handling for ListFormatter.Field.ELEMENT
+                    if (cfpos.matchesField(ListFormatter.Field.ELEMENT, null)) {
+                        fieldStart = i - self.zero;
+                        int end = fieldStart + ph.length;
+                        cfpos.setState(ListFormatter.Field.ELEMENT, null, fieldStart, end);
+                        return true;
+                    } else {
+                        // Failed to match; jump ahead
+                        i += ph.length - 1;
+                        // goto loopend
+                    }
                 }
             }
             // Special case: skip over INTEGER; will be coalesced later.
-            if (_field == NumberFormat.Field.INTEGER) {
+            else if (_field == NumberFormat.Field.INTEGER) {
                 _field = null;
             }
-            // Case 2: no field starting at this position.
-            if (_field == null || _field == NullField.END) {
-                continue;
+            // No field starting at this position.
+            else if (_field == null || _field == NullField.END) {
+                // goto loopend
             }
-            // Case 3: check for field starting at this position
-            // Case 3a: SpanField placeholder
-            if (_field instanceof SpanFieldPlaceholder) {
-                SpanFieldPlaceholder ph = (SpanFieldPlaceholder) _field;
-                if (cfpos.matchesField(ph.normalField, null) || cfpos.matchesField(ph.spanField, ph.value)) {
-                    fieldStart = i - self.zero;
-                    currField = _field;
-                }
-            }
-            // Case 3b: No SpanField
+            // No SpanField
             else if (cfpos.matchesField((Field) _field, null)) {
                 fieldStart = i - self.zero;
                 currField = _field;
             }
+            // loopend:
+            prevIsSpan = false;
+            prevIsNumeric = false;
+            prevIsInteger = false;
         }
 
         assert currField == null;
@@ -240,10 +305,12 @@
     }
 
     private static boolean isIntOrGroup(Object field) {
+        field = FormattedStringBuilder.unwrapField(field);
         return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR;
     }
 
     private static boolean isNumericField(Object field) {
+        field = FormattedStringBuilder.unwrapField(field);
         return field == null || NumberFormat.Field.class.isAssignableFrom(field.getClass());
     }
 
@@ -261,19 +328,4 @@
         return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES)
                 .span(self, start, UnicodeSet.SpanCondition.CONTAINED);
     }
-
-    private static boolean handleSpan(Object field, ConstrainedFieldPosition cfpos, int start, int limit) {
-        SpanFieldPlaceholder ph = (SpanFieldPlaceholder) field;
-        if (cfpos.matchesField(ph.spanField, ph.value)
-                && cfpos.getLimit() < limit) {
-            cfpos.setState(ph.spanField, ph.value, start, limit);
-            return true;
-        }
-        if (cfpos.matchesField(ph.normalField, null)
-                && (cfpos.getLimit() < limit || cfpos.getField() != ph.normalField)) {
-            cfpos.setState(ph.normalField, null, start, limit);
-            return true;
-        }
-        return false;
-    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUDebug.java b/android_icu4j/src/main/java/android/icu/impl/ICUDebug.java
index e3a7ae1..804c749 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUDebug.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUDebug.java
@@ -9,8 +9,6 @@
  */
 package android.icu.impl;
 
-import android.icu.util.VersionInfo;
-
 /**
  * @hide Only a subset of ICU is exposed in Android
  */
@@ -32,55 +30,6 @@
         }
     }
 
-    public static final String javaVersionString = System.getProperty("java.version", "0");
-    public static final boolean isJDK14OrHigher;
-    public static final VersionInfo javaVersion;
-
-    public static VersionInfo getInstanceLenient(String s) {
-        // Extracting ASCII numbers up to 4 delimited by
-        // any non digit characters
-        int[] ver = new int[4];
-        boolean numeric = false;
-        int i = 0, vidx = 0;
-        while (i < s.length()) {
-            char c = s.charAt(i++);
-            if (c < '0' || c > '9') {
-                if (numeric) {
-                    if (vidx == 3) {
-                        // up to 4 numbers
-                        break;
-                    }
-                    numeric = false;
-                    vidx++;
-                }
-            } else {
-                if (numeric) {
-                    ver[vidx] = ver[vidx] * 10 + (c - '0');
-                    if (ver[vidx] > 255) {
-                        // VersionInfo does not support numbers
-                        // greater than 255.  In such case, we
-                        // ignore the number and the rest
-                        ver[vidx] = 0;
-                        break;
-                    }
-                } else {
-                    numeric = true;
-                    ver[vidx] = c - '0';
-                }
-            }
-        }
-
-        return VersionInfo.getInstance(ver[0], ver[1], ver[2], ver[3]);
-    }
-
-    static {
-        javaVersion = getInstanceLenient(javaVersionString);
-
-        VersionInfo java14Version = VersionInfo.getInstance("1.4.0");
-
-        isJDK14OrHigher = javaVersion.compareTo(java14Version) >= 0;
-    }
-
     public static boolean enabled() {
         return debug;
     }
@@ -113,23 +62,4 @@
         }
         return result;
     }
-
-//    static public void main(String[] args) {
-//        // test
-//        String[] tests = {
-//            "1.3.0",
-//            "1.3.0_02",
-//            "1.3.1ea",
-//            "1.4.1b43",
-//            "___41___5",
-//            "x1.4.51xx89ea.7f",
-//            "1.6_2009",
-//            "10-100-1000-10000",
-//            "beta",
-//            "0",
-//        };
-//        for (int i = 0; i < tests.length; ++i) {
-//            System.out.println(tests[i] + " => " + getInstanceLenient(tests[i]));
-//        }
-//    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICULocaleService.java b/android_icu4j/src/main/java/android/icu/impl/ICULocaleService.java
index 035225f..7db1ab4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICULocaleService.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICULocaleService.java
@@ -620,9 +620,9 @@
         if (loc != fallbackLocale) {
             synchronized (this) {
                 if (loc != fallbackLocale) {
-                    fallbackLocale = loc;
                     fallbackLocaleName = loc.getBaseName();
                     clearServiceCache();
+                    fallbackLocale = loc;
                 }
             }
         }
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 fe64de1..7c4d92e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
@@ -400,6 +400,13 @@
         }
     }
 
+    /**
+     * Locates the resource specified by `path` in this resource bundle (performing any necessary fallback and
+     * following any aliases) and calls the specified `sink`'s `put()` method with that resource.  Then walks the
+     * bundle's parent chain, calling `put()` on the sink for each item in the parent chain.
+     * @param path The path of the desired resource
+     * @param sink A `UResource.Sink` that gets called for each resource in the parent chain
+     */
     public void getAllItemsWithFallback(String path, UResource.Sink sink)
             throws MissingResourceException {
         // Collect existing and parsed key objects into an array of keys,
@@ -426,6 +433,47 @@
         rb.getAllItemsWithFallback(key, readerValue, sink);
     }
 
+    /**
+     * Locates the resource specified by `path` in this resource bundle (performing any necessary fallback and
+     * following any aliases) and, if the resource is a table resource, iterates over its immediate child resources (again,
+     * following any aliases to get the individual resource values), and calls the specified `sink`'s `put()` method
+     * for each child resource (passing it that resource's key and either its actual value or, if that value is an
+     * alias, the value you get by following the alias).  Then walks back over the bundle's parent chain,
+     * similarly iterating over each parent table resource's child resources.
+     * Does not descend beyond one level of table children.
+     * @param path The path of the desired resource
+     * @param sink A `UResource.Sink` that gets called for each child resource of the specified resource (and each child
+     * of the resources in its parent chain).
+     */
+    public void getAllChildrenWithFallback(final String path, final UResource.Sink sink)
+            throws MissingResourceException {
+        class AllChildrenSink extends UResource.Sink {
+            @Override
+            public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+                UResource.Table itemsTable = value.getTable();
+                for (int i = 0; itemsTable.getKeyAndValue(i, key, value); ++i) {
+                    if (value.getType() == ALIAS) {
+                        // if the current entry in the table is an alias, re-fetch it using getAliasedResource():
+                        // this will follow the alias (and any aliases it points to) and bring back the real value
+                        String aliasPath = value.getAliasString();
+                        ICUResourceBundle aliasedResource = getAliasedResource(aliasPath, wholeBundle.loader,
+                                "", null, 0, null,
+                                null, ICUResourceBundle.this);
+                        ICUResourceBundleImpl aliasedResourceImpl = (ICUResourceBundleImpl)aliasedResource;
+                        ReaderValue aliasedValue = new ReaderValue();
+                        aliasedValue.reader = aliasedResourceImpl.wholeBundle.reader;
+                        aliasedValue.res = aliasedResourceImpl.getResource();
+                        sink.put(key, aliasedValue, noFallback);
+                    } else {
+                        sink.put(key, value, noFallback);
+                    }
+                }
+            }
+        }
+
+        getAllItemsWithFallback(path, new AllChildrenSink());
+    }
+
     private void getAllItemsWithFallback(
             UResource.Key key, ReaderValue readerValue, UResource.Sink sink) {
         // We recursively enumerate child-first,
@@ -1490,10 +1538,27 @@
             UResourceBundle requested) {
         WholeBundle wholeBundle = base.wholeBundle;
         ClassLoader loaderToUse = wholeBundle.loader;
+        String rpath = wholeBundle.reader.getAlias(_resource);
+        String baseName = wholeBundle.baseName;
+
+        // TODO: We need not build the baseKeyPath array if the rpath includes a keyPath
+        // (except for an exception message string).
+        // Try to avoid unnecessary work+allocation.
+        int baseDepth = base.getResDepth();
+        String[] baseKeyPath = new String[baseDepth + 1];
+        base.getResPathKeys(baseKeyPath, baseDepth);
+        baseKeyPath[baseDepth] = key;
+        return getAliasedResource(rpath, loaderToUse, baseName, keys, depth, baseKeyPath, aliasesVisited, requested);
+    }
+
+    protected static ICUResourceBundle getAliasedResource(
+            String rpath, ClassLoader loaderToUse, String baseName,
+            String[] keys, int depth, String[] baseKeyPath,
+            HashMap<String, String> aliasesVisited,
+            UResourceBundle requested) {
         String locale;
         String keyPath = null;
         String bundleName;
-        String rpath = wholeBundle.reader.getAlias(_resource);
         if (aliasesVisited == null) {
             aliasesVisited = new HashMap<>();
         }
@@ -1532,12 +1597,12 @@
             } else {
                 locale = rpath;
             }
-            bundleName = wholeBundle.baseName;
+            bundleName = baseName;
         }
         ICUResourceBundle bundle = null;
         ICUResourceBundle sub = null;
         if(bundleName.equals(LOCALE)){
-            bundleName = wholeBundle.baseName;
+            bundleName = baseName;
             keyPath = rpath.substring(LOCALE.length() + 2/* prepending and appending / */, rpath.length());
 
             // Get the top bundle of the requested bundle
@@ -1559,11 +1624,8 @@
             } else if (keys != null) {
                 numKeys = depth;
             } else {
-                depth = base.getResDepth();
-                numKeys = depth + 1;
-                keys = new String[numKeys];
-                base.getResPathKeys(keys, depth);
-                keys[depth] = key;
+                keys = baseKeyPath;
+                numKeys = baseKeyPath.length;
             }
             if (numKeys > 0) {
                 sub = bundle;
@@ -1573,7 +1635,7 @@
             }
         }
         if (sub == null) {
-            throw new MissingResourceException(wholeBundle.localeID, wholeBundle.baseName, key);
+            throw new MissingResourceException(locale, baseName, baseKeyPath[baseKeyPath.length - 1]);
         }
         // TODO: If we know that sub is not cached,
         // then we should set its container and key to the alias' location,
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUService.java b/android_icu4j/src/main/java/android/icu/impl/ICUService.java
index 5ee1620..4ece905 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUService.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUService.java
@@ -202,7 +202,7 @@
          * If the key has a fallback, modify the key and return true,
          * otherwise return false.  The current ID will change if there
          * is a fallback.  No currentIDs should be repeated, and fallback
-         * must eventually return false.  This implmentation has no fallbacks
+         * must eventually return false.  This implementation has no fallbacks
          * and always returns false.
          */
         public boolean fallback() {
@@ -918,7 +918,7 @@
 
     /**
      * ServiceListener is the listener that ICUService provides by default.
-     * ICUService will notifiy this listener when factories are added to
+     * ICUService will notify this listener when factories are added to
      * or removed from the service.  Subclasses can provide
      * different listener interfaces that extend EventListener, and modify
      * acceptsListener and notifyListener as appropriate.
diff --git a/android_icu4j/src/main/java/android/icu/impl/IDNA2003.java b/android_icu4j/src/main/java/android/icu/impl/IDNA2003.java
index f4ee5ae..478b11e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/IDNA2003.java
+++ b/android_icu4j/src/main/java/android/icu/impl/IDNA2003.java
@@ -183,7 +183,7 @@
         int poLen = processOut.length();
         
         if(poLen==0){
-            throw new StringPrepParseException("Found zero length lable after NamePrep.",StringPrepParseException.ZERO_LENGTH_LABEL);
+            throw new StringPrepParseException("Found zero length label after NamePrep.",StringPrepParseException.ZERO_LENGTH_LABEL);
         }
         StringBuffer dest = new StringBuffer();
         
@@ -411,7 +411,7 @@
             sepIndex = getSeparatorIndex(srcArr,sepIndex,srcArr.length);
             String label = new String(srcArr,oldSepIndex,sepIndex-oldSepIndex);
             if(label.length()==0 && sepIndex!=srcArr.length ){
-                throw new StringPrepParseException("Found zero length lable after NamePrep.",StringPrepParseException.ZERO_LENGTH_LABEL);
+                throw new StringPrepParseException("Found zero length label after NamePrep.",StringPrepParseException.ZERO_LENGTH_LABEL);
             }
             UCharacterIterator iter = UCharacterIterator.getInstance(label);
             result.append(convertToUnicode(iter,options));
diff --git a/android_icu4j/src/main/java/android/icu/impl/IntTrieBuilder.java b/android_icu4j/src/main/java/android/icu/impl/IntTrieBuilder.java
index d2ee3cc..c0b5c6e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/IntTrieBuilder.java
+++ b/android_icu4j/src/main/java/android/icu/impl/IntTrieBuilder.java
@@ -56,7 +56,7 @@
      * Constructs a build table
      * @param aliasdata data to be filled into table
      * @param maxdatalength maximum data length allowed in table
-     * @param initialvalue inital data value
+     * @param initialvalue initial data value
      * @param latin1linear is latin 1 to be linear
      */
     public IntTrieBuilder(int aliasdata[], int maxdatalength, 
diff --git a/android_icu4j/src/main/java/android/icu/impl/LocaleDisplayNamesImpl.java b/android_icu4j/src/main/java/android/icu/impl/LocaleDisplayNamesImpl.java
index 8ee53be..1715ad4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/LocaleDisplayNamesImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleDisplayNamesImpl.java
@@ -430,13 +430,28 @@
     }
 
     private String localeIdName(String localeId) {
+        String locIdName;
         if (nameLength == DisplayContext.LENGTH_SHORT) {
-            String locIdName = langData.get("Languages%short", localeId);
+            locIdName = langData.get("Languages%short", localeId);
             if (locIdName != null && !locIdName.equals(localeId)) {
                 return locIdName;
             }
         }
-        return langData.get("Languages", localeId);
+        locIdName = langData.get("Languages", localeId);
+        if ((locIdName == null || locIdName.equals(localeId)) && localeId.indexOf('_') < 0) {
+            // Canonicalize lang and try again, ICU-20870
+            // (only for language codes without script or region)
+            ULocale canonLocale = ULocale.createCanonical(localeId);
+            String canonLocId = canonLocale.getName();
+            if (nameLength == DisplayContext.LENGTH_SHORT) {
+                locIdName = langData.get("Languages%short", canonLocId);
+                if (locIdName != null && !locIdName.equals(canonLocId)) {
+                    return locIdName;
+                }
+            }
+            locIdName = langData.get("Languages", canonLocId);
+        }
+        return locIdName;
     }
 
     @Override
@@ -445,13 +460,27 @@
         if (lang.equals("root") || lang.indexOf('_') != -1) {
             return substituteHandling == DisplayContext.SUBSTITUTE ? lang : null;
         }
+        String langName;
         if (nameLength == DisplayContext.LENGTH_SHORT) {
-            String langName = langData.get("Languages%short", lang);
+            langName = langData.get("Languages%short", lang);
             if (langName != null && !langName.equals(lang)) {
                 return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
             }
         }
-        return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langData.get("Languages", lang));
+        langName = langData.get("Languages", lang);
+        if (langName == null || langName.equals(lang)) {
+            // Canonicalize lang and try again, ICU-20870
+            ULocale canonLocale = ULocale.createCanonical(lang);
+            String canonLocId = canonLocale.getName();
+            if (nameLength == DisplayContext.LENGTH_SHORT) {
+                langName = langData.get("Languages%short", canonLocId);
+                if (langName != null && !langName.equals(canonLocId)) {
+                    return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
+                }
+            }
+            langName = langData.get("Languages", canonLocId);
+        }
+        return adjustForUsageAndContext(CapitalizationContextUsage.LANGUAGE, langName);
     }
 
     @Override
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 15cfe36..f1c0ac5 100644
--- a/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleIDs.java
@@ -227,11 +227,11 @@
         "zxx", "zza" };
         
     private static final String[] _replacementLanguages = {
-        "id", "he", "yi", "jv", "sr", "nb",/* replacement language codes */
+        "id", "he", "yi", "jv", "sr", /* replacement language codes */
     };
     
     private static final String[] _obsoleteLanguages = {
-        "in", "iw", "ji", "jw", "sh", "no",    /* obsolete language codes */
+        "in", "iw", "ji", "jw", "sh", /* obsolete language codes */
     };
     
     /* This list MUST contain a three-letter code for every two-letter code in the
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 b2b4085..8cf25cc 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Normalizer2Impl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Normalizer2Impl.java
@@ -26,7 +26,7 @@
  * Low-level implementation of the Unicode Normalization Algorithm.
  * For the data structure and details see the documentation at the end of
  * C++ normalizer2impl.h and in the design doc at
- * http://site.icu-project.org/design/normalization/custom
+ * https://icu.unicode.org/design/normalization/custom
  * @hide Only a subset of ICU is exposed in Android
  */
 public final class Normalizer2Impl {
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 0e8a20b..1d200a9 100644
--- a/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
+++ b/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
@@ -282,11 +282,11 @@
      */
     @Override
     public void getOffsetFromLocal(long date,
-            int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
+            LocalOption nonExistingTimeOpt, LocalOption duplicatedTimeOpt, int[] offsets) {
         if (finalZone != null && date >= finalStartMillis) {
             finalZone.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
         } else {
-            getHistoricalOffset(date, true, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
+            getHistoricalOffset(date, true, getLocalOptionValue(nonExistingTimeOpt), getLocalOptionValue(duplicatedTimeOpt), offsets);
         }
     }
 
@@ -1298,4 +1298,4 @@
         tz.isFrozen = false;
         return tz;
     }
-}
+}
\ No newline at end of file
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 bf3448c..5bdb37c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/PatternProps.java
+++ b/android_icu4j/src/main/java/android/icu/impl/PatternProps.java
@@ -122,6 +122,29 @@
     }
 
     /**
+     * @return s except with leading and trailing SpaceChar characters removed.
+     */
+    public static String trimSpaceChar(String s) {
+        if (s.length() == 0 ||
+            (!Character.isSpaceChar(s.charAt(0)) && !Character.isSpaceChar(s.charAt(s.length() - 1)))) {
+            return s;
+        }
+        int start = 0;
+        int limit = s.length();
+        while (start < limit && Character.isSpaceChar(s.charAt(start))) {
+            ++start;
+        }
+        if (start < limit) {
+            // There is non-SpaceChar at start; we will not move limit below that,
+            // so we need not test start<limit in the loop.
+            while (isWhiteSpace(s.charAt(limit - 1))) {
+                --limit;
+            }
+        }
+        return s.substring(start, limit);
+    }
+
+    /**
      * Tests whether the CharSequence contains a "pattern identifier", that is,
      * whether it contains only non-Pattern_White_Space, non-Pattern_Syntax characters.
      * @return true if there are no Pattern_White_Space or Pattern_Syntax characters in s.
diff --git a/android_icu4j/src/main/java/android/icu/impl/ReplaceableUCharacterIterator.java b/android_icu4j/src/main/java/android/icu/impl/ReplaceableUCharacterIterator.java
index 2c820a9..69b6858 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ReplaceableUCharacterIterator.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ReplaceableUCharacterIterator.java
@@ -104,7 +104,7 @@
         // trail surrogate, check for surrogates
 
         int ch = current();
-        if(UTF16.isLeadSurrogate((char)ch)){
+        if(UTF16.isLeadSurrogate(ch)){
             // advance the index to get the next code point
             next();
             // due to post increment semantics current() after next()
@@ -113,7 +113,7 @@
             // current should never change the current index so back off
             previous();
 
-            if(UTF16.isTrailSurrogate((char)ch2)){
+            if(UTF16.isTrailSurrogate(ch2)){
                 // we found a surrogate pair
                 return Character.toCodePoint((char)ch, (char)ch2);
             }
diff --git a/android_icu4j/src/main/java/android/icu/impl/RuleCharacterIterator.java b/android_icu4j/src/main/java/android/icu/impl/RuleCharacterIterator.java
index 7a824a3..5b7c16a 100644
--- a/android_icu4j/src/main/java/android/icu/impl/RuleCharacterIterator.java
+++ b/android_icu4j/src/main/java/android/icu/impl/RuleCharacterIterator.java
@@ -53,7 +53,7 @@
     /**
      * Current variable expansion, or null if none.
      */
-    private char[] buf;
+    private String buf;
 
     /**
      * Position within buf[].  Meaningless if buf == null.
@@ -80,7 +80,7 @@
     /**
      * Bitmask option to enable parsing of escape sequences.  If (options &
      * PARSE_ESCAPES) != 0, then an embedded escape sequence will be expanded
-     * to its value.  Escapes are parsed using Utility.unescapeAt().
+     * to its value.  Escapes are parsed using Utility.unescapeAndLengthAt().
      */
     public static final int PARSE_ESCAPES   = 2;
 
@@ -91,12 +91,20 @@
      */
     public static final int SKIP_WHITESPACE = 4;
 
+    /** For use with {@link #getPos(Position)} & {@link #setPos(Position)}. 
+     * @hide Only a subset of ICU is exposed in Android*/
+    public static final class Position {
+        private String buf;
+        private int bufPos;
+        private int posIndex;
+    };
+
     /**
      * Constructs an iterator over the given text, starting at the given
      * position.
      * @param text the text to be iterated
      * @param sym the symbol table, or null if there is none.  If sym is null,
-     * then variables will not be deferenced, even if the PARSE_VARIABLES
+     * then variables will not be dereferenced, even if the PARSE_VARIABLES
      * option is set.
      * @param pos upon input, the index of the next character to return.  If a
      * variable has been dereferenced, then pos will <em>not</em> increment as
@@ -145,15 +153,17 @@
                     break;
                 }
                 bufPos = 0;
-                buf = sym.lookup(name);
-                if (buf == null) {
+                char[] chars = sym.lookup(name);
+                if (chars == null) {
+                    buf = null;
                     throw new IllegalArgumentException(
                                 "Undefined variable: " + name);
                 }
                 // Handle empty variable value
-                if (buf.length == 0) {
+                if (chars.length == 0) {
                     buf = null;
                 }
+                buf = new String(chars);
                 continue;
             }
 
@@ -163,13 +173,14 @@
             }
 
             if (c == '\\' && (options & PARSE_ESCAPES) != 0) {
-                int offset[] = new int[] { 0 };
-                c = Utility.unescapeAt(lookahead(), offset);
-                jumpahead(offset[0]);
-                isEscaped = true;
-                if (c < 0) {
+                int cpAndLength = Utility.unescapeAndLengthAt(
+                        getCurrentBuffer(), getCurrentBufferPos());
+                if (cpAndLength < 0) {
                     throw new IllegalArgumentException("Invalid escape");
                 }
+                c = Utility.cpFromCodePointAndLength(cpAndLength);
+                jumpahead(Utility.lengthFromCodePointAndLength(cpAndLength));
+                isEscaped = true;
             }
 
             break;
@@ -200,7 +211,7 @@
      * restore this iterator's position.  Usage idiom:
      *
      * RuleCharacterIterator iterator = ...;
-     * Object pos = iterator.getPos(null); // allocate position object
+     * Position pos = iterator.getPos(null); // allocate position object
      * for (;;) {
      *   pos = iterator.getPos(pos); // reuse position object
      *   int c = iterator.next(...);
@@ -214,15 +225,13 @@
      * @return a position object which may be passed to setPos(),
      * either `p,' or if `p' == null, a newly-allocated object
      */
-    public Object getPos(Object p) {
+    public Position getPos(Position p) {
         if (p == null) {
-            return new Object[] {buf, new int[] {pos.getIndex(), bufPos}};
+            p = new Position();
         }
-        Object[] a = (Object[]) p;
-        a[0] = buf;
-        int[] v = (int[]) a[1];
-        v[0] = pos.getIndex();
-        v[1] = bufPos;
+        p.buf = buf;
+        p.bufPos = bufPos;
+        p.posIndex = pos.getIndex();
         return p;
     }
 
@@ -231,12 +240,10 @@
      * returned the given object.
      * @param p a position object previously returned by getPos()
      */
-    public void setPos(Object p) {
-        Object[] a = (Object[]) p;
-        buf = (char[]) a[0];
-        int[] v = (int[]) a[1];
-        pos.setIndex(v[0]);
-        bufPos = v[1];
+    public void setPos(Position p) {
+        buf = p.buf;
+        pos.setIndex(p.posIndex);
+        bufPos = p.bufPos;
     }
 
     /**
@@ -261,25 +268,35 @@
      * Returns a string containing the remainder of the characters to be
      * returned by this iterator, without any option processing.  If the
      * iterator is currently within a variable expansion, this will only
-     * extend to the end of the variable expansion.  This method is provided
-     * so that iterators may interoperate with string-based APIs.  The typical
-     * sequence of calls is to call skipIgnored(), then call lookahead(), then
-     * parse the string returned by lookahead(), then call jumpahead() to
+     * extend to the end of the variable expansion.
+     * This method, together with getCurrentBufferPos() (which replace the former lookahead()),
+     * is provided so that iterators may interoperate with string-based APIs. The typical
+     * sequence of calls is to call skipIgnored(), then call these methods, then
+     * parse that substring, then call jumpahead() to
      * resynchronize the iterator.
      * @return a string containing the characters to be returned by future
      * calls to next()
      */
-    public String lookahead() {
+    public String getCurrentBuffer() {
         if (buf != null) {
-            return new String(buf, bufPos, buf.length - bufPos);
+            return buf;
         } else {
-            return text.substring(pos.getIndex());
+            return text;
+        }
+    }
+
+    public int getCurrentBufferPos() {
+        if (buf != null) {
+            return bufPos;
+        } else {
+            return pos.getIndex();
         }
     }
 
     /**
      * Advances the position by the given number of 16-bit code units.
-     * This is useful in conjunction with the lookahead() method.
+     * This is useful in conjunction with getCurrentBuffer()+getCurrentBufferPos()
+     * (formerly lookahead()).
      * @param count the number of 16-bit code units to jump over
      */
     public void jumpahead(int count) {
@@ -288,10 +305,10 @@
         }
         if (buf != null) {
             bufPos += count;
-            if (bufPos > buf.length) {
+            if (bufPos > buf.length()) {
                 throw new IllegalArgumentException();
             }
-            if (bufPos == buf.length) {
+            if (bufPos == buf.length()) {
                 buf = null;
             }
         } else {
@@ -322,7 +339,7 @@
      */
     private int _current() {
         if (buf != null) {
-            return UTF16.charAt(buf, 0, buf.length, bufPos);
+            return UTF16.charAt(buf, bufPos);
         } else {
             int i = pos.getIndex();
             return (i < text.length()) ? UTF16.charAt(text, i) : DONE;
@@ -336,7 +353,7 @@
     private void _advance(int count) {
         if (buf != null) {
             bufPos += count;
-            if (bufPos == buf.length) {
+            if (bufPos == buf.length()) {
                 buf = null;
             }
         } else {
diff --git a/android_icu4j/src/main/java/android/icu/impl/SimpleFilteredSentenceBreakIterator.java b/android_icu4j/src/main/java/android/icu/impl/SimpleFilteredSentenceBreakIterator.java
index c8bfae7..382003c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/SimpleFilteredSentenceBreakIterator.java
+++ b/android_icu4j/src/main/java/android/icu/impl/SimpleFilteredSentenceBreakIterator.java
@@ -20,6 +20,7 @@
 import android.icu.util.BytesTrie;
 import android.icu.util.CharsTrie;
 import android.icu.util.CharsTrieBuilder;
+import android.icu.util.ICUCloneNotSupportedException;
 import android.icu.util.StringTrieBuilder;
 import android.icu.util.ULocale;
 
@@ -74,8 +75,6 @@
         backwardsTrie.reset();
         int uch;
 
-
-
         // Assume a space is following the '.' (so we handle the case: "Mr. /Brown")
         if ((uch = text.previousCodePoint()) == ' ') { // TODO: skip a class of chars here??
             // TODO only do this the 1st time?
@@ -83,20 +82,17 @@
             uch = text.nextCodePoint();
         }
 
-        BytesTrie.Result r = BytesTrie.Result.INTERMEDIATE_VALUE;
-
-        while ((uch = text.previousCodePoint()) != UCharacterIterator.DONE && // more to consume backwards and..
-                ((r = backwardsTrie.nextForCodePoint(uch)).hasNext())) {// more in the trie
+        while ((uch = text.previousCodePoint()) >= 0) { // more to consume backwards
+            BytesTrie.Result r = backwardsTrie.nextForCodePoint(uch);
             if (r.hasValue()) { // remember the best match so far
                 bestPosn = text.getIndex();
                 bestValue = backwardsTrie.getValue();
             }
+            if (!r.hasNext()) {
+                break;
+            }
         }
-
-        if (r.matches()) { // exact match?
-            bestValue = backwardsTrie.getValue();
-            bestPosn = text.getIndex();
-        }
+        backwardsTrie.reset(); // for equals() & hashCode()
 
         if (bestPosn >= 0) {
             if (bestValue == Builder.MATCH) { // exact match!
@@ -112,6 +108,7 @@
                 while ((uch = text.nextCodePoint()) != BreakIterator.DONE
                         && ((rfwd = forwardsPartialTrie.nextForCodePoint(uch)).hasNext())) {
                 }
+                forwardsPartialTrie.reset(); // for equals() & hashCode()
                 if (rfwd.matches()) {
                     // Exception here
                     return true;
@@ -188,18 +185,39 @@
         if (getClass() != obj.getClass())
             return false;
         SimpleFilteredSentenceBreakIterator other = (SimpleFilteredSentenceBreakIterator) obj;
-        return delegate.equals(other.delegate) && text.equals(other.text) && backwardsTrie.equals(other.backwardsTrie)
+        // TODO(ICU-21575): CharsTrie.equals() is not defined.
+        // Should compare the underlying data, and can then stop resetting after iteration.
+        return delegate.equals(other.delegate) && text.equals(other.text)
+                && backwardsTrie.equals(other.backwardsTrie)
                 && forwardsPartialTrie.equals(other.forwardsPartialTrie);
     }
 
     @Override
     public int hashCode() {
-        return (forwardsPartialTrie.hashCode() * 39) + (backwardsTrie.hashCode() * 11) + delegate.hashCode();
+        // TODO(ICU-21575): CharsTrie.hashCode() is not defined.
+        return (forwardsPartialTrie.hashCode() * 39) + (backwardsTrie.hashCode() * 11)
+                + delegate.hashCode();
     }
 
     @Override
     public Object clone() {
         SimpleFilteredSentenceBreakIterator other = (SimpleFilteredSentenceBreakIterator) super.clone();
+        try {
+            if (delegate != null) {
+                other.delegate = (BreakIterator) delegate.clone();
+            }
+            if (text != null) {
+                other.text = (UCharacterIterator) text.clone();
+            }
+            if (backwardsTrie != null) {
+                other.backwardsTrie = backwardsTrie.clone();
+            }
+            if (forwardsPartialTrie != null) {
+                other.forwardsPartialTrie = forwardsPartialTrie.clone();
+            }
+        } catch (CloneNotSupportedException e) {
+            throw new ICUCloneNotSupportedException(e);
+        }
         return other;
     }
 
@@ -278,7 +296,7 @@
         /**
          * filter set to store all exceptions
          */
-        private HashSet<CharSequence> filterSet = new HashSet<CharSequence>();
+        private HashSet<CharSequence> filterSet = new HashSet<>();
 
         static final int PARTIAL = (1 << 0); // < partial - need to run through forward trie
         static final int MATCH = (1 << 1); // < exact match - skip this one.
diff --git a/android_icu4j/src/main/java/android/icu/impl/StandardPlural.java b/android_icu4j/src/main/java/android/icu/impl/StandardPlural.java
index f84c24b..04f40d2 100644
--- a/android_icu4j/src/main/java/android/icu/impl/StandardPlural.java
+++ b/android_icu4j/src/main/java/android/icu/impl/StandardPlural.java
@@ -24,7 +24,9 @@
     TWO("two"),
     FEW("few"),
     MANY("many"),
-    OTHER("other");
+    OTHER("other"),
+    EQ_0("=0"),
+    EQ_1("=1");
 
     /**
      * Numeric index of OTHER, same as OTHER.ordinal().
@@ -62,6 +64,20 @@
      */
     public static final StandardPlural orNullFromString(CharSequence keyword) {
         switch (keyword.length()) {
+        case 1:
+            if (keyword.charAt(0) == '0') {
+                return EQ_0;
+            } else if (keyword.charAt(0) == '1') {
+                return EQ_1;
+            }
+            break;
+        case 2:
+            if ("=0".contentEquals(keyword)) {
+                return EQ_0;
+            } else if ("=1".contentEquals(keyword)) {
+                return EQ_1;
+            }
+            break;    
         case 3:
             if ("one".contentEquals(keyword)) {
                 return ONE;
diff --git a/android_icu4j/src/main/java/android/icu/impl/StaticUnicodeSets.java b/android_icu4j/src/main/java/android/icu/impl/StaticUnicodeSets.java
index 028339c..56e04f3 100644
--- a/android_icu4j/src/main/java/android/icu/impl/StaticUnicodeSets.java
+++ b/android_icu4j/src/main/java/android/icu/impl/StaticUnicodeSets.java
@@ -3,8 +3,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl;
 
-import static android.icu.impl.number.parse.ParsingUtils.safeContains;
-
 import java.util.EnumMap;
 import java.util.Map;
 
@@ -100,7 +98,7 @@
      * @return key1 if the set contains str, or COUNT if not.
      */
     public static Key chooseFrom(String str, Key key1) {
-        return safeContains(get(key1), str) ? key1 : null;
+        return get(key1).contains(str) ? key1 : null;
     }
 
     /**
@@ -118,7 +116,7 @@
      *         contains str.
      */
     public static Key chooseFrom(String str, Key key1, Key key2) {
-        return safeContains(get(key1), str) ? key1 : chooseFrom(str, key2);
+        return get(key1).contains(str) ? key1 : chooseFrom(str, key2);
     }
 
     /**
diff --git a/android_icu4j/src/main/java/android/icu/impl/StringSegment.java b/android_icu4j/src/main/java/android/icu/impl/StringSegment.java
index 5704c98..b27cbb1 100644
--- a/android_icu4j/src/main/java/android/icu/impl/StringSegment.java
+++ b/android_icu4j/src/main/java/android/icu/impl/StringSegment.java
@@ -204,23 +204,13 @@
     }
 
     /**
-     * Equals any CharSequence with the same chars as this segment.
+     * Returns true if this segment contains the same characters as the other CharSequence.
      *
-     * <p>
-     * This method does not perform case folding; if you want case-insensitive equality, use
+     * <p>This method does not perform case folding; if you want case-insensitive equality, use
      * {@link #getCommonPrefixLength}.
      */
-    @Override
-    public boolean equals(Object other) {
-        if (!(other instanceof CharSequence))
-            return false;
-        return Utility.charSequenceEquals(this, (CharSequence) other);
-    }
-
-    /** Returns a hash code equivalent to calling .toString().hashCode() */
-    @Override
-    public int hashCode() {
-        return Utility.charSequenceHashCode(this);
+    public boolean contentEquals(CharSequence other) {
+        return Utility.charSequenceEquals(this, other);
     }
 
     /** Returns a string representation useful for debugging. */
diff --git a/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java b/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java
index 0c7df93..3149681 100644
--- a/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java
+++ b/android_icu4j/src/main/java/android/icu/impl/TextTrieMap.java
@@ -187,7 +187,7 @@
          */
         @Override
         public void remove() {
-            throw new UnsupportedOperationException("remove() not supproted");
+            throw new UnsupportedOperationException("remove() not supported");
         }
 
         public int nextIndex() {
diff --git a/android_icu4j/src/main/java/android/icu/impl/TimeZoneAdapter.java b/android_icu4j/src/main/java/android/icu/impl/TimeZoneAdapter.java
index 37ae2b5..e265ae6 100644
--- a/android_icu4j/src/main/java/android/icu/impl/TimeZoneAdapter.java
+++ b/android_icu4j/src/main/java/android/icu/impl/TimeZoneAdapter.java
@@ -30,7 +30,6 @@
  * @author Alan Liu
  * @hide Only a subset of ICU is exposed in Android
  */
-@libcore.api.CorePlatformApi
 public class TimeZoneAdapter extends java.util.TimeZone {
 
     // Generated by serialver from JDK 1.4.1_01
@@ -46,7 +45,6 @@
      * Given a java.util.TimeZone, wrap it in the appropriate adapter
      * subclass of android.icu.util.TimeZone and return the adapter.
      */
-    @libcore.api.CorePlatformApi
     public static java.util.TimeZone wrap(android.icu.util.TimeZone tz) {
         return new TimeZoneAdapter(tz);
     }
diff --git a/android_icu4j/src/main/java/android/icu/impl/TimeZoneGenericNames.java b/android_icu4j/src/main/java/android/icu/impl/TimeZoneGenericNames.java
index 17db218..39d7af8 100644
--- a/android_icu4j/src/main/java/android/icu/impl/TimeZoneGenericNames.java
+++ b/android_icu4j/src/main/java/android/icu/impl/TimeZoneGenericNames.java
@@ -466,7 +466,7 @@
     /**
      * Private method returning LocaleDisplayNames instance for the locale of this
      * instance. Because LocaleDisplayNames is only used for generic
-     * location formant and partial location format, the LocaleDisplayNames
+     * location format and partial location format, the LocaleDisplayNames
      * is instantiated lazily.
      *
      * @return the instance of LocaleDisplayNames for the locale of this object.
diff --git a/android_icu4j/src/main/java/android/icu/impl/TrieIterator.java b/android_icu4j/src/main/java/android/icu/impl/TrieIterator.java
index 1bf2090..86736f4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/TrieIterator.java
+++ b/android_icu4j/src/main/java/android/icu/impl/TrieIterator.java
@@ -50,7 +50,7 @@
  * <p>There are basically 3 usage scenarios for porting:</p>
  * <p>1) UTrieEnumValue is the only implemented callback then just implement a
  * subclass of TrieIterator and override the extract(int) method. The
- * extract(int) method is analogus to UTrieEnumValue callback.
+ * extract(int) method is analogous to UTrieEnumValue callback.
  * </p>
  * <p>2) UTrieEnumValue and UTrieEnumRange both are implemented then implement
  * a subclass of TrieIterator, override the extract method and iterate, e.g
@@ -115,7 +115,7 @@
     * <p>Returns true if we are not at the end of the iteration, false
     * otherwise.</p>
     * <p>The next set of codepoints with the same value type will be
-    * calculated during this call and returned in the arguement element.</p>
+    * calculated during this call and returned in the argument element.</p>
     * @param element return result
     * @return true if we are not at the end of the iteration, false otherwise.
     * @exception NoSuchElementException - if no more elements exist.
diff --git a/android_icu4j/src/main/java/android/icu/impl/UCaseProps.java b/android_icu4j/src/main/java/android/icu/impl/UCaseProps.java
index 8301e1c..1c6875b 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCaseProps.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCaseProps.java
@@ -404,7 +404,7 @@
         if(max==0 || unfold[unfoldOffset]==0) {
             return 0; /* equal to length of both strings */
         } else {
-            return -max; /* return lengh difference */
+            return -max; /* return length difference */
         }
     }
 
@@ -569,7 +569,7 @@
      *   - In [CoreProps], C has one of the properties Uppercase, or Lowercase
      *   - Given D = NFD(C), then it is not the case that:
      *     D = UCD_lower(D) = UCD_upper(D) = UCD_title(D)
-     *     (This third criterium does not add any characters to the list
+     *     (This third criterion does not add any characters to the list
      *      for Unicode 3.2. Ignored.)
      *
      * D2. A character C is defined to be case-ignorable
diff --git a/android_icu4j/src/main/java/android/icu/impl/UCharacterName.java b/android_icu4j/src/main/java/android/icu/impl/UCharacterName.java
index 60aa8d1..a02daa1 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCharacterName.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCharacterName.java
@@ -23,8 +23,8 @@
 /**
 * Internal class to manage character names.
 * Since data for names are stored
-* in an array of char, by default indexes used in this class is refering to
-* a 2 byte count, unless otherwise stated. Cases where the index is refering
+* in an array of char, by default indexes used in this class is referring to
+* a 2 byte count, unless otherwise stated. Cases where the index is referring
 * to a byte count, the index is halved and depending on whether the index is
 * even or odd, the MSB or LSB of the result char at the halved index is
 * returned. For indexes to an array of int, the index is multiplied by 2,
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 dafeb79..7acb688 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
@@ -109,8 +109,9 @@
     public static final int SRC_INPC=12;
     public static final int SRC_INSC=13;
     public static final int SRC_VO=14;
+    public static final int SRC_EMOJI=15;
     /** One more than the highest UPropertySource (SRC_) constant. */
-    public static final int SRC_COUNT=15;
+    public static final int SRC_COUNT=16;
 
     private static final class LayoutProps {
         private static final class IsAcceptable implements ICUBinary.Authenticate {
@@ -353,6 +354,18 @@
         }
     }
 
+    private class EmojiBinaryProperty extends BinaryProperty {
+        int which;
+        EmojiBinaryProperty(int which) {
+            super(SRC_EMOJI);
+            this.which=which;
+        }
+        @Override
+        boolean contains(int c) {
+            return EmojiProps.INSTANCE.hasBinaryProperty(c, which);
+        }
+    }
+
     private class NormInertBinaryProperty extends BinaryProperty {  // UCHAR_NF*_INERT properties
         int which;
         NormInertBinaryProperty(int source, int which) {
@@ -535,11 +548,11 @@
                 return !Normalizer2Impl.UTF16Plus.equal(dest, src);
             }
         },
-        new BinaryProperty(2, 1<<PROPS_2_EMOJI),
-        new BinaryProperty(2, 1<<PROPS_2_EMOJI_PRESENTATION),
-        new BinaryProperty(2, 1<<PROPS_2_EMOJI_MODIFIER),
-        new BinaryProperty(2, 1<<PROPS_2_EMOJI_MODIFIER_BASE),
-        new BinaryProperty(2, 1<<PROPS_2_EMOJI_COMPONENT),
+        new EmojiBinaryProperty(UProperty.EMOJI),
+        new EmojiBinaryProperty(UProperty.EMOJI_PRESENTATION),
+        new EmojiBinaryProperty(UProperty.EMOJI_MODIFIER),
+        new EmojiBinaryProperty(UProperty.EMOJI_MODIFIER_BASE),
+        new EmojiBinaryProperty(UProperty.EMOJI_COMPONENT),
         new BinaryProperty(SRC_PROPSVEC) {  // REGIONAL_INDICATOR
             // Property starts are a subset of lb=RI etc.
             @Override
@@ -548,7 +561,14 @@
             }
         },
         new BinaryProperty(1, 1<<PREPENDED_CONCATENATION_MARK),
-        new BinaryProperty(2, 1<<PROPS_2_EXTENDED_PICTOGRAPHIC),
+        new EmojiBinaryProperty(UProperty.EXTENDED_PICTOGRAPHIC),
+        new EmojiBinaryProperty(UProperty.BASIC_EMOJI),
+        new EmojiBinaryProperty(UProperty.EMOJI_KEYCAP_SEQUENCE),
+        new EmojiBinaryProperty(UProperty.RGI_EMOJI_MODIFIER_SEQUENCE),
+        new EmojiBinaryProperty(UProperty.RGI_EMOJI_FLAG_SEQUENCE),
+        new EmojiBinaryProperty(UProperty.RGI_EMOJI_TAG_SEQUENCE),
+        new EmojiBinaryProperty(UProperty.RGI_EMOJI_ZWJ_SEQUENCE),
+        new EmojiBinaryProperty(UProperty.RGI_EMOJI),
     };
 
     public boolean hasBinaryProperty(int c, int which) {
@@ -1366,19 +1386,20 @@
     /*
      * Properties in vector word 2
      * Bits
-     * 31..26   http://www.unicode.org/reports/tr51/#Emoji_Properties
+     * 31..26   unused since ICU 70 added uemoji.icu;
+     *          in ICU 57..69 stored emoji properties
      * 25..20   Line Break
      * 19..15   Sentence Break
      * 14..10   Word Break
      *  9.. 5   Grapheme Cluster Break
      *  4.. 0   Decomposition Type
      */
-    private static final int PROPS_2_EXTENDED_PICTOGRAPHIC=26;
-    private static final int PROPS_2_EMOJI_COMPONENT = 27;
-    private static final int PROPS_2_EMOJI = 28;
-    private static final int PROPS_2_EMOJI_PRESENTATION = 29;
-    private static final int PROPS_2_EMOJI_MODIFIER = 30;
-    private static final int PROPS_2_EMOJI_MODIFIER_BASE = 31;
+    //ivate static final int PROPS_2_EXTENDED_PICTOGRAPHIC=26;  // ICU 62..69
+    //ivate static final int PROPS_2_EMOJI_COMPONENT = 27;  // ICU 60..69
+    //ivate static final int PROPS_2_EMOJI = 28;  // ICU 57..69
+    //ivate static final int PROPS_2_EMOJI_PRESENTATION = 29;  // ICU 57..69
+    //ivate static final int PROPS_2_EMOJI_MODIFIER = 30;  // ICU 57..69
+    //ivate static final int PROPS_2_EMOJI_MODIFIER_BASE = 31;  // ICU 57..69
 
     private static final int LB_MASK          = 0x03f00000;
     private static final int LB_SHIFT         = 20;
diff --git a/android_icu4j/src/main/java/android/icu/impl/UCharacterUtility.java b/android_icu4j/src/main/java/android/icu/impl/UCharacterUtility.java
index abf8b27..63a8c68 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCharacterUtility.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCharacterUtility.java
@@ -187,7 +187,7 @@
       
     ///CLOVER:OFF
     /**
-    * private constructor to avoid initialisation
+    * private constructor to avoid initialization
     */
     private UCharacterUtility()
     {
diff --git a/android_icu4j/src/main/java/android/icu/impl/UnicodeRegex.java b/android_icu4j/src/main/java/android/icu/impl/UnicodeRegex.java
index e0ecad7..e1ec959 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UnicodeRegex.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UnicodeRegex.java
@@ -42,6 +42,8 @@
  * @hide Only a subset of ICU is exposed in Android
  */
 public class UnicodeRegex implements Cloneable, Freezable<UnicodeRegex>, StringTransform {
+    private static final Pattern SUPP_ESCAPE = Pattern.compile("\\\\U00([0-9a-fA-F]{6})");
+
     // Note: we don't currently have any state, but intend to in the future,
     // particularly for the regex style supported.
 
@@ -77,7 +79,7 @@
      * <p>Not thread-safe; create a separate copy for different threads.
      * <p>In the future, we may extend this to support other regex packages.
      *
-     * @regex A modified Java regex pattern, as in the input to
+     * @param regex A modified Java regex pattern, as in the input to
      *        Pattern.compile(), except that all "character classes" are
      *        processed as if they were UnicodeSet patterns. Example:
      *        "abc[:bc=N:]. See UnicodeSet for the differences in syntax.
@@ -210,7 +212,7 @@
      */
     public String compileBnf(List<String> lines) {
         Map<String, String> variables = getVariables(lines);
-        Set<String> unused = new LinkedHashSet<String>(variables.keySet());
+        Set<String> unused = new LinkedHashSet<>(variables.keySet());
         // brute force replacement; do twice to allow for different order
         // later on can optimize
         for (int i = 0; i < 2; ++i) {
@@ -345,7 +347,12 @@
             pos.setIndex(i);
             UnicodeSet x = temp.clear().applyPattern(regex, pos, symbolTable, 0);
             x.complement().complement(); // hack to fix toPattern
-            result.append(x.toPattern(false));
+            String pattern = x.toPattern(false);
+            // Escaping of supplementary code points differs between ICU UnicodeSet and Java regex.
+            if (pattern.contains("\\U")) {
+                pattern = SUPP_ESCAPE.matcher(pattern).replaceAll("\\\\x{$1}");
+            }
+            result.append(pattern);
             i = pos.getIndex() - 1; // allow for the loop increment
             return i;
         } catch (Exception e) {
@@ -372,7 +379,7 @@
     };
 
     private Map<String, String> getVariables(List<String> lines) {
-        Map<String, String> variables = new TreeMap<String, String>(LongestFirst);
+        Map<String, String> variables = new TreeMap<>(LongestFirst);
         String variable = null;
         StringBuffer definition = new StringBuffer();
         int count = 0;
diff --git a/android_icu4j/src/main/java/android/icu/impl/UnicodeSetStringSpan.java b/android_icu4j/src/main/java/android/icu/impl/UnicodeSetStringSpan.java
index c3ed68f..ee71171 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UnicodeSetStringSpan.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UnicodeSetStringSpan.java
@@ -114,9 +114,15 @@
         int i, spanLength;
         int maxLength16 = 0;
         someRelevant = false;
-        for (i = 0; i < stringsLength; ++i) {
+        for (i = 0; i < stringsLength;) {
             String string = strings.get(i);
             int length16 = string.length();
+            if (length16 == 0) {
+                // Remove the empty string.
+                strings.remove(i);
+                --stringsLength;
+                continue;
+            }
             spanLength = spanSet.span(string, SpanCondition.CONTAINED);
             if (spanLength < length16) { // Relevant string.
                 someRelevant = true;
@@ -124,6 +130,7 @@
             if (/* (0 != (which & UTF16)) && */ length16 > maxLength16) {
                 maxLength16 = length16;
             }
+            ++i;
         }
         this.maxLength16 = maxLength16;
         if (!someRelevant && (which & WITH_COUNT) == 0) {
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 b10e705..110663d 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Utility.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Utility.java
@@ -781,35 +781,59 @@
         /*v*/ 0x76, 0x0b
     };
 
+    /* Convert one octal digit to a numeric value 0..7, or -1 on failure */
+    private static final int _digit8(int c) {
+        if (c >= '0' && c <= '7') {
+            return c - '0';
+        }
+        return -1;
+    }
+
+    /* Convert one hex digit to a numeric value 0..F, or -1 on failure */
+    private static final int _digit16(int c) {
+        if (c >= '0' && c <= '9') {
+            return c - '0';
+        }
+        if (c >= 'A' && c <= 'F') {
+            return c - ('A' - 10);
+        }
+        if (c >= 'a' && c <= 'f') {
+            return c - ('a' - 10);
+        }
+        return -1;
+    }
+
     /**
-     * Convert an escape to a 32-bit code point value.  We attempt
+     * Converts an escape to a code point value. We attempt
      * to parallel the icu4c unescapeAt() function.
-     * @param offset16 an array containing offset to the character
-     * <em>after</em> the backslash.  Upon return offset16[0] will
-     * be updated to point after the escape sequence.
-     * @return character value from 0 to 10FFFF, or -1 on error.
+     * This function returns an integer with
+     * both the code point (bits 28..8) and the length of the escape sequence (bits 7..0).
+     * offset+length is the index after the escape sequence.
+     *
+     * @param offset the offset to the character <em>after</em> the backslash.
+     * @return the code point and length, or -1 on error.
      */
-    public static int unescapeAt(String s, int[] offset16) {
-        int c;
+    public static int unescapeAndLengthAt(CharSequence s, int offset) {
+        return unescapeAndLengthAt(s, offset, s.length());
+    }
+
+    private static int unescapeAndLengthAt(CharSequence s, int offset, int length) {
         int result = 0;
         int n = 0;
         int minDig = 0;
         int maxDig = 0;
         int bitsPerDigit = 4;
         int dig;
-        int i;
         boolean braces = false;
 
         /* Check that offset is in range */
-        int offset = offset16[0];
-        int length = s.length();
         if (offset < 0 || offset >= length) {
             return -1;
         }
+        int start = offset;
 
         /* Fetch first UChar after '\\' */
-        c = Character.codePointAt(s, offset);
-        offset += UTF16.getCharCount(c);
+        int c = s.charAt(offset++);
 
         /* Convert hexadecimal and octal escapes */
         switch (c) {
@@ -821,7 +845,7 @@
             break;
         case 'x':
             minDig = 1;
-            if (offset < length && UTF16.charAt(s, offset) == 0x7B /*{*/) {
+            if (offset < length && s.charAt(offset) == '{') {
                 ++offset;
                 braces = true;
                 maxDig = 8;
@@ -830,7 +854,7 @@
             }
             break;
         default:
-            dig = UCharacter.digit(c, 8);
+            dig = _digit8(c);
             if (dig >= 0) {
                 minDig = 1;
                 maxDig = 3;
@@ -842,20 +866,20 @@
         }
         if (minDig != 0) {
             while (offset < length && n < maxDig) {
-                c = UTF16.charAt(s, offset);
-                dig = UCharacter.digit(c, (bitsPerDigit == 3) ? 8 : 16);
+                c = s.charAt(offset);
+                dig = (bitsPerDigit == 3) ? _digit8(c) : _digit16(c);
                 if (dig < 0) {
                     break;
                 }
                 result = (result << bitsPerDigit) | dig;
-                offset += UTF16.getCharCount(c);
+                ++offset;
                 ++n;
             }
             if (n < minDig) {
                 return -1;
             }
             if (braces) {
-                if (c != 0x7D /*}*/) {
+                if (c != '}') {
                     return -1;
                 }
                 ++offset;
@@ -867,29 +891,35 @@
             // if there is a trail surrogate after it, either as an
             // escape or as a literal.  If so, join them up into a
             // supplementary.
-            if (offset < length &&
-                    UTF16.isLeadSurrogate((char) result)) {
+            if (offset < length && UTF16.isLeadSurrogate(result)) {
                 int ahead = offset+1;
-                c = s.charAt(offset); // [sic] get 16-bit code unit
+                c = s.charAt(offset);
                 if (c == '\\' && ahead < length) {
-                    int o[] = new int[] { ahead };
-                    c = unescapeAt(s, o);
-                    ahead = o[0];
+                    // Calling ourselves recursively may cause a stack overflow if
+                    // we have repeated escaped lead surrogates.
+                    // Limit the length to 11 ("x{0000DFFF}") after ahead.
+                    int tailLimit = ahead + 11;
+                    if (tailLimit > length) {
+                        tailLimit = length;
+                    }
+                    int cpAndLength = unescapeAndLengthAt(s, ahead, tailLimit);
+                    if (cpAndLength >= 0) {
+                        c = cpAndLength >> 8;
+                        ahead += cpAndLength & 0xff;
+                    }
                 }
-                if (UTF16.isTrailSurrogate((char) c)) {
+                if (UTF16.isTrailSurrogate(c)) {
                     offset = ahead;
-                    result = Character.toCodePoint((char) result, (char) c);
+                    result = UCharacter.toCodePoint(result, c);
                 }
             }
-            offset16[0] = offset;
-            return result;
+            return codePointAndLength(result, start, offset);
         }
 
         /* Convert C-style escapes in table */
-        for (i=0; i<UNESCAPE_MAP.length; i+=2) {
+        for (int i=0; i<UNESCAPE_MAP.length; i+=2) {
             if (c == UNESCAPE_MAP[i]) {
-                offset16[0] = offset;
-                return UNESCAPE_MAP[i+1];
+                return codePointAndLength(UNESCAPE_MAP[i+1], start, offset);
             } else if (c < UNESCAPE_MAP[i]) {
                 break;
             }
@@ -897,65 +927,103 @@
 
         /* Map \cX to control-X: X & 0x1F */
         if (c == 'c' && offset < length) {
-            c = UTF16.charAt(s, offset);
-            offset16[0] = offset + UTF16.getCharCount(c);
-            return 0x1F & c;
+            c = Character.codePointAt(s, offset);
+            return codePointAndLength(c & 0x1F, start, offset + Character.charCount(c));
         }
 
         /* If no special forms are recognized, then consider
-         * the backslash to generically escape the next character. */
-        offset16[0] = offset;
-        return c;
+         * the backslash to generically escape the next character.
+         * Deal with surrogate pairs. */
+        if (UTF16.isLeadSurrogate(c) && offset < length) {
+            int c2 = s.charAt(offset);
+            if (UTF16.isTrailSurrogate(c2)) {
+                ++offset;
+                c = UCharacter.toCodePoint(c, c2);
+            }
+        }
+        return codePointAndLength(c, start, offset);
+    }
+
+    private static int codePointAndLength(int c, int length) {
+        assert 0 <= c && c <= 0x10ffff;
+        assert 0 <= length && length <= 0xff;
+        return c << 8 | length;
+    }
+
+    private static int codePointAndLength(int c, int start, int limit) {
+        return codePointAndLength(c, limit - start);
+    }
+
+    public static int cpFromCodePointAndLength(int cpAndLength) {
+        assert cpAndLength >= 0;
+        return cpAndLength >> 8;
+    }
+
+    public static int lengthFromCodePointAndLength(int cpAndLength) {
+        assert cpAndLength >= 0;
+        return cpAndLength & 0xff;
     }
 
     /**
-     * Convert all escapes in a given string using unescapeAt().
+     * Convert all escapes in a given string using unescapeAndLengthAt().
      * @exception IllegalArgumentException if an invalid escape is
      * seen.
      */
-    public static String unescape(String s) {
-        StringBuilder buf = new StringBuilder();
-        int[] pos = new int[1];
+    public static String unescape(CharSequence s) {
+        StringBuilder buf = null;
         for (int i=0; i<s.length(); ) {
             char c = s.charAt(i++);
             if (c == '\\') {
-                pos[0] = i;
-                int e = unescapeAt(s, pos);
-                if (e < 0) {
-                    throw new IllegalArgumentException("Invalid escape sequence " +
-                            s.substring(i-1, Math.min(i+8, s.length())));
+                if (buf == null) {
+                    buf = new StringBuilder(s.length()).append(s, 0, i - 1);
                 }
-                buf.appendCodePoint(e);
-                i = pos[0];
-            } else {
+                int cpAndLength = unescapeAndLengthAt(s, i);
+                if (cpAndLength < 0) {
+                    throw new IllegalArgumentException("Invalid escape sequence " +
+                            s.subSequence(i-1, Math.min(i+9, s.length())));
+                }
+                buf.appendCodePoint(cpAndLength >> 8);
+                i += cpAndLength & 0xff;
+            } else if (buf != null) {
+                // We could optimize this further by appending whole substrings between escapes.
                 buf.append(c);
             }
         }
+        if (buf == null) {
+            // No escapes in s.
+            return s.toString();
+        }
         return buf.toString();
     }
 
     /**
-     * Convert all escapes in a given string using unescapeAt().
+     * Convert all escapes in a given string using unescapeAndLengthAt().
      * Leave invalid escape sequences unchanged.
      */
-    public static String unescapeLeniently(String s) {
-        StringBuilder buf = new StringBuilder();
-        int[] pos = new int[1];
+    public static String unescapeLeniently(CharSequence s) {
+        StringBuilder buf = null;
         for (int i=0; i<s.length(); ) {
             char c = s.charAt(i++);
             if (c == '\\') {
-                pos[0] = i;
-                int e = unescapeAt(s, pos);
-                if (e < 0) {
+                if (buf == null) {
+                    buf = new StringBuilder(s.length()).append(s, 0, i - 1);
+                }
+                int cpAndLength = unescapeAndLengthAt(s, i);
+                if (cpAndLength < 0) {
                     buf.append(c);
                 } else {
-                    buf.appendCodePoint(e);
-                    i = pos[0];
+                    buf.appendCodePoint(cpAndLength >> 8);
+                    i += cpAndLength & 0xff;
                 }
-            } else {
+            } else if (buf != null) {
+                // We could optimize this further by appending whole substrings between escapes.
                 buf.append(c);
             }
         }
+        if (buf == null) {
+            // No escapes in s.
+            return s.toString();
+        }
         return buf.toString();
     }
 
@@ -1049,7 +1117,7 @@
      * this character are not included in the output
      * @param output an array to receive the substrings between
      * instances of divider.  It must be large enough on entry to
-     * accomodate all output.  Adjacent instances of the divider
+     * accommodate all output.  Adjacent instances of the divider
      * character will place empty strings into output.  Before
      * returning, output is padded out with empty strings.
      */
@@ -1315,7 +1383,7 @@
      * position.  Return the identifier, or null if there is no
      * identifier.
      * @param str the string to parse
-     * @param pos INPUT-OUPUT parameter.  On INPUT, pos[0] is the
+     * @param pos INPUT-OUTPUT parameter.  On INPUT, pos[0] is the
      * first character to examine.  It must be less than str.length(),
      * and it must not point to a whitespace character.  That is, must
      * have pos[0] < str.length().  On
@@ -1472,34 +1540,65 @@
     }
 
     /**
-     * Escape unprintable characters using <backslash>uxxxx notation
+     * @return true for control codes and for surrogate and noncharacter code points
+     */
+    public static boolean shouldAlwaysBeEscaped(int c) {
+        if (c < 0x20) {
+            return true;  // C0 control codes
+        } else if (c <= 0x7e) {
+            return false;  // printable ASCII
+        } else if (c <= 0x9f) {
+            return true;  // C1 control codes
+        } else if (c < 0xd800) {
+            return false;  // most of the BMP
+        } else if (c <= 0xdfff || (0xfdd0 <= c && c <= 0xfdef) || (c & 0xfffe) == 0xfffe) {
+            return true;  // surrogate or noncharacter code points
+        } else if (c <= 0x10ffff) {
+            return false;  // all else
+        } else {
+            return true;  // not a code point
+        }
+    }
+
+    /**
+     * 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.
      */
     public static <T extends Appendable> boolean escapeUnprintable(T result, int c) {
+        if (isUnprintable(c)) {
+            escape(result, c);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Escapes one code point using <backslash>uxxxx notation
+     * for U+0000 to U+FFFF and <backslash>Uxxxxxxxx for U+10000 and above.
+     * @return result
+     */
+    public static <T extends Appendable> T escape(T result, int c) {
         try {
-            if (isUnprintable(c)) {
-                result.append('\\');
-                if ((c & ~0xFFFF) != 0) {
-                    result.append('U');
-                    result.append(DIGITS[0xF&(c>>28)]);
-                    result.append(DIGITS[0xF&(c>>24)]);
-                    result.append(DIGITS[0xF&(c>>20)]);
-                    result.append(DIGITS[0xF&(c>>16)]);
-                } else {
-                    result.append('u');
-                }
-                result.append(DIGITS[0xF&(c>>12)]);
-                result.append(DIGITS[0xF&(c>>8)]);
-                result.append(DIGITS[0xF&(c>>4)]);
-                result.append(DIGITS[0xF&c]);
-                return true;
+            result.append('\\');
+            if ((c & ~0xFFFF) != 0) {
+                result.append('U');
+                result.append(DIGITS[0xF&(c>>28)]);
+                result.append(DIGITS[0xF&(c>>24)]);
+                result.append(DIGITS[0xF&(c>>20)]);
+                result.append(DIGITS[0xF&(c>>16)]);
+            } else {
+                result.append('u');
             }
-            return false;
+            result.append(DIGITS[0xF&(c>>12)]);
+            result.append(DIGITS[0xF&(c>>8)]);
+            result.append(DIGITS[0xF&(c>>4)]);
+            result.append(DIGITS[0xF&c]);
+            return result;
         } catch (IOException e) {
-            throw new IllegalIcuArgumentException(e);
+            throw new ICUUncheckedIOException(e);
         }
     }
 
diff --git a/android_icu4j/src/main/java/android/icu/text/BurmeseBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java
similarity index 87%
rename from android_icu4j/src/main/java/android/icu/text/BurmeseBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java
index f52f7e8..6b0bfbc 100644
--- a/android_icu4j/src/main/java/android/icu/text/BurmeseBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java
@@ -7,7 +7,7 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.io.IOException;
 import java.text.CharacterIterator;
@@ -15,8 +15,13 @@
 import android.icu.lang.UCharacter;
 import android.icu.lang.UProperty;
 import android.icu.lang.UScript;
+import android.icu.text.UnicodeSet;
 
-class BurmeseBreakEngine extends DictionaryBreakEngine {
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class BurmeseBreakEngine extends DictionaryBreakEngine {
 
     // Constants for BurmeseBreakIterator
     // How many words in a row are "good enough"?
@@ -30,24 +35,16 @@
     private static final byte BURMESE_MIN_WORD = 2;
 
     private DictionaryMatcher fDictionary;
-    private static UnicodeSet fBurmeseWordSet;
-    private static UnicodeSet fEndWordSet;
-    private static UnicodeSet fBeginWordSet;
-    private static UnicodeSet fMarkSet;
+    private UnicodeSet fEndWordSet;
+    private UnicodeSet fBeginWordSet;
+    private UnicodeSet fMarkSet;
 
-    static {
+    public BurmeseBreakEngine() throws IOException {
         // Initialize UnicodeSets
-        fBurmeseWordSet = new UnicodeSet();
-        fMarkSet = new UnicodeSet();
-        fBeginWordSet = new UnicodeSet();
-
-        fBurmeseWordSet.applyPattern("[[:Mymr:]&[:LineBreak=SA:]]");
-        fBurmeseWordSet.compact();
-
-        fMarkSet.applyPattern("[[:Mymr:]&[:LineBreak=SA:]&[:M:]]");
+        fBeginWordSet = new UnicodeSet(0x1000, 0x102A);      // basic consonants and independent vowels
+        fEndWordSet = new UnicodeSet("[[:Mymr:]&[:LineBreak=SA:]]");
+        fMarkSet = new UnicodeSet("[[:Mymr:]&[:LineBreak=SA:]&[:M:]]");
         fMarkSet.add(0x0020);
-        fEndWordSet = new UnicodeSet(fBurmeseWordSet);
-        fBeginWordSet.add(0x1000, 0x102A);      // basic consonants and independent vowels
 
         // Compact for caching
         fMarkSet.compact();
@@ -55,14 +52,11 @@
         fBeginWordSet.compact();
 
         // Freeze the static UnicodeSet
-        fBurmeseWordSet.freeze();
         fMarkSet.freeze();
         fEndWordSet.freeze();
         fBeginWordSet.freeze();
-    }
 
-    public BurmeseBreakEngine() throws IOException {
-        setCharacters(fBurmeseWordSet);
+        setCharacters(fEndWordSet);
         // Initialize dictionary
         fDictionary = DictionaryData.loadDictionaryFor("Mymr");
     }
@@ -87,7 +81,7 @@
 
     @Override
     public int divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd,
-            DequeI foundBreaks) {
+            DequeI foundBreaks, boolean isPhraseBreaking) {
 
 
         if ((rangeEnd - rangeStart) < BURMESE_MIN_WORD) {
@@ -121,13 +115,9 @@
                 // If we're already at the end of the range, we're done
                 if (fIter.getIndex() < rangeEnd) {
                     do {
-                        int wordsMatched = 1;
                         if (words[(wordsFound+1)%BURMESE_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) > 0) {
-                            if (wordsMatched < 2) {
-                                // Followed by another dictionary word; mark first word as a good candidate
-                                words[wordsFound%BURMESE_LOOKAHEAD].markCurrent();
-                                wordsMatched = 2;
-                            }
+                            // Followed by another dictionary word; mark first word as a good candidate
+                            words[wordsFound%BURMESE_LOOKAHEAD].markCurrent();
 
                             // If we're already at the end of the range, we're done
                             if (fIter.getIndex() >= rangeEnd) {
diff --git a/android_icu4j/src/main/java/android/icu/text/BytesDictionaryMatcher.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/BytesDictionaryMatcher.java
similarity index 97%
rename from android_icu4j/src/main/java/android/icu/text/BytesDictionaryMatcher.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/BytesDictionaryMatcher.java
index 91ed2d3..b9ada1e 100644
--- a/android_icu4j/src/main/java/android/icu/text/BytesDictionaryMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/BytesDictionaryMatcher.java
@@ -7,11 +7,12 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.text.CharacterIterator;
 
 import android.icu.impl.Assert;
+import android.icu.text.UCharacterIterator;
 import android.icu.util.BytesTrie;
 import android.icu.util.BytesTrie.Result;
 
diff --git a/android_icu4j/src/main/java/android/icu/text/CharsDictionaryMatcher.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/CharsDictionaryMatcher.java
similarity index 96%
rename from android_icu4j/src/main/java/android/icu/text/CharsDictionaryMatcher.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/CharsDictionaryMatcher.java
index cc50062..c20a8e9 100644
--- a/android_icu4j/src/main/java/android/icu/text/CharsDictionaryMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/CharsDictionaryMatcher.java
@@ -7,10 +7,11 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.text.CharacterIterator;
 
+import android.icu.text.UCharacterIterator;
 import android.icu.util.BytesTrie.Result;
 import android.icu.util.CharsTrie;
 
diff --git a/android_icu4j/src/main/java/android/icu/text/CjkBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java
similarity index 62%
rename from android_icu4j/src/main/java/android/icu/text/CjkBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java
index 6e5d328..0a41f08 100644
--- a/android_icu4j/src/main/java/android/icu/text/CjkBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/CjkBreakEngine.java
@@ -7,7 +7,7 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import static android.icu.impl.CharacterIteration.DONE32;
 import static android.icu.impl.CharacterIteration.current32;
@@ -15,41 +15,68 @@
 
 import java.io.IOException;
 import java.text.CharacterIterator;
+import java.util.HashSet;
 
 import android.icu.impl.Assert;
+import android.icu.impl.ICUData;
+import android.icu.text.Normalizer;
+import android.icu.text.UnicodeSet;
+import android.icu.text.UnicodeSetIterator;
+import android.icu.util.UResourceBundle;
+import android.icu.util.UResourceBundleIterator;
 
-class CjkBreakEngine extends DictionaryBreakEngine {
-    private static final UnicodeSet fHangulWordSet = new UnicodeSet();
-    private static final UnicodeSet fHanWordSet = new UnicodeSet();
-    private static final UnicodeSet fKatakanaWordSet = new UnicodeSet();
-    private static final UnicodeSet fHiraganaWordSet = new UnicodeSet();
-    static {
-        fHangulWordSet.applyPattern("[\\uac00-\\ud7a3]");
-        fHanWordSet.applyPattern("[:Han:]");
-        fKatakanaWordSet.applyPattern("[[:Katakana:]\\uff9e\\uff9f]");
-        fHiraganaWordSet.applyPattern("[:Hiragana:]");
-
-        // freeze them all
-        fHangulWordSet.freeze();
-        fHanWordSet.freeze();
-        fKatakanaWordSet.freeze();
-        fHiraganaWordSet.freeze();
-    }
-
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class CjkBreakEngine extends DictionaryBreakEngine {
+    private UnicodeSet fHangulWordSet;
+    private UnicodeSet fDigitOrOpenPunctuationOrAlphabetSet;
+    private UnicodeSet fClosePunctuationSet;
     private DictionaryMatcher fDictionary = null;
+    private HashSet<String> fSkipSet;
 
     public CjkBreakEngine(boolean korean) throws IOException {
+        fHangulWordSet = new UnicodeSet("[\\uac00-\\ud7a3]");
+        fHangulWordSet.freeze();
+        // Digit, open punctuation and Alphabetic characters.
+        fDigitOrOpenPunctuationOrAlphabetSet = new UnicodeSet("[[:Nd:][:Pi:][:Ps:][:Alphabetic:]]");
+        fDigitOrOpenPunctuationOrAlphabetSet.freeze();
+
+        fClosePunctuationSet = new UnicodeSet("[[:Pc:][:Pd:][:Pe:][:Pf:][:Po:]]");
+        fClosePunctuationSet.freeze();
+        fSkipSet = new HashSet<String>();
+
         fDictionary = DictionaryData.loadDictionaryFor("Hira");
         if (korean) {
             setCharacters(fHangulWordSet);
         } else { //Chinese and Japanese
-            UnicodeSet cjSet = new UnicodeSet();
-            cjSet.addAll(fHanWordSet);
-            cjSet.addAll(fKatakanaWordSet);
-            cjSet.addAll(fHiraganaWordSet);
-            cjSet.add(0xFF70); // HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK
-            cjSet.add(0x30FC); // KATAKANA-HIRAGANA PROLONGED SOUND MARK
+            UnicodeSet cjSet = new UnicodeSet("[[:Han:][:Hiragana:][:Katakana:]\\u30fc\\uff70\\uff9e\\uff9f]");
             setCharacters(cjSet);
+            initializeJapanesePhraseParamater();
+        }
+    }
+
+    private void initializeJapanesePhraseParamater() {
+        loadJapaneseExtensions();
+        loadHiragana();
+    }
+
+    private void loadJapaneseExtensions() {
+        UResourceBundle rb = UResourceBundle.getBundleInstance(ICUData.ICU_BRKITR_BASE_NAME, "ja");
+        final String tag = "extensions";
+        UResourceBundle bundle = rb.get(tag);
+        UResourceBundleIterator iterator = bundle.getIterator();
+        while (iterator.hasNext()) {
+            fSkipSet.add(iterator.nextString());
+        }
+    }
+
+    private void loadHiragana() {
+        UnicodeSet hiraganaWordSet = new UnicodeSet("[:Hiragana:]");
+        hiraganaWordSet.freeze();
+        UnicodeSetIterator iterator = new UnicodeSetIterator(hiraganaWordSet);
+        while (iterator.next()) {
+            fSkipSet.add(iterator.getString());
         }
     }
 
@@ -83,7 +110,7 @@
 
     @Override
     public int divideUpDictionaryRange(CharacterIterator inText, int startPos, int endPos,
-            DequeI foundBreaks) {
+            DequeI foundBreaks, boolean isPhraseBreaking) {
         if (startPos >= endPos) {
             return 0;
         }
@@ -213,6 +240,25 @@
         if (bestSnlp[numCodePts] == kint32max) {
             t_boundary[numBreaks] = numCodePts;
             numBreaks++;
+        } else if (isPhraseBreaking) {
+            t_boundary[numBreaks] = numCodePts;
+            numBreaks++;
+            int prevIdx = numCodePts;
+            int codeUnitIdx = 0, length = 0;
+            for (int i = prev[numCodePts]; i > 0; i = prev[i]) {
+                codeUnitIdx = prenormstr.offsetByCodePoints(0, i);
+                length = prevIdx - i;
+                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);
+                if (!fSkipSet.contains(pattern)
+                        && (!isKatakana(current32(text)) || !isKatakana(next32(text)))) {
+                    t_boundary[numBreaks] = i;
+                    numBreaks++;
+                }
+            }
         } else {
             for (int i = numCodePts; i > 0; i = prev[i]) {
                 t_boundary[numBreaks] = i;
@@ -226,20 +272,54 @@
         }
 
         int correctedNumBreaks = 0;
+        int previous = -1;
         for (int i = numBreaks - 1; i >= 0; i--) {
             int pos = charPositions[t_boundary[i]] + startPos;
-            if (!(foundBreaks.contains(pos) || pos == startPos)) {
-                foundBreaks.push(charPositions[t_boundary[i]] + startPos);
-                correctedNumBreaks++;
+            // In phrase breaking, there has to be a breakpoint between Cj character and close
+            // punctuation.
+            // E.g.[携帯電話]正しい選択 -> [携帯▁電話]▁正しい▁選択 -> breakpoint between ] and 正
+            if (pos > previous) {
+                if (pos != startPos
+                        || (isPhraseBreaking && pos > 0
+                        && fClosePunctuationSet.contains(inText.setIndex(pos - 1)))) {
+                    foundBreaks.push(charPositions[t_boundary[i]] + startPos);
+                    correctedNumBreaks++;
+                }
             }
+            previous = pos;
         }
 
         if (!foundBreaks.isEmpty() && foundBreaks.peek() == endPos) {
-            foundBreaks.pop();
-            correctedNumBreaks--;
+            // 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
+            if (isPhraseBreaking) {
+                if (!fDigitOrOpenPunctuationOrAlphabetSet.contains(inText.setIndex(endPos))) {
+                    foundBreaks.pop();
+                    correctedNumBreaks--;
+                }
+            } else {
+                foundBreaks.pop();
+                correctedNumBreaks--;
+            }
         }
         if (!foundBreaks.isEmpty())
             inText.setIndex(foundBreaks.peek());
         return correctedNumBreaks;
     }
+
+    private String getPatternFromText(CharacterIterator text, StringBuffer sb, int start,
+            int length) {
+        sb.setLength(0);
+        if(length > 0) {
+            text.setIndex(start);
+            sb.appendCodePoint(current32(text));
+            for (int j = 1; j < length; j++) {
+                sb.appendCodePoint(next32(text));
+            }
+        }
+        return sb.toString();
+    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/text/DictionaryBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryBreakEngine.java
similarity index 90%
rename from android_icu4j/src/main/java/android/icu/text/DictionaryBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryBreakEngine.java
index d0e5ec5..f90d93e 100644
--- a/android_icu4j/src/main/java/android/icu/text/DictionaryBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryBreakEngine.java
@@ -7,13 +7,17 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.text.CharacterIterator;
 
 import android.icu.impl.CharacterIteration;
+import android.icu.text.UnicodeSet;
 
-abstract class DictionaryBreakEngine implements LanguageBreakEngine {
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public abstract class DictionaryBreakEngine implements LanguageBreakEngine {
 
     /* Helper class for improving readability of the Thai/Lao/Khmer word break
      * algorithm.
@@ -87,9 +91,10 @@
      *  A deque-like structure holding raw ints.
      *  Partial, limited implementation, only what is needed by the dictionary implementation.
      *  For internal use only.
+     * @hide Only a subset of ICU is exposed in Android
      * @hide draft / provisional / internal are hidden on Android
      */
-    static class DequeI implements Cloneable {
+    public static class DequeI implements Cloneable {
         private int[] data = new int[50];
         private int lastIdx = 4;   // or base of stack. Index of element.
         private int firstIdx = 4;  // or Top of Stack. Index of element + 1.
@@ -101,11 +106,11 @@
             return result;
         }
 
-        int size() {
+        public int size() {
             return firstIdx - lastIdx;
         }
 
-        boolean isEmpty() {
+        public boolean isEmpty() {
             return size() == 0;
         }
 
@@ -115,26 +120,26 @@
             data = newData;
         }
 
-        void offer(int v) {
+        public void offer(int v) {
             // Note that the actual use cases of offer() add at most one element.
             //   We make no attempt to handle more than a few.
             assert lastIdx > 0;
             data[--lastIdx] = v;
         }
 
-        void push(int v) {
+        public void push(int v) {
             if (firstIdx >= data.length) {
                 grow();
             }
             data[firstIdx++] = v;
         }
 
-        int pop() {
+        public int pop() {
             assert size() > 0;
             return data[--firstIdx];
         }
 
-        int peek() {
+        public int peek() {
             assert size() > 0;
             return data[firstIdx - 1];
         }
@@ -158,12 +163,12 @@
             return false;
         }
 
-        int elementAt(int i) {
+        public int elementAt(int i) {
             assert i < size();
             return data[lastIdx + i];
         }
 
-        void removeAllElements() {
+        public void removeAllElements() {
             lastIdx = firstIdx = 4;
         }
     }
@@ -183,7 +188,7 @@
 
     @Override
     public int findBreaks(CharacterIterator text, int startPos, int endPos,
-            DequeI foundBreaks) {
+            DequeI foundBreaks, boolean isPhraseBreaking) {
         int result = 0;
 
          // Find the span of characters included in the set.
@@ -202,7 +207,7 @@
         rangeStart = start;
         rangeEnd = current;
 
-        result = divideUpDictionaryRange(text, rangeStart, rangeEnd, foundBreaks);
+        result = divideUpDictionaryRange(text, rangeStart, rangeEnd, foundBreaks, isPhraseBreaking);
         text.setIndex(current);
 
         return result;
@@ -226,5 +231,6 @@
      abstract int divideUpDictionaryRange(CharacterIterator text,
                                           int               rangeStart,
                                           int               rangeEnd,
-                                          DequeI            foundBreaks );
+                                          DequeI            foundBreaks,
+                                          boolean isPhraseBreaking);
 }
diff --git a/android_icu4j/src/main/java/android/icu/text/DictionaryData.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryData.java
similarity index 98%
rename from android_icu4j/src/main/java/android/icu/text/DictionaryData.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryData.java
index ad2b39b..4208a4f 100644
--- a/android_icu4j/src/main/java/android/icu/text/DictionaryData.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryData.java
@@ -8,7 +8,7 @@
  *******************************************************************************
  */
 
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
diff --git a/android_icu4j/src/main/java/android/icu/text/DictionaryMatcher.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryMatcher.java
similarity index 97%
rename from android_icu4j/src/main/java/android/icu/text/DictionaryMatcher.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryMatcher.java
index 3b27240..70ac324 100644
--- a/android_icu4j/src/main/java/android/icu/text/DictionaryMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/DictionaryMatcher.java
@@ -7,7 +7,7 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.text.CharacterIterator;
 
diff --git a/android_icu4j/src/main/java/android/icu/text/KhmerBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java
similarity index 88%
rename from android_icu4j/src/main/java/android/icu/text/KhmerBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java
index d40cb1b..f746b98 100644
--- a/android_icu4j/src/main/java/android/icu/text/KhmerBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java
@@ -7,7 +7,7 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.io.IOException;
 import java.text.CharacterIterator;
@@ -15,8 +15,12 @@
 import android.icu.lang.UCharacter;
 import android.icu.lang.UProperty;
 import android.icu.lang.UScript;
+import android.icu.text.UnicodeSet;
 
-class KhmerBreakEngine extends DictionaryBreakEngine {
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class KhmerBreakEngine extends DictionaryBreakEngine {
 
     // Constants for KhmerBreakIterator
     // How many words in a row are "good enough"?
@@ -33,24 +37,20 @@
 
 
     private DictionaryMatcher fDictionary;
-    private static UnicodeSet fKhmerWordSet;
-    private static UnicodeSet fEndWordSet;
-    private static UnicodeSet fBeginWordSet;
-    private static UnicodeSet fMarkSet;
+    private UnicodeSet fEndWordSet;
+    private UnicodeSet fBeginWordSet;
+    private UnicodeSet fMarkSet;
 
-    static {
+    public KhmerBreakEngine() throws IOException {
         // Initialize UnicodeSets
-        fKhmerWordSet = new UnicodeSet();
-        fMarkSet = new UnicodeSet();
-        fBeginWordSet = new UnicodeSet();
-
-        fKhmerWordSet.applyPattern("[[:Khmer:]&[:LineBreak=SA:]]");
-        fKhmerWordSet.compact();
-
-        fMarkSet.applyPattern("[[:Khmer:]&[:LineBreak=SA:]&[:M:]]");
+        UnicodeSet khmerWordSet = new UnicodeSet("[[:Khmer:]&[:LineBreak=SA:]]");
+        fMarkSet = new UnicodeSet("[[:Khmer:]&[:LineBreak=SA:]&[:M:]]");
         fMarkSet.add(0x0020);
-        fEndWordSet = new UnicodeSet(fKhmerWordSet);
-        fBeginWordSet.add(0x1780, 0x17B3);
+        fBeginWordSet = new UnicodeSet(0x1780, 0x17B3);
+
+        khmerWordSet.compact();
+
+        fEndWordSet = new UnicodeSet(khmerWordSet);
         fEndWordSet.remove(0x17D2); // KHMER SIGN COENG that combines some following characters
 
         // Compact for caching
@@ -59,14 +59,12 @@
         fBeginWordSet.compact();
 
         // Freeze the static UnicodeSet
-        fKhmerWordSet.freeze();
+        khmerWordSet.freeze();
         fMarkSet.freeze();
         fEndWordSet.freeze();
         fBeginWordSet.freeze();
-    }
 
-    public KhmerBreakEngine() throws IOException {
-        setCharacters(fKhmerWordSet);
+        setCharacters(khmerWordSet);
         // Initialize dictionary
         fDictionary = DictionaryData.loadDictionaryFor("Khmr");
     }
@@ -91,7 +89,7 @@
 
     @Override
     public int divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd,
-            DequeI foundBreaks) {
+            DequeI foundBreaks, boolean isPhraseBreaking) {
 
         if ((rangeEnd - rangeStart) < KHMER_MIN_WORD_SPAN) {
             return 0;  // Not enough characters for word
@@ -125,13 +123,9 @@
                 // If we're already at the end of the range, we're done
                 if (fIter.getIndex() < rangeEnd) {
                     do {
-                        int wordsMatched = 1;
                         if (words[(wordsFound+1)%KHMER_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) > 0) {
-                            if (wordsMatched < 2) {
-                                // Followed by another dictionary word; mark first word as a good candidate
-                                words[wordsFound%KHMER_LOOKAHEAD].markCurrent();
-                                wordsMatched = 2;
-                            }
+                            // Followed by another dictionary word; mark first word as a good candidate
+                            words[wordsFound%KHMER_LOOKAHEAD].markCurrent();
 
                             // If we're already at the end of the range, we're done
                             if (fIter.getIndex() >= rangeEnd) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/LSTMBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/LSTMBreakEngine.java
new file mode 100644
index 0000000..4b49b07
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/LSTMBreakEngine.java
@@ -0,0 +1,454 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2021 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+//
+/**
+ * A LSTMBreakEngine
+ */
+package android.icu.impl.breakiter;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.CharacterIterator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.icu.impl.ICUData;
+import android.icu.impl.ICUResourceBundle;
+import android.icu.lang.UCharacter;
+import android.icu.lang.UProperty;
+import android.icu.lang.UScript;
+import android.icu.text.BreakIterator;
+import android.icu.text.UnicodeSet;
+import android.icu.util.UResourceBundle;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+public class LSTMBreakEngine extends DictionaryBreakEngine {
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public enum EmbeddingType {
+      UNKNOWN,
+      CODE_POINTS,
+      GRAPHEME_CLUSTER
+    }
+
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public enum LSTMClass {
+      BEGIN,
+      INSIDE,
+      END,
+      SINGLE,
+    }
+
+    private static float[][] make2DArray(int[] data, int start, int d1, int d2) {
+        byte[] bytes = new byte[4];
+        float [][] result = new float[d1][d2];
+        for (int i = 0; i < d1 ; i++) {
+            for (int j = 0; j < d2 ; j++) {
+                int d = data[start++];
+                bytes[0] = (byte) (d >> 24);
+                bytes[1] = (byte) (d >> 16);
+                bytes[2] = (byte) (d >> 8);
+                bytes[3] = (byte) (d /*>> 0*/);
+                result[i][j] = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
+            }
+        }
+        return result;
+    }
+
+    private static float[] make1DArray(int[] data, int start, int d1) {
+        byte[] bytes = new byte[4];
+        float [] result = new float[d1];
+        for (int i = 0; i < d1 ; i++) {
+            int d = data[start++];
+            bytes[0] = (byte) (d >> 24);
+            bytes[1] = (byte) (d >> 16);
+            bytes[2] = (byte) (d >> 8);
+            bytes[3] = (byte) (d /*>> 0*/);
+            result[i] = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
+        }
+        return result;
+    }
+
+    /** @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android*/
+    public static class LSTMData {
+        private LSTMData() {
+        }
+
+        public LSTMData(UResourceBundle rb) {
+            int embeddings = rb.get("embeddings").getInt();
+            int hunits = rb.get("hunits").getInt();
+            this.fType = EmbeddingType.UNKNOWN;
+            this.fName = rb.get("model").getString();
+            String typeString = rb.get("type").getString();
+            if (typeString.equals("codepoints")) {
+                this.fType = EmbeddingType.CODE_POINTS;
+            } else if (typeString.equals("graphclust")) {
+                this.fType = EmbeddingType.GRAPHEME_CLUSTER;
+            }
+            String[] dict = rb.get("dict").getStringArray();
+            int[] data = rb.get("data").getIntVector();
+            int dataLen = data.length;
+            int numIndex = dict.length;
+            fDict = new HashMap<String, Integer>(numIndex + 1);
+            int idx = 0;
+            for (String embedding : dict){
+                fDict.put(embedding, idx++);
+            }
+            int mat1Size = (numIndex + 1) * embeddings;
+            int mat2Size = embeddings * 4 * hunits;
+            int mat3Size = hunits * 4 * hunits;
+            int mat4Size = 4 * hunits;
+            int mat5Size = mat2Size;
+            int mat6Size = mat3Size;
+            int mat7Size = mat4Size;
+            int mat8Size = 2 * hunits * 4;
+            int mat9Size = 4;
+            assert dataLen == mat1Size + mat2Size + mat3Size + mat4Size + mat5Size + mat6Size + mat7Size + mat8Size + mat9Size;
+            int start = 0;
+            this.fEmbedding = make2DArray(data, start, (numIndex+1), embeddings);
+            start += mat1Size;
+            this.fForwardW = make2DArray(data, start, embeddings, 4 * hunits);
+            start += mat2Size;
+            this.fForwardU = make2DArray(data, start, hunits, 4 * hunits);
+            start += mat3Size;
+            this.fForwardB = make1DArray(data, start, 4 * hunits);
+            start += mat4Size;
+            this.fBackwardW = make2DArray(data, start, embeddings, 4 * hunits);
+            start += mat5Size;
+            this.fBackwardU = make2DArray(data, start, hunits, 4 * hunits);
+            start += mat6Size;
+            this.fBackwardB = make1DArray(data, start, 4 * hunits);
+            start += mat7Size;
+            this.fOutputW = make2DArray(data, start, 2 * hunits, 4);
+            start += mat8Size;
+            this.fOutputB = make1DArray(data, start, 4);
+        }
+
+        public EmbeddingType fType;
+        public String fName;
+        public Map<String, Integer> fDict;
+        public float fEmbedding[][];
+        public float fForwardW[][];
+        public float fForwardU[][];
+        public float fForwardB[];
+        public float fBackwardW[][];
+        public float fBackwardU[][];
+        public float fBackwardB[];
+        public float fOutputW[][];
+        public float fOutputB[];
+    }
+
+    // Minimum word size
+    private static final byte MIN_WORD = 2;
+
+    // Minimum number of characters for two words
+    private static final byte MIN_WORD_SPAN = MIN_WORD * 2;
+
+    abstract class Vectorizer {
+        public Vectorizer(Map<String, Integer> dict) {
+            this.fDict = dict;
+        }
+        abstract public void vectorize(CharacterIterator fIter, int rangeStart, int rangeEnd,
+                              List<Integer> offsets, List<Integer> indicies);
+        protected int getIndex(String token) {
+            Integer res = fDict.get(token);
+            return (res == null) ? fDict.size() : res;
+        }
+        private Map<String, Integer> fDict;
+    }
+
+    class CodePointsVectorizer extends Vectorizer {
+        public CodePointsVectorizer(Map<String, Integer> dict) {
+            super(dict);
+        }
+
+        public void vectorize(CharacterIterator fIter, int rangeStart, int rangeEnd,
+                              List<Integer> offsets, List<Integer> indicies) {
+            fIter.setIndex(rangeStart);
+            for (char c = fIter.current();
+                 c != CharacterIterator.DONE && fIter.getIndex() < rangeEnd;
+                 c = fIter.next()) {
+                offsets.add(fIter.getIndex());
+                indicies.add(getIndex(String.valueOf(c)));
+            }
+        }
+    }
+
+    class GraphemeClusterVectorizer extends Vectorizer {
+        public GraphemeClusterVectorizer(Map<String, Integer> dict) {
+            super(dict);
+        }
+
+        private String substring(CharacterIterator text, int startPos, int endPos) {
+            int saved = text.getIndex();
+            text.setIndex(startPos);
+            StringBuilder sb = new StringBuilder();
+            for (char c = text.current();
+                 c != CharacterIterator.DONE && text.getIndex() < endPos;
+                 c = text.next()) {
+                sb.append(c);
+            }
+            text.setIndex(saved);
+            return sb.toString();
+        }
+
+        public void vectorize(CharacterIterator text, int startPos, int endPos,
+                              List<Integer> offsets, List<Integer> indicies) {
+            BreakIterator iter = BreakIterator.getCharacterInstance();
+            iter.setText(text);
+            int last = iter.next(startPos);
+            for (int curr = iter.next(); curr != BreakIterator.DONE && curr <= endPos; curr = iter.next()) {
+                offsets.add(last);
+                String segment = substring(text, last, curr);
+                int index = getIndex(segment);
+                indicies.add(index);
+                last = curr;
+            }
+        }
+    }
+
+    private final LSTMData fData;
+    private int fScript;
+    private final Vectorizer fVectorizer;
+
+    private Vectorizer makeVectorizer(LSTMData data) {
+        switch(data.fType) {
+            case CODE_POINTS:
+                return new CodePointsVectorizer(data.fDict);
+            case GRAPHEME_CLUSTER:
+                return new GraphemeClusterVectorizer(data.fDict);
+            default:
+                return null;
+        }
+    }
+
+    public LSTMBreakEngine(int script, UnicodeSet set, LSTMData data) {
+        setCharacters(set);
+        this.fScript = script;
+        this.fData = data;
+        this.fVectorizer = makeVectorizer(this.fData);
+    }
+
+    @Override
+    public int hashCode() {
+        return getClass().hashCode();
+    }
+
+    @Override
+    public boolean handles(int c) {
+        return fScript == UCharacter.getIntPropertyValue(c, UProperty.SCRIPT);
+    }
+
+    static private void addDotProductTo(final float [] a, final float[][] b, float[] result) {
+        assert a.length == b.length;
+        assert b[0].length == result.length;
+        for (int i = 0; i < result.length; i++) {
+            for (int j = 0; j < a.length; j++) {
+                result[i] += a[j] * b[j][i];
+            }
+        }
+    }
+
+    static private void addTo(final float [] a, float[] result) {
+        assert a.length == result.length;
+        for (int i = 0; i < result.length; i++) {
+            result[i] += a[i];
+        }
+    }
+
+    static private void hadamardProductTo(final float [] a, float[] result) {
+        assert a.length == result.length;
+        for (int i = 0; i < result.length; i++) {
+            result[i] *= a[i];
+        }
+    }
+
+    static private void addHadamardProductTo(final float [] a, final float [] b, float[] result) {
+        assert a.length == result.length;
+        assert b.length == result.length;
+        for (int i = 0; i < result.length; i++) {
+            result[i] += a[i] * b[i];
+        }
+    }
+
+    static private void sigmoid(float [] result, int start, int length) {
+        assert start < result.length;
+        assert start + length <= result.length;
+        for (int i = start; i < start + length; i++) {
+            result[i] = (float)(1.0/(1.0 + Math.exp(-result[i])));
+        }
+    }
+
+    static private void tanh(float [] result, int start, int length) {
+        assert start < result.length;
+        assert start + length <= result.length;
+        for (int i = start; i < start + length; i++) {
+            result[i] = (float)Math.tanh(result[i]);
+        }
+    }
+
+    static private int maxIndex(float [] data) {
+        int index = 0;
+        float max = data[0];
+        for (int i = 1; i < data.length; i++) {
+            if (data[i] > max) {
+                max = data[i];
+                index = i;
+            }
+        }
+        return index;
+    }
+
+    /*
+    static private void print(float [] data) {
+        for (int i=0; i < data.length; i++) {
+            System.out.format("  %e", data[i]);
+            if (i % 4 == 3) {
+              System.out.println();
+            }
+        }
+        System.out.println();
+    }
+    */
+
+    private float[] compute(final float[][] W, final float[][] U, final float[] B,
+                            final float[] x, float[] h, float[] c) {
+        // ifco = x * W + h * U + b
+        float[] ifco = Arrays.copyOf(B, B.length);
+        addDotProductTo(x, W, ifco);
+        float[] hU = new float[B.length];
+        addDotProductTo(h, U, ifco);
+
+        int hunits = B.length / 4;
+        sigmoid(ifco, 0*hunits, hunits);  // i
+        sigmoid(ifco, 1*hunits, hunits);  // f
+        tanh(ifco, 2*hunits, hunits);  // c_
+        sigmoid(ifco, 3*hunits, hunits);  // o
+
+        hadamardProductTo(Arrays.copyOfRange(ifco, hunits, 2*hunits), c);
+        addHadamardProductTo(Arrays.copyOf(ifco, hunits),
+            Arrays.copyOfRange(ifco, 2*hunits, 3*hunits), c);
+
+        h = Arrays.copyOf(c, c.length);
+        tanh(h, 0, h.length);
+        hadamardProductTo(Arrays.copyOfRange(ifco, 3*hunits, 4*hunits), h);
+        // System.out.println("c");
+        // print(c);
+        // System.out.println("h");
+        // print(h);
+        return h;
+    }
+
+    @Override
+    public int divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd,
+            DequeI foundBreaks, boolean isPhraseBreaking) {
+        int beginSize = foundBreaks.size();
+
+        if ((rangeEnd - rangeStart) < MIN_WORD_SPAN) {
+            return 0;  // Not enough characters for word
+        }
+        List<Integer> offsets = new ArrayList<Integer>(rangeEnd - rangeStart);
+        List<Integer> indicies = new ArrayList<Integer>(rangeEnd - rangeStart);
+
+        fVectorizer.vectorize(fIter, rangeStart, rangeEnd, offsets, indicies);
+
+        // To save the needed memory usage, the following is different from the
+        // Python or ICU4X implementation. We first perform the Backward LSTM
+        // and then merge the iteration of the forward LSTM and the output layer
+        // together because we only need to remember the h[t-1] for Forward LSTM.
+        int inputSeqLength = indicies.size();
+        int hunits = this.fData.fForwardU.length;
+        float c[] = new float[hunits];
+
+        // TODO: limit size of hBackward. If input_seq_len is too big, we could
+        // run out of memory.
+        // Backward LSTM
+        float hBackward[][] = new float[inputSeqLength][hunits];
+        for (int i = inputSeqLength - 1; i >= 0;  i--) {
+            if (i != inputSeqLength - 1) {
+                hBackward[i] = Arrays.copyOf(hBackward[i+1], hunits);
+            }
+            // System.out.println("Backward LSTM " + i);
+            hBackward[i] = compute(this.fData.fBackwardW, this.fData.fBackwardU, this.fData.fBackwardB,
+                    this.fData.fEmbedding[indicies.get(i)],
+                    hBackward[i], c);
+        }
+
+        c = new float[hunits];
+        float forwardH[] = new float[hunits];
+        float both[] = new float[2*hunits];
+
+        // The following iteration merge the forward LSTM and the output layer
+        // together.
+        for (int i = 0 ; i < inputSeqLength; i++) {
+            // Forward LSTM
+            forwardH = compute(this.fData.fForwardW, this.fData.fForwardU, this.fData.fForwardB,
+                    this.fData.fEmbedding[indicies.get(i)],
+                    forwardH, c);
+
+            System.arraycopy(forwardH, 0, both, 0, hunits);
+            System.arraycopy(hBackward[i], 0, both, hunits, hunits);
+
+            //System.out.println("Merged " + i);
+            //print(both);
+
+            // Output layer
+            // logp = fbRow * fOutputW + fOutputB
+            float logp[] = Arrays.copyOf(this.fData.fOutputB, this.fData.fOutputB.length);
+            addDotProductTo(both, this.fData.fOutputW, logp);
+
+            int current = maxIndex(logp);
+
+            // BIES logic.
+            if (current == LSTMClass.BEGIN.ordinal() ||
+                current == LSTMClass.SINGLE.ordinal()) {
+                if (i != 0) {
+                    foundBreaks.push(offsets.get(i));
+                }
+            }
+        }
+
+        return foundBreaks.size() - beginSize;
+    }
+
+    public static LSTMData createData(UResourceBundle bundle) {
+        return new LSTMData(bundle);
+    }
+
+    private static String defaultLSTM(int script) {
+        ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BRKITR_BASE_NAME);
+        return rb.getStringWithFallback("lstm/" + UScript.getShortName(script));
+    }
+
+    public static LSTMData createData(int script) {
+        if (script != UScript.KHMER && script != UScript.LAO && script != UScript.MYANMAR && script != UScript.THAI) {
+            return null;
+        }
+        String name = defaultLSTM(script);
+        name = name.substring(0, name.indexOf("."));
+
+        UResourceBundle rb = UResourceBundle.getBundleInstance(
+             ICUData.ICU_BRKITR_BASE_NAME, name,
+             ICUResourceBundle.ICU_DATA_CLASS_LOADER);
+        return createData(rb);
+    }
+
+    public static LSTMBreakEngine create(int script, LSTMData data) {
+        String setExpr = "[[:" + UScript.getShortName(script) + ":]&[:LineBreak=SA:]]";
+        UnicodeSet set = new UnicodeSet();
+        set.applyPattern(setExpr);
+        set.compact();
+        return new LSTMBreakEngine(script, set, data);
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/text/LanguageBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/LanguageBreakEngine.java
similarity index 87%
copy from android_icu4j/src/main/java/android/icu/text/LanguageBreakEngine.java
copy to android_icu4j/src/main/java/android/icu/impl/breakiter/LanguageBreakEngine.java
index 5e1f38a..1b11cc3 100644
--- a/android_icu4j/src/main/java/android/icu/text/LanguageBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/LanguageBreakEngine.java
@@ -7,15 +7,16 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.text.CharacterIterator;
 
 /**
  * The LanguageBreakEngine interface is to be used to implement any
  * language-specific logic for break iteration.
+ * @hide Only a subset of ICU is exposed in Android
  */
-interface LanguageBreakEngine {
+public interface LanguageBreakEngine {
     /**
      * @param c A Unicode codepoint value
      * @return true if the engine can handle this character, false otherwise
@@ -33,7 +34,7 @@
      * @return the number of breaks found
      */
     int findBreaks(CharacterIterator text, int startPos, int endPos,
-            DictionaryBreakEngine.DequeI foundBreaks);
+            DictionaryBreakEngine.DequeI foundBreaks, boolean isPhraseBreaking);
 }
 
 
diff --git a/android_icu4j/src/main/java/android/icu/text/LaoBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java
similarity index 86%
rename from android_icu4j/src/main/java/android/icu/text/LaoBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java
index 687b593..9db413d 100644
--- a/android_icu4j/src/main/java/android/icu/text/LaoBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java
@@ -7,7 +7,7 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.io.IOException;
 import java.text.CharacterIterator;
@@ -15,8 +15,12 @@
 import android.icu.lang.UCharacter;
 import android.icu.lang.UProperty;
 import android.icu.lang.UScript;
+import android.icu.text.UnicodeSet;
 
-class LaoBreakEngine extends DictionaryBreakEngine {
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class LaoBreakEngine extends DictionaryBreakEngine {
 
     // Constants for LaoBreakIterator
     // How many words in a row are "good enough"?
@@ -30,27 +34,24 @@
     private static final byte LAO_MIN_WORD = 2;
 
     private DictionaryMatcher fDictionary;
-    private static UnicodeSet fLaoWordSet;
-    private static UnicodeSet fEndWordSet;
-    private static UnicodeSet fBeginWordSet;
-    private static UnicodeSet fMarkSet;
+    private UnicodeSet fEndWordSet;
+    private UnicodeSet fBeginWordSet;
+    private UnicodeSet fMarkSet;
 
-    static {
+    public LaoBreakEngine() throws IOException {
         // Initialize UnicodeSets
-        fLaoWordSet = new UnicodeSet();
-        fMarkSet = new UnicodeSet();
-        fBeginWordSet = new UnicodeSet();
-
-        fLaoWordSet.applyPattern("[[:Laoo:]&[:LineBreak=SA:]]");
-        fLaoWordSet.compact();
-
-        fMarkSet.applyPattern("[[:Laoo:]&[:LineBreak=SA:]&[:M:]]");
+        UnicodeSet laoWordSet = new UnicodeSet("[[:Laoo:]&[:LineBreak=SA:]]");
+        fMarkSet = new UnicodeSet("[[:Laoo:]&[:LineBreak=SA:]&[:M:]]");
         fMarkSet.add(0x0020);
-        fEndWordSet = new UnicodeSet(fLaoWordSet);
+        fBeginWordSet = new UnicodeSet(
+            0x0E81, 0x0EAE,  // basic consonants (including holes for corresponding Thai characters)
+            0x0EC0, 0x0EC4,  // prefix vowels
+            0x0EDC, 0x0EDD); // digraph consonants (no Thai equivalent)
+
+        laoWordSet.compact();
+
+        fEndWordSet = new UnicodeSet(laoWordSet);
         fEndWordSet.remove(0x0EC0, 0x0EC4); // prefix vowels
-        fBeginWordSet.add(0x0E81, 0x0EAE); // basic consonants (including holes for corresponding Thai characters)
-        fBeginWordSet.add(0x0EDC, 0x0EDD); // digraph consonants (no Thai equivalent)
-        fBeginWordSet.add(0x0EC0, 0x0EC4); // prefix vowels
 
         // Compact for caching
         fMarkSet.compact();
@@ -58,14 +59,12 @@
         fBeginWordSet.compact();
 
         // Freeze the static UnicodeSet
-        fLaoWordSet.freeze();
+        laoWordSet.freeze();
         fMarkSet.freeze();
         fEndWordSet.freeze();
         fBeginWordSet.freeze();
-    }
 
-    public LaoBreakEngine() throws IOException {
-        setCharacters(fLaoWordSet);
+        setCharacters(laoWordSet);
         // Initialize dictionary
         fDictionary = DictionaryData.loadDictionaryFor("Laoo");
     }
@@ -90,7 +89,7 @@
 
     @Override
     public int divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd,
-            DequeI foundBreaks) {
+            DequeI foundBreaks, boolean isPhraseBreaking) {
 
 
         if ((rangeEnd - rangeStart) < LAO_MIN_WORD) {
@@ -124,13 +123,9 @@
                 // If we're already at the end of the range, we're done
                 if (fIter.getIndex() < rangeEnd) {
                     do {
-                        int wordsMatched = 1;
                         if (words[(wordsFound+1)%LAO_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) > 0) {
-                            if (wordsMatched < 2) {
-                                // Followed by another dictionary word; mark first word as a good candidate
-                                words[wordsFound%LAO_LOOKAHEAD].markCurrent();
-                                wordsMatched = 2;
-                            }
+                            // Followed by another dictionary word; mark first word as a good candidate
+                            words[wordsFound%LAO_LOOKAHEAD].markCurrent();
 
                             // If we're already at the end of the range, we're done
                             if (fIter.getIndex() >= rangeEnd) {
diff --git a/android_icu4j/src/main/java/android/icu/text/ThaiBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java
similarity index 89%
rename from android_icu4j/src/main/java/android/icu/text/ThaiBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java
index 776046c..9d663bd 100644
--- a/android_icu4j/src/main/java/android/icu/text/ThaiBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java
@@ -7,7 +7,7 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.io.IOException;
 import java.text.CharacterIterator;
@@ -15,8 +15,12 @@
 import android.icu.lang.UCharacter;
 import android.icu.lang.UProperty;
 import android.icu.lang.UScript;
+import android.icu.text.UnicodeSet;
 
-class ThaiBreakEngine extends DictionaryBreakEngine {
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class ThaiBreakEngine extends DictionaryBreakEngine {
 
     // Constants for ThaiBreakIterator
     // How many words in a row are "good enough"?
@@ -36,32 +40,28 @@
     private static final byte THAI_MIN_WORD_SPAN = THAI_MIN_WORD * 2;
 
     private DictionaryMatcher fDictionary;
-    private static UnicodeSet fThaiWordSet;
-    private static UnicodeSet fEndWordSet;
-    private static UnicodeSet fBeginWordSet;
-    private static UnicodeSet fSuffixSet;
-    private static UnicodeSet fMarkSet;
+    private UnicodeSet fEndWordSet;
+    private UnicodeSet fBeginWordSet;
+    private UnicodeSet fSuffixSet;
+    private UnicodeSet fMarkSet;
 
-    static {
+    public ThaiBreakEngine() throws IOException {
         // Initialize UnicodeSets
-        fThaiWordSet = new UnicodeSet();
-        fMarkSet = new UnicodeSet();
-        fBeginWordSet = new UnicodeSet();
-        fSuffixSet = new UnicodeSet();
-
-        fThaiWordSet.applyPattern("[[:Thai:]&[:LineBreak=SA:]]");
-        fThaiWordSet.compact();
-
-        fMarkSet.applyPattern("[[:Thai:]&[:LineBreak=SA:]&[:M:]]");
+        UnicodeSet thaiWordSet = new UnicodeSet("[[:Thai:]&[:LineBreak=SA:]]");
+        fMarkSet = new UnicodeSet("[[:Thai:]&[:LineBreak=SA:]&[:M:]]");
         fMarkSet.add(0x0020);
-        fEndWordSet = new UnicodeSet(fThaiWordSet);
-        fEndWordSet.remove(0x0E31); // MAI HAN-AKAT
-        fEndWordSet.remove(0x0E40, 0x0E44); // SARA E through SARA AI MAIMALAI
-        fBeginWordSet.add(0x0E01, 0x0E2E); //KO KAI through HO NOKHUK
-        fBeginWordSet.add(0x0E40, 0x0E44); // SARA E through SARA AI MAIMALAI
+        fBeginWordSet = new UnicodeSet(0x0E01, 0x0E2E,  //KO KAI through HO NOKHUK
+                                       0x0E40, 0x0E44); // SARA E through SARA AI MAIMALAI
+        fSuffixSet = new UnicodeSet();
         fSuffixSet.add(THAI_PAIYANNOI);
         fSuffixSet.add(THAI_MAIYAMOK);
 
+        thaiWordSet.compact();
+
+        fEndWordSet = new UnicodeSet(thaiWordSet);
+        fEndWordSet.remove(0x0E31); // MAI HAN-AKAT
+        fEndWordSet.remove(0x0E40, 0x0E44); // SARA E through SARA AI MAIMALAI
+
         // Compact for caching
         fMarkSet.compact();
         fEndWordSet.compact();
@@ -69,15 +69,13 @@
         fSuffixSet.compact();
 
         // Freeze the static UnicodeSet
-        fThaiWordSet.freeze();
+        thaiWordSet.freeze();
         fMarkSet.freeze();
         fEndWordSet.freeze();
         fBeginWordSet.freeze();
         fSuffixSet.freeze();
-    }
 
-    public ThaiBreakEngine() throws IOException {
-        setCharacters(fThaiWordSet);
+        setCharacters(thaiWordSet);
         // Initialize dictionary
         fDictionary = DictionaryData.loadDictionaryFor("Thai");
     }
@@ -102,7 +100,7 @@
 
     @Override
     public int divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd,
-            DequeI foundBreaks) {
+            DequeI foundBreaks, boolean isPhraseBreaking) {
 
         if ((rangeEnd - rangeStart) < THAI_MIN_WORD_SPAN) {
             return 0;  // Not enough characters for word
@@ -135,13 +133,9 @@
                 if (fIter.getIndex() < rangeEnd) {
                   foundBest:
                     do {
-                        int wordsMatched = 1;
                         if (words[(wordsFound+1)%THAI_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) > 0) {
-                            if (wordsMatched < 2) {
-                                // Followed by another dictionary word; mark first word as a good candidate
-                                words[wordsFound%THAI_LOOKAHEAD].markCurrent();
-                                wordsMatched = 2;
-                            }
+                            // Followed by another dictionary word; mark first word as a good candidate
+                            words[wordsFound%THAI_LOOKAHEAD].markCurrent();
 
                             // If we're already at the end of the range, we're done
                             if (fIter.getIndex() >= rangeEnd) {
diff --git a/android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/UnhandledBreakEngine.java
similarity index 90%
rename from android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java
rename to android_icu4j/src/main/java/android/icu/impl/breakiter/UnhandledBreakEngine.java
index b035185..b5c0bed 100644
--- a/android_icu4j/src/main/java/android/icu/text/UnhandledBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/UnhandledBreakEngine.java
@@ -7,15 +7,19 @@
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  */
-package android.icu.text;
+package android.icu.impl.breakiter;
 
 import java.text.CharacterIterator;
 
 import android.icu.impl.CharacterIteration;
 import android.icu.lang.UCharacter;
 import android.icu.lang.UProperty;
+import android.icu.text.UnicodeSet;
 
-final class UnhandledBreakEngine implements LanguageBreakEngine {
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public final class UnhandledBreakEngine implements LanguageBreakEngine {
     // TODO: Use two UnicodeSets, one with all frozen sets, one with unfrozen.
     // in handleChar(), update the unfrozen version, clone, freeze, replace the frozen one.
 
@@ -44,7 +48,7 @@
 
     @Override
     public int findBreaks(CharacterIterator text, int startPos, int endPos,
-            DictionaryBreakEngine.DequeI foundBreaks) {
+            DictionaryBreakEngine.DequeI foundBreaks, boolean isPhraseBreaking) {
 
         UnicodeSet uniset = fHandled;
         int c = CharacterIteration.current32(text);
diff --git a/android_icu4j/src/main/java/android/icu/impl/coll/Collation.java b/android_icu4j/src/main/java/android/icu/impl/coll/Collation.java
index abb465f..03a4f69 100644
--- a/android_icu4j/src/main/java/android/icu/impl/coll/Collation.java
+++ b/android_icu4j/src/main/java/android/icu/impl/coll/Collation.java
@@ -254,7 +254,7 @@
      * Tag for a lead surrogate code unit.
      * Optional optimization for UTF-16 string processing.
      * Bits 31..10: Unused, 0.
-     *       9.. 8: =0: All associated supplementary code points are unassigned-implict.
+     *       9.. 8: =0: All associated supplementary code points are unassigned-implicit.
      *              =1: All associated supplementary code points fall back to the base data.
      *              else: (Normally 2) Look up the data for the supplementary code point.
      */
diff --git a/android_icu4j/src/main/java/android/icu/impl/coll/CollationBuilder.java b/android_icu4j/src/main/java/android/icu/impl/coll/CollationBuilder.java
index 0b77abe..219d37e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/coll/CollationBuilder.java
+++ b/android_icu4j/src/main/java/android/icu/impl/coll/CollationBuilder.java
@@ -472,7 +472,7 @@
             // A Hangul syllable completely inside a contraction is ok.
         }
         // Note: If there is a prefix, then the parser checked that
-        // both the prefix and the string beging with NFC boundaries (not Jamo V or T).
+        // both the prefix and the string begin with NFC boundaries (not Jamo V or T).
         // Therefore: prefix.isEmpty() || !isJamoVOrT(nfdString.charAt(0))
         // (While handling a Hangul syllable, prefixes on Jamo V or T
         // would not see the previous Jamo of that syllable.)
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 9bac086..533faee 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
@@ -125,27 +125,27 @@
 0,0,0,0,0,0,0,0,1,1,2,3,0,0,0,0,
 0,0,0,0,4,0,0,0,0,0,0,0,5,6,7,0,
 8,0,9,0xa,0,0,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0x10,
-0x11,0x12,0x13,0,0,0,0x14,0x15,0,0x16,0x17,0,0,0x16,0x18,0x19,
-0,0x16,0x18,0,0,0x16,0x18,0,0,0x16,0x18,0,0,0,0x18,0,
-0,0,0x1a,0,0,0x16,0x18,0,0,0x1b,0x18,0,0,0,0x1c,0,
-0,0x1d,0x1e,0,0,0x1d,0x1e,0,0x1f,0x20,0,0x21,0x22,0,0x23,0,
-0,0x24,0,0,0x18,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0x25,0,0,0,0,0,
+0x11,0x12,0x13,0,0x14,0,0x15,0x16,0,0x17,0x18,0,0,0x17,0x19,0x1a,
+0,0x17,0x19,0,0,0x17,0x19,0,0,0x17,0x19,0,0,0,0x19,0,
+0,0x17,0x1b,0,0,0x17,0x19,0,0,0x1c,0x19,0,0,0,0x1d,0,
+0,0x1e,0x1f,0,0,0x1e,0x1f,0,0x20,0x21,0,0x22,0x23,0,0x24,0,
+0,0x25,0,0,0x19,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0x26,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0x26,0x26,0,0,0,0,0x27,0,
-0,0,0,0,0,0x28,0,0,0,0x13,0,0,0,0,0,0,
-0x29,0,0,0x2a,0,0x2b,0x2c,0,0,0x26,0x2d,0x2e,0,0x2f,0,0x30,
-0,0x31,0,0,0,0,0x32,0x33,0,0,0,0,0,0,1,0x34,
+0,0,0,0,0,0,0,0,0x27,0x28,0,0,0,0,0x29,0,
+0,0,0,0,0,0x2a,0,0,0,0x13,0,0,0,0,0,0,
+0x2b,0,0,0x2c,0,0x2d,0x2e,0,0,0x28,0x2f,0x30,0,0x31,0,0x32,
+0,0x33,0,0,0,0,0x34,0x35,0,0,0,0,0,0,1,1,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0x35,0x36,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0x36,0x37,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0x37,0,0,0,0x38,0,0,0,1,
+0,0,0,0,0,0,0,0x38,0,0,0,0x39,0,0,0,1,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0x39,0,0,0x3a,0,0,0,0,0,0,0,0,0,0,0,
+0,0x3a,0,0,0x3b,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -204,9 +204,9 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0x3b,0x3c,0,0,0x3d,0,0,0,0,0,0,0,0,
-0x23,0x3e,0,0,0,0,0x2d,0x3f,0,0x40,0x41,0,0,0x41,0x2c,0,
-0,0,0,0,0,0x42,0x43,0x44,0,0,0,0,0,0,0,0x18,
+0,0,0,0x3c,0x3d,0,0,0x3e,0,0,0,0,0,0,0,0,
+0x24,0x3f,0,0,0,0,0x2f,0x40,0,0x41,0x42,0,0,0x42,0x43,0,
+0,0,0,0,0,0x44,0x45,0x46,0,0,0,0,0,0,0,0x19,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -229,7 +229,7 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0x45,0x46,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0x47,0x48,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
@@ -246,37 +246,45 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0x19,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0x1a,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0
 };
 
+    private static final int[] lcccBits={
+0,0xffffffff,0xffff7fff,0xffff,0xf8,0xfffe0000,0xbfffffff,0xb6,0x7ff0000,0xfffff800,0x10000,0x9fc00000,0x3d9f,0x20000,0xffff0000,0x7ff,
+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
+};
+
     private static final byte[] tcccIndex={
 0,0,0,0,0,0,2,3,4,5,6,7,0,8,9,0xa,
 0xb,0xc,0,0,0,0,0,0,1,1,0xd,0xe,0xf,0x10,0x11,0,
 0x12,0x13,0x14,0x15,0x16,0,0x17,0x18,0,0,0,0,0x19,0x1a,0x1b,0,
 0x1c,0x1d,0x1e,0x1f,0,0,0x20,0x21,0x22,0x23,0x24,0,0,0,0,0x25,
-0x26,0x27,0x28,0,0,0,0x29,0x2a,0,0x2b,0x2c,0,0,0x2d,0x2e,0x2f,
-0,0x30,0x31,0,0,0x2d,0x32,0,0,0x2d,0x33,0,0,0,0x32,0,
-0,0,0x34,0,0,0x2d,0x32,0,0,0x35,0x32,0,0,0,0x36,0,
-0,0x37,0x38,0,0,0x37,0x38,0,0x39,0x3a,0,0x3b,0x3c,0,0x3d,0,
-0,0x3e,0,0,0x32,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0x3f,0,0,0,0,0,
+0x26,0x27,0x28,0,0x29,0,0x2a,0x2b,0,0x2c,0x2d,0,0,0x2e,0x2f,0x30,
+0,0x31,0x32,0,0,0x2e,0x33,0,0,0x2e,0x34,0,0,0,0x33,0,
+0,0x2e,0x35,0,0,0x2e,0x33,0,0,0x36,0x33,0,0,0,0x37,0,
+0,0x38,0x39,0,0,0x38,0x39,0,0x3a,0x3b,0,0x3c,0x3d,0,0x3e,0,
+0,0x3f,0,0,0x33,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0x40,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0x40,0x40,0,0,0,0,0x41,0,
-0,0,0,0,0,0x42,0,0,0,0x28,0,0,0,0,0,0,
-0x43,0,0,0x44,0,0x45,0x46,0,0,0x40,0x47,0x48,0,0x49,0,0x4a,
-0,0x4b,0,0,0,0,0x4c,0x4d,0,0,0,0,0,0,1,0x4e,
-1,1,1,1,0x4f,1,1,0x50,0x51,1,0x52,0x53,1,0x54,0x55,0x56,
-0,0,0,0,0,0,0x57,0x58,0,0x59,0,0,0x5a,0x5b,0x5c,0,
-0x5d,0x5e,0x5f,0x60,0x61,0x62,0,0x63,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0x41,0x42,0,0,0,0,0x43,0,
+0,0,0,0,0,0x44,0,0,0,0x28,0,0,0,0,0,0,
+0x45,0,0,0x46,0,0x47,0x48,0,0,0x42,0x49,0x4a,0,0x4b,0,0x4c,
+0,0x4d,0,0,0,0,0x4e,0x4f,0,0,0,0,0,0,1,1,
+1,1,1,1,0x50,1,1,0x51,0x52,1,0x53,0x54,1,0x55,0x56,0x57,
+0,0,0,0,0,0,0x58,0x59,0,0x5a,0,0,0x5b,0x5c,0x5d,0,
+0x5e,0x5f,0x60,0x61,0x62,0x63,0,0x64,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0x2d,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0x64,0,0,0,0x65,0,0,0,1,
+0,0,0,0,0,0,0x2e,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0x65,0,0,0,0x66,0,0,0,1,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0x66,0x67,0x68,0x69,0x67,0x68,0x6a,0,0,0,0,0,0,0,0,
+0,0x67,0x68,0x69,0x6a,0x68,0x69,0x6b,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -335,9 +343,9 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0x6b,0x6c,0,0,0x6d,0,0,0,0,0,0,0,0,
-0x3d,0x6e,0,0,0,0,0x47,0x6f,0,0x70,0x71,0,0,0x71,0x46,0,
-0,0,0,0,0,0x72,0x73,0x74,0,0,0,0,0,0,0,0x32,
+0,0,0,0x6c,0x6d,0,0,0x6e,0,0,0,0,0,0,0,0,
+0x3e,0x6f,0,0,0,0,0x49,0x70,0,0x71,0x72,0,0,0x72,0x73,0,
+0,0,0,0,0,0x74,0x75,0x76,0,0,0,0,0,0,0,0x33,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -360,7 +368,7 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0x75,0x76,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0x77,0x78,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -377,27 +385,20 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0x3f,0x77,0x78,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0x40,0x79,0x7a,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0xe,0,0,0,0,0,0,0,0,0,0,0,0,0,0
 };
 
-    private static final int[] lcccBits={
-0,0xffffffff,0xffff7fff,0xffff,0xf8,0xfffe0000,0xbfffffff,0xb6,0x7ff0000,0xfffff800,0x10000,0x9fc00000,0x3d9f,0x20000,0xffff0000,0x7ff,
-0x200ff800,0xfbc00000,0x3eef,0xe000000,0xfff80000,0xfffffffb,0x10000000,0x1e2000,0x2000,0x40000000,0x602000,0x18000000,0x400,0x7000000,0xf00,0x3000000,
-0x2a00000,0x3c3e0000,0xdf,0x40,0x6800000,0xe0000000,0x100000,0x20040000,0x200,0x1800000,0x9fe00001,0xbfff0000,1,0x10,0xff800,0xc00,
-0xc0040,0x800000,0xfff70000,0x31021fd,0xfbffffff,0x1fff0000,0x1ffe2,0x38000,0x80000000,0xfc00,0x6000000,0x3ff08000,0xc0000000,0x30000,0x1000,0x3ffff,
-0x3800,0x80000,0xc19d0000,2,0x400000,0xc0000fd,0x5108000
-};
     private static final int[] tcccBits={
 0,0xffffffff,0x3e7effbf,0xbe7effbf,0xfffcffff,0x7ef1ff3f,0xfff3f1f8,0x7fffff3f,0x18003,0xdfffe000,0xff31ffcf,0xcfffffff,0xfffc0,0xffff7fff,0xffff,0x1d760,
 0x1fc00,0x187c00,0x200708b,0x2000000,0x708b0000,0xc00000,0xf8,0xfccf0006,0x33ffcfc,0xfffe0000,0xbfffffff,0xb6,0x7ff0000,0x7c,0xfffff800,0x10000,
-0x9fc80005,0x3d9f,0x20000,0xffff0000,0x7ff,0x200ff800,0xfbc00000,0x3eef,0xe000000,0xfff80000,0xfffffffb,0x10120200,0xff1e2000,0x10000000,0xb0002000,0x40000000,
-0x10480000,0x4e002000,0x2000,0x30002000,0x602100,0x18000000,0x24000400,0x7000000,0xf00,0x3000000,0x2a00000,0x3d7e0000,0xdf,0x40,0x6800000,0xe0000000,
-0x100000,0x20040000,0x200,0x1800000,0x9fe00001,0xbfff0000,1,0x10,0xff800,0xc00,0xc0040,0x800000,0xfff70000,0x31021fd,0xfbffffff,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,0xc19d0000,2,0x400000,0xc0000fd,0x5108000,0x5f7ffc00,0x7fdb
+0x9fc80005,0x3d9f,0x20000,0xffff0000,0x7ff,0x200ff800,0xfbc00000,0x3eef,0xe000000,0xff000000,0xfffffc00,0xfffffffb,0x10120200,0xff1e2000,0x10000000,0xb0002000,
+0x40000000,0x10480000,0x4e002000,0x2000,0x30002000,0x602100,0x18000000,0x24000400,0x7000000,0xf00,0x3000000,0x2a00000,0x3d7e0000,0xdf,0x40,0x6800000,
+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
 };
 
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/coll/CollationLoader.java b/android_icu4j/src/main/java/android/icu/impl/coll/CollationLoader.java
index d61e666..257365f 100644
--- a/android_icu4j/src/main/java/android/icu/impl/coll/CollationLoader.java
+++ b/android_icu4j/src/main/java/android/icu/impl/coll/CollationLoader.java
@@ -119,7 +119,7 @@
 
         ULocale validLocale = bundle.getULocale();
         // Normalize the root locale. See
-        // http://bugs.icu-project.org/trac/ticket/10715
+        // https://unicode-org.atlassian.net/browse/ICU-10715
         String validLocaleName = validLocale.getName();
         if (validLocaleName.length() == 0 || validLocaleName.equals("root")) {
             validLocale = ULocale.ROOT;
@@ -187,7 +187,7 @@
 
         // Is this the same as the root collator? If so, then use that instead.
         ULocale actualLocale = data.getULocale();
-        // http://bugs.icu-project.org/trac/ticket/10715 ICUResourceBundle(root).getULocale() != ULocale.ROOT
+        // https://unicode-org.atlassian.net/browse/ICU-10715 ICUResourceBundle(root).getULocale() != ULocale.ROOT
         // Therefore not just if (actualLocale.equals(ULocale.ROOT) && type.equals("standard")) {
         String actualLocaleName = actualLocale.getName();
         if (actualLocaleName.length() == 0 || actualLocaleName.equals("root")) {
diff --git a/android_icu4j/src/main/java/android/icu/impl/data/TokenIterator.java b/android_icu4j/src/main/java/android/icu/impl/data/TokenIterator.java
index 87a3be4..6610a18 100644
--- a/android_icu4j/src/main/java/android/icu/impl/data/TokenIterator.java
+++ b/android_icu4j/src/main/java/android/icu/impl/data/TokenIterator.java
@@ -89,7 +89,7 @@
     public int getLineNumber() {
         return reader.getLineNumber();
     }
-    
+
     /**
      * Return a string description of the position of the last line
      * returned by readLine() or readLineSkippingComments().
@@ -97,7 +97,7 @@
     public String describePosition() {
         return reader.describePosition() + ':' + (lastpos+1);
     }
-    
+
     /**
      * Read the next token from 'this.line' and append it to
      * 'this.buf'.  Tokens are separated by Pattern_White_Space.  Tokens
@@ -129,22 +129,17 @@
             buf.append(c);
             break;
         }
-        int[] posref = null;
         while (position < line.length()) {
             c = line.charAt(position); // 16-bit ok
             if (c == '\\') {
-                if (posref == null) {
-                    posref = new int[1];
-                }
-                posref[0] = position+1;
-                int c32 = Utility.unescapeAt(line, posref);
-                if (c32 < 0) {
+                int cpAndLength = Utility.unescapeAndLengthAt(line, position + 1);
+                if (cpAndLength < 0) {
                     throw new RuntimeException("Invalid escape at " +
                                                reader.describePosition() + ':' +
                                                position);
                 }
-                UTF16.append(buf, c32);
-                position = posref[0];
+                UTF16.append(buf, Utility.cpFromCodePointAndLength(cpAndLength));
+                position += 1 + Utility.lengthFromCodePointAndLength(cpAndLength);
             } else if ((quote != 0 && c == quote) ||
                        (quote == 0 && PatternProps.isWhiteSpace(c))) {
                 return ++position;
diff --git a/android_icu4j/src/main/java/android/icu/impl/duration/PeriodBuilder.java b/android_icu4j/src/main/java/android/icu/impl/duration/PeriodBuilder.java
index b809425..2331312 100644
--- a/android_icu4j/src/main/java/android/icu/impl/duration/PeriodBuilder.java
+++ b/android_icu4j/src/main/java/android/icu/impl/duration/PeriodBuilder.java
@@ -35,7 +35,7 @@
   /**
    * Create a period of the given duration using the provided reference date.
    *
-   * @param duration the duration in milliseconds from the referenct time
+   * @param duration the duration in milliseconds from the referenced time
    * to the target time.  A negative duration indicates a time before the
    * reference time
    * @param referenceDate the reference date from which to compute the period
diff --git a/android_icu4j/src/main/java/android/icu/impl/duration/TimeUnit.java b/android_icu4j/src/main/java/android/icu/impl/duration/TimeUnit.java
index 316feb9..f663e56 100644
--- a/android_icu4j/src/main/java/android/icu/impl/duration/TimeUnit.java
+++ b/android_icu4j/src/main/java/android/icu/impl/duration/TimeUnit.java
@@ -81,7 +81,7 @@
       they are measured */
 
   // hack, initialization long array using expressions with 'L' at end doesn't
-  // compute entire expression using 'long'.  differs from initializtion of
+  // compute entire expression using 'long'.  differs from initialization of
   // a single constant
   static final long[] approxDurations = {
     36525L*24*60*60*10, 3045*24*60*60*10L, 7*24*60*60*1000L, 24*60*60*1000L,
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java b/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java
index 00d2ee6..41c3292 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/LanguageTag.java
@@ -599,6 +599,12 @@
         return false;
     }
 
+    public static boolean isTKey(String s) {
+        // tkey        =  = alpha digit ;
+        return (s.length() == 2) && AsciiUtil.isAlpha(s.charAt(0))
+            && AsciiUtil.isNumeric(s.charAt(1));
+    }
+
     public static boolean isExtensionSingleton(String s) {
         // singleton     = DIGIT               ; 0 - 9
         //               / %x41-57             ; A - W
@@ -661,18 +667,20 @@
 
     public static String canonicalizeExtension(String s) {
         s = AsciiUtil.toLowerString(s);
-        int found;
-        while (s.endsWith("-true")) {
-            s = s.substring(0, s.length() - 5);  // length of "-true" is 5
-        }
-        while ((found = s.indexOf("-true-")) > 0) {
-            s = s.substring(0, found) + s.substring(found + 5);  // length of "-true" is 5
-        }
-        while (s.endsWith("-yes")) {
-            s = s.substring(0, s.length() - 4);  // length of "-yes" is 4
-        }
-        while ((found = s.indexOf("-yes-")) > 0) {
-            s = s.substring(0, found) + s.substring(found + 4);  // length of "-yes" is 5
+        if (s.startsWith("u-")) {
+            int found;
+            while (s.endsWith("-true")) {
+                s = s.substring(0, s.length() - 5);  // length of "-true" is 5
+            }
+            while ((found = s.indexOf("-true-")) > 0) {
+                s = s.substring(0, found) + s.substring(found + 5);  // length of "-true" is 5
+            }
+            while (s.endsWith("-yes")) {
+                s = s.substring(0, s.length() - 4);  // length of "-yes" is 4
+            }
+            while ((found = s.indexOf("-yes-")) > 0) {
+                s = s.substring(0, found) + s.substring(found + 4);  // length of "-yes" is 5
+            }
         }
         return s;
     }
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java b/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java
index 4e290e0..f8aea60 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java
@@ -196,8 +196,9 @@
         String name = locale.getName();  // Faster than .toLanguageTag().
         if (name.startsWith("@x=")) {
             String tag = locale.toLanguageTag();
-            assert tag.startsWith("x-");
-            // Private use language tag x-subtag-subtag...
+            assert tag.startsWith("und-x-");
+            // Private use language tag x-subtag-subtag... which CLDR changes to
+            // und-x-subtag-subtag...
             return new LSR(tag, "", "", LSR.EXPLICIT_LSR);
         }
         return makeMaximizedLsr(locale.getLanguage(), locale.getScript(), locale.getCountry(),
@@ -206,8 +207,9 @@
 
     public LSR makeMaximizedLsrFrom(Locale locale) {
         String tag = locale.toLanguageTag();
-        if (tag.startsWith("x-")) {
-            // Private use language tag x-subtag-subtag...
+        if (tag.startsWith("x-") || tag.startsWith("und-x-")) {
+            // Private use language tag x-subtag-subtag... which CLDR changes to
+            // und-x-subtag-subtag...
             return new LSR(tag, "", "", LSR.EXPLICIT_LSR);
         }
         return makeMaximizedLsr(locale.getLanguage(), locale.getScript(), locale.getCountry(),
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternProvider.java b/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternProvider.java
index 251656a..c7f8820 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternProvider.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/AffixPatternProvider.java
@@ -45,4 +45,9 @@
      * number instead of rendering the number.
      */
     public boolean hasBody();
+
+    /**
+     * True if the currency symbol should replace the decimal separator.
+     */
+    public boolean currencyAsDecimal();
 }
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 44b847e..4bacf6d 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
@@ -84,26 +84,29 @@
     /** Represents a plus sign symbol '+'. */
     public static final int TYPE_PLUS_SIGN = -2;
 
+    // Represents an approximately sign symbol '~'.
+    public static final int TYPE_APPROXIMATELY_SIGN = -3;
+
     /** Represents a percent sign symbol '%'. */
-    public static final int TYPE_PERCENT = -3;
+    public static final int TYPE_PERCENT = -4;
 
     /** Represents a permille sign symbol '‰'. */
-    public static final int TYPE_PERMILLE = -4;
+    public static final int TYPE_PERMILLE = -5;
 
     /** Represents a single currency symbol '¤'. */
-    public static final int TYPE_CURRENCY_SINGLE = -5;
+    public static final int TYPE_CURRENCY_SINGLE = -6;
 
     /** Represents a double currency symbol '¤¤'. */
-    public static final int TYPE_CURRENCY_DOUBLE = -6;
+    public static final int TYPE_CURRENCY_DOUBLE = -7;
 
     /** Represents a triple currency symbol '¤¤¤'. */
-    public static final int TYPE_CURRENCY_TRIPLE = -7;
+    public static final int TYPE_CURRENCY_TRIPLE = -8;
 
     /** Represents a quadruple currency symbol '¤¤¤¤'. */
-    public static final int TYPE_CURRENCY_QUAD = -8;
+    public static final int TYPE_CURRENCY_QUAD = -9;
 
     /** Represents a quintuple currency symbol '¤¤¤¤¤'. */
-    public static final int TYPE_CURRENCY_QUINT = -9;
+    public static final int TYPE_CURRENCY_QUINT = -10;
 
     /** Represents a sequence of six or more currency symbols. */
     public static final int TYPE_CURRENCY_OVERFLOW = -15;
@@ -275,6 +278,9 @@
             return NumberFormat.Field.SIGN;
         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;
         case TYPE_PERCENT:
             return NumberFormat.Field.PERCENT;
         case TYPE_PERMILLE:
@@ -511,6 +517,8 @@
                     return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
                 case '+':
                     return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
+                case '~':
+                    return makeTag(offset + count, TYPE_APPROXIMATELY_SIGN, STATE_BASE, 0);
                 case '%':
                     return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
                 case '‰':
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 c65f2d0..4b33b63 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
@@ -12,6 +12,7 @@
 import android.icu.impl.StandardPlural;
 import android.icu.impl.UResource;
 import android.icu.text.CompactDecimalFormat.CompactStyle;
+import android.icu.text.PluralRules;
 import android.icu.util.ICUException;
 import android.icu.util.ULocale;
 import android.icu.util.UResourceBundle;
@@ -104,10 +105,6 @@
             byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1);
             for (Map.Entry<String, String> pluralEntry : magnitudeEntry.getValue().entrySet()) {
                 String pluralString = pluralEntry.getKey().toString();
-                if ("0".equals(pluralString) || "1".equals(pluralString)) {
-                    // TODO(ICU-21258): Handle this case. For now, skip.
-                    continue;
-                }
                 StandardPlural plural = StandardPlural.fromString(pluralString);
                 String patternString = pluralEntry.getValue().toString();
                 patterns[getIndex(magnitude, plural)] = patternString;
@@ -135,14 +132,27 @@
         return multipliers[magnitude];
     }
 
-    public String getPattern(int magnitude, StandardPlural plural) {
+    public String getPattern(int magnitude, PluralRules rules, DecimalQuantity dq) {
         if (magnitude < 0) {
             return null;
         }
         if (magnitude > largestMagnitude) {
             magnitude = largestMagnitude;
         }
-        String patternString = patterns[getIndex(magnitude, plural)];
+        String patternString = null;
+        if (dq.isHasIntegerValue()) {
+            long i = dq.toLong(true);
+            if (i == 0) {
+                patternString = patterns[getIndex(magnitude, StandardPlural.EQ_0)];
+            } else if (i == 1) {
+                patternString = patterns[getIndex(magnitude, StandardPlural.EQ_1)];
+            }
+            if (patternString != null) {
+                return patternString;
+            }
+        }
+        StandardPlural plural = dq.getStandardPlural(rules);
+        patternString = patterns[getIndex(magnitude, plural)];
         if (patternString == null && plural != StandardPlural.OTHER) {
             // Fall back to "other" plural variant
             patternString = patterns[getIndex(magnitude, StandardPlural.OTHER)];
@@ -186,12 +196,6 @@
                 // Iterate over the plural variants ("one", "other", etc)
                 UResource.Table pluralVariantsTable = value.getTable();
                 for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
-
-                    if ("0".equals(key.toString()) || "1".equals(key.toString())) {
-                        // TODO(ICU-21258): Handle this case. For now, skip.
-                        continue;
-                    }
-
                     // Skip this magnitude/plural if we already have it from a child locale.
                     // Note: This also skips USE_FALLBACK entries.
                     StandardPlural plural = StandardPlural.fromString(key.toString());
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/CurrencyPluralInfoAffixProvider.java b/android_icu4j/src/main/java/android/icu/impl/number/CurrencyPluralInfoAffixProvider.java
index 213ffac..8edcedf 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/CurrencyPluralInfoAffixProvider.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/CurrencyPluralInfoAffixProvider.java
@@ -72,4 +72,9 @@
     public boolean hasBody() {
         return affixesByPlural[StandardPlural.OTHER.ordinal()].hasBody();
     }
+
+    @Override
+    public boolean currencyAsDecimal() {
+        return affixesByPlural[StandardPlural.OTHER.ordinal()].currencyAsDecimal();
+    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java b/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java
index d701344..001390f 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/DecimalFormatProperties.java
@@ -97,6 +97,7 @@
     private transient boolean decimalPatternMatchRequired;
     private transient boolean decimalSeparatorAlwaysShown;
     private transient boolean exponentSignAlwaysShown;
+    private transient boolean currencyAsDecimal;
     private transient int formatWidth;
     private transient int groupingSize;
     private transient boolean groupingUsed;
@@ -169,6 +170,7 @@
         decimalPatternMatchRequired = false;
         decimalSeparatorAlwaysShown = false;
         exponentSignAlwaysShown = false;
+        currencyAsDecimal = false;
         formatWidth = -1;
         groupingSize = -1;
         groupingUsed = true;
@@ -215,6 +217,7 @@
         decimalPatternMatchRequired = other.decimalPatternMatchRequired;
         decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
         exponentSignAlwaysShown = other.exponentSignAlwaysShown;
+        currencyAsDecimal = other.currencyAsDecimal;
         formatWidth = other.formatWidth;
         groupingSize = other.groupingSize;
         groupingUsed = other.groupingUsed;
@@ -262,6 +265,7 @@
         eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired);
         eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown);
         eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown);
+        eq = eq && _equalsHelper(currencyAsDecimal, other.currencyAsDecimal);
         eq = eq && _equalsHelper(formatWidth, other.formatWidth);
         eq = eq && _equalsHelper(groupingSize, other.groupingSize);
         eq = eq && _equalsHelper(groupingUsed, other.groupingUsed);
@@ -325,6 +329,7 @@
         hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
         hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
         hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
+        hashCode ^= _hashCodeHelper(currencyAsDecimal);
         hashCode ^= _hashCodeHelper(formatWidth);
         hashCode ^= _hashCodeHelper(groupingSize);
         hashCode ^= _hashCodeHelper(groupingUsed);
@@ -448,6 +453,10 @@
         return exponentSignAlwaysShown;
     }
 
+    public boolean getCurrencyAsDecimal() {
+        return currencyAsDecimal;
+    }
+
     public int getFormatWidth() {
         return formatWidth;
     }
@@ -775,6 +784,18 @@
     }
 
     /**
+     * Sets whether the currency symbol should replace the decimal separator.
+     *
+     * @param currencyAsDecimal
+     *              Whether the currency symbol should replace the decimal separator.
+     * @return The property bag, for chaining.
+     */
+    public DecimalFormatProperties setCurrencyAsDecimal(boolean currencyAsDecimal) {
+        this.currencyAsDecimal = currencyAsDecimal;
+        return this;
+    }
+
+    /**
      * Sets the minimum width of the string output by the formatting pipeline. For example, if padding is
      * enabled and paddingWidth is set to 6, formatting the number "3.14159" with the pattern "0.00" will
      * result in "··3.14" if '·' is your padding string.
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 ddf546b..b73fc87 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
@@ -145,6 +145,11 @@
     public void adjustExponent(int delta);
 
     /**
+     * Resets the DecimalQuantity to the value before adjustMagnitude and adjustExponent.
+     */
+    public void resetExponent();
+
+    /**
      * @return Whether the value represented by this {@link DecimalQuantity} is
      * zero, infinity, or NaN.
      */
@@ -169,6 +174,18 @@
 
     public BigDecimal toBigDecimal();
 
+    /**
+     * Returns a long approximating the decimal quantity. A long can only represent the
+     * integral part of the number.  Note: this method incorporates the value of
+     * {@code getExponent} (for cases such as compact notation) to return the proper long
+     * value represented by the result.
+     *
+     * @param truncateIfOverflow if false and the number does NOT fit, fails with an error.
+     *        See comment about call site guards in DecimalQuantity_AbstractBCD.java
+     * @return A 64-bit integer representation of the internal number.
+     */
+    public long toLong(boolean truncateIfOverflow);
+
     public void setToBigDecimal(BigDecimal input);
 
     public int maxRepresentableDigits();
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 0bb8254..046024e 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
@@ -236,6 +236,17 @@
     }
 
     @Override
+    public void resetExponent() {
+        adjustMagnitude(exponent);
+        exponent = 0;
+    }
+
+    @Override
+    public boolean isHasIntegerValue() {
+        return scale >= 0;
+    }
+
+    @Override
     public StandardPlural getStandardPlural(PluralRules rules) {
         if (rules == null) {
             // Fail gracefully if the user didn't provide a PluralRules
@@ -267,6 +278,9 @@
             return fractionCountWithoutTrailingZeros();
         case e:
             return getExponent();
+        case c:
+            // Plural operand `c` is currently an alias for `e`.
+            return getExponent();
         default:
             return Math.abs(toDouble());
         }
@@ -602,15 +616,7 @@
         scale -= fracLength;
     }
 
-    /**
-     * Returns a long approximating the internal BCD. A long can only represent the integral part of the
-     * number.  Note: this method incorporates the value of {@code exponent}
-     * (for cases such as compact notation) to return the proper long value
-     * represented by the result.
-     *
-     * @param truncateIfOverflow if false and the number does NOT fit, fails with an assertion error.
-     * @return A 64-bit integer representation of the internal BCD.
-     */
+    @Override
     public long toLong(boolean truncateIfOverflow) {
         // NOTE: Call sites should be guarded by fitsInLong(), like this:
         // if (dq.fitsInLong()) { /* use dq.toLong() */ } else { /* use some fallback */ }
@@ -920,6 +926,7 @@
 
             // Perform truncation
             if (position >= precision) {
+                assert trailingDigit == 0;
                 setBcdToZero();
                 scale = magnitude;
             } else {
@@ -937,6 +944,10 @@
                     // do not return: use the bubbling logic below
                 } else {
                     setDigitPos(0, (byte) 5);
+                    // If the quantity was set to 0, we may need to restore a digit.
+                    if (precision == 0) {
+                        precision = 1;
+                    }
                     // compact not necessary: digit at position 0 is nonzero
                     return;
                 }
@@ -1161,8 +1172,10 @@
     protected abstract byte getDigitPos(int position);
 
     /**
-     * Sets the digit in the BCD list. This method only sets the digit; it is the caller's responsibility
-     * to call {@link #compact} after setting the digit.
+     * Sets the digit in the BCD list. This method only sets the digit; it is the caller's
+     * responsibility to call {@link #compact} after setting the digit, and to ensure
+     * that the precision field is updated to reflect the correct number of digits if a
+     * nonzero digit is added to the decimal.
      *
      * @param position
      *            The position of the digit to pop, counted in BCD units from the least significant
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Grouper.java b/android_icu4j/src/main/java/android/icu/impl/number/Grouper.java
index c5db57b..932a814 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/Grouper.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Grouper.java
@@ -106,7 +106,7 @@
     private final short grouping2;
 
     /**
-     * The minimum gropuing size, with the following special values:
+     * The minimum grouping size, with the following special values:
      * <ul>
      * <li>-2 = needs locale data
      * <li>-3 = no less than 2
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 33dd783..34f5595 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
@@ -3,6 +3,7 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.number;
 
+import java.util.ArrayList;
 import java.util.EnumMap;
 import java.util.Map;
 import java.util.MissingResourceException;
@@ -10,35 +11,47 @@
 import android.icu.impl.CurrencyData;
 import android.icu.impl.ICUData;
 import android.icu.impl.ICUResourceBundle;
+import android.icu.impl.PatternProps;
 import android.icu.impl.SimpleFormatterImpl;
 import android.icu.impl.StandardPlural;
 import android.icu.impl.UResource;
 import android.icu.impl.number.Modifier.Signum;
+import android.icu.impl.units.MeasureUnitImpl;
+import android.icu.impl.units.SingleUnitImpl;
+import android.icu.lang.UCharacter;
 import android.icu.number.NumberFormatter.UnitWidth;
 import android.icu.text.NumberFormat;
 import android.icu.text.PluralRules;
 import android.icu.util.Currency;
 import android.icu.util.ICUException;
 import android.icu.util.MeasureUnit;
+import android.icu.util.MeasureUnit.Complexity;
+import android.icu.util.MeasureUnit.MeasurePrefix;
 import android.icu.util.ULocale;
 import android.icu.util.UResourceBundle;
 
 /**
+ * Takes care of formatting currency and measurement unit names, as well as populating the gender of measure units.
  * @hide Only a subset of ICU is exposed in Android
  */
 public class LongNameHandler
     implements MicroPropsGenerator, ModifierStore, LongNameMultiplexer.ParentlessMicroPropsGenerator {
 
-    private static final int DNAM_INDEX = StandardPlural.COUNT;
-    private static final int PER_INDEX = StandardPlural.COUNT + 1;
-    static final int ARRAY_LENGTH = StandardPlural.COUNT + 2;
+    private static int i = 0;
+    private static final int DNAM_INDEX = StandardPlural.COUNT + i++;
+    private static final int PER_INDEX = StandardPlural.COUNT + i++;
+    private static final int GENDER_INDEX = StandardPlural.COUNT + i++;
+    static final int ARRAY_LENGTH = StandardPlural.COUNT + i++;
 
+    // Returns the array index that corresponds to the given pluralKeyword.
     private static int getIndex(String pluralKeyword) {
-        // pluralKeyword can also be "dnam" or "per"
+        // pluralKeyword can also be "dnam", "per" or "gender"
         if (pluralKeyword.equals("dnam")) {
             return DNAM_INDEX;
         } else if (pluralKeyword.equals("per")) {
             return PER_INDEX;
+        } else if (pluralKeyword.equals("gender")) {
+            return GENDER_INDEX;
         } else {
             return StandardPlural.fromString(pluralKeyword).ordinal();
         }
@@ -56,43 +69,215 @@
         return result;
     }
 
+    private enum PlaceholderPosition { NONE, BEGINNING, MIDDLE, END }
+
+    private static class ExtractCorePatternResult {
+        String coreUnit;
+        PlaceholderPosition placeholderPosition;
+        char joinerChar;
+    }
+
+    /**
+     * Returns three outputs extracted from pattern.
+     *
+     * @param coreUnit is extracted as per Extract(...) in the spec:
+     *   https://unicode.org/reports/tr35/tr35-general.html#compound-units
+     * @param PlaceholderPosition indicates where in the string the placeholder
+     *   was found.
+     * @param joinerChar Iff the placeholder was at the beginning or end,
+     *   joinerChar contains the space character (if any) that separated the
+     *   placeholder from the rest of the pattern. Otherwise, joinerChar is set
+     *   to NUL. Only one space character is considered.
+     */
+    private static ExtractCorePatternResult extractCorePattern(String pattern) {
+        ExtractCorePatternResult result = new ExtractCorePatternResult();
+        result.joinerChar = 0;
+        int len = pattern.length();
+        if (pattern.startsWith("{0}")) {
+            result.placeholderPosition = PlaceholderPosition.BEGINNING;
+            if (len > 3 && Character.isSpaceChar(pattern.charAt(3))) {
+                result.joinerChar = pattern.charAt(3);
+                result.coreUnit = pattern.substring(4);
+            } else {
+                result.coreUnit = pattern.substring(3);
+            }
+        } else if (pattern.endsWith("{0}")) {
+            result.placeholderPosition = PlaceholderPosition.END;
+            if (Character.isSpaceChar(pattern.charAt(len - 4))) {
+                result.coreUnit = pattern.substring(0, len - 4);
+                result.joinerChar = pattern.charAt(len - 4);
+            } else {
+                result.coreUnit = pattern.substring(0, len - 3);
+            }
+        } else if (pattern.indexOf("{0}", 1) == -1) {
+            result.placeholderPosition = PlaceholderPosition.NONE;
+            result.coreUnit = pattern;
+        } else {
+            result.placeholderPosition = PlaceholderPosition.MIDDLE;
+            result.coreUnit = pattern;
+        }
+        return result;
+    }
+
     //////////////////////////
     /// BEGIN DATA LOADING ///
     //////////////////////////
 
-    private static final class PluralTableSink extends UResource.Sink {
+    // Gets the gender of a built-in unit: unit must be a built-in. Returns an empty
+    // string both in case of unknown gender and in case of unknown unit.
+    private static String getGenderForBuiltin(ULocale locale, MeasureUnit builtinUnit) {
+        ICUResourceBundle unitsBundle = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
 
-        String[] outArray;
+        StringBuilder key = new StringBuilder();
+        key.append("units/");
+        key.append(builtinUnit.getType());
+        key.append("/");
 
-        public PluralTableSink(String[] outArray) {
+        // Map duration-year-person, duration-week-person, etc. to duration-year, duration-week, ...
+        // TODO(ICU-20400): Get duration-*-person data properly with aliases.
+        if (builtinUnit.getSubtype() != null && builtinUnit.getSubtype().endsWith("-person")) {
+            key.append(builtinUnit.getSubtype(), 0, builtinUnit.getSubtype().length() - 7);
+        } else {
+            key.append(builtinUnit.getSubtype());
+        }
+        key.append("/gender");
+
+        try {
+            return unitsBundle.getWithFallback(key.toString()).getString();
+        } catch (MissingResourceException e) {
+            // TODO(icu-units#28): "$unitRes/gender" does not exist. Do we want to
+            // check whether the parent "$unitRes" exists? Then we could return
+            // U_MISSING_RESOURCE_ERROR for incorrect usage (e.g. builtinUnit not
+            // being a builtin).
+            return "";
+        }
+    }
+
+    // Loads data from a resource tree with paths matching
+    // $key/$pluralForm/$gender/$case, with lateral inheritance for missing cases
+    // and genders.
+    //
+    // An InflectedPluralSink is configured to load data for a specific gender and
+    // case. It loads all plural forms, because selection between plural forms is
+    // dependent upon the value being formatted.
+    //
+    // See data/unit/de.txt and data/unit/fr.txt for examples - take a look at
+    // units/compound/power2: German has case, French has differences for
+    // gender, but no case.
+    //
+    // TODO(icu-units#138): Conceptually similar to PluralTableSink, however the
+    // tree structures are different. After homogenizing the structures, we may be
+    // able to unify the two classes.
+    //
+    // TODO: Spec violation: expects presence of "count" - does not fallback to an
+    // absent "count"! If this fallback were added, getCompoundValue could be
+    // superseded?
+    private static final class InflectedPluralSink extends UResource.Sink {
+        // NOTE: outArray MUST have a length of at least ARRAY_LENGTH. No bounds
+        // checking is performed.
+        public InflectedPluralSink(String gender, String caseVariant, String[] outArray) {
+            this.gender = gender;
+            this.caseVariant = caseVariant;
             this.outArray = outArray;
+            for (int i = 0; i < ARRAY_LENGTH; i++) {
+                outArray[i] = null;
+            }
         }
 
+        // See ResourceSink::put().
         @Override
         public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
             UResource.Table pluralsTable = value.getTable();
             for (int i = 0; pluralsTable.getKeyAndValue(i, key, value); ++i) {
-                int index = getIndex(key.toString());
-                if (outArray[index] != null) {
+                String keyString = key.toString();
+                int pluralIndex = getIndex(keyString);
+                if (outArray[pluralIndex] != null) {
+                    // We already have a pattern
                     continue;
                 }
-                String formatString = value.getString();
-                outArray[index] = formatString;
+                UResource.Table genderTable = value.getTable();
+                if (loadForPluralForm(genderTable, value)) {
+                    outArray[pluralIndex] = value.getString();
+                }
             }
         }
+
+        // Tries to load data for the configured gender from `genderTable`. The
+        // returned data will be for the configured gender if found, falling
+        // back to "neuter" and no-gender. If none of those are found, null is
+        // returned.
+        private boolean loadForPluralForm(UResource.Table genderTable, UResource.Value value) {
+            if (gender != null && !gender.isEmpty()) {
+                if (loadForGender(genderTable, gender, value)) {
+                    return true;
+                }
+                if (gender != "neuter") {
+                    if (loadForGender(genderTable, "neuter", value)) {
+                        return true;
+                    }
+                }
+            }
+            if (loadForGender(genderTable, "_", value)) {
+                return true;
+            }
+            return false;
+        }
+
+        // Tries to load data for the given gender from `genderTable`. Returns true
+        // if found, returning the data in `value`. The returned data will be for
+        // the configured case if found, falling back to "nominative" and no-case if
+        // not.
+        private boolean
+        loadForGender(UResource.Table genderTable, String genderVal, UResource.Value value) {
+            if (!genderTable.findValue(genderVal, value)) {
+                return false;
+            }
+            UResource.Table caseTable = value.getTable();
+            if (caseVariant != null && !caseVariant.isEmpty()) {
+                if (loadForCase(caseTable, caseVariant, value)) {
+                    return true;
+                }
+                if (caseVariant != "nominative") {
+                    if (loadForCase(caseTable, "nominative", value)) {
+                        return true;
+                    }
+                }
+            }
+            if (loadForCase(caseTable, "_", value)) {
+                return true;
+            }
+            return false;
+        }
+
+        // Tries to load data for the given case from `caseTable`. Returns null
+        // if not found.
+        private boolean loadForCase(UResource.Table caseTable, String caseValue, UResource.Value value) {
+            if (!caseTable.findValue(caseValue, value)) {
+                return false;
+            }
+            return true;
+        }
+
+        String gender;
+        String caseVariant;
+        String[] outArray;
     }
 
-    // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed.
+    // Fetches localised formatting patterns for the given subKey. See
+    // documentation for InflectedPluralSink for details.
+    //
+    // Data is loaded for the appropriate unit width, with missing data filled
+    // in from unitsShort.
+    static void getInflectedMeasureData(String subKey,
+                                        ULocale locale,
+                                        UnitWidth width,
+                                        String gender,
+                                        String caseVariant,
+                                        String[] outArray) {
+        InflectedPluralSink sink = new InflectedPluralSink(gender, caseVariant, outArray);
+        ICUResourceBundle unitsBundle =
+            (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
 
-    static void getMeasureData(
-            ULocale locale,
-            MeasureUnit unit,
-            UnitWidth width,
-            String[] outArray) {
-        PluralTableSink sink = new PluralTableSink(outArray);
-        ICUResourceBundle resource;
-        resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME,
-                locale);
         StringBuilder key = new StringBuilder();
         key.append("units");
         if (width == UnitWidth.NARROW) {
@@ -101,17 +286,130 @@
             key.append("Short");
         }
         key.append("/");
-        key.append(unit.getType());
-        key.append("/");
+        key.append(subKey);
+
+        try {
+            unitsBundle.getAllItemsWithFallback(key.toString(), sink);
+            if (width == UnitWidth.SHORT) {
+                return;
+            }
+        } catch (MissingResourceException e) {
+            // 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);
+    }
+
+    private static final class PluralTableSink extends UResource.Sink {
+
+        String[] outArray;
+
+        // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds
+        // checking is performed.
+        public PluralTableSink(String[] outArray) {
+            this.outArray = outArray;
+        }
+
+        @Override
+        public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+            UResource.Table pluralsTable = value.getTable();
+            for (int i = 0; pluralsTable.getKeyAndValue(i, key, value); ++i) {
+                String keyString = key.toString();
+
+                if (keyString.equals("case")) {
+                    continue;
+                }
+
+                int index = getIndex(keyString);
+                if (outArray[index] != null) {
+                    continue;
+                }
+
+                String formatString = value.getString();
+                outArray[index] = formatString;
+            }
+        }
+    }
+
+    // NOTE: outArray MUST have at least ARRAY_LENGTH entries. No bounds checking is performed.
+    static void getMeasureData(
+            ULocale locale,
+            MeasureUnit unit,
+            UnitWidth width,
+            String unitDisplayCase,
+            String[] outArray) {
+        PluralTableSink sink = new PluralTableSink(outArray);
+        ICUResourceBundle resource;
+        resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME,
+                locale);
+
+        StringBuilder subKey = new StringBuilder();
+        subKey.append("/");
+        subKey.append(unit.getType());
+        subKey.append("/");
 
         // 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")) {
-            key.append(unit.getSubtype(), 0, unit.getSubtype().length() - 7);
+            subKey.append(unit.getSubtype(), 0, unit.getSubtype().length() - 7);
         } else {
-            key.append(unit.getSubtype());
+            subKey.append(unit.getSubtype());
         }
 
+        if (width != UnitWidth.FULL_NAME) {
+            StringBuilder genderKey = new StringBuilder();
+            genderKey.append("units");
+            genderKey.append(subKey);
+            genderKey.append("/gender");
+            try {
+                outArray[GENDER_INDEX] = resource.getWithFallback(genderKey.toString()).getString();
+            } catch (MissingResourceException e) {
+                // continue
+            }
+        }
+
+        StringBuilder key = new StringBuilder();
+        key.append("units");
+        if (width == UnitWidth.NARROW) {
+            key.append("Narrow");
+        } else if (width == UnitWidth.SHORT) {
+            key.append("Short");
+        }
+        key.append(subKey);
+
+        // Grab desired case first, if available. Then grab nominative case to fill
+        // in the gaps.
+        //
+        // TODO(icu-units#138): check that fallback is spec-compliant
+        if (width == UnitWidth.FULL_NAME
+                        && unitDisplayCase != null
+                        && !unitDisplayCase.isEmpty()) {
+            StringBuilder caseKey = new StringBuilder();
+            caseKey.append(key);
+            caseKey.append("/case/");
+            caseKey.append(unitDisplayCase);
+
+            try {
+                // TODO(icu-units#138): our fallback logic is not spec-compliant:
+                // lateral fallback should happen before locale fallback. Switch to
+                // getInflectedMeasureData after homogenizing data format? Find a unit
+                // test case that demonstrates the incorrect fallback logic (via
+                // regional variant of an inflected language?)
+                resource.getAllItemsWithFallback(caseKey.toString(), sink);
+                // TODO(icu-units#138): our fallback logic is not spec-compliant: we
+                // check the given case, then go straight to the no-case data. The spec
+                // states we should first look for case="nominative". As part of #138,
+                // either get the spec changed, or add unit tests that warn us if
+                // case="nominative" data differs from no-case data?
+            } catch (MissingResourceException e) {
+                // continue.
+            }
+        }
         try {
             resource.getAllItemsWithFallback(key.toString(), sink);
         } catch (MissingResourceException e) {
@@ -138,7 +436,7 @@
         }
     }
 
-    private static String getPerUnitFormat(ULocale locale, UnitWidth width) {
+    private static String getCompoundValue(String compoundKey, ULocale locale, UnitWidth width) {
         ICUResourceBundle resource;
         resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME,
                 locale);
@@ -149,22 +447,298 @@
         } else if (width == UnitWidth.SHORT) {
             key.append("Short");
         }
-        key.append("/compound/per");
+        key.append("/compound/");
+        key.append(compoundKey);
         try {
             return resource.getStringWithFallback(key.toString());
         } catch (MissingResourceException e) {
-            throw new IllegalArgumentException(
-                    "Could not find x-per-y format for " + locale + ", width " + width);
+            if (width == UnitWidth.SHORT) {
+                return "";
+            }
         }
+
+        // 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) {
+            return "";
+        }
+    }
+
+    /**
+     * Loads and applies deriveComponent rules from CLDR's
+     * grammaticalFeatures.xml.
+     * <p>
+     * Consider a deriveComponent rule that looks like this:
+     * <pre>
+     *   &lt;deriveComponent feature="case" structure="per" value0="compound" value1="nominative"/&gt;
+     * </pre>
+     * Instantiating an instance as follows:
+     * <pre>
+     *   DerivedComponents d(loc, "case", "per");
+     * </pre>
+     * <p>
+     * Applying the rule in the XML element above, <code>d.value0("foo")</code>
+     * will be "foo", and <code>d.value1("foo")</code> will be "nominative".
+     * <p>
+     * In case of any kind of failure, value0() and value1() will simply return
+     * "".
+     */
+    private static class DerivedComponents {
+        /**
+         * Constructor.
+         */
+        DerivedComponents(ULocale locale, String feature, String structure) {
+            try {
+                ICUResourceBundle derivationsBundle =
+                    (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
+                                                                         "grammaticalFeatures");
+                derivationsBundle = (ICUResourceBundle)derivationsBundle.get("grammaticalData");
+                derivationsBundle = (ICUResourceBundle)derivationsBundle.get("derivations");
+
+                ICUResourceBundle stackBundle;
+                try {
+                    // TODO: use standard normal locale resolution algorithms rather than just grabbing
+                    // language:
+                    stackBundle = (ICUResourceBundle)derivationsBundle.get(locale.getLanguage());
+                } catch (MissingResourceException e) {
+                    stackBundle = (ICUResourceBundle)derivationsBundle.get("root");
+                }
+
+                stackBundle = (ICUResourceBundle)stackBundle.get("component");
+                stackBundle = (ICUResourceBundle)stackBundle.get(feature);
+                stackBundle = (ICUResourceBundle)stackBundle.get(structure);
+
+                String value = stackBundle.getString(0);
+                if (value.compareTo("compound") == 0) {
+                    this.value0 = null;
+                } else {
+                    this.value0 = value;
+                }
+
+                value = stackBundle.getString(1);
+                if (value.compareTo("compound") == 0) {
+                    this.value1 = null;
+                } else {
+                    this.value1 = value;
+                }
+            } catch (MissingResourceException e) {
+                // Fall back to uninflected.
+            }
+        }
+
+        String value0(String compoundValue) {
+            return (this.value0 != null) ? this.value0 : compoundValue;
+        }
+
+        String value1(String compoundValue) {
+            return (this.value1 != null) ? this.value1 : compoundValue;
+        }
+
+        private String value0 = "", value1 = "";
+    }
+
+    // TODO(icu-units#28): test somehow? Associate with an ICU ticket for adding
+    // testsuite support for testing with synthetic data?
+    /**
+     * Loads and returns the value in rules that look like these:
+     *
+     * <deriveCompound feature="gender" structure="per" value="0"/>
+     * <deriveCompound feature="gender" structure="times" value="1"/>
+     *
+     * Currently a fake example, but spec compliant:
+     * <deriveCompound feature="gender" structure="power" value="feminine"/>
+     *
+     * NOTE: If U_FAILURE(status), returns an empty string.
+     */
+    private static String getDeriveCompoundRule(ULocale locale, String feature, String structure) {
+        ICUResourceBundle derivationsBundle =
+                (ICUResourceBundle) UResourceBundle
+                        .getBundleInstance(ICUData.ICU_BASE_NAME, "grammaticalFeatures");
+
+        derivationsBundle = (ICUResourceBundle) derivationsBundle.get("grammaticalData");
+        derivationsBundle = (ICUResourceBundle) derivationsBundle.get("derivations");
+
+        ICUResourceBundle stackBundle;
+        try {
+            // TODO: use standard normal locale resolution algorithms rather than just grabbing language:
+            stackBundle = (ICUResourceBundle) derivationsBundle.get(locale.getLanguage());
+        } catch (MissingResourceException e) {
+            stackBundle = (ICUResourceBundle) derivationsBundle.get("root");
+        }
+
+        stackBundle = (ICUResourceBundle) stackBundle.get("compound");
+        stackBundle = (ICUResourceBundle) stackBundle.get(feature);
+
+        return stackBundle.getString(structure);
+    }
+
+    // Returns the gender string for structures following these rules:
+    //
+    // <deriveCompound feature="gender" structure="per" value="0"/>
+    // <deriveCompound feature="gender" structure="times" value="1"/>
+    //
+    // Fake example:
+    // <deriveCompound feature="gender" structure="power" value="feminine"/>
+    //
+    // data0 and data1 should be pattern arrays (UnicodeString[ARRAY_SIZE]) that
+    // correspond to value="0" and value="1".
+    //
+    // Pass a null to data1 if the structure has no concept of value="1" (e.g.
+    // "prefix" doesn't).
+    private static String
+    getDerivedGender(ULocale locale, String structure, String[] data0, String[] data1) {
+        String val = getDeriveCompoundRule(locale, "gender", structure);
+        if (val.length() == 1) {
+            switch (val.charAt(0)) {
+            case '0':
+                return data0[GENDER_INDEX];
+            case '1':
+                if (data1 == null) {
+                    return null;
+                }
+                return data1[GENDER_INDEX];
+            }
+        }
+        return val;
     }
 
     ////////////////////////
     /// END DATA LOADING ///
     ////////////////////////
 
+    /**
+     * Calculates the gender of an arbitrary unit: this is the *second*
+     * implementation of an algorithm to do this:
+     *
+     * Gender is also calculated in "processPatternTimes": that code path is
+     * "bottom up", loading the gender for every component of a compound unit
+     * (at the same time as loading the Long Names formatting patterns), even if
+     * the gender is unneeded, then combining the single units' genders into the
+     * compound unit's gender, according to the rules. This algorithm does a
+     * lazier "top-down" evaluation, starting with the compound unit,
+     * calculating which single unit's gender is needed by breaking it down
+     * according to the rules, and then loading only the gender of the one
+     * single unit who's gender is needed.
+     *
+     * For future refactorings:
+     * 1. we could drop processPatternTimes' gender calculation and just call
+     *    this function: for UNUM_UNIT_WIDTH_FULL_NAME, the unit gender is in
+     *    the very same table as the formatting patterns, so loading it then may
+     *    be efficient. For other unit widths however, it needs to be explicitly
+     *    looked up anyway.
+     * 2. alternatively, if CLDR is providing all the genders we need such that
+     *    we don't need to calculate them in ICU anymore, we could drop this
+     *    function and keep only processPatternTimes' calculation. (And optimise
+     *    it a bit?)
+     *
+     * @param locale The desired locale.
+     * @param unit The measure unit to calculate the gender for.
+     * @return The gender string for the unit, or an empty string if unknown or
+     *     ungendered.
+     */
+    private static String calculateGenderForUnit(ULocale locale, MeasureUnit unit) {
+        MeasureUnitImpl mui = unit.getCopyOfMeasureUnitImpl();
+        ArrayList<SingleUnitImpl> singleUnits = mui.getSingleUnits();
+        int singleUnitIndex = 0;
+        if (mui.getComplexity() == MeasureUnit.Complexity.COMPOUND) {
+            int startSlice = 0;
+            // inclusive
+            int endSlice = singleUnits.size() - 1;
+            assert endSlice > 0 : "COMPOUND units have more than one single unit";
+            if (singleUnits.get(endSlice).getDimensionality() < 0) {
+                // We have a -per- construct
+                String perRule = getDeriveCompoundRule(locale, "gender", "per");
+                if (perRule.length() != 1) {
+                    // Fixed gender for -per- units
+                    return perRule;
+                }
+                if (perRule.charAt(0) == '1') {
+                    // Find the start of the denominator. We already know there is one.
+                    while (singleUnits.get(startSlice).getDimensionality() >= 0) {
+                        startSlice++;
+                    }
+                } else {
+                    // Find the end of the numerator
+                    while (endSlice >= 0 && singleUnits.get(endSlice).getDimensionality() < 0) {
+                        endSlice--;
+                    }
+                    if (endSlice < 0) {
+                        // We have only a denominator, e.g. "per-second".
+                        // TODO(icu-units#28): find out what gender to use in the
+                        // absence of a first value - mentioned in CLDR-14253.
+                        return "";
+                    }
+                }
+            }
+            if (endSlice > startSlice) {
+                // We have a -times- construct
+                String timesRule = getDeriveCompoundRule(locale, "gender", "times");
+                if (timesRule.length() != 1) {
+                    // Fixed gender for -times- units
+                    return timesRule;
+                }
+                if (timesRule.charAt(0) == '0') {
+                    endSlice = startSlice;
+                } else {
+                    // We assume timesRule[0] == u'1'
+                    startSlice = endSlice;
+                }
+            }
+            assert startSlice == endSlice;
+            singleUnitIndex = startSlice;
+        } else if (mui.getComplexity() == MeasureUnit.Complexity.MIXED) {
+            throw new ICUException("calculateGenderForUnit does not support MIXED units");
+        } else {
+            assert mui.getComplexity() == MeasureUnit.Complexity.SINGLE;
+            assert singleUnits.size() == 1;
+        }
+
+        // Now we know which singleUnit's gender we want
+        SingleUnitImpl singleUnit = singleUnits.get(singleUnitIndex);
+        // Check for any power-prefix gender override:
+        if (Math.abs(singleUnit.getDimensionality()) != 1) {
+            String powerRule = getDeriveCompoundRule(locale, "gender", "power");
+            if (powerRule.length() != 1) {
+                // Fixed gender for -powN- units
+                return powerRule;
+            }
+            // powerRule[0] == u'0'; u'1' not currently in spec.
+        }
+        // Check for any SI and binary prefix gender override:
+        if (Math.abs(singleUnit.getDimensionality()) != 1) {
+            String prefixRule = getDeriveCompoundRule(locale, "gender", "prefix");
+            if (prefixRule.length() != 1) {
+                // Fixed gender for -powN- units
+                return prefixRule;
+            }
+            // prefixRule[0] == u'0'; u'1' not currently in spec.
+        }
+        // Now we've boiled it down to the gender of one simple unit identifier:
+        return getGenderForBuiltin(locale, MeasureUnit.forIdentifier(singleUnit.getSimpleUnitID()));
+    }
+
+    private static void maybeCalculateGender(ULocale locale, MeasureUnit unit, String[] outArray) {
+        if (outArray[GENDER_INDEX] == null) {
+            String meterGender = getGenderForBuiltin(locale, MeasureUnit.METER);
+            if (meterGender.isEmpty()) {
+                // No gender for meter: assume ungendered language
+                return;
+            }
+            // We have a gendered language, but are lacking gender for unitRef.
+            outArray[GENDER_INDEX] = calculateGenderForUnit(locale, unit);
+        }
+    }
+
     private final Map<StandardPlural, SimpleModifier> modifiers;
     private final PluralRules rules;
     private final MicroPropsGenerator parent;
+    // Grammatical gender of the formatted result.
+    private String gender = "";
 
     private LongNameHandler(
             Map<StandardPlural, SimpleModifier> modifiers,
@@ -177,7 +751,7 @@
 
     public static String getUnitDisplayName(ULocale locale, MeasureUnit unit, UnitWidth width) {
         String[] measureData = new String[ARRAY_LENGTH];
-        getMeasureData(locale, unit, width, measureData);
+        getMeasureData(locale, unit, width, "", measureData);
         return measureData[DNAM_INDEX];
     }
 
@@ -193,103 +767,508 @@
                 StandardPlural.class);
         LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
         result.simpleFormatsToModifiers(simpleFormats, NumberFormat.Field.CURRENCY);
+        // TODO(icu-units#28): currency gender?
         return result;
     }
 
     /**
      * Construct a localized LongNameHandler for the specified MeasureUnit.
      * <p>
-     * Compound units can be constructed via `unit` and `perUnit`. Both of these
-     * must then be built-in units.
-     * <p>
      * Mixed units are not supported, use MixedUnitLongNameHandler.forMeasureUnit.
      *
      * @param locale The desired locale.
-     * @param unit The measure unit to construct a LongNameHandler for. If
-     *     `perUnit` is also defined, `unit` must not be a mixed unit.
-     * @param perUnit If `unit` is a mixed unit, `perUnit` must be null.
+     * @param unit The measure unit to construct a LongNameHandler for.
      * @param width Specifies the desired unit rendering.
+     * @param unitDisplayCase Specifies the desired grammatical case. If the
+     *     specified case is not found, we fall back to nominative or no-case.
      * @param rules Plural rules.
      * @param parent Plural rules.
      */
     public static LongNameHandler forMeasureUnit(
             ULocale locale,
             MeasureUnit unit,
-            MeasureUnit perUnit,
             UnitWidth width,
+            String unitDisplayCase,
             PluralRules rules,
             MicroPropsGenerator parent) {
-        if (perUnit != null) {
-            // Compound unit: first try to simplify (e.g., meters per second is its own unit).
-            MeasureUnit simplified = unit.product(perUnit.reciprocal());
-            if (simplified.getType() != null) {
-                unit = simplified;
+        // From https://unicode.org/reports/tr35/tr35-general.html#compound-units -
+        // Points 1 and 2 are mostly handled by MeasureUnit:
+        //
+        // 1. If the unitId is empty or invalid, fail
+        // 2. Put the unitId into normalized order
+        if (unit.getType() != null) {
+            String[] simpleFormats = new String[ARRAY_LENGTH];
+            getMeasureData(locale, unit, width, unitDisplayCase, simpleFormats);
+            maybeCalculateGender(locale, unit, simpleFormats);
+            // TODO(ICU4J): Reduce the number of object creations here?
+            Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(StandardPlural.class);
+            LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
+            result.simpleFormatsToModifiers(simpleFormats, NumberFormat.Field.MEASURE_UNIT);
+            if (simpleFormats[GENDER_INDEX] != null) {
+                result.gender = simpleFormats[GENDER_INDEX];
+            }
+            return result;
+        } else {
+            assert unit.getComplexity() != Complexity.MIXED
+                : "Mixed units not supported by LongNameHandler: use MixedUnitLongNameHandler";
+            return forArbitraryUnit(locale, unit, width, unitDisplayCase, rules, parent);
+        }
+    }
+
+    private static LongNameHandler forArbitraryUnit(ULocale loc,
+                                                    MeasureUnit unit,
+                                                    UnitWidth width,
+                                                    String unitDisplayCase,
+                                                    PluralRules rules,
+                                                    MicroPropsGenerator parent) {
+        // Numbered list items are from the algorithms at
+        // https://unicode.org/reports/tr35/tr35-general.html#compound-units:
+        //
+        // 4. Divide the unitId into numerator (the part before the "-per-") and
+        //    denominator (the part after the "-per-). If both are empty, fail
+        MeasureUnitImpl fullUnit = unit.getCopyOfMeasureUnitImpl();
+        unit = null;
+        MeasureUnit perUnit = null;
+        // TODO(icu-units#28): lots of inefficiency in the handling of
+        // MeasureUnit/MeasureUnitImpl:
+        for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) {
+            if (subUnit.getDimensionality() > 0) {
+                if (unit == null) {
+                    unit = subUnit.build();
+                } else {
+                    unit = unit.product(subUnit.build());
+                }
             } else {
-                // No simplified form is available.
-                return forCompoundUnit(locale, unit, perUnit, width, rules, parent);
+                // It's okay to mutate fullUnit, we made a temporary copy:
+                subUnit.setDimensionality(subUnit.getDimensionality() * -1);
+                if (perUnit == null) {
+                    perUnit = subUnit.build();
+                } else {
+                    perUnit = perUnit.product(subUnit.build());
+                }
             }
         }
+        MeasureUnitImpl unitImpl = unit == null ? null : unit.getCopyOfMeasureUnitImpl();
+        MeasureUnitImpl perUnitImpl = perUnit == null ? null : perUnit.getCopyOfMeasureUnitImpl();
 
-        if (unit.getType() == null) {
-            // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported.
-            throw new UnsupportedOperationException("Unsanctioned unit, not yet supported: " +
-                                                    unit.getIdentifier());
-        }
+        // TODO(icu-units#28): check placeholder logic, see if it needs to be
+        // present here instead of only in processPatternTimes:
+        //
+        // 5. Set both globalPlaceholder and globalPlaceholderPosition to be empty
 
-        String[] simpleFormats = new String[ARRAY_LENGTH];
-        getMeasureData(locale, unit, width, simpleFormats);
-        // TODO(ICU4J): Reduce the number of object creations here?
-        Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
-                StandardPlural.class);
-        LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
-        result.simpleFormatsToModifiers(simpleFormats, NumberFormat.Field.MEASURE_UNIT);
-        return result;
-    }
+        DerivedComponents derivedPerCases = new DerivedComponents(loc, "case", "per");
 
-    private static LongNameHandler forCompoundUnit(
-            ULocale locale,
-            MeasureUnit unit,
-            MeasureUnit perUnit,
-            UnitWidth width,
-            PluralRules rules,
-            MicroPropsGenerator parent) {
-        if (unit.getType() == null || perUnit.getType() == null) {
-            // TODO(ICU-20941): Unsanctioned unit. Not yet fully supported. Set an
-            // error code.
-            throw new UnsupportedOperationException(
-                "Unsanctioned units, not yet supported: " + unit.getIdentifier() + "/" +
-                perUnit.getIdentifier());
-        }
-        String[] primaryData = new String[ARRAY_LENGTH];
-        getMeasureData(locale, unit, width, primaryData);
-        String[] secondaryData = new String[ARRAY_LENGTH];
-        getMeasureData(locale, perUnit, width, secondaryData);
-        String perUnitFormat;
-        if (secondaryData[PER_INDEX] != null) {
-            perUnitFormat = secondaryData[PER_INDEX];
+        // 6. numeratorUnitString
+        String[] numeratorUnitData = new String[ARRAY_LENGTH];
+        processPatternTimes(unitImpl, loc, width, derivedPerCases.value0(unitDisplayCase),
+                            numeratorUnitData);
+
+        // 7. denominatorUnitString
+        String[] denominatorUnitData = new String[ARRAY_LENGTH];
+        processPatternTimes(perUnitImpl, loc, width, derivedPerCases.value1(unitDisplayCase),
+                            denominatorUnitData);
+
+        // TODO(icu-units#139):
+        // - implement DerivedComponents for "plural/times" and "plural/power":
+        //   French has different rules, we'll be producing the wrong results
+        //   currently. (Prove via tests!)
+        // - implement DerivedComponents for "plural/per", "plural/prefix",
+        //   "case/times", "case/power", and "case/prefix" - although they're
+        //   currently hardcoded. Languages with different rules are surely on the
+        //   way.
+        //
+        // Currently we only use "case/per", "plural/times", "case/times", and
+        // "case/power".
+        //
+        // This may have impact on multiSimpleFormatsToModifiers(...) below too?
+        // These rules are currently (ICU 69) all the same and hard-coded below.
+        String perUnitPattern = null;
+        if (denominatorUnitData[PER_INDEX] != null) {
+            // If we have no denominator, we obtain the empty string:
+            perUnitPattern = denominatorUnitData[PER_INDEX];
         } else {
-            String rawPerUnitFormat = getPerUnitFormat(locale, width);
-            // rawPerUnitFormat is something like "{0}/{1}"; we need to substitute in the secondary unit.
-            // TODO: Lots of thrashing. Improve?
             StringBuilder sb = new StringBuilder();
-            String compiled = SimpleFormatterImpl
-                    .compileToStringMinMaxArguments(rawPerUnitFormat, sb, 2, 2);
-            String secondaryFormat = getWithPlural(secondaryData, StandardPlural.ONE);
 
+            // 8. Set perPattern to be getValue([per], locale, length)
+            String rawPerUnitFormat = getCompoundValue("per", loc, width);
+            // rawPerUnitFormat is something like "{0} per {1}"; we need to substitute in the secondary
+            // unit.
+            String perPatternFormatter =
+                SimpleFormatterImpl.compileToStringMinMaxArguments(rawPerUnitFormat, sb, 2, 2);
+            // Plural and placeholder handling for 7. denominatorUnitString:
+            // TODO(icu-units#139): hardcoded:
+            // <deriveComponent feature="plural" structure="per" value0="compound" value1="one"/>
+            String rawDenominatorFormat = getWithPlural(denominatorUnitData, StandardPlural.ONE);
             // Some "one" pattern may not contain "{0}". For example in "ar" or "ne" locale.
-            String secondaryCompiled = SimpleFormatterImpl
-                    .compileToStringMinMaxArguments(secondaryFormat, sb, 0, 1);
-            String secondaryString = SimpleFormatterImpl.getTextWithNoArguments(secondaryCompiled)
-                    .trim();
-            perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString);
+            String denominatorFormatter =
+                SimpleFormatterImpl.compileToStringMinMaxArguments(rawDenominatorFormat, sb, 0, 1);
+            String denominatorString = PatternProps.trimSpaceChar(
+                SimpleFormatterImpl.getTextWithNoArguments(denominatorFormatter));
+
+            // 9. If the denominatorString is empty, set result to
+            //    [numeratorString], otherwise set result to format(perPattern,
+            //    numeratorString, denominatorString)
+            //
+            // TODO(icu-units#28): Why does UnicodeString need to be explicit in the
+            // following line?
+            perUnitPattern =
+                SimpleFormatterImpl.formatCompiledPattern(perPatternFormatter, "{0}", denominatorString);
         }
         Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
                 StandardPlural.class);
         LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
-        result.multiSimpleFormatsToModifiers(primaryData, perUnitFormat, NumberFormat.Field.MEASURE_UNIT);
+        if (perUnitPattern.length() == 0) {
+            result.simpleFormatsToModifiers(numeratorUnitData, NumberFormat.Field.MEASURE_UNIT);
+        } else {
+            result.multiSimpleFormatsToModifiers(numeratorUnitData, perUnitPattern,
+                                                 NumberFormat.Field.MEASURE_UNIT);
+        }
+
+        // Gender
+        //
+        // TODO(icu-units#28): find out what gender to use in the absence of a first
+        // value - e.g. what's the gender of "per-second"? Mentioned in CLDR-14253.
+        //
+        // gender/per deriveCompound rules don't say:
+        // <deriveCompound feature="gender" structure="per" value="0"/> <!-- gender(gram-per-meter) ←
+        // gender(gram) -->
+        result.gender = getDerivedGender(loc, "per", numeratorUnitData, denominatorUnitData);
         return result;
     }
 
+    /**
+     * Roughly corresponds to patternTimes(...) in the spec:
+     * https://unicode.org/reports/tr35/tr35-general.html#compound-units
+     */
+    private static void processPatternTimes(MeasureUnitImpl productUnit,
+                                            ULocale loc,
+                                            UnitWidth width,
+                                            String caseVariant,
+                                            String[] outArray) {
+        assert outArray[StandardPlural.OTHER.ordinal()] == null : "outArray must have only null values!";
+        assert outArray[PER_INDEX] == null : "outArray must have only null values!";
+
+        if (productUnit == null) {
+            outArray[StandardPlural.OTHER.ordinal()] = "";
+            outArray[PER_INDEX] = "";
+            return;
+        }
+        if (productUnit.getComplexity() == Complexity.MIXED) {
+            // These are handled by MixedUnitLongNameHandler
+            throw new UnsupportedOperationException("Mixed units not supported by LongNameHandler");
+        }
+
+        if (productUnit.getIdentifier() == null) {
+            // TODO(icu-units#28): consider when serialize should be called.
+            // identifier might also be empty for MeasureUnit().
+            productUnit.serialize();
+        }
+        if (productUnit.getIdentifier().length() == 0) {
+            // MeasureUnit(): no units: return empty strings.
+            return;
+        }
+
+        MeasureUnit simpleUnit = MeasureUnit.findBySubType(productUnit.getIdentifier());
+        if (simpleUnit != null) {
+            // TODO(icu-units#145): spec doesn't cover builtin-per-builtin, it
+            // breaks them all down. Do we want to drop this?
+            // - findBySubType isn't super efficient, if we skip it and go to basic
+            //   singles, we don't have to construct MeasureUnit's anymore.
+            // - Check all the existing unit tests that fail without this: is it due
+            //   to incorrect fallback via getMeasureData?
+            // - Do those unit tests cover this code path representatively?
+            getMeasureData(loc, simpleUnit, width, caseVariant, outArray);
+            maybeCalculateGender(loc, simpleUnit, outArray);
+            return;
+        }
+
+        // 2. Set timesPattern to be getValue(times, locale, length)
+        String timesPattern = getCompoundValue("times", loc, width);
+        StringBuilder sb = new StringBuilder();
+        String timesPatternFormatter = SimpleFormatterImpl.compileToStringMinMaxArguments(timesPattern, sb, 2, 2);
+
+        PlaceholderPosition[] globalPlaceholder = new PlaceholderPosition[ARRAY_LENGTH];
+        char globalJoinerChar = 0;
+        // Numbered list items are from the algorithms at
+        // https://unicode.org/reports/tr35/tr35-general.html#compound-units:
+        //
+        // pattern(...) point 5:
+        // - Set both globalPlaceholder and globalPlaceholderPosition to be empty
+        //
+        // 3. Set result to be empty
+        for (StandardPlural plural : StandardPlural.values()) {
+            int pluralIndex = plural.ordinal();
+            // Initial state: empty string pattern, via all falling back to OTHER:
+            if (plural == StandardPlural.OTHER) {
+                outArray[pluralIndex] = "";
+            } else {
+                outArray[pluralIndex] = null;
+            }
+            globalPlaceholder[pluralIndex] = null;
+        }
+
+        // null represents "compound" (propagate the plural form).
+        String pluralCategory = null;
+        DerivedComponents derivedTimesPlurals = new DerivedComponents(loc, "plural", "times");
+        DerivedComponents derivedTimesCases = new DerivedComponents(loc, "case", "times");
+        DerivedComponents derivedPowerCases = new DerivedComponents(loc, "case", "power");
+
+        // 4. For each single_unit in product_unit
+        ArrayList<SingleUnitImpl> singleUnits = productUnit.getSingleUnits();
+        for (int singleUnitIndex = 0; singleUnitIndex < singleUnits.size(); singleUnitIndex++) {
+            SingleUnitImpl singleUnit = singleUnits.get(singleUnitIndex);
+            String singlePluralCategory;
+            String singleCaseVariant;
+            // TODO(icu-units#28): ensure we have unit tests that change/fail if we
+            // assign incorrect case variants here:
+            if (singleUnitIndex < singleUnits.size() - 1) {
+                // 4.1. If hasMultiple
+                singlePluralCategory = derivedTimesPlurals.value0(pluralCategory);
+                singleCaseVariant = derivedTimesCases.value0(caseVariant);
+                pluralCategory = derivedTimesPlurals.value1(pluralCategory);
+                caseVariant = derivedTimesCases.value1(caseVariant);
+            } else {
+                singlePluralCategory = derivedTimesPlurals.value1(pluralCategory);
+                singleCaseVariant = derivedTimesCases.value1(caseVariant);
+            }
+
+            // 4.2. Get the gender of that single_unit
+            simpleUnit = MeasureUnit.findBySubType(singleUnit.getSimpleUnitID());
+            if (simpleUnit == null) {
+                // Ideally all simple units should be known, but they're not:
+                // 100-kilometer is internally treated as a simple unit, but it is
+                // not a built-in unit and does not have formatting data in CLDR 39.
+                //
+                // TODO(icu-units#28): test (desirable) invariants in unit tests.
+                throw new UnsupportedOperationException("Unsupported sinlgeUnit: " +
+                                                        singleUnit.getSimpleUnitID());
+            }
+            String gender = getGenderForBuiltin(loc, simpleUnit);
+
+            // 4.3. If singleUnit starts with a dimensionality_prefix, such as 'square-'
+            assert singleUnit.getDimensionality() > 0;
+            int dimensionality = singleUnit.getDimensionality();
+            String[] dimensionalityPrefixPatterns = new String[ARRAY_LENGTH];
+            if (dimensionality != 1) {
+                // 4.3.1. set dimensionalityPrefixPattern to be
+                //   getValue(that dimensionality_prefix, locale, length, singlePluralCategory,
+                //   singleCaseVariant, gender), such as "{0} kwadratowym"
+                StringBuilder dimensionalityKey = new StringBuilder("compound/power");
+                dimensionalityKey.append(dimensionality);
+                try {
+                    getInflectedMeasureData(dimensionalityKey.toString(), loc, width, gender,
+                                            singleCaseVariant, dimensionalityPrefixPatterns);
+                } catch (MissingResourceException e) {
+                    // At the time of writing, only pow2 and pow3 are supported.
+                    // Attempting to format other powers results in a
+                    // U_RESOURCE_TYPE_MISMATCH. We convert the error if we
+                    // understand it:
+                    if (dimensionality > 3) {
+                        throw new UnsupportedOperationException("powerN not supported for N > 3: " +
+                                                                productUnit.getIdentifier());
+                    } else {
+                        throw e;
+                    }
+                }
+
+                // TODO(icu-units#139):
+                // 4.3.2. set singlePluralCategory to be power0(singlePluralCategory)
+
+                // 4.3.3. set singleCaseVariant to be power0(singleCaseVariant)
+                singleCaseVariant = derivedPowerCases.value0(singleCaseVariant);
+                // 4.3.4. remove the dimensionality_prefix from singleUnit
+                singleUnit.setDimensionality(1);
+            }
+
+            // 4.4. if singleUnit starts with an si_prefix, such as 'centi'
+            MeasurePrefix prefix = singleUnit.getPrefix();
+            String prefixPattern = "";
+            if (prefix != MeasurePrefix.ONE) {
+                // 4.4.1. set siPrefixPattern to be getValue(that si_prefix, locale,
+                //        length), such as "centy{0}"
+                StringBuilder prefixKey = new StringBuilder();
+                // prefixKey looks like "1024p3" or "10p-2":
+                prefixKey.append(prefix.getBase());
+                prefixKey.append('p');
+                prefixKey.append(prefix.getPower());
+                // Contains a pattern like "centy{0}".
+                prefixPattern = getCompoundValue(prefixKey.toString(), loc, width);
+
+                // 4.4.2. set singlePluralCategory to be prefix0(singlePluralCategory)
+                //
+                // TODO(icu-units#139): that refers to these rules:
+                // <deriveComponent feature="plural" structure="prefix" value0="one" value1="compound"/>
+                // though I'm not sure what other value they might end up having.
+                //
+                // 4.4.3. set singleCaseVariant to be prefix0(singleCaseVariant)
+                //
+                // TODO(icu-units#139): that refers to:
+                // <deriveComponent feature="case" structure="prefix" value0="nominative"
+                // value1="compound"/> but the prefix (value0) doesn't have case, the rest simply
+                // propagates.
+
+                // 4.4.4. remove the si_prefix from singleUnit
+                singleUnit.setPrefix(MeasurePrefix.ONE);
+            }
+
+            // 4.5. Set corePattern to be the getValue(singleUnit, locale, length,
+            //      singlePluralCategory, singleCaseVariant), such as "{0} metrem"
+            String[] singleUnitArray = new String[ARRAY_LENGTH];
+            // At this point we are left with a Simple Unit:
+            assert singleUnit.build().getIdentifier().equals(singleUnit.getSimpleUnitID())
+                : "Should be equal: singleUnit.build().getIdentifier() produced " +
+                singleUnit.build().getIdentifier() + ", singleUnit.getSimpleUnitID() produced " +
+                singleUnit.getSimpleUnitID();
+            getMeasureData(loc, singleUnit.build(), width, singleCaseVariant, singleUnitArray);
+
+            // Calculate output gender
+            if (singleUnitArray[GENDER_INDEX] != null) {
+                assert !singleUnitArray[GENDER_INDEX].isEmpty();
+
+                if (prefix != MeasurePrefix.ONE) {
+                    singleUnitArray[GENDER_INDEX] =
+                        getDerivedGender(loc, "prefix", singleUnitArray, null);
+                }
+
+                if (dimensionality != 1) {
+                    singleUnitArray[GENDER_INDEX] =
+                        getDerivedGender(loc, "power", singleUnitArray, null);
+                }
+
+                String timesGenderRule = getDeriveCompoundRule(loc, "gender", "times");
+                if (timesGenderRule.length() == 1) {
+                    switch (timesGenderRule.charAt(0)) {
+                    case '0':
+                        if (singleUnitIndex == 0) {
+                            assert outArray[GENDER_INDEX] == null;
+                            outArray[GENDER_INDEX] = singleUnitArray[GENDER_INDEX];
+                        }
+                        break;
+                    case '1':
+                        if (singleUnitIndex == singleUnits.size() - 1) {
+                            assert outArray[GENDER_INDEX] == null;
+                            outArray[GENDER_INDEX] = singleUnitArray[GENDER_INDEX];
+                        }
+                    }
+                } else {
+                    if (outArray[GENDER_INDEX] == null) {
+                        outArray[GENDER_INDEX] = timesGenderRule;
+                    }
+                }
+            }
+
+            // Calculate resulting patterns for each plural form
+            for (StandardPlural plural_ : StandardPlural.values()) {
+                StandardPlural plural = plural_;
+                int pluralIndex = plural.ordinal();
+
+                // singleUnitArray[pluralIndex] looks something like "{0} Meter"
+                if (outArray[pluralIndex] == null) {
+                    if (singleUnitArray[pluralIndex] == null) {
+                        // Let the usual plural fallback mechanism take care of this
+                        // plural form
+                        continue;
+                    } else {
+                        // Since our singleUnit can have a plural form that outArray
+                        // doesn't yet have (relying on fallback to OTHER), we start
+                        // by grabbing it with the normal plural fallback mechanism
+                        outArray[pluralIndex] = getWithPlural(outArray, plural);
+                    }
+                }
+
+                if (singlePluralCategory != null) {
+                    plural = StandardPlural.fromString(singlePluralCategory);
+                }
+
+                // 4.6. Extract(corePattern, coreUnit, placeholder, placeholderPosition) from that
+                // pattern.
+                ExtractCorePatternResult r = extractCorePattern(getWithPlural(singleUnitArray, plural));
+
+                // 4.7 If the position is middle, then fail
+                if (r.placeholderPosition == PlaceholderPosition.MIDDLE) {
+                    throw new UnsupportedOperationException();
+                }
+
+                // 4.8. If globalPlaceholder is empty
+                if (globalPlaceholder[pluralIndex] == null) {
+                    globalPlaceholder[pluralIndex] = r.placeholderPosition;
+                    globalJoinerChar = r.joinerChar;
+                } else {
+                    // Expect all units involved to have the same placeholder position
+                    assert globalPlaceholder[pluralIndex] == r.placeholderPosition;
+                    // TODO(icu-units#28): Do we want to add a unit test that checks
+                    // for consistent joiner chars? Probably not, given how
+                    // inconsistent they are. File a CLDR ticket with examples?
+                }
+                // Now coreUnit would be just "Meter"
+
+                // 4.9. If siPrefixPattern is not empty
+                if (prefix != MeasurePrefix.ONE) {
+                    String prefixCompiled =
+                        SimpleFormatterImpl.compileToStringMinMaxArguments(prefixPattern, sb, 1, 1);
+
+                    // 4.9.1. Set coreUnit to be the combineLowercasing(locale, length, siPrefixPattern,
+                    //        coreUnit)
+                    // combineLowercasing(locale, length, prefixPattern, coreUnit)
+                    //
+                    // TODO(icu-units#28): run this only if prefixPattern does not
+                    // contain space characters - do languages "as", "bn", "hi",
+                    // "kk", etc have concepts of upper and lower case?:
+                    if (width == UnitWidth.FULL_NAME) {
+                        r.coreUnit = UCharacter.toLowerCase(loc, r.coreUnit);
+                    }
+                    r.coreUnit = SimpleFormatterImpl.formatCompiledPattern(prefixCompiled, r.coreUnit);
+                }
+
+                // 4.10. If dimensionalityPrefixPattern is not empty
+                if (dimensionality != 1) {
+                    String dimensionalityCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(
+                        getWithPlural(dimensionalityPrefixPatterns, plural), sb, 1, 1);
+
+                    // 4.10.1. Set coreUnit to be the combineLowercasing(locale, length,
+                    //         dimensionalityPrefixPattern, coreUnit)
+                    // combineLowercasing(locale, length, prefixPattern, coreUnit)
+                    //
+                    // TODO(icu-units#28): run this only if prefixPattern does not
+                    // contain space characters - do languages "as", "bn", "hi",
+                    // "kk", etc have concepts of upper and lower case?:
+                    if (width == UnitWidth.FULL_NAME) {
+                        r.coreUnit = UCharacter.toLowerCase(loc, r.coreUnit);
+                    }
+                    r.coreUnit =
+                        SimpleFormatterImpl.formatCompiledPattern(dimensionalityCompiled, r.coreUnit);
+                }
+
+                if (outArray[pluralIndex].length() == 0) {
+                    // 4.11. If the result is empty, set result to be coreUnit
+                    outArray[pluralIndex] = r.coreUnit;
+                } else {
+                    // 4.12. Otherwise set result to be format(timesPattern, result, coreUnit)
+                    outArray[pluralIndex] = SimpleFormatterImpl.formatCompiledPattern(
+                        timesPatternFormatter, outArray[pluralIndex], r.coreUnit);
+                }
+            }
+        }
+        for (StandardPlural plural : StandardPlural.values()) {
+            int pluralIndex = plural.ordinal();
+            if (globalPlaceholder[pluralIndex] == PlaceholderPosition.BEGINNING) {
+                StringBuilder tmp = new StringBuilder();
+                tmp.append("{0}");
+                if (globalJoinerChar != 0) {
+                    tmp.append(globalJoinerChar);
+                }
+                tmp.append(outArray[pluralIndex]);
+                outArray[pluralIndex] = tmp.toString();
+            } else if (globalPlaceholder[pluralIndex] == PlaceholderPosition.END) {
+                if (globalJoinerChar != 0) {
+                    outArray[pluralIndex] = outArray[pluralIndex] + globalJoinerChar;
+                }
+                outArray[pluralIndex] = outArray[pluralIndex] + "{0}";
+            }
+        }
+    }
+
+    /** Sets modifiers to use the patterns from simpleFormats. */
     private void simpleFormatsToModifiers(
             String[] simpleFormats,
             NumberFormat.Field field) {
@@ -305,6 +1284,14 @@
         }
     }
 
+    /**
+     * Sets modifiers to a combination of `leadFormats` (one per plural form)
+     * and `trailFormat` appended to each.
+     *
+     * With a leadFormat of "{0}m" and a trailFormat of "{0}/s", it produces a
+     * pattern of "{0}m/s" by inserting each leadFormat pattern into
+     * trailFormat.
+     */
     private void multiSimpleFormatsToModifiers(
             String[] leadFormats,
             String trailFormat,
@@ -313,9 +1300,14 @@
         String trailCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(trailFormat, sb, 1, 1);
         for (StandardPlural plural : StandardPlural.VALUES) {
             String leadFormat = getWithPlural(leadFormats, plural);
-            String compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat);
-            String compoundCompiled = SimpleFormatterImpl
-                    .compileToStringMinMaxArguments(compoundFormat, sb, 0, 1);
+            String compoundFormat;
+            if (leadFormat.length() == 0) {
+                compoundFormat = trailFormat;
+            } else {
+                compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat);
+            }
+            String compoundCompiled =
+                SimpleFormatterImpl.compileToStringMinMaxArguments(compoundFormat, sb, 0, 1);
             Modifier.Parameters parameters = new Modifier.Parameters();
             parameters.obj = this;
             parameters.signum = null; // Signum ignored
@@ -329,6 +1321,7 @@
         MicroProps micros = parent.processQuantity(quantity);
         StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
         micros.modOuter = modifiers.get(pluralForm);
+        micros.gender = this.gender;
         return micros;
     }
 
@@ -340,6 +1333,7 @@
      * Does not call parent.processQuantity, so cannot get a MicroProps instance
      * that way. Instead, the instance is passed in as a parameter.
      */
+    @Override
     public MicroProps processQuantityWithMicros(DecimalQuantity quantity, MicroProps micros) {
         StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
         micros.modOuter = modifiers.get(pluralForm);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/LongNameMultiplexer.java b/android_icu4j/src/main/java/android/icu/impl/number/LongNameMultiplexer.java
index 024cc12..6a5bc6d 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/LongNameMultiplexer.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/LongNameMultiplexer.java
@@ -9,7 +9,6 @@
 import android.icu.number.NumberFormatter;
 import android.icu.text.PluralRules;
 import android.icu.util.MeasureUnit;
-import android.icu.util.NoUnit;
 import android.icu.util.ULocale;
 
 /**
@@ -49,6 +48,7 @@
     public static LongNameMultiplexer forMeasureUnits(ULocale locale,
                                                       List<MeasureUnit> units,
                                                       NumberFormatter.UnitWidth width,
+                                                      String unitDisplayCase,
                                                       PluralRules rules,
                                                       MicroPropsGenerator parent) {
         LongNameMultiplexer result = new LongNameMultiplexer(parent);
@@ -64,11 +64,10 @@
             result.fMeasureUnits.add(unit);
             if (unit.getComplexity() == MeasureUnit.Complexity.MIXED) {
                 MixedUnitLongNameHandler mlnh = MixedUnitLongNameHandler
-                        .forMeasureUnit(locale, unit, width, rules, null);
+                        .forMeasureUnit(locale, unit, width, unitDisplayCase, rules, null);
                 result.fHandlers.add(mlnh);
             } else {
-                LongNameHandler lnh = LongNameHandler
-                        .forMeasureUnit(locale, unit, NoUnit.BASE, width, rules, null);
+                LongNameHandler lnh = LongNameHandler.forMeasureUnit(locale, unit, width, unitDisplayCase, rules, null);
                 result.fHandlers.add(lnh);
             }
         }
@@ -80,7 +79,7 @@
     // one of the units provided to the factory function.
     @Override
     public MicroProps processQuantity(DecimalQuantity quantity) {
-        // We call parent->processQuantity() from the Multiplexer, instead of
+        // We call parent.processQuantity() from the Multiplexer, instead of
         // letting LongNameHandler handle it: we don't know which LongNameHandler to
         // call until we've called the parent!
         MicroProps micros = this.fParent.processQuantity(quantity);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/MacroProps.java b/android_icu4j/src/main/java/android/icu/impl/number/MacroProps.java
index 1a97450..9bf64cc 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/MacroProps.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/MacroProps.java
@@ -31,7 +31,9 @@
     public IntegerWidth integerWidth;
     public Object symbols;
     public UnitWidth unitWidth;
+    public String unitDisplayCase;
     public SignDisplay sign;
+    public Boolean approximately;
     public DecimalSeparatorDisplay decimal;
     public Scale scale;
     public String usage;
@@ -67,8 +69,12 @@
             symbols = fallback.symbols;
         if (unitWidth == null)
             unitWidth = fallback.unitWidth;
+        if (unitDisplayCase == null)
+            unitDisplayCase = fallback.unitDisplayCase;
         if (sign == null)
             sign = fallback.sign;
+        if (approximately == null)
+            approximately = fallback.approximately;
         if (decimal == null)
             decimal = fallback.decimal;
         if (affixProvider == null)
@@ -95,7 +101,9 @@
                 integerWidth,
                 symbols,
                 unitWidth,
+                unitDisplayCase,
                 sign,
+                approximately,
                 decimal,
                 affixProvider,
                 scale,
@@ -123,7 +131,9 @@
                 && Objects.equals(integerWidth, other.integerWidth)
                 && Objects.equals(symbols, other.symbols)
                 && Objects.equals(unitWidth, other.unitWidth)
+                && Objects.equals(unitDisplayCase, other.unitDisplayCase)
                 && Objects.equals(sign, other.sign)
+                && Objects.equals(approximately, other.approximately)
                 && Objects.equals(decimal, other.decimal)
                 && Objects.equals(affixProvider, other.affixProvider)
                 && Objects.equals(scale, other.scale)
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/MicroProps.java b/android_icu4j/src/main/java/android/icu/impl/number/MicroProps.java
index 606b0af..be75b29 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/MicroProps.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/MicroProps.java
@@ -46,6 +46,10 @@
     public Precision rounder;
     public Grouper grouping;
     public boolean useCurrency;
+    public String gender;
+
+    // Currency symbol to be used as the decimal separator
+    public String currencyAsDecimal;
 
     // Internal fields:
     private final boolean immutable;
@@ -55,10 +59,16 @@
     // play.
     public MeasureUnit outputUnit;
 
-    // In the case of mixed units, this is the set of integer-only units
-    // *preceding* the final unit.
+    /**
+     * Contains all the measures.
+     */
     public List<Measure> mixedMeasures;
 
+    /**
+     * Points to quantity position, -1 if the position is not set yet.
+     */
+    public int indexOfQuantity = -1;
+
     private volatile boolean exhausted;
 
     /**
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/MixedUnitLongNameHandler.java b/android_icu4j/src/main/java/android/icu/impl/number/MixedUnitLongNameHandler.java
index 9d1e0e5..1d21c87 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/MixedUnitLongNameHandler.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/MixedUnitLongNameHandler.java
@@ -53,13 +53,24 @@
      * @param mixedUnit The mixed measure unit to construct a
      *                  MixedUnitLongNameHandler for.
      * @param width     Specifies the desired unit rendering.
+     * @param unitDisplayCase Specifies the desired grammatical case. If the
+     *     specified case is not found, we fall back to nominative or no-case.
      * @param rules     PluralRules instance.
      * @param parent    MicroPropsGenerator instance.
      */
-    public static MixedUnitLongNameHandler forMeasureUnit(ULocale locale, MeasureUnit mixedUnit,
-                                                          NumberFormatter.UnitWidth width, PluralRules rules,
+    public static MixedUnitLongNameHandler forMeasureUnit(ULocale locale,
+                                                          MeasureUnit mixedUnit,
+                                                          NumberFormatter.UnitWidth width,
+                                                          String unitDisplayCase,
+                                                          PluralRules rules,
                                                           MicroPropsGenerator parent) {
-        assert (mixedUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
+        assert mixedUnit.getComplexity() == MeasureUnit.Complexity.MIXED
+            : "MixedUnitLongNameHandler only supports MIXED units";
+        // In ICU4C, in addition to an assert, we return a failure status if the
+        // unit is not mixed (commented by: "Defensive, for production code").
+        // In Java, we don't have efficient access to MeasureUnitImpl, so we
+        // skip this check - relying on unit tests and the assert above to help
+        // enforce the invariant.
 
         MixedUnitLongNameHandler result = new MixedUnitLongNameHandler(rules, parent);
         List<MeasureUnit> individualUnits = mixedUnit.splitToSingleUnits();
@@ -68,7 +79,10 @@
         for (int i = 0; i < individualUnits.size(); i++) {
             // Grab data for each of the components.
             String[] unitData = new String[LongNameHandler.ARRAY_LENGTH];
-            LongNameHandler.getMeasureData(locale, individualUnits.get(i), width, unitData);
+            LongNameHandler.getMeasureData(locale, individualUnits.get(i), width, unitDisplayCase,
+                                           unitData);
+            // TODO(ICU-21494): if we add support for gender for mixed units, we may
+            // need LongNameHandler.maybeCalculateGender() here.
             result.fMixedUnitData.add(unitData);
         }
 
@@ -129,7 +143,7 @@
      */
     @Override
     public Modifier getModifier(Modifier.Signum signum, StandardPlural plural) {
-        // TODO(units): investigate this method while investigating where
+        // TODO(icu-units#28): investigate this method while investigating where
         // LongNameHandler.getModifier() gets used. To be sure it remains
         // unreachable:
         assert false : "should be unreachable";
@@ -170,14 +184,33 @@
 
         List<String> outputMeasuresList = new ArrayList<>();
 
+        StandardPlural quantityPlural = StandardPlural.OTHER;
         for (int i = 0; i < micros.mixedMeasures.size(); i++) {
+
+            if ( i  == micros.indexOfQuantity) {
+                if (i > 0 && quantity.isNegative()) {
+                    // If numbers are negative, only the first number needs to have its
+                    // negative sign formatted.
+                    quantity.negate();
+                }
+
+                quantityPlural = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
+                String quantitySimpleFormat = LongNameHandler.getWithPlural(this.fMixedUnitData.get(i), quantityPlural);
+                SimpleFormatter finalFormatter = SimpleFormatter.compileMinMaxArguments(quantitySimpleFormat, 0, 1);
+                outputMeasuresList.add(finalFormatter.format("{0}"));
+
+                continue;
+            }
+
+
             DecimalQuantity fdec = new DecimalQuantity_DualStorageBCD(micros.mixedMeasures.get(i).getNumber());
             if (i > 0 && fdec.isNegative()) {
                 // If numbers are negative, only the first number needs to have its
                 // negative sign formatted.
                 fdec.negate();
             }
-            StandardPlural pluralForm = fdec.getStandardPlural(rules);
+
+            StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, fdec);
 
             String simpleFormat = LongNameHandler.getWithPlural(this.fMixedUnitData.get(i), pluralForm);
             SimpleFormatter compiledFormatter = SimpleFormatter.compileMinMaxArguments(simpleFormat, 0, 1);
@@ -188,18 +221,6 @@
             // TODO(icu-units#67): fix field positions
         }
 
-        // Reiterated: we have at least one mixedMeasure:
-        assert micros.mixedMeasures.size() > 0;
-        // Thus if negative, a negative has already been formatted:
-        if (quantity.isNegative()) {
-            quantity.negate();
-        }
-
-        String[] finalSimpleFormats = this.fMixedUnitData.get(this.fMixedUnitData.size() - 1);
-        StandardPlural finalPlural = RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
-        String finalSimpleFormat = LongNameHandler.getWithPlural(finalSimpleFormats, finalPlural);
-        SimpleFormatter finalFormatter = SimpleFormatter.compileMinMaxArguments(finalSimpleFormat, 0, 1);
-        outputMeasuresList.add(finalFormatter.format("{0}"));
 
         // Combine list into a "premixed" pattern
         String premixedFormatPattern = this.fListFormatter.format(outputMeasuresList);
@@ -211,7 +232,7 @@
         Modifier.Parameters params = new Modifier.Parameters();
         params.obj = this;
         params.signum = Modifier.Signum.POS_ZERO;
-        params.plural = finalPlural;
+        params.plural = quantityPlural;
         // Return a SimpleModifier for the "premixed" pattern
         return new SimpleModifier(premixedCompiled, null, false, params);
     }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java
index 3d7baa5..e84af1e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/MutablePatternModifier.java
@@ -44,6 +44,7 @@
     Field field;
     SignDisplay signDisplay;
     boolean perMilleReplacesPercent;
+    boolean approximately;
 
     // Symbol details
     DecimalFormatSymbols symbols;
@@ -91,10 +92,13 @@
      *            Whether to force a plus sign on positive numbers.
      * @param perMille
      *            Whether to substitute the percent sign in the pattern with a permille sign.
+     * @param approximately
+     *            Whether to prepend approximately to the sign
      */
-    public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
+    public void setPatternAttributes(SignDisplay signDisplay, boolean perMille, boolean approximately) {
         this.signDisplay = signDisplay;
         this.perMilleReplacesPercent = perMille;
+        this.approximately = approximately;
     }
 
     /**
@@ -380,6 +384,7 @@
         PatternStringUtils.patternInfoToStringBuilder(patternInfo,
                 isPrefix,
                 PatternStringUtils.resolveSignDisplay(signDisplay, signum),
+                approximately,
                 plural,
                 perMilleReplacesPercent,
                 currentAffix);
@@ -395,36 +400,14 @@
             return symbols.getMinusSignString();
         case AffixUtils.TYPE_PLUS_SIGN:
             return symbols.getPlusSignString();
+        case AffixUtils.TYPE_APPROXIMATELY_SIGN:
+            return symbols.getApproximatelySignString();
         case AffixUtils.TYPE_PERCENT:
             return symbols.getPercentString();
         case AffixUtils.TYPE_PERMILLE:
             return symbols.getPerMillString();
         case AffixUtils.TYPE_CURRENCY_SINGLE:
-            // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
-            if (unitWidth == UnitWidth.ISO_CODE) {
-                return currency.getCurrencyCode();
-            } else if (unitWidth == UnitWidth.HIDDEN) {
-                return "";
-            } else {
-                int selector;
-                switch (unitWidth) {
-                    case SHORT:
-                        selector = Currency.SYMBOL_NAME;
-                        break;
-                    case NARROW:
-                        selector = Currency.NARROW_SYMBOL_NAME;
-                        break;
-                    case FORMAL:
-                        selector = Currency.FORMAL_SYMBOL_NAME;
-                        break;
-                    case VARIANT:
-                        selector = Currency.VARIANT_SYMBOL_NAME;
-                        break;
-                    default:
-                        throw new AssertionError();
-                }
-                return currency.getName(symbols.getULocale(), selector, null);
-            }
+            return getCurrencySymbolForUnitWidth();
         case AffixUtils.TYPE_CURRENCY_DOUBLE:
             return currency.getCurrencyCode();
         case AffixUtils.TYPE_CURRENCY_TRIPLE:
@@ -442,4 +425,35 @@
             throw new AssertionError();
         }
     }
+
+    /**
+     * Returns the currency symbol for the unit width specified in setSymbols()
+     */
+    public String getCurrencySymbolForUnitWidth() {
+        // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
+        if (unitWidth == UnitWidth.ISO_CODE) {
+            return currency.getCurrencyCode();
+        } else if (unitWidth == UnitWidth.HIDDEN) {
+            return "";
+        } else {
+            int selector;
+            switch (unitWidth) {
+                case SHORT:
+                    selector = Currency.SYMBOL_NAME;
+                    break;
+                case NARROW:
+                    selector = Currency.NARROW_SYMBOL_NAME;
+                    break;
+                case FORMAL:
+                    selector = Currency.FORMAL_SYMBOL_NAME;
+                    break;
+                case VARIANT:
+                    selector = Currency.VARIANT_SYMBOL_NAME;
+                    break;
+                default:
+                    throw new AssertionError();
+            }
+            return currency.getName(symbols.getULocale(), selector, null);
+        }
+    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PatternStringParser.java b/android_icu4j/src/main/java/android/icu/impl/number/PatternStringParser.java
index 1c83604..52be19c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/PatternStringParser.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/PatternStringParser.java
@@ -177,6 +177,11 @@
         public boolean hasBody() {
             return positive.integerTotal > 0;
         }
+
+        @Override
+        public boolean currencyAsDecimal() {
+            return positive.hasCurrencyDecimal;
+        }
     }
 
     /**
@@ -201,6 +206,7 @@
         public boolean hasPercentSign = false;
         public boolean hasPerMilleSign = false;
         public boolean hasCurrencySign = false;
+        public boolean hasCurrencyDecimal = false;
         public boolean hasMinusSign = false;
         public boolean hasPlusSign = false;
 
@@ -223,6 +229,7 @@
             this.offset = 0;
         }
 
+        /** Returns the next code point, or -1 if string is too short. */
         int peek() {
             if (offset == pattern.length()) {
                 return -1;
@@ -231,6 +238,20 @@
             }
         }
 
+        /** Returns the code point after the next code point, or -1 if string is too short. */
+        int peek2() {
+            if (offset == pattern.length()) {
+                return -1;
+            }
+            int cp1 = pattern.codePointAt(offset);
+            int offset2 = offset + Character.charCount(cp1);
+            if (offset2 == pattern.length()) {
+                return -1;
+            }
+            return pattern.codePointAt(offset2);
+        }
+
+        /** Returns the next code point and then steps forward. */
         int next() {
             int codePoint = peek();
             offset += Character.charCount(codePoint);
@@ -372,6 +393,34 @@
             result.hasDecimal = true;
             result.widthExceptAffixes += 1;
             consumeFractionFormat(state, result);
+        } else if (state.peek() == '¤') {
+            // Check if currency is a decimal separator
+            switch (state.peek2()) {
+                case '#':
+                case '0':
+                case '1':
+                case '2':
+                case '3':
+                case '4':
+                case '5':
+                case '6':
+                case '7':
+                case '8':
+                case '9':
+                    break;
+                default:
+                    // Currency symbol followed by a non-numeric character;
+                    // treat as a normal affix.
+                    return;
+            }
+            // Currency symbol is followed by a numeric character;
+            // treat as a decimal separator.
+            result.hasCurrencySign = true;
+            result.hasCurrencyDecimal = true;
+            result.hasDecimal = true;
+            result.widthExceptAffixes += 1;
+            state.next(); // consume the symbol
+            consumeFractionFormat(state, result);
         }
     }
 
@@ -634,6 +683,9 @@
             properties.setDecimalSeparatorAlwaysShown(false);
         }
 
+        // Persist the currency as decimal separator
+        properties.setCurrencyAsDecimal(positive.hasCurrencyDecimal);
+
         // Scientific notation settings
         if (positive.exponentZeros > 0) {
             properties.setExponentSignAlwaysShown(positive.exponentHasPlusSign);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java b/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java
index b36bc13..d1c7215 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/PatternStringUtils.java
@@ -96,6 +96,7 @@
         int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
         int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
         boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+        boolean currencyAsDecimal = properties.getCurrencyAsDecimal();
         int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
         boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
         AffixPatternProvider affixes = PropertiesAffixPatternProvider.forProperties(properties);
@@ -158,7 +159,11 @@
             }
             // Decimal separator
             if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
-                sb.append('.');
+                if (currencyAsDecimal) {
+                    sb.append('¤');
+                } else {
+                    sb.append('.');
+                }
             }
             if (!useGrouping) {
                 continue;
@@ -300,6 +305,7 @@
         String[][] table = new String[21][2];
         int standIdx = toLocalized ? 0 : 1;
         int localIdx = toLocalized ? 1 : 0;
+        // TODO: Add approximately sign here?
         table[0][standIdx] = "%";
         table[0][localIdx] = symbols.getPercentString();
         table[1][standIdx] = "‰";
@@ -435,6 +441,7 @@
             AffixPatternProvider patternInfo,
             boolean isPrefix,
             PatternSignType patternSignType,
+            boolean approximately,
             StandardPlural plural,
             boolean perMilleReplacesPercent,
             StringBuilder output) {
@@ -446,7 +453,7 @@
         // (If not, we will use the positive subpattern.)
         boolean useNegativeAffixPattern = patternInfo.hasNegativeSubpattern()
                 && (patternSignType == PatternSignType.NEG
-                    || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
+                    || (patternInfo.negativeHasMinusSign() && (plusReplacesMinusSign || approximately)));
 
         // Resolve the flags for the affix pattern.
         int flags = 0;
@@ -468,10 +475,25 @@
         } else if (patternSignType == PatternSignType.NEG) {
             prependSign = true;
         } else {
-            prependSign = plusReplacesMinusSign;
+            prependSign = plusReplacesMinusSign || approximately;
         }
 
         // Compute the length of the affix pattern.
+        // What symbols should take the place of the sign placeholder?
+        String signSymbols = "-";
+        if (approximately) {
+            if (plusReplacesMinusSign) {
+                signSymbols = "~+";
+            } else if (patternSignType == PatternSignType.NEG) {
+                signSymbols = "~-";
+            } else {
+                signSymbols = "~";
+            }
+        } else if (plusReplacesMinusSign) {
+            signSymbols = "+";
+        }
+
+        // Compute the number of tokens in the affix pattern (signSymbols is considered one token).
         int length = patternInfo.length(flags) + (prependSign ? 1 : 0);
 
         // Finally, set the result into the StringBuilder.
@@ -485,8 +507,13 @@
             } else {
                 candidate = patternInfo.charAt(flags, index);
             }
-            if (plusReplacesMinusSign && candidate == '-') {
-                candidate = '+';
+            if (candidate == '-') {
+                if (signSymbols.length() == 1) {
+                    candidate = signSymbols.charAt(0);
+                } else {
+                    output.append(signSymbols.charAt(0));
+                    candidate = signSymbols.charAt(1);
+                }
             }
             if (perMilleReplacesPercent && candidate == '%') {
                 candidate = '‰';
@@ -534,6 +561,18 @@
                 }
                 break;
 
+            case NEGATIVE:
+            case ACCOUNTING_NEGATIVE:
+                switch (signum) {
+                    case NEG:
+                        return PatternSignType.NEG;
+                    case NEG_ZERO:
+                    case POS_ZERO:
+                    case POS:
+                        return PatternSignType.POS;
+                }
+                break;
+
             case NEVER:
                 return PatternSignType.POS;
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java b/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java
index 749babf..e77707b 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/PropertiesAffixPatternProvider.java
@@ -12,6 +12,7 @@
     private final String negPrefix;
     private final String negSuffix;
     private final boolean isCurrencyPattern;
+    private final boolean currencyAsDecimal;
 
     public static AffixPatternProvider forProperties(DecimalFormatProperties properties) {
         if (properties.getCurrencyPluralInfo() == null) {
@@ -88,7 +89,10 @@
             AffixUtils.hasCurrencySymbols(ppp) ||
             AffixUtils.hasCurrencySymbols(psp) ||
             AffixUtils.hasCurrencySymbols(npp) ||
-            AffixUtils.hasCurrencySymbols(nsp));
+            AffixUtils.hasCurrencySymbols(nsp) ||
+            properties.getCurrencyAsDecimal());
+
+        currencyAsDecimal = properties.getCurrencyAsDecimal();
     }
 
     @Override
@@ -155,6 +159,11 @@
     }
 
     @Override
+    public boolean currencyAsDecimal() {
+        return currencyAsDecimal;
+    }
+
+    @Override
     public String toString() {
         return super.toString()
                 + " {"
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/UnitConversionHandler.java b/android_icu4j/src/main/java/android/icu/impl/number/UnitConversionHandler.java
index b120ae3..407c637 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/UnitConversionHandler.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/UnitConversionHandler.java
@@ -3,12 +3,9 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.number;
 
-import java.util.List;
-
 import android.icu.impl.units.ComplexUnitsConverter;
+import android.icu.impl.units.ConversionRates;
 import android.icu.impl.units.MeasureUnitImpl;
-import android.icu.impl.units.UnitsData;
-import android.icu.util.Measure;
 import android.icu.util.MeasureUnit;
 
 /**
@@ -24,22 +21,17 @@
     private ComplexUnitsConverter fComplexUnitConverter;
 
     /**
-     * Constructor.
-     *
-     * @param inputUnit Specifies the input MeasureUnit. Mixed units are not
-     *     supported as input (because input is just a single decimal quantity).
-     * @param outputUnit Specifies the output MeasureUnit.
-     * @param parent The parent MicroPropsGenerator.
+     * @param targetUnit Specifies the output MeasureUnit. The input MeasureUnit
+     *     is derived from it: in case of a mixed unit, the biggest unit is
+     *     taken as the input unit. If not a mixed unit, the input unit will be
+     *     the same as the output unit and no unit conversion takes place.
+     * @param parent    The parent MicroPropsGenerator.
      */
-    public UnitConversionHandler(MeasureUnit inputUnit,
-                                 MeasureUnit outputUnit,
-                                 MicroPropsGenerator parent) {
-        this.fOutputUnit = outputUnit;
+    public UnitConversionHandler(MeasureUnit targetUnit, MicroPropsGenerator parent) {
+        this.fOutputUnit = targetUnit;
         this.fParent = parent;
-        MeasureUnitImpl inputUnitImpl = MeasureUnitImpl.forIdentifier(inputUnit.getIdentifier());
-        MeasureUnitImpl outputUnitImpl = MeasureUnitImpl.forIdentifier(outputUnit.getIdentifier());
-        this.fComplexUnitConverter = new ComplexUnitsConverter(inputUnitImpl, outputUnitImpl,
-                                                               new UnitsData().getConversionRates());
+        MeasureUnitImpl targetUnitImpl = MeasureUnitImpl.forIdentifier(targetUnit.getIdentifier());
+        this.fComplexUnitConverter = new ComplexUnitsConverter(targetUnitImpl, new ConversionRates());
     }
 
     /**
@@ -50,10 +42,11 @@
         MicroProps result = this.fParent.processQuantity(quantity);
 
         quantity.roundToInfinity(); // Enables toDouble
-        List<Measure> measures = this.fComplexUnitConverter.convert(quantity.toBigDecimal(), result.rounder);
+        ComplexUnitsConverter.ComplexConverterResult complexConverterResult
+                = this.fComplexUnitConverter.convert(quantity.toBigDecimal(), result.rounder);
 
         result.outputUnit = this.fOutputUnit;
-        UsagePrefsHandler.mixedMeasuresToMicros(measures, quantity, result);
+        UsagePrefsHandler.mixedMeasuresToMicros(complexConverterResult, quantity, result);
 
         return result;
     }
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 d2e887a..a9a299a 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
@@ -4,12 +4,11 @@
 package android.icu.impl.number;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
 import java.util.List;
 
+import android.icu.impl.units.ComplexUnitsConverter;
 import android.icu.impl.units.MeasureUnitImpl;
 import android.icu.impl.units.UnitsRouter;
-import android.icu.util.Measure;
 import android.icu.util.MeasureUnit;
 import android.icu.util.ULocale;
 
@@ -34,24 +33,10 @@
      * in measures.
      */
     protected static void
-    mixedMeasuresToMicros(List<Measure> measures, DecimalQuantity outQuantity, MicroProps outMicros) {
-        outMicros.mixedMeasures = new ArrayList<>();
-        if (measures.size() > 1) {
-            // For debugging
-            assert (outMicros.outputUnit.getComplexity() == MeasureUnit.Complexity.MIXED);
-
-            // Check that we received the expected number of measurements:
-            assert measures.size() == outMicros.outputUnit.splitToSingleUnits().size();
-
-            // Mixed units: except for the last value, we pass all values to the
-            // LongNameHandler via micros->mixedMeasures.
-            for (int i = 0, n = measures.size() - 1; i < n; i++) {
-                outMicros.mixedMeasures.add(measures.get(i));
-            }
-        }
-
-        // The last value (potentially the only value) gets passed on via quantity.
-        outQuantity.setToBigDecimal((BigDecimal) measures.get(measures.size()- 1).getNumber());
+    mixedMeasuresToMicros(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, DecimalQuantity quantity, MicroProps outMicros) {
+        outMicros.mixedMeasures = complexConverterResult.measures;
+        outMicros.indexOfQuantity = complexConverterResult.indexOfQuantity;
+        quantity.setToBigDecimal((BigDecimal) outMicros.mixedMeasures.get(outMicros.indexOfQuantity).getNumber());
     }
 
     /**
@@ -78,11 +63,8 @@
 
         quantity.roundToInfinity(); // Enables toDouble
         final UnitsRouter.RouteResult routed = fUnitsRouter.route(quantity.toBigDecimal(), micros);
-
-        final List<Measure> routedMeasures = routed.measures;
         micros.outputUnit = routed.outputUnit.build();
-
-        UsagePrefsHandler.mixedMeasuresToMicros(routedMeasures, quantity, micros);
+        UsagePrefsHandler.mixedMeasuresToMicros(routed.complexConverterResult, quantity, micros);
         return micros;
     }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java
index 7651470..7cd4ffb 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/AffixMatcher.java
@@ -110,9 +110,11 @@
             }
 
             // Generate Prefix
+            // TODO: Handle approximately sign?
             PatternStringUtils.patternInfoToStringBuilder(patternInfo,
                     true,
                     type,
+                    false,
                     StandardPlural.OTHER,
                     false,
                     sb);
@@ -120,9 +122,11 @@
                     .fromAffixPattern(sb.toString(), factory, parseFlags);
 
             // Generate Suffix
+            // TODO: Handle approximately sign?
             PatternStringUtils.patternInfoToStringBuilder(patternInfo,
                     false,
                     type,
+                    false,
                     StandardPlural.OTHER,
                     false,
                     sb);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/CombinedCurrencyMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/CombinedCurrencyMatcher.java
index dba1592..d9aa18d 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/CombinedCurrencyMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/CombinedCurrencyMatcher.java
@@ -76,7 +76,7 @@
 
         // TODO: Figure out how to make this faster and re-enable.
         // Computing the "lead code points" set for fastpathing is too slow to use in production.
-        // See http://bugs.icu-project.org/trac/ticket/13584
+        // See https://unicode-org.atlassian.net/browse/ICU-13584
         // // Compute the full set of characters that could be the first in a currency to allow for
         // // efficient smoke test.
         // leadCodePoints = new UnicodeSet();
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/DecimalMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/DecimalMatcher.java
index 8b74020..d638e75 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/DecimalMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/DecimalMatcher.java
@@ -26,7 +26,7 @@
     private final boolean groupingDisabled;
 
     // Fraction grouping parsing is disabled for now but could be enabled later.
-    // See http://bugs.icu-project.org/trac/ticket/10794
+    // See https://unicode-org.atlassian.net/browse/ICU-10794
     // private final boolean fractionGrouping;
 
     /** If true, do not accept numbers in the fraction */
@@ -103,7 +103,7 @@
         grouping2 = grouper.getSecondary();
 
         // Fraction grouping parsing is disabled for now but could be enabled later.
-        // See http://bugs.icu-project.org/trac/ticket/10794
+        // See https://unicode-org.atlassian.net/browse/ICU-10794
         // fractionGrouping = 0 != (parseFlags & ParsingUtils.PARSE_FLAG_FRACTION_GROUPING_ENABLED);
     }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/InfinityMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/InfinityMatcher.java
index 505a3e6..71f86b3 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/InfinityMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/InfinityMatcher.java
@@ -3,8 +3,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.number.parse;
 
-import static android.icu.impl.number.parse.ParsingUtils.safeContains;
-
 import android.icu.impl.StaticUnicodeSets;
 import android.icu.impl.StringSegment;
 import android.icu.text.DecimalFormatSymbols;
@@ -20,7 +18,7 @@
 
     public static InfinityMatcher getInstance(DecimalFormatSymbols symbols) {
         String symbolString = symbols.getInfinity();
-        if (safeContains(DEFAULT.uniSet, symbolString)) {
+        if (DEFAULT.uniSet.contains(symbolString)) {
             return DEFAULT;
         } else {
             return new InfinityMatcher(symbolString);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/MinusSignMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/MinusSignMatcher.java
index bd67d6c..150a167 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/MinusSignMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/MinusSignMatcher.java
@@ -3,8 +3,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.number.parse;
 
-import static android.icu.impl.number.parse.ParsingUtils.safeContains;
-
 import android.icu.impl.StaticUnicodeSets;
 import android.icu.impl.StringSegment;
 import android.icu.text.DecimalFormatSymbols;
@@ -21,7 +19,7 @@
 
     public static MinusSignMatcher getInstance(DecimalFormatSymbols symbols, boolean allowTrailing) {
         String symbolString = symbols.getMinusSignString();
-        if (safeContains(DEFAULT.uniSet, symbolString)) {
+        if (DEFAULT.uniSet.contains(symbolString)) {
             return allowTrailing ? DEFAULT_ALLOW_TRAILING : DEFAULT;
         } else {
             return new MinusSignMatcher(symbolString, allowTrailing);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/ParsingUtils.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/ParsingUtils.java
index 306ef5e..62c33aa 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/ParsingUtils.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/ParsingUtils.java
@@ -35,7 +35,9 @@
             output.add(range.codepoint, range.codepointEnd);
         }
         for (String str : input.strings()) {
-            output.add(str.codePointAt(0));
+            if (!str.isEmpty()) {
+                output.add(str.codePointAt(0));
+            }
         }
     }
 
@@ -44,10 +46,4 @@
             output.add(input.codePointAt(0));
         }
     }
-
-    // TODO: Remove this helper function (and update call sites) when #13805 is fixed
-    public static boolean safeContains(UnicodeSet uniset, CharSequence str) {
-        return str.length() != 0 && uniset.contains(str);
-    }
-
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/PlusSignMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/PlusSignMatcher.java
index 0abdfd9..53f6a33 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/PlusSignMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/PlusSignMatcher.java
@@ -3,8 +3,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.number.parse;
 
-import static android.icu.impl.number.parse.ParsingUtils.safeContains;
-
 import android.icu.impl.StaticUnicodeSets;
 import android.icu.impl.StringSegment;
 import android.icu.text.DecimalFormatSymbols;
@@ -21,7 +19,7 @@
 
     public static PlusSignMatcher getInstance(DecimalFormatSymbols symbols, boolean allowTrailing) {
         String symbolString = symbols.getPlusSignString();
-        if (safeContains(DEFAULT.uniSet, symbolString)) {
+        if (DEFAULT.uniSet.contains(symbolString)) {
             return allowTrailing ? DEFAULT_ALLOW_TRAILING : DEFAULT;
         } else {
             return new PlusSignMatcher(symbolString, allowTrailing);
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/parse/ScientificMatcher.java b/android_icu4j/src/main/java/android/icu/impl/number/parse/ScientificMatcher.java
index 88af56c..3d4d582 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/parse/ScientificMatcher.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/parse/ScientificMatcher.java
@@ -3,8 +3,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.number.parse;
 
-import static android.icu.impl.number.parse.ParsingUtils.safeContains;
-
 import android.icu.impl.StaticUnicodeSets;
 import android.icu.impl.StringSegment;
 import android.icu.impl.number.DecimalQuantity_DualStorageBCD;
@@ -38,9 +36,9 @@
         ignorablesMatcher = IgnorablesMatcher.getInstance(ParsingUtils.PARSE_FLAG_STRICT_IGNORABLES);
 
         String minusSign = symbols.getMinusSignString();
-        customMinusSign = safeContains(minusSignSet(), minusSign) ? null : minusSign;
+        customMinusSign = minusSignSet().contains(minusSign) ? null : minusSign;
         String plusSign = symbols.getPlusSignString();
-        customPlusSign = safeContains(plusSignSet(), plusSign) ? null : plusSign;
+        customPlusSign = plusSignSet().contains(plusSign) ? null : plusSign;
     }
 
     private static UnicodeSet minusSignSet() {
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java b/android_icu4j/src/main/java/android/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java
index 22b4172..f2af55b 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/range/PrefixInfixSuffixLengthHelper.java
@@ -29,4 +29,8 @@
     public int index3() {
         return lengthPrefix + length1 + lengthInfix + length2;
     }
+
+    public int index4() {
+        return lengthPrefix + length1 + lengthInfix + length2 + lengthSuffix;
+    }
 }
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 109f8f5..2d8bcf1 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
@@ -4,6 +4,7 @@
 package android.icu.impl.units;
 
 import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -13,50 +14,99 @@
 import android.icu.impl.number.DecimalQuantity_DualStorageBCD;
 import android.icu.number.Precision;
 import android.icu.util.Measure;
-import android.icu.util.MeasureUnit;
 
 /**
- * Converts from single or compound unit to single, compound or mixed units.
- * For example, from `meter` to `foot+inch`.
+ * Converts from single or compound unit to single, compound or mixed units. For example, from `meter` to `foot+inch`.
  * <p>
- * DESIGN:
- * This class uses `UnitConverter` in order to perform the single converter (i.e. converters from a
- * single unit to another single unit). Therefore, `ComplexUnitsConverter` class contains multiple
- * instances of the `UnitConverter` to perform the conversion.
+ * DESIGN: This class uses <code>UnitsConverter</code> in order to perform the single converter (i.e. converters from
+ * a single unit to another single unit). Therefore, <code>ComplexUnitsConverter</code> class contains multiple
+ * instances of the <code>UnitsConverter</code> to perform the conversion.
  * @hide Only a subset of ICU is exposed in Android
  */
 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<UnitConverter> unitConverters_;
-    // Individual units of mixed units, sorted big to small
-    private ArrayList<MeasureUnitImpl> units_;
-    // Individual units of mixed units, sorted in desired output order
-    private ArrayList<MeasureUnit> outputUnits_;
+    private 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_;
+    private MeasureUnitImpl inputUnit_;
 
     /**
-     * Constructor of `ComplexUnitsConverter`.
-     * NOTE:
-     * - inputUnit and outputUnits must be under the same category
-     * - e.g. meter to feet and inches --> all of them are length units.
+     * Constructs <code>ComplexUnitsConverter</code> for an <code>inputUnit</code> that could be Single, Compound or
+     * Mixed. In case of: 1- Single and Compound units, the conversion will not perform anything, the input will be
+     * equal to the output. 2- Mixed Unit the conversion will consider the input in the biggest unit. and will convert
+     * it to be spread throw the input units. For example: if input unit is "inch-and-foot", and the input is 2.5. The
+     * converter will consider the input value in "foot", because foot is the biggest unit. Then, it will convert 2.5
+     * feet to "inch-and-foot".
      *
-     * @param inputUnit   represents the source unit. (should be single or compound unit).
-     * @param outputUnits represents the output unit. could be any type. (single, compound or mixed).
+     * @param targetUnit
+     *            represents the input unit. could be any type. (single, compound or mixed).
+     */
+    public ComplexUnitsConverter(MeasureUnitImpl targetUnit, ConversionRates conversionRates) {
+        this.units_ = targetUnit.extractIndividualUnitsWithIndices();
+        assert (!this.units_.isEmpty());
+
+        // Assign the biggest unit to inputUnit_.
+        this.inputUnit_ = this.units_.get(0).unitImpl;
+        MeasureUnitImpl.MeasureUnitImplComparator comparator = new MeasureUnitImpl.MeasureUnitImplComparator(
+                conversionRates);
+        for (MeasureUnitImpl.MeasureUnitImplWithIndex unitWithIndex : this.units_) {
+            if (comparator.compare(unitWithIndex.unitImpl, this.inputUnit_) > 0) {
+                this.inputUnit_ = unitWithIndex.unitImpl;
+            }
+        }
+
+        this.init(conversionRates);
+    }
+
+    /**
+     * Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under the same category -
+     * e.g. meter to feet and inches --> all of them are length units.
+     *
+     * @param inputUnitIdentifier
+     *              represents the source unit identifier. (should be single or compound unit).
+     * @param outputUnitsIdentifier
+     *              represents the output unit identifier. could be any type. (single, compound or mixed).
+     */
+    public ComplexUnitsConverter(String inputUnitIdentifier, String outputUnitsIdentifier) {
+        this(
+                MeasureUnitImpl.forIdentifier(inputUnitIdentifier),
+                MeasureUnitImpl.forIdentifier(outputUnitsIdentifier),
+                new ConversionRates()
+        );
+    }
+
+    /**
+     * Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under the same category -
+     * e.g. meter to feet and inches --> all of them are length units.
+     *
+     * @param inputUnit
+     *            represents the source unit. (should be single or compound unit).
+     * @param outputUnits
+     *            represents the output unit. could be any type. (single, compound or mixed).
+     * @param conversionRates
+     *            a ConversionRates instance containing the unit conversion rates.
      */
     public ComplexUnitsConverter(MeasureUnitImpl inputUnit, MeasureUnitImpl outputUnits,
-                                 ConversionRates conversionRates) {
-        units_ = outputUnits.extractIndividualUnits();
-        outputUnits_ = new ArrayList<>(units_.size());
-        for (MeasureUnitImpl itr : units_) {
-            outputUnits_.add(itr.build());
-        }
-        assert (!units_.isEmpty());
+            ConversionRates conversionRates) {
+        this.inputUnit_ = inputUnit;
+        this.units_ = outputUnits.extractIndividualUnitsWithIndices();
+        assert (!this.units_.isEmpty());
 
+        this.init(conversionRates);
+    }
+
+    /**
+     * Sorts units_, which must be populated before calling this, and populates
+     * unitsConverters_.
+     */
+    private void init(ConversionRates conversionRates) {
         // Sort the units in a descending order.
-        Collections.sort(
-                this.units_,
-                Collections.reverseOrder(new MeasureUnitImpl.MeasureUnitImplComparator(conversionRates)));
-
+        Collections.sort(this.units_,
+                Collections.reverseOrder(new MeasureUnitImpl.MeasureUnitImplWithIndexComparator(conversionRates)));
 
         // If the `outputUnits` is `UMEASURE_UNIT_MIXED` such as `foot+inch`. Thus means there is more than one unit
         //  and In this case we need more converters to convert from the `inputUnit` to the first unit in the
@@ -72,29 +122,42 @@
         //              2. convert the residual of 6.56168 feet (0.56168) to inches, which will be (6.74016
         //              inches)
         //              3. then, the final result will be (6 feet and 6.74016 inches)
-        unitConverters_ = new ArrayList<>();
+        unitsConverters_ = new ArrayList<>();
         for (int i = 0, n = units_.size(); i < n; i++) {
             if (i == 0) { // first element
-                unitConverters_.add(new UnitConverter(inputUnit, units_.get(i), conversionRates));
+                unitsConverters_.add(new UnitsConverter(this.inputUnit_, units_.get(i).unitImpl, conversionRates));
             } else {
-                unitConverters_.add(new UnitConverter(units_.get(i - 1), units_.get(i), conversionRates));
+                unitsConverters_
+                        .add(new UnitsConverter(units_.get(i - 1).unitImpl, units_.get(i).unitImpl, conversionRates));
             }
         }
     }
 
     /**
-     * Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest
-     * unit in the MeasureUnit `outputUnit`, is greater than or equal to `limit`.
+     * Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest unit in the
+     * MeasureUnit `outputUnit`, is greater than or equal to `limit`.
      * <p>
-     * For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this
-     * function will convert the `quantity` from `meter` to `foot`, then, it will compare the value in
-     * `foot` with the `limit`.
+     * For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this function will
+     * convert the `quantity` from `meter` to `foot`, then, it will compare the value in `foot` with the `limit`.
      */
     public boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
         assert !units_.isEmpty();
 
         // NOTE: First converter converts to the biggest quantity.
-        return unitConverters_.get(0).convert(quantity).multiply(EPSILON_MULTIPLIER).compareTo(limit) >= 0;
+        return unitsConverters_.get(0).convert(quantity).multiply(EPSILON_MULTIPLIER).compareTo(limit) >= 0;
+    }
+
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public static class ComplexConverterResult {
+        public final int indexOfQuantity;
+        public final List<Measure> measures;
+
+        ComplexConverterResult(int indexOfQuantity, List<Measure> measures) {
+            this.indexOfQuantity = indexOfQuantity;
+            this.measures = measures;
+        }
     }
 
     /**
@@ -105,9 +168,8 @@
      * the smallest element is the only element that could have fractional values. And all
      * other elements are floored to the nearest integer
      */
-    public List<Measure> convert(BigDecimal quantity, Precision rounder) {
-        List<Measure> result = new ArrayList<>(unitConverters_.size());
-        BigDecimal sign = BigDecimal.ONE;
+    public ComplexConverterResult convert(BigDecimal quantity, Precision rounder) {
+        BigInteger sign = BigInteger.ONE;
         if (quantity.compareTo(BigDecimal.ZERO) < 0) {
             quantity = quantity.abs();
             sign = sign.negate();
@@ -119,10 +181,9 @@
         // - N-1 converters convert to bigger units for which we want integers,
         // - the Nth converter (index N-1) converts to the smallest unit, which
         //   isn't (necessarily) an integer.
-        List<BigDecimal> intValues = new ArrayList<>(unitConverters_.size() - 1);
-
-        for (int i = 0, n = unitConverters_.size(); i < n; ++i) {
-            quantity = (unitConverters_.get(i)).convert(quantity);
+        List<BigInteger> intValues = new ArrayList<>(unitsConverters_.size() - 1);
+        for (int i = 0, n = unitsConverters_.size(); i < n; ++i) {
+            quantity = (unitsConverters_.get(i)).convert(quantity);
 
             if (i < n - 1) {
                 // The double type has 15 decimal digits of precision. For choosing
@@ -131,85 +192,93 @@
                 // decision is made. However after the thresholding, we use the
                 // original values to ensure unbiased accuracy (to the extent of
                 // double's capabilities).
-                BigDecimal roundedQuantity =
-                    quantity.multiply(EPSILON_MULTIPLIER).setScale(0, RoundingMode.FLOOR);
-                intValues.add(roundedQuantity);
+                BigInteger flooredQuantity = quantity.multiply(EPSILON_MULTIPLIER).setScale(0, RoundingMode.FLOOR).toBigInteger();
+                intValues.add(flooredQuantity);
 
                 // Keep the residual of the quantity.
-                //   For example: `3.6 feet`, keep only `0.6 feet`
-                quantity = quantity.subtract(roundedQuantity);
-                if (quantity.compareTo(BigDecimal.ZERO) == -1) {
+                // For example: `3.6 feet`, keep only `0.6 feet`
+                BigDecimal remainder = quantity.subtract(BigDecimal.valueOf(flooredQuantity.longValue()));
+                if (remainder.compareTo(BigDecimal.ZERO) == -1) {
                     quantity = BigDecimal.ZERO;
-                }
-            } else { // LAST ELEMENT
-                if (rounder == null) {
-                    // Nothing to do for the last element.
-                    break;
-                }
-
-                // Round the last value
-                // TODO(ICU-21288): get smarter about precision for mixed units.
-                DecimalQuantity quant = new DecimalQuantity_DualStorageBCD(quantity);
-                rounder.apply(quant);
-                quantity = quant.toBigDecimal();
-                if (i == 0) {
-                    // Last element is also the first element, so we're done
-                    break;
-                }
-
-                // Check if there's a carry, and bubble it back up the resulting intValues.
-                BigDecimal carry = unitConverters_.get(i)
-                                       .convertInverse(quantity)
-                                       .multiply(EPSILON_MULTIPLIER)
-                                       .setScale(0, RoundingMode.FLOOR);
-                if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
-                    break;
-                }
-                quantity = quantity.subtract(unitConverters_.get(i).convert(carry));
-                intValues.set(i - 1, intValues.get(i - 1).add(carry));
-
-                // We don't use the first converter: that one is for the input unit
-                for (int j = i - 1; j > 0; j--) {
-                    carry = unitConverters_.get(j)
-                                .convertInverse(intValues.get(j))
-                                .multiply(EPSILON_MULTIPLIER)
-                                .setScale(0, RoundingMode.FLOOR);
-                    if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
-                        break;
-                    }
-                    intValues.set(j, intValues.get(j).subtract(unitConverters_.get(j).convert(carry)));
-                    intValues.set(j - 1, intValues.get(j - 1).add(carry));
+                } else {
+                    quantity = remainder;
                 }
             }
         }
 
-        // Package values into Measure instances in result:
-        for (int i = 0, n = unitConverters_.size(); i < n; ++i) {
+        quantity = applyRounder(intValues, quantity, rounder);
+
+        // Initialize empty measures.
+        List<Measure> measures = new ArrayList<>(unitsConverters_.size());
+        for (int i = 0; i < unitsConverters_.size(); i++) {
+            measures.add(null);
+        }
+
+        // Package values into Measure instances in measures:
+        int indexOfQuantity = -1;
+        for (int i = 0, n = unitsConverters_.size(); i < n; ++i) {
             if (i < n - 1) {
-                result.add(new Measure(intValues.get(i).multiply(sign), units_.get(i).build()));
+                Measure measure = new Measure(intValues.get(i).multiply(sign), units_.get(i).unitImpl.build());
+                measures.set(units_.get(i).index, measure);
             } else {
-                result.add(new Measure(quantity.multiply(sign), units_.get(i).build()));
+                indexOfQuantity = units_.get(i).index;
+                Measure measure =
+                        new Measure(quantity.multiply(BigDecimal.valueOf(sign.longValue())),
+                                units_.get(i).unitImpl.build());
+                measures.set(indexOfQuantity, measure);
             }
         }
 
-        for (int i = 0; i < result.size(); i++) {
-            for (int j = i; j < result.size(); j++) {
-                // Find the next expected unit, and swap it into place.
-                if (result.get(j).getUnit().equals(outputUnits_.get(i))) {
-                    if (j != i) {
-                        Measure tmp = result.get(j);
-                        result.set(j, result.get(i));
-                        result.set(i, tmp);
-                    }
-                }
-            }
+        return new ComplexConverterResult(indexOfQuantity , measures);
+    }
+
+    /**
+     * Applies the rounder to the quantity (last element) and bubble up any carried value to all the intValues.
+     *
+     * @return the rounded quantity
+     */
+    private BigDecimal applyRounder(List<BigInteger> intValues, BigDecimal quantity, Precision rounder) {
+        if (rounder == null) {
+            return quantity;
         }
-    
-        return result;
+
+        DecimalQuantity quantityBCD = new DecimalQuantity_DualStorageBCD(quantity);
+        rounder.apply(quantityBCD);
+        quantity = quantityBCD.toBigDecimal();
+
+        if (intValues.size() == 0) {
+            // There is only one element, Therefore, nothing to be done
+            return quantity;
+        }
+
+        // Check if there's a carry, and bubble it back up the resulting intValues.
+        int lastIndex = unitsConverters_.size() - 1;
+        BigDecimal carry = unitsConverters_.get(lastIndex).convertInverse(quantity).multiply(EPSILON_MULTIPLIER)
+                .setScale(0, RoundingMode.FLOOR);
+        if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
+            return quantity;
+        }
+        quantity = quantity.subtract(unitsConverters_.get(lastIndex).convert(carry));
+        intValues.set(lastIndex - 1, intValues.get(lastIndex - 1).add(carry.toBigInteger()));
+
+        // We don't use the first converter: that one is for the input unit
+        for (int j = lastIndex - 1; j > 0; j--) {
+            carry = unitsConverters_.get(j)
+                    .convertInverse(BigDecimal.valueOf(intValues.get(j).longValue()))
+                    .multiply(EPSILON_MULTIPLIER)
+                    .setScale(0, RoundingMode.FLOOR);
+            if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
+                break;
+            }
+            intValues.set(j, intValues.get(j).subtract(unitsConverters_.get(j).convert(carry).toBigInteger()));
+            intValues.set(j - 1, intValues.get(j - 1).add(carry.toBigInteger()));
+        }
+
+        return quantity;
     }
 
     @Override
     public String toString() {
-        return "ComplexUnitsConverter [unitConverters_=" + unitConverters_ + ", units_=" + units_ + "]";
+        return "ComplexUnitsConverter [unitsConverters_=" + unitsConverters_ + ", units_=" + units_ + "]";
     }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/ConversionRates.java b/android_icu4j/src/main/java/android/icu/impl/units/ConversionRates.java
index 036b9a0..4e6c60f 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/ConversionRates.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/ConversionRates.java
@@ -41,16 +41,20 @@
      * @param singleUnit
      * @return
      */
-    private UnitConverter.Factor getFactorToBase(SingleUnitImpl singleUnit) {
+    // In ICU4C, this is called loadCompoundFactor().
+    private UnitsConverter.Factor getFactorToBase(SingleUnitImpl singleUnit) {
         int power = singleUnit.getDimensionality();
-        MeasureUnit.SIPrefix siPrefix = singleUnit.getSiPrefix();
-        UnitConverter.Factor result = UnitConverter.Factor.processFactor(mapToConversionRate.get(singleUnit.getSimpleUnit()).getConversionRate());
+        MeasureUnit.MeasurePrefix unitPrefix = singleUnit.getPrefix();
+        UnitsConverter.Factor result = UnitsConverter.Factor.processFactor(mapToConversionRate.get(singleUnit.getSimpleUnitID()).getConversionRate());
 
-        return result.applySiPrefix(siPrefix).power(power); // NOTE: you must apply the SI prefixes before the power.
+        // Prefix before power, because:
+        // - square-kilometer to square-meter: (1000)^2
+        // - square-kilometer to square-foot (approximate): (3.28*1000)^2
+        return result.applyPrefix(unitPrefix).power(power);
     }
 
-    public UnitConverter.Factor getFactorToBase(MeasureUnitImpl measureUnit) {
-        UnitConverter.Factor result = new UnitConverter.Factor();
+    public UnitsConverter.Factor getFactorToBase(MeasureUnitImpl measureUnit) {
+        UnitsConverter.Factor result = new UnitsConverter.Factor();
         for (SingleUnitImpl singleUnit :
                 measureUnit.getSingleUnits()) {
             result = result.multiply(getFactorToBase(singleUnit));
@@ -59,13 +63,14 @@
         return result;
     }
 
-    protected BigDecimal getOffset(MeasureUnitImpl source, MeasureUnitImpl target, UnitConverter.Factor
-            sourceToBase, UnitConverter.Factor targetToBase, UnitConverter.Convertibility convertibility) {
-        if (convertibility != UnitConverter.Convertibility.CONVERTIBLE) return BigDecimal.valueOf(0);
+    // In ICU4C, this functionality is found in loadConversionRate().
+    protected BigDecimal getOffset(MeasureUnitImpl source, MeasureUnitImpl target, UnitsConverter.Factor
+            sourceToBase, UnitsConverter.Factor targetToBase, UnitsConverter.Convertibility convertibility) {
+        if (convertibility != UnitsConverter.Convertibility.CONVERTIBLE) return BigDecimal.valueOf(0);
         if (!(checkSimpleUnit(source) && checkSimpleUnit(target))) return BigDecimal.valueOf(0);
 
-        String sourceSimpleIdentifier = source.getSingleUnits().get(0).getSimpleUnit();
-        String targetSimpleIdentifier = target.getSingleUnits().get(0).getSimpleUnit();
+        String sourceSimpleIdentifier = source.getSingleUnits().get(0).getSimpleUnitID();
+        String targetSimpleIdentifier = target.getSingleUnits().get(0).getSimpleUnitID();
 
         BigDecimal sourceOffset = this.mapToConversionRate.get(sourceSimpleIdentifier).getOffset();
         BigDecimal targetOffset = this.mapToConversionRate.get(targetSimpleIdentifier).getOffset();
@@ -107,7 +112,7 @@
      * This method is helpful when checking the convertibility because no need to check convertibility.
      */
     public ArrayList<SingleUnitImpl> extractBaseUnits(SingleUnitImpl singleUnit) {
-        String target = mapToConversionRate.get(singleUnit.getSimpleUnit()).getTarget();
+        String target = mapToConversionRate.get(singleUnit.getSimpleUnitID()).getTarget();
         MeasureUnitImpl targetImpl = MeasureUnitImpl.UnitsParser.parseForIdentifier(target);
 
         // Each unit must be powered by the same dimension
@@ -128,7 +133,7 @@
         if (measureUnitImpl.getComplexity() != MeasureUnit.Complexity.SINGLE) return false;
         SingleUnitImpl singleUnit = measureUnitImpl.getSingleUnits().get(0);
 
-        if (singleUnit.getSiPrefix() != MeasureUnit.SIPrefix.ONE) return false;
+        if (singleUnit.getPrefix() != MeasureUnit.MeasurePrefix.ONE) return false;
         if (singleUnit.getDimensionality() != 1) return false;
 
         return true;
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 135d3ba..824539e 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
@@ -21,21 +21,18 @@
 public class MeasureUnitImpl {
 
     /**
-     * The full unit identifier.  Null if not computed.
+     * The full unit identifier. Null if not computed.
      */
     private String identifier = null;
-
     /**
      * The complexity, either SINGLE, COMPOUND, or MIXED.
      */
     private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE;
-
     /**
-     * The list of simple units. These may be summed or multiplied, based on the
+     * The list of single units. These may be summed or multiplied, based on the
      * value of the complexity field.
      * <p>
-     * The "dimensionless" unit (SingleUnitImpl default constructor) must not be
-     * added to this list.
+     * The "dimensionless" unit (SingleUnitImpl default constructor) must not be added to this list.
      * <p>
      * The "dimensionless" <code>MeasureUnitImpl</code> has an empty <code>singleUnits</code>.
      */
@@ -74,13 +71,46 @@
         MeasureUnitImpl result = new MeasureUnitImpl();
         result.complexity = this.complexity;
         result.identifier = this.identifier;
-        for (SingleUnitImpl single : this.singleUnits) {
-            result.singleUnits.add(single.copy());
+        for (SingleUnitImpl singleUnit : this.singleUnits) {
+            result.singleUnits.add(singleUnit.copy());
         }
         return result;
     }
 
     /**
+     * Returns a simplified version of the unit.
+     * NOTE: the simplification happen when there are two units equals in their base unit and their
+     * prefixes.
+     *
+     * Example 1: "square-meter-per-meter" --> "meter"
+     * Example 2: "square-millimeter-per-meter" --> "square-millimeter-per-meter"
+     */
+    public MeasureUnitImpl copyAndSimplify() {
+        MeasureUnitImpl result = new MeasureUnitImpl();
+        for (SingleUnitImpl singleUnit : this.getSingleUnits()) {
+            // This `for` loop will cause time complexity to be O(n^2).
+            // However, n is very small (number of units, generally, at maximum equal to 10)
+            boolean unitExist = false;
+            for (SingleUnitImpl resultSingleUnit : result.getSingleUnits()) {
+                if(resultSingleUnit.getSimpleUnitID().compareTo(singleUnit.getSimpleUnitID()) == 0
+                &&
+                        resultSingleUnit.getPrefix().getIdentifier().compareTo(singleUnit.getPrefix().getIdentifier()) == 0
+                ) {
+                    unitExist = true;
+                    resultSingleUnit.setDimensionality(resultSingleUnit.getDimensionality() + singleUnit.getDimensionality());
+                    break;
+                }
+            }
+
+            if(!unitExist) {
+                result.appendSingleUnit(singleUnit);
+            }
+        }
+
+        return result;
+    }
+
+    /**
      * Returns the list of simple units.
      */
     public ArrayList<SingleUnitImpl> getSingleUnits() {
@@ -98,29 +128,20 @@
         }
     }
 
-    /**
-     * Extracts the list of all the individual units inside the `MeasureUnitImpl`.
-     * For example:
-     * -   if the <code>MeasureUnitImpl</code> is <code>foot-per-hour</code>
-     * it will return a list of 1 <code>{foot-per-hour}</code>
-     * -   if the <code>MeasureUnitImpl</code> is <code>foot-and-inch</code>
-     * it will return a list of 2 <code>{ foot, inch}</code>
-     *
-     * @return a list of <code>MeasureUnitImpl</code>
-     */
-    public ArrayList<MeasureUnitImpl> extractIndividualUnits() {
-        ArrayList<MeasureUnitImpl> result = new ArrayList<>();
+    public ArrayList<MeasureUnitImplWithIndex> extractIndividualUnitsWithIndices() {
+        ArrayList<MeasureUnitImplWithIndex> result = new ArrayList<>();
         if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
             // In case of mixed units, each single unit can be considered as a stand alone MeasureUnitImpl.
+            int i = 0;
             for (SingleUnitImpl singleUnit :
                     this.getSingleUnits()) {
-                result.add(new MeasureUnitImpl(singleUnit));
+                result.add(new MeasureUnitImplWithIndex(i++, new MeasureUnitImpl(singleUnit)));
             }
 
             return result;
         }
 
-        result.add(this.copy());
+        result.add(new MeasureUnitImplWithIndex(0, this.copy()));
         return result;
     }
 
@@ -145,7 +166,7 @@
         identifier = null;
 
         if (singleUnit == null) {
-            // We don't append dimensionless units.
+            // Do not append dimensionless units.
             return false;
         }
 
@@ -169,8 +190,8 @@
         // Add a copy of singleUnit
         this.singleUnits.add(singleUnit.copy());
 
-        // If the MeasureUnitImpl is `UMEASURE_UNIT_SINGLE` and after the appending a unit, the singleUnits are more
-        // than one singleUnit. thus means the complexity should be `UMEASURE_UNIT_COMPOUND`
+        // If the MeasureUnitImpl is `UMEASURE_UNIT_SINGLE` and after the appending a unit, the singleUnits contains
+        // more than one. thus means the complexity should be `UMEASURE_UNIT_COMPOUND`
         if (this.singleUnits.size() > 1 && this.complexity == MeasureUnit.Complexity.SINGLE) {
             this.setComplexity(MeasureUnit.Complexity.COMPOUND);
         }
@@ -202,7 +223,6 @@
         throw new UnsupportedOperationException();
     }
 
-
     /**
      * Returns the CLDR unit identifier and null if not computed.
      */
@@ -227,6 +247,8 @@
             // to this.result, we wish it to contain the zero-length string.
             return;
         }
+
+
         if (this.complexity == MeasureUnit.Complexity.COMPOUND) {
             // Note: don't sort a MIXED unit
             Collections.sort(this.getSingleUnits(), new SingleUnitComparator());
@@ -244,7 +266,6 @@
                 firstTimeNegativeDimension = false;
             }
 
-            String singleUnitIdentifier = singleUnit.getNeutralIdentifier();
             if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
                 if (result.length() != 0) {
                     result.append("-and-");
@@ -263,12 +284,17 @@
                 }
             }
 
-            result.append(singleUnitIdentifier);
+            result.append(singleUnit.getNeutralIdentifier());
         }
 
         this.identifier = result.toString();
     }
 
+    @Override
+    public String toString() {
+        return "MeasureUnitImpl [" + build().getIdentifier() + "]";
+    }
+
     /**
      * @hide Only a subset of ICU is exposed in Android
      */
@@ -384,6 +410,19 @@
     /**
      * @hide Only a subset of ICU is exposed in Android
      */
+    public static class MeasureUnitImplWithIndex {
+        int index;
+        MeasureUnitImpl unitImpl;
+
+        MeasureUnitImplWithIndex(int index, MeasureUnitImpl unitImpl) {
+            this.index = index;
+            this.unitImpl = unitImpl;
+        }
+    }
+
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
     public static class UnitsParser {
         // This used only to not build the trie each time we use the parser
         private volatile static CharsTrie savedTrie = null;
@@ -401,6 +440,11 @@
         //     * unit", sawAnd is set to true. If not, it is left as is.
         private boolean fSawAnd = false;
 
+        // Cache the MeasurePrefix values array to make getPrefixFromTrieIndex()
+        // more efficient
+        private static MeasureUnit.MeasurePrefix[] measurePrefixValues =
+            MeasureUnit.MeasurePrefix.values();
+
         private UnitsParser(String identifier) {
             this.fSource = identifier;
 
@@ -416,6 +460,11 @@
             CharsTrieBuilder trieBuilder;
             trieBuilder = new CharsTrieBuilder();
 
+            // Add SI and binary prefixes
+            for (MeasureUnit.MeasurePrefix unitPrefix : measurePrefixValues) {
+                trieBuilder.add(unitPrefix.getIdentifier(), getTrieIndexForPrefix(unitPrefix));
+            }
+
             // Add syntax parts (compound, power prefixes)
             trieBuilder.add("-per-", CompoundPart.PER.getTrieIndex());
             trieBuilder.add("-", CompoundPart.TIMES.getTrieIndex());
@@ -438,12 +487,6 @@
             trieBuilder.add("pow14-", PowerPart.P14.getTrieIndex());
             trieBuilder.add("pow15-", PowerPart.P15.getTrieIndex());
 
-            // Add SI prefixes
-            for (MeasureUnit.SIPrefix siPrefix :
-                    MeasureUnit.SIPrefix.values()) {
-                trieBuilder.add(siPrefix.getIdentifier(), getTrieIndex(siPrefix));
-            }
-
             // Add simple units
             String[] simpleUnits = UnitsData.getSimpleUnits();
             for (int i = 0; i < simpleUnits.length; i++) {
@@ -472,18 +515,12 @@
 
         }
 
-        private static MeasureUnit.SIPrefix getSiPrefixFromTrieIndex(int trieIndex) {
-            for (MeasureUnit.SIPrefix element :
-                    MeasureUnit.SIPrefix.values()) {
-                if (getTrieIndex(element) == trieIndex)
-                    return element;
-            }
-
-            throw new IllegalArgumentException("Incorrect trieIndex");
+        private static MeasureUnit.MeasurePrefix getPrefixFromTrieIndex(int trieIndex) {
+            return measurePrefixValues[trieIndex - UnitsData.Constants.kPrefixOffset];
         }
 
-        private static int getTrieIndex(MeasureUnit.SIPrefix prefix) {
-            return prefix.getPower() + UnitsData.Constants.kSIPrefixOffset;
+        private static int getTrieIndexForPrefix(MeasureUnit.MeasurePrefix prefix) {
+            return prefix.ordinal() + UnitsData.Constants.kPrefixOffset;
         }
 
         private MeasureUnitImpl parse() {
@@ -511,7 +548,7 @@
                     MeasureUnit.Complexity complexity =
                             fSawAnd ? MeasureUnit.Complexity.MIXED : MeasureUnit.Complexity.COMPOUND;
                     if (result.getSingleUnits().size() == 2) {
-                        // After appending two singleUnits, the complexity will be `UMEASURE_UNIT_COMPOUND`
+                        // After appending two singleUnits, the complexity will be MeasureUnit.Complexity.COMPOUND
                         assert result.getComplexity() == MeasureUnit.Complexity.COMPOUND;
                         result.setComplexity(complexity);
                     } else if (result.getComplexity() != complexity) {
@@ -537,9 +574,9 @@
             SingleUnitImpl result = new SingleUnitImpl();
 
             // state:
-            // 0 = no tokens seen yet (will accept power, SI prefix, or simple unit)
+            // 0 = no tokens seen yet (will accept power, SI or binary prefix, or simple unit)
             // 1 = power token seen (will not accept another power token)
-            // 2 = SI prefix token seen (will not accept a power or SI prefix token)
+            // 2 = SI or binary prefix token seen (will not accept a power, or SI or binary prefix token)
             int state = 0;
 
             boolean atStart = fIndex == 0;
@@ -604,12 +641,12 @@
                         state = 1;
                         break;
 
-                    case TYPE_SI_PREFIX:
+                    case TYPE_PREFIX:
                         if (state > 1) {
                             throw new IllegalArgumentException();
                         }
 
-                        result.setSiPrefix(token.getSIPrefix());
+                        result.setPrefix(token.getPrefix());
                         state = 2;
                         break;
 
@@ -687,9 +724,9 @@
                 return this.type;
             }
 
-            public MeasureUnit.SIPrefix getSIPrefix() {
-                assert this.type == Type.TYPE_SI_PREFIX;
-                return getSiPrefixFromTrieIndex(this.fMatch);
+            public MeasureUnit.MeasurePrefix getPrefix() {
+                assert this.type == Type.TYPE_PREFIX;
+                return getPrefixFromTrieIndex(this.fMatch);
             }
 
             // Valid only for tokens with type TYPE_COMPOUND_PART.
@@ -713,6 +750,7 @@
             }
 
             public int getSimpleUnitIndex() {
+                assert this.type == Type.TYPE_SIMPLE_UNIT;
                 return this.fMatch - UnitsData.Constants.kSimpleUnitOffset;
             }
 
@@ -724,7 +762,7 @@
                 }
 
                 if (fMatch < UnitsData.Constants.kCompoundPartOffset) {
-                    return Type.TYPE_SI_PREFIX;
+                    return Type.TYPE_PREFIX;
                 }
                 if (fMatch < UnitsData.Constants.kInitialCompoundPartOffset) {
                     return Type.TYPE_COMPOUND_PART;
@@ -741,7 +779,7 @@
 
             enum Type {
                 TYPE_UNDEFINED,
-                TYPE_SI_PREFIX,
+                TYPE_PREFIX,
                 // Token type for "-per-", "-", and "-and-".
                 TYPE_COMPOUND_PART,
                 // Token type for "per-".
@@ -761,8 +799,23 @@
 
         @Override
         public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) {
-            UnitConverter fromO1toO2 = new UnitConverter(o1, o2, conversionRates);
-            return fromO1toO2.convert(BigDecimal.valueOf(1)).compareTo(BigDecimal.valueOf(1));
+            BigDecimal factor1 = this.conversionRates.getFactorToBase(o1).getConversionRate();
+            BigDecimal factor2 = this.conversionRates.getFactorToBase(o2).getConversionRate();
+
+            return factor1.compareTo(factor2);
+        }
+    }
+
+    static class MeasureUnitImplWithIndexComparator implements Comparator<MeasureUnitImplWithIndex> {
+        private MeasureUnitImplComparator measureUnitImplComparator;
+
+        public MeasureUnitImplWithIndexComparator(ConversionRates conversionRates) {
+            this.measureUnitImplComparator = new MeasureUnitImplComparator(conversionRates);
+        }
+
+        @Override
+        public int compare(MeasureUnitImplWithIndex o1, MeasureUnitImplWithIndex o2) {
+            return this.measureUnitImplComparator.compare(o1.unitImpl, o2.unitImpl);
         }
     }
 
@@ -772,9 +825,4 @@
             return o1.compareTo(o2);
         }
     }
-
-    @Override
-    public String toString() {
-        return "MeasureUnitImpl [" + build().getIdentifier() + "]";
-    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/SingleUnitImpl.java b/android_icu4j/src/main/java/android/icu/impl/units/SingleUnitImpl.java
index 22ffe07..3fc1b4e 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/SingleUnitImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/SingleUnitImpl.java
@@ -6,7 +6,11 @@
 
 import android.icu.util.MeasureUnit;
 
+// TODO: revisit documentation in this file. E.g. we don't do dimensionless
+// units in Java? We use null instead.
+
 /**
+ * A class representing a single unit (optional SI or binary prefix, and dimensionality).
  * @hide Only a subset of ICU is exposed in Android
  */
 public class SingleUnitImpl {
@@ -24,7 +28,7 @@
      * The default value is "", meaning the dimensionless unit:
      * isDimensionless() will return true, until index is changed.
      */
-    private String simpleUnit = "";
+    private String simpleUnitID = "";
     /**
      * Determine the power of the `SingleUnit`. For example, for "square-meter", the dimensionality will be `2`.
      * <p>
@@ -33,16 +37,16 @@
      */
     private int dimensionality = 1;
     /**
-     * SI Prefix
+     * SI or binary prefix.
      */
-    private MeasureUnit.SIPrefix siPrefix = MeasureUnit.SIPrefix.ONE;
+    private MeasureUnit.MeasurePrefix unitPrefix = MeasureUnit.MeasurePrefix.ONE;
 
     public SingleUnitImpl copy() {
         SingleUnitImpl result = new SingleUnitImpl();
         result.index = this.index;
         result.dimensionality = this.dimensionality;
-        result.simpleUnit = this.simpleUnit;
-        result.siPrefix = this.siPrefix;
+        result.simpleUnitID = this.simpleUnitID;
+        result.unitPrefix = this.unitPrefix;
 
         return result;
     }
@@ -53,31 +57,30 @@
     }
 
     /**
-     * Generates an neutral identifier string for a single unit which means we do not include the dimension signal.
+     * Generates a neutral identifier string for a single unit which means we do not include the dimension signal.
      */
     public String getNeutralIdentifier() {
         StringBuilder result = new StringBuilder();
-        int posPower = Math.abs(this.getDimensionality());
+        int absPower = Math.abs(this.getDimensionality());
 
-        assert posPower > 0 : "getIdentifier does not support the dimensionless";
+        assert absPower > 0 : "this function does not support the dimensionless single units";
 
-        if (posPower == 1) {
+        if (absPower == 1) {
             // no-op
-        } else if (posPower == 2) {
+        } else if (absPower == 2) {
             result.append("square-");
-        } else if (posPower == 3) {
+        } else if (absPower == 3) {
             result.append("cubic-");
-        } else if (posPower <= 15) {
+        } else if (absPower <= 15) {
             result.append("pow");
-            result.append(posPower);
+            result.append(absPower);
             result.append('-');
         } else {
-            // TODO: IllegalArgumentException might not be appropriate here
             throw new IllegalArgumentException("Unit Identifier Syntax Error");
         }
 
-        result.append(this.getSiPrefix().getIdentifier());
-        result.append(this.getSimpleUnit());
+        result.append(this.getPrefix().getIdentifier());
+        result.append(this.getSimpleUnitID());
 
         return result.toString();
     }
@@ -86,6 +89,9 @@
      * Compare this SingleUnitImpl to another SingleUnitImpl for the sake of
      * sorting and coalescing.
      * <p>
+     * Sort order of units is specified by UTS #35
+     * (https://unicode.org/reports/tr35/tr35-info.html#Unit_Identifier_Normalization).
+     * <p>
      * Takes the sign of dimensionality into account, but not the absolute
      * value: per-meter is not considered the same as meter, but meter is
      * considered the same as square-meter.
@@ -102,38 +108,71 @@
         if (dimensionality > 0 && other.dimensionality < 0) {
             return -1;
         }
+        // Sort by official quantity order
+        int thisCategoryIndex = UnitsData.getCategoryIndexOfSimpleUnit(index);
+        int otherCategoryIndex = UnitsData.getCategoryIndexOfSimpleUnit(other.index);
+        if (thisCategoryIndex < otherCategoryIndex) {
+            return -1;
+        }
+        if (thisCategoryIndex > otherCategoryIndex) {
+            return 1;
+        }
+        // If quantity order didn't help, then we go by index.
         if (index < other.index) {
             return -1;
         }
         if (index > other.index) {
             return 1;
         }
-        if (this.getSiPrefix().getPower() < other.getSiPrefix().getPower()) {
-            return -1;
-        }
-        if (this.getSiPrefix().getPower() > other.getSiPrefix().getPower()) {
+
+        // When comparing binary prefixes vs SI prefixes, instead of comparing the actual values, we can
+        // multiply the binary prefix power by 3 and compare the powers. if they are equal, we can can
+        // compare the bases.
+        // NOTE: this methodology will fail if the binary prefix more than or equal 98.
+        int unitBase = this.unitPrefix.getBase();
+        int otherUnitBase = other.unitPrefix.getBase();
+        // Values for comparison purposes only.
+        int unitPowerComp =
+                unitBase == 1024 /* Binary Prefix */ ? this.unitPrefix.getPower() * 3
+                        : this.unitPrefix.getPower();
+        int otherUnitPowerComp =
+                otherUnitBase == 1024 /* Binary Prefix */ ? other.unitPrefix.getPower() * 3
+                        : other.unitPrefix.getPower();
+
+        if (unitPowerComp < otherUnitPowerComp) {
             return 1;
         }
+        if (unitPowerComp > otherUnitPowerComp) {
+            return -1;
+        }
+
+        if (unitBase < otherUnitBase) {
+            return 1;
+        }
+        if (unitBase > otherUnitBase) {
+            return -1;
+        }
+
         return 0;
     }
 
     /**
      * Checks whether this SingleUnitImpl is compatible with another for the purpose of coalescing.
      * <p>
-     * Units with the same base unit and SI prefix should match, except that they must also have
-     * the same dimensionality sign, such that we don't merge numerator and denominator.
+     * Units with the same base unit and SI or binary prefix should match, except that they must also
+     * have the same dimensionality sign, such that we don't merge numerator and denominator.
      */
     boolean isCompatibleWith(SingleUnitImpl other) {
         return (compareTo(other) == 0);
     }
 
-    public String getSimpleUnit() {
-        return simpleUnit;
+    public String getSimpleUnitID() {
+        return simpleUnitID;
     }
 
     public void setSimpleUnit(int simpleUnitIndex, String[] simpleUnits) {
         this.index = simpleUnitIndex;
-        this.simpleUnit = simpleUnits[simpleUnitIndex];
+        this.simpleUnitID = simpleUnits[simpleUnitIndex];
     }
 
     public int getDimensionality() {
@@ -144,14 +183,15 @@
         this.dimensionality = dimensionality;
     }
 
-    public MeasureUnit.SIPrefix getSiPrefix() {
-        return siPrefix;
+    public MeasureUnit.MeasurePrefix getPrefix() {
+        return unitPrefix;
     }
 
-    public void setSiPrefix(MeasureUnit.SIPrefix siPrefix) {
-        this.siPrefix = siPrefix;
+    public void setPrefix(MeasureUnit.MeasurePrefix unitPrefix) {
+        this.unitPrefix = unitPrefix;
     }
 
+    // TODO: unused? Delete?
     public int getIndex() {
         return index;
     }
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitConverter.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitConverter.java
deleted file mode 100644
index 81bdc5e..0000000
--- a/android_icu4j/src/main/java/android/icu/impl/units/UnitConverter.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/* GENERATED SOURCE. DO NOT MODIFY. */
-// © 2020 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
-package android.icu.impl.units;
-
-import static java.math.MathContext.DECIMAL128;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.regex.Pattern;
-
-import android.icu.util.MeasureUnit;
-
-/**
- * @hide Only a subset of ICU is exposed in Android
- */
-public class UnitConverter {
-    private BigDecimal conversionRate;
-    private BigDecimal offset;
-
-    /**
-     * Constructor of `UnitConverter`.
-     * NOTE:
-     * - source and target must be under the same category
-     * - e.g. meter to mile --> both of them are length units.
-     *
-     * @param source          represents the source unit.
-     * @param target          represents the target unit.
-     * @param conversionRates contains all the needed conversion rates.
-     */
-    public UnitConverter(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) {
-        Convertibility convertibility = extractConvertibility(source, target, conversionRates);
-        assert (convertibility == Convertibility.CONVERTIBLE || convertibility == Convertibility.RECIPROCAL);
-
-        Factor sourceToBase = conversionRates.getFactorToBase(source);
-        Factor targetToBase = conversionRates.getFactorToBase(target);
-
-        if (convertibility == Convertibility.CONVERTIBLE) {
-            this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate();
-        } else {
-            this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate();
-        }
-
-        // calculate the offset
-        this.offset = conversionRates.getOffset(source, target, sourceToBase, targetToBase, convertibility);
-    }
-
-    static public Convertibility extractConvertibility(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) {
-        ArrayList<SingleUnitImpl> sourceSingleUnits = conversionRates.extractBaseUnits(source);
-        ArrayList<SingleUnitImpl> targetSingleUnits = conversionRates.extractBaseUnits(target);
-
-        HashMap<String, Integer> dimensionMap = new HashMap<>();
-
-        insertInMap(dimensionMap, sourceSingleUnits, 1);
-        insertInMap(dimensionMap, targetSingleUnits, -1);
-
-        if (areDimensionsZeroes(dimensionMap)) return Convertibility.CONVERTIBLE;
-
-        insertInMap(dimensionMap, targetSingleUnits, 2);
-        if (areDimensionsZeroes(dimensionMap)) return Convertibility.RECIPROCAL;
-
-        return Convertibility.UNCONVERTIBLE;
-    }
-
-    /**
-     * Helpers
-     */
-    private static void insertInMap(HashMap<String, Integer> dimensionMap, ArrayList<SingleUnitImpl> singleUnits, int multiplier) {
-        for (SingleUnitImpl singleUnit :
-                singleUnits) {
-            if (dimensionMap.containsKey(singleUnit.getSimpleUnit())) {
-                dimensionMap.put(singleUnit.getSimpleUnit(), dimensionMap.get(singleUnit.getSimpleUnit()) + singleUnit.getDimensionality() * multiplier);
-            } else {
-                dimensionMap.put(singleUnit.getSimpleUnit(), singleUnit.getDimensionality() * multiplier);
-            }
-        }
-    }
-
-    private static boolean areDimensionsZeroes(HashMap<String, Integer> dimensionMap) {
-        for (Integer value :
-                dimensionMap.values()) {
-            if (!value.equals(0)) return false;
-        }
-
-        return true;
-    }
-
-    public BigDecimal convert(BigDecimal inputValue) {
-        return inputValue.multiply(this.conversionRate).add(offset);
-    }
-
-    public BigDecimal convertInverse(BigDecimal inputValue) {
-        return inputValue.subtract(offset).divide(this.conversionRate, DECIMAL128);
-    }
-
-    /**
-     * @hide Only a subset of ICU is exposed in Android
-     */
-    public enum Convertibility {
-        CONVERTIBLE,
-        RECIPROCAL,
-        UNCONVERTIBLE,
-    }
-
-    // TODO: improve documentation and Constant implementation
-
-    /**
-     * Responsible for all the Factor operation
-     * NOTE:
-     * This class is immutable
-     */
-    static class Factor {
-        private BigDecimal factorNum;
-        private BigDecimal factorDen;
-        /* FACTOR CONSTANTS */
-        private int
-                CONSTANT_FT2M = 0,    // ft2m stands for foot to meter.
-                CONSTANT_PI = 0,      // PI
-                CONSTANT_GRAVITY = 0, // Gravity
-                CONSTANT_G = 0,
-                CONSTANT_GAL_IMP2M3 = 0, // Gallon imp to m3
-                CONSTANT_LB2KG = 0;      // Pound to Kilogram
-
-
-        /**
-         * Creates Empty Factor
-         */
-        public Factor() {
-            this.factorNum = BigDecimal.valueOf(1);
-            this.factorDen = BigDecimal.valueOf(1);
-        }
-
-        public static Factor processFactor(String factor) {
-            assert (!factor.isEmpty());
-
-            // Remove all spaces in the factor
-            factor = factor.replaceAll("\\s+", "");
-
-            String[] fractions = factor.split("/");
-            assert (fractions.length == 1 || fractions.length == 2);
-
-            if (fractions.length == 1) {
-                return processFactorWithoutDivision(fractions[0]);
-            }
-
-            Factor num = processFactorWithoutDivision(fractions[0]);
-            Factor den = processFactorWithoutDivision(fractions[1]);
-            return num.divide(den);
-        }
-
-        private static Factor processFactorWithoutDivision(String factorWithoutDivision) {
-            Factor result = new Factor();
-            for (String poweredEntity :
-                    factorWithoutDivision.split(Pattern.quote("*"))) {
-                result.addPoweredEntity(poweredEntity);
-            }
-
-            return result;
-        }
-
-        /**
-         * Copy this <code>Factor</code>.
-         */
-        protected Factor copy() {
-            Factor result = new Factor();
-            result.factorNum = this.factorNum;
-            result.factorDen = this.factorDen;
-
-            result.CONSTANT_FT2M = this.CONSTANT_FT2M;
-            result.CONSTANT_PI = this.CONSTANT_PI;
-            result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY;
-            result.CONSTANT_G = this.CONSTANT_G;
-            result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3;
-            result.CONSTANT_LB2KG = this.CONSTANT_LB2KG;
-
-            return result;
-        }
-
-        /**
-         * Returns a single `BigDecimal` that represent the conversion rate after substituting all the constants.
-         */
-        public BigDecimal getConversionRate() {
-            Factor resultCollector = this.copy();
-
-            resultCollector.substitute(new BigDecimal("0.3048"), this.CONSTANT_FT2M);
-            resultCollector.substitute(new BigDecimal("411557987.0").divide(new BigDecimal("131002976.0"), DECIMAL128), this.CONSTANT_PI);
-            resultCollector.substitute(new BigDecimal("9.80665"), this.CONSTANT_GRAVITY);
-            resultCollector.substitute(new BigDecimal("6.67408E-11"), this.CONSTANT_G);
-            resultCollector.substitute(new BigDecimal("0.00454609"), this.CONSTANT_GAL_IMP2M3);
-            resultCollector.substitute(new BigDecimal("0.45359237"), this.CONSTANT_LB2KG);
-
-            return resultCollector.factorNum.divide(resultCollector.factorDen, DECIMAL128);
-        }
-
-        private void substitute(BigDecimal value, int power) {
-            if (power == 0) return;
-
-            BigDecimal absPoweredValue = value.pow(Math.abs(power), DECIMAL128);
-            if (power > 0) {
-                this.factorNum = this.factorNum.multiply(absPoweredValue);
-            } else {
-                this.factorDen = this.factorDen.multiply(absPoweredValue);
-            }
-        }
-
-        public Factor applySiPrefix(MeasureUnit.SIPrefix siPrefix) {
-            Factor result = this.copy();
-            if (siPrefix == MeasureUnit.SIPrefix.ONE) {
-                return result;
-            }
-
-            BigDecimal siApplied = BigDecimal.valueOf(Math.pow(10.0, Math.abs(siPrefix.getPower())));
-
-            if (siPrefix.getPower() < 0) {
-                result.factorDen = this.factorDen.multiply(siApplied);
-                return result;
-            }
-
-            result.factorNum = this.factorNum.multiply(siApplied);
-            return result;
-        }
-
-        public Factor power(int power) {
-            Factor result = new Factor();
-            if (power == 0) return result;
-            if (power > 0) {
-                result.factorNum = this.factorNum.pow(power);
-                result.factorDen = this.factorDen.pow(power);
-            } else {
-                result.factorNum = this.factorDen.pow(power * -1);
-                result.factorDen = this.factorNum.pow(power * -1);
-            }
-
-            result.CONSTANT_FT2M = this.CONSTANT_FT2M * power;
-            result.CONSTANT_PI = this.CONSTANT_PI * power;
-            result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY * power;
-            result.CONSTANT_G = this.CONSTANT_G * power;
-            result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3 * power;
-            result.CONSTANT_LB2KG = this.CONSTANT_LB2KG * power;
-
-            return result;
-        }
-
-        public Factor divide(Factor other) {
-            Factor result = new Factor();
-            result.factorNum = this.factorNum.multiply(other.factorDen);
-            result.factorDen = this.factorDen.multiply(other.factorNum);
-
-            result.CONSTANT_FT2M = this.CONSTANT_FT2M - other.CONSTANT_FT2M;
-            result.CONSTANT_PI = this.CONSTANT_PI - other.CONSTANT_PI;
-            result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY - other.CONSTANT_GRAVITY;
-            result.CONSTANT_G = this.CONSTANT_G - other.CONSTANT_G;
-            result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3 - other.CONSTANT_GAL_IMP2M3;
-            result.CONSTANT_LB2KG = this.CONSTANT_LB2KG - other.CONSTANT_LB2KG;
-
-            return result;
-        }
-
-        public Factor multiply(Factor other) {
-            Factor result = new Factor();
-            result.factorNum = this.factorNum.multiply(other.factorNum);
-            result.factorDen = this.factorDen.multiply(other.factorDen);
-
-            result.CONSTANT_FT2M = this.CONSTANT_FT2M + other.CONSTANT_FT2M;
-            result.CONSTANT_PI = this.CONSTANT_PI + other.CONSTANT_PI;
-            result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY + other.CONSTANT_GRAVITY;
-            result.CONSTANT_G = this.CONSTANT_G + other.CONSTANT_G;
-            result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3 + other.CONSTANT_GAL_IMP2M3;
-            result.CONSTANT_LB2KG = this.CONSTANT_LB2KG + other.CONSTANT_LB2KG;
-
-            return result;
-        }
-
-        /**
-         * Adds Entity with power or not. For example, `12 ^ 3` or `12`.
-         *
-         * @param poweredEntity
-         */
-        private void addPoweredEntity(String poweredEntity) {
-            String[] entities = poweredEntity.split(Pattern.quote("^"));
-            assert (entities.length == 1 || entities.length == 2);
-
-            int power = entities.length == 2 ? Integer.parseInt(entities[1]) : 1;
-            this.addEntity(entities[0], power);
-        }
-
-        private void addEntity(String entity, int power) {
-            if ("ft_to_m".equals(entity)) {
-                this.CONSTANT_FT2M += power;
-            } else if ("ft2_to_m2".equals(entity)) {
-                this.CONSTANT_FT2M += 2 * power;
-            } else if ("ft3_to_m3".equals(entity)) {
-                this.CONSTANT_FT2M += 3 * power;
-            } else if ("in3_to_m3".equals(entity)) {
-                this.CONSTANT_FT2M += 3 * power;
-                this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(Math.pow(12, 3)));
-            } else if ("gal_to_m3".equals(entity)) {
-                this.factorNum = this.factorNum.multiply(BigDecimal.valueOf(231));
-                this.CONSTANT_FT2M += 3 * power;
-                this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(12 * 12 * 12));
-            } else if ("gal_imp_to_m3".equals(entity)) {
-                this.CONSTANT_GAL_IMP2M3 += power;
-            } else if ("G".equals(entity)) {
-                this.CONSTANT_G += power;
-            } else if ("gravity".equals(entity)) {
-                this.CONSTANT_GRAVITY += power;
-            } else if ("lb_to_kg".equals(entity)) {
-                this.CONSTANT_LB2KG += power;
-            } else if ("PI".equals(entity)) {
-                this.CONSTANT_PI += power;
-            } else {
-                BigDecimal decimalEntity = new BigDecimal(entity).pow(power, DECIMAL128);
-                this.factorNum = this.factorNum.multiply(decimalEntity);
-            }
-        }
-    }
-
-    @Override
-    public String toString() {
-        return "UnitConverter [conversionRate=" + conversionRate + ", offset=" + offset + "]";
-    }
-}
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 c8deed7..b6ef278 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
@@ -68,7 +68,9 @@
             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.
         assert (result != null) : "At least the category must be exist";
         return result;
     }
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
new file mode 100644
index 0000000..91c3968
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
@@ -0,0 +1,432 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2020 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.impl.units;
+
+import static java.math.MathContext.DECIMAL128;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.regex.Pattern;
+
+import android.icu.impl.IllegalIcuArgumentException;
+import android.icu.util.MeasureUnit;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public class UnitsConverter {
+    private BigDecimal conversionRate;
+    private boolean reciprocal;
+    private BigDecimal offset;
+
+    /**
+     * Constructor of <code>UnitsConverter</code>.
+     * NOTE:
+     * - source and target must be under the same category
+     * - e.g. meter to mile --> both of them are length units.
+     * <p>
+     * NOTE:
+     * This constructor creates an instance of <code>UnitsConverter</code> internally.
+     *
+     * @param sourceIdentifier represents the source unit identifier.
+     * @param targetIdentifier represents the target unit identifier.
+     */
+    public UnitsConverter(String sourceIdentifier, String targetIdentifier) {
+        this(
+                MeasureUnitImpl.forIdentifier(sourceIdentifier),
+                MeasureUnitImpl.forIdentifier(targetIdentifier),
+                new ConversionRates()
+        );
+    }
+
+    /**
+     * Constructor of <code>UnitsConverter</code>.
+     * NOTE:
+     * - source and target must be under the same category
+     * - e.g. meter to mile --> both of them are length units.
+     *
+     * @param source          represents the source unit.
+     * @param target          represents the target unit.
+     * @param conversionRates contains all the needed conversion rates.
+     */
+    public UnitsConverter(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) {
+        Convertibility convertibility = extractConvertibility(source, target, conversionRates);
+        if (convertibility != Convertibility.CONVERTIBLE && convertibility != Convertibility.RECIPROCAL) {
+            throw new IllegalIcuArgumentException("input units must be convertible or reciprocal");
+        }
+
+        Factor sourceToBase = conversionRates.getFactorToBase(source);
+        Factor targetToBase = conversionRates.getFactorToBase(target);
+
+        if (convertibility == Convertibility.CONVERTIBLE) {
+            this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate();
+        } else {
+            assert convertibility == Convertibility.RECIPROCAL;
+            this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate();
+        }
+        this.reciprocal = convertibility == Convertibility.RECIPROCAL;
+
+        // calculate the offset
+        this.offset = conversionRates.getOffset(source, target, sourceToBase, targetToBase, convertibility);
+        // We should see no offsets for reciprocal conversions - they don't make sense:
+        assert convertibility != Convertibility.RECIPROCAL || this.offset == BigDecimal.ZERO;
+    }
+
+    static public Convertibility extractConvertibility(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) {
+        ArrayList<SingleUnitImpl> sourceSingleUnits = conversionRates.extractBaseUnits(source);
+        ArrayList<SingleUnitImpl> targetSingleUnits = conversionRates.extractBaseUnits(target);
+
+        HashMap<String, Integer> dimensionMap = new HashMap<>();
+
+        insertInMap(dimensionMap, sourceSingleUnits, 1);
+        insertInMap(dimensionMap, targetSingleUnits, -1);
+
+        if (areDimensionsZeroes(dimensionMap)) return Convertibility.CONVERTIBLE;
+
+        insertInMap(dimensionMap, targetSingleUnits, 2);
+        if (areDimensionsZeroes(dimensionMap)) return Convertibility.RECIPROCAL;
+
+        return Convertibility.UNCONVERTIBLE;
+    }
+
+    /**
+     * Helpers
+     */
+    private static void insertInMap(HashMap<String, Integer> dimensionMap, ArrayList<SingleUnitImpl> singleUnits, int multiplier) {
+        for (SingleUnitImpl singleUnit :
+                singleUnits) {
+            if (dimensionMap.containsKey(singleUnit.getSimpleUnitID())) {
+                dimensionMap.put(singleUnit.getSimpleUnitID(), dimensionMap.get(singleUnit.getSimpleUnitID()) + singleUnit.getDimensionality() * multiplier);
+            } else {
+                dimensionMap.put(singleUnit.getSimpleUnitID(), singleUnit.getDimensionality() * multiplier);
+            }
+        }
+    }
+
+    private static boolean areDimensionsZeroes(HashMap<String, Integer> dimensionMap) {
+        for (Integer value :
+                dimensionMap.values()) {
+            if (!value.equals(0)) return false;
+        }
+
+        return true;
+    }
+
+    public BigDecimal convert(BigDecimal inputValue) {
+        BigDecimal result = inputValue.multiply(this.conversionRate).add(offset);
+        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.)
+                return BigDecimal.ZERO;
+            }
+            result = BigDecimal.ONE.divide(result, DECIMAL128);
+        }
+        return result;
+    }
+
+    public BigDecimal convertInverse(BigDecimal inputValue) {
+        BigDecimal result = inputValue;
+        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.)
+                return BigDecimal.ZERO;
+            }
+            result = BigDecimal.ONE.divide(result, DECIMAL128);
+        }
+        result = result.subtract(offset).divide(this.conversionRate, DECIMAL128);
+        return result;
+    }
+
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public enum Convertibility {
+        CONVERTIBLE,
+        RECIPROCAL,
+        UNCONVERTIBLE,
+    }
+
+    public ConversionInfo getConversionInfo() {
+        ConversionInfo result = new ConversionInfo();
+        result.conversionRate = this.conversionRate;
+        result.offset = this.offset;
+        result.reciprocal = this.reciprocal;
+
+        return result;
+    }
+
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public static class ConversionInfo {
+        public BigDecimal conversionRate;
+        public BigDecimal offset;
+        public boolean reciprocal;
+    }
+
+    /**
+     * Responsible for all the Factor operation
+     * NOTE:
+     * This class is immutable
+     */
+    static class Factor {
+        private BigDecimal factorNum;
+        private BigDecimal factorDen;
+
+        // The exponents below correspond to ICU4C's Factor::exponents[].
+
+        /** Exponent for the ft_to_m constant */
+        private int exponentFtToM = 0;
+        /** Exponent for PI */
+        private int exponentPi = 0;
+        /** Exponent for gravity (gravity-of-earth, "g") */
+        private int exponentGravity = 0;
+        /** Exponent for Newtonian constant of gravitation "G". */
+        private int exponentG = 0;
+        /** Exponent for the imperial-gallon to cubic-meter conversion rate constant */
+        private int exponentGalImpToM3 = 0;
+        /** Exponent for the pound to kilogram conversion rate constant */
+        private int exponentLbToKg = 0;
+        /** Exponent for the glucose molar mass conversion rate constant */
+        private int exponentGlucoseMolarMass = 0;
+        /** Exponent for the item per mole conversion rate constant */
+        private int exponentItemPerMole = 0;
+
+        /**
+         * Creates Empty Factor
+         */
+        public Factor() {
+            this.factorNum = BigDecimal.valueOf(1);
+            this.factorDen = BigDecimal.valueOf(1);
+        }
+
+        public static Factor processFactor(String factor) {
+            assert (!factor.isEmpty());
+
+            // Remove all spaces in the factor
+            factor = factor.replaceAll("\\s+", "");
+
+            String[] fractions = factor.split("/");
+            assert (fractions.length == 1 || fractions.length == 2);
+
+            if (fractions.length == 1) {
+                return processFactorWithoutDivision(fractions[0]);
+            }
+
+            Factor num = processFactorWithoutDivision(fractions[0]);
+            Factor den = processFactorWithoutDivision(fractions[1]);
+            return num.divide(den);
+        }
+
+        private static Factor processFactorWithoutDivision(String factorWithoutDivision) {
+            Factor result = new Factor();
+            for (String poweredEntity :
+                    factorWithoutDivision.split(Pattern.quote("*"))) {
+                result.addPoweredEntity(poweredEntity);
+            }
+
+            return result;
+        }
+
+        /**
+         * Copy this <code>Factor</code>.
+         */
+        protected Factor copy() {
+            Factor result = new Factor();
+            result.factorNum = this.factorNum;
+            result.factorDen = this.factorDen;
+
+            result.exponentFtToM = this.exponentFtToM;
+            result.exponentPi = this.exponentPi;
+            result.exponentGravity = this.exponentGravity;
+            result.exponentG = this.exponentG;
+            result.exponentGalImpToM3 = this.exponentGalImpToM3;
+            result.exponentLbToKg = this.exponentLbToKg;
+            result.exponentGlucoseMolarMass = this.exponentGlucoseMolarMass;
+            result.exponentItemPerMole = this.exponentItemPerMole;
+
+            return result;
+        }
+
+        /**
+         * Returns a single `BigDecimal` that represent the conversion rate after substituting all the constants.
+         *
+         * In ICU4C, see Factor::substituteConstants().
+         */
+        public BigDecimal getConversionRate() {
+            // TODO: this copies all the exponents then doesn't use them at all.
+            Factor resultCollector = this.copy();
+
+            // TODO(icu-units#92): port C++ unit tests to Java.
+            // These values are a hard-coded subset of unitConstants in the
+            // units resources file. A unit test should check that all constants
+            // in the resource file are at least recognised by the code.
+            // In ICU4C, these constants live in constantsValues[].
+            resultCollector.multiply(new BigDecimal("0.3048"), this.exponentFtToM);
+            // TODO: this recalculates this division every time this is called.
+            resultCollector.multiply(new BigDecimal("411557987.0").divide(new BigDecimal("131002976.0"), DECIMAL128), this.exponentPi);
+            resultCollector.multiply(new BigDecimal("9.80665"), this.exponentGravity);
+            resultCollector.multiply(new BigDecimal("6.67408E-11"), this.exponentG);
+            resultCollector.multiply(new BigDecimal("0.00454609"), this.exponentGalImpToM3);
+            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);
+
+            return resultCollector.factorNum.divide(resultCollector.factorDen, DECIMAL128);
+        }
+
+        /** Multiplies the Factor instance by value^power. */
+        private void multiply(BigDecimal value, int power) {
+            if (power == 0) return;
+
+            BigDecimal absPoweredValue = value.pow(Math.abs(power), DECIMAL128);
+            if (power > 0) {
+                this.factorNum = this.factorNum.multiply(absPoweredValue);
+            } else {
+                this.factorDen = this.factorDen.multiply(absPoweredValue);
+            }
+        }
+
+        /** Apply SI or binary prefix to the Factor. */
+        public Factor applyPrefix(MeasureUnit.MeasurePrefix unitPrefix) {
+            Factor result = this.copy();
+            if (unitPrefix == MeasureUnit.MeasurePrefix.ONE) {
+                return result;
+            }
+
+            int base = unitPrefix.getBase();
+            int power = unitPrefix.getPower();
+            BigDecimal absFactor =
+                BigDecimal.valueOf(base).pow(Math.abs(power), DECIMAL128);
+
+            if (power < 0) {
+                result.factorDen = this.factorDen.multiply(absFactor);
+                return result;
+            }
+
+            result.factorNum = this.factorNum.multiply(absFactor);
+            return result;
+        }
+
+        public Factor power(int power) {
+            Factor result = new Factor();
+            if (power == 0) return result;
+            if (power > 0) {
+                result.factorNum = this.factorNum.pow(power);
+                result.factorDen = this.factorDen.pow(power);
+            } else {
+                result.factorNum = this.factorDen.pow(power * -1);
+                result.factorDen = this.factorNum.pow(power * -1);
+            }
+
+            result.exponentFtToM = this.exponentFtToM * power;
+            result.exponentPi = this.exponentPi * power;
+            result.exponentGravity = this.exponentGravity * power;
+            result.exponentG = this.exponentG * power;
+            result.exponentGalImpToM3 = this.exponentGalImpToM3 * power;
+            result.exponentLbToKg = this.exponentLbToKg * power;
+            result.exponentGlucoseMolarMass = this.exponentGlucoseMolarMass * power;
+            result.exponentItemPerMole = this.exponentItemPerMole * power;
+
+            return result;
+        }
+
+        public Factor divide(Factor other) {
+            Factor result = new Factor();
+            result.factorNum = this.factorNum.multiply(other.factorDen);
+            result.factorDen = this.factorDen.multiply(other.factorNum);
+
+            result.exponentFtToM = this.exponentFtToM - other.exponentFtToM;
+            result.exponentPi = this.exponentPi - other.exponentPi;
+            result.exponentGravity = this.exponentGravity - other.exponentGravity;
+            result.exponentG = this.exponentG - other.exponentG;
+            result.exponentGalImpToM3 = this.exponentGalImpToM3 - other.exponentGalImpToM3;
+            result.exponentLbToKg = this.exponentLbToKg - other.exponentLbToKg;
+            result.exponentGlucoseMolarMass =
+                this.exponentGlucoseMolarMass - other.exponentGlucoseMolarMass;
+            result.exponentItemPerMole = this.exponentItemPerMole - other.exponentItemPerMole;
+
+            return result;
+        }
+
+        public Factor multiply(Factor other) {
+            Factor result = new Factor();
+            result.factorNum = this.factorNum.multiply(other.factorNum);
+            result.factorDen = this.factorDen.multiply(other.factorDen);
+
+            result.exponentFtToM = this.exponentFtToM + other.exponentFtToM;
+            result.exponentPi = this.exponentPi + other.exponentPi;
+            result.exponentGravity = this.exponentGravity + other.exponentGravity;
+            result.exponentG = this.exponentG + other.exponentG;
+            result.exponentGalImpToM3 = this.exponentGalImpToM3 + other.exponentGalImpToM3;
+            result.exponentLbToKg = this.exponentLbToKg + other.exponentLbToKg;
+            result.exponentGlucoseMolarMass =
+                this.exponentGlucoseMolarMass + other.exponentGlucoseMolarMass;
+            result.exponentItemPerMole = this.exponentItemPerMole + other.exponentItemPerMole;
+
+            return result;
+        }
+
+        /**
+         * Adds Entity with power or not. For example, `12 ^ 3` or `12`.
+         *
+         * @param poweredEntity
+         */
+        private void addPoweredEntity(String poweredEntity) {
+            String[] entities = poweredEntity.split(Pattern.quote("^"));
+            assert (entities.length == 1 || entities.length == 2);
+
+            int power = entities.length == 2 ? Integer.parseInt(entities[1]) : 1;
+            this.addEntity(entities[0], power);
+        }
+
+        private void addEntity(String entity, int power) {
+            if ("ft_to_m".equals(entity)) {
+                this.exponentFtToM += power;
+            } else if ("ft2_to_m2".equals(entity)) {
+                this.exponentFtToM += 2 * power;
+            } else if ("ft3_to_m3".equals(entity)) {
+                this.exponentFtToM += 3 * power;
+            } else if ("in3_to_m3".equals(entity)) {
+                this.exponentFtToM += 3 * power;
+                this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(Math.pow(12, 3)));
+            } else if ("gal_to_m3".equals(entity)) {
+                this.factorNum = this.factorNum.multiply(BigDecimal.valueOf(231));
+                this.exponentFtToM += 3 * power;
+                this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(12 * 12 * 12));
+            } else if ("gal_imp_to_m3".equals(entity)) {
+                this.exponentGalImpToM3 += power;
+            } else if ("G".equals(entity)) {
+                this.exponentG += power;
+            } else if ("gravity".equals(entity)) {
+                this.exponentGravity += power;
+            } else if ("lb_to_kg".equals(entity)) {
+                this.exponentLbToKg += power;
+            } else if ("glucose_molar_mass".equals(entity)) {
+                this.exponentGlucoseMolarMass += power;
+            } else if ("item_per_mole".equals(entity)) {
+                this.exponentItemPerMole += power;
+            } else if ("PI".equals(entity)) {
+                this.exponentPi += power;
+            } else {
+                BigDecimal decimalEntity = new BigDecimal(entity).pow(power, DECIMAL128);
+                this.factorNum = this.factorNum.multiply(decimalEntity);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "UnitsConverter [conversionRate=" + conversionRate + ", offset=" + offset + "]";
+    }
+}
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 e637315..373be1c 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
@@ -2,16 +2,16 @@
 // © 2020 and later: Unicode, Inc. and others.
 // License & terms of use: http://www.unicode.org/copyright.html
 
-
 package android.icu.impl.units;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 
 import android.icu.impl.ICUData;
 import android.icu.impl.ICUResourceBundle;
+import android.icu.impl.IllegalIcuArgumentException;
 import android.icu.impl.UResource;
-import android.icu.util.MeasureUnit;
 import android.icu.util.UResourceBundle;
 
 /**
@@ -19,33 +19,38 @@
  * @hide Only a subset of ICU is exposed in Android
  */
 public class UnitsData {
-    private volatile static String[] simpleUnits = null;
+    // TODO(icu-units#122): this class can use static initialization to load the
+    // data once, and provide access to it via static methods. (Partial change
+    // has been done already.)
+
+    // Array of simple unit IDs.
+    private static String[] simpleUnits = null;
+
+    // Maps from the value associated with each simple unit ID to a category
+    // index number.
+    private static int[] simpleUnitCategories = null;
+
     private ConversionRates conversionRates;
     private UnitPreferences unitPreferences;
-    /**
-     * Pairs of categories and the corresponding base units.
-     */
-    private Categories categories;
+
 
     public UnitsData() {
         this.conversionRates = new ConversionRates();
         this.unitPreferences = new UnitPreferences();
-        this.categories = new Categories();
     }
 
     public static String[] getSimpleUnits() {
-        if (simpleUnits != null) {
-            return simpleUnits;
-        }
+        return simpleUnits;
+    }
 
+    static {
         // Read simple units
         ICUResourceBundle resource;
         resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
         SimpleUnitIdentifiersSink sink = new SimpleUnitIdentifiersSink();
         resource.getAllItemsWithFallback("convertUnits", sink);
         simpleUnits = sink.simpleUnits;
-
-        return simpleUnits;
+        simpleUnitCategories = sink.simpleUnitCategories;
     }
 
     public ConversionRates getConversionRates() {
@@ -56,24 +61,54 @@
         return unitPreferences;
     }
 
+    public static int getCategoryIndexOfSimpleUnit(int simpleUnitIndex) {
+        return simpleUnitCategories[simpleUnitIndex];
+    }
+
     /**
      * @param measureUnit An instance of MeasureUnitImpl.
      * @return the corresponding category.
      */
     public String getCategory(MeasureUnitImpl measureUnit) {
-        MeasureUnitImpl baseMeasureUnit
+        MeasureUnitImpl baseMeasureUnitImpl
                 = this.getConversionRates().extractCompoundBaseUnit(measureUnit);
-        String baseUnitIdentifier = MeasureUnit.fromMeasureUnitImpl(baseMeasureUnit).getIdentifier();
+        baseMeasureUnitImpl.serialize();
+        String identifier = baseMeasureUnitImpl.getIdentifier();
 
-        if (baseUnitIdentifier.equals("meter-per-cubic-meter")) {
-            // TODO(CLDR-13787,hugovdm): special-casing the consumption-inverse
-            // case. Once CLDR-13787 is clarified, this should be generalised (or
-            // possibly removed):
 
-            return "consumption-inverse";
+        Integer index = Categories.baseUnitToIndex.get(identifier);
+
+        // In case the base unit identifier did not match any entry.
+        if (index == null) {
+            baseMeasureUnitImpl.takeReciprocal();
+            baseMeasureUnitImpl.serialize();
+            identifier = baseMeasureUnitImpl.getIdentifier();
+            index = Categories.baseUnitToIndex.get(identifier);
         }
 
-        return this.categories.mapFromUnitToCategory.get(baseUnitIdentifier);
+        // In case the reciprocal of the base unit identifier did not match any entry.
+        baseMeasureUnitImpl.takeReciprocal(); // return to original form
+        MeasureUnitImpl simplifiedUnit = baseMeasureUnitImpl.copyAndSimplify();
+        if (index == null) {
+            simplifiedUnit.serialize();
+            identifier = simplifiedUnit.getIdentifier();
+            index = Categories.baseUnitToIndex.get(identifier);
+        }
+
+        // In case the simplified base unit identifier did not match any entry.
+        if (index == null) {
+            simplifiedUnit.takeReciprocal();
+            simplifiedUnit.serialize();
+            identifier = simplifiedUnit.getIdentifier();
+            index = Categories.baseUnitToIndex.get(identifier);
+        }
+
+        // If there is no match at all, throw an exception.
+        if (index == null) {
+            throw new IllegalIcuArgumentException("This unit does not has a category" + measureUnit.getIdentifier());
+        }
+
+        return Categories.indexToCategory[index];
     }
 
     public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, String region) {
@@ -85,6 +120,7 @@
      */
     public static class SimpleUnitIdentifiersSink extends UResource.Sink {
         String[] simpleUnits = null;
+        int[] simpleUnitCategories = null;
 
         @Override
         public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
@@ -93,6 +129,7 @@
 
             UResource.Table simpleUnitsTable = value.getTable();
             ArrayList<String> simpleUnits = new ArrayList<>();
+            ArrayList<Integer> simpleUnitCategories = new ArrayList<>();
             for (int i = 0; simpleUnitsTable.getKeyAndValue(i, key, value); i++) {
                 if (key.toString().equals("kilogram")) {
 
@@ -103,10 +140,28 @@
                     continue;
                 }
 
+                // Find the base target unit for this simple unit
+                UResource.Table table = value.getTable();
+                if (!table.findValue("target", value)) {
+                    // TODO: is there a more idiomatic way to deal with Resource
+                    // Sink data errors in ICU4J? For now we just assert-fail,
+                    // and otherwise skip bad data:
+                    assert false : "Could not find \"target\" for simple unit: " + key;
+                    continue;
+                }
+                String target = value.getString();
+
                 simpleUnits.add(key.toString());
+                simpleUnitCategories.add(Categories.baseUnitToIndex.get(target));
             }
 
             this.simpleUnits = simpleUnits.toArray(new String[0]);
+            this.simpleUnitCategories = new int[simpleUnitCategories.size()];
+            Iterator<Integer> iter = simpleUnitCategories.iterator();
+            for (int i = 0; i < this.simpleUnitCategories.length; i++)
+            {
+                this.simpleUnitCategories[i] = iter.next().intValue();
+            }
         }
     }
 
@@ -115,6 +170,9 @@
      * @hide Only a subset of ICU is exposed in Android
      */
     public static class Constants {
+        // TODO: consider moving the Trie-offset-related constants into
+        // MeasureUnitImpl.java, the only place they're being used?
+
         // Trie value offset for simple units, e.g. "gram", "nautical-mile",
         // "fluid-ounce-imperial".
         public static final int kSimpleUnitOffset = 512;
@@ -129,9 +187,9 @@
         // Trie value offset for compound parts, e.g. "-per-", "-", "-and-".
         public final static int kCompoundPartOffset = 128;
 
-        // Trie value offset for SI Prefixes. This is big enough to ensure we only
-        // insert positive integers into the trie.
-        public static final int kSIPrefixOffset = 64;
+        // Trie value offset for SI or binary prefixes. This is big enough to
+        // ensure we only insert positive integers into the trie.
+        public static final int kPrefixOffset = 64;
 
 
         /* Tables Names*/
@@ -142,29 +200,44 @@
         public static final String DEFAULT_USAGE = "default";
     }
 
+    // Deals with base units and categories, e.g. "meter-per-second" --> "speed".
     /**
      * @hide Only a subset of ICU is exposed in Android
      */
     public static class Categories {
+        /**
+         * Maps from base unit to an index value: an index into the
+         * indexToCategory array.
+         */
+        static HashMap<String, Integer> baseUnitToIndex;
 
         /**
-         * Contains the map between units in their base units into their category.
-         * For example:  meter-per-second --> "speed"
+         * Our official array of category strings - categories are identified by
+         * indeces into this array.
          */
-        HashMap<String, String> mapFromUnitToCategory;
+        static String[] indexToCategory;
 
-
-        public Categories() {
+        static {
             // Read unit Categories
             ICUResourceBundle resource;
             resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
             CategoriesSink sink = new CategoriesSink();
             resource.getAllItemsWithFallback(Constants.CATEGORY_TABLE_NAME, sink);
-            this.mapFromUnitToCategory = sink.getMapFromUnitToCategory();
+            baseUnitToIndex = sink.mapFromUnitToIndex;
+            indexToCategory = sink.categories.toArray(new String[0]);
         }
     }
 
     /**
+     * A Resource Sink that collects information from `unitQuantities` in the
+     * `units` resource to provide key->value lookups from base unit to
+     * category, as well as preserving ordering information for these
+     * categories. See `units.txt`.
+     *
+     * For example: "kilogram" -> "mass", "meter-per-second" -> "speed".
+     *
+     * In Java unitQuantity values are collected in order into an ArrayList,
+     * while unitQuantity key-to-index lookups are handled with a HashMap.
      * @hide Only a subset of ICU is exposed in Android
      */
     public static class CategoriesSink extends UResource.Sink {
@@ -172,26 +245,30 @@
          * Contains the map between units in their base units into their category.
          * For example:  meter-per-second --> "speed"
          */
-        HashMap<String, String> mapFromUnitToCategory;
+        HashMap<String, Integer> mapFromUnitToIndex;
+        ArrayList<String> categories;
 
         public CategoriesSink() {
-            mapFromUnitToCategory = new HashMap<>();
+            mapFromUnitToIndex = new HashMap<>();
+            categories = new ArrayList<>();
         }
 
         @Override
         public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
             assert (key.toString().equals(Constants.CATEGORY_TABLE_NAME));
-            assert (value.getType() == UResourceBundle.TABLE);
+            assert (value.getType() == UResourceBundle.ARRAY);
 
-            UResource.Table categoryTable = value.getTable();
-            for (int i = 0; categoryTable.getKeyAndValue(i, key, value); i++) {
-                assert (value.getType() == UResourceBundle.STRING);
-                mapFromUnitToCategory.put(key.toString(), value.toString());
+            UResource.Array categoryArray = value.getArray();
+            for (int i=0; categoryArray.getValue(i, value); i++) {
+                assert (value.getType() == UResourceBundle.TABLE);
+                UResource.Table table = value.getTable();
+                assert (table.getSize() == 1)
+                    : "expecting single-entry table, got size: " + table.getSize();
+                table.getKeyAndValue(0, key, value);
+                assert value.getType() == UResourceBundle.STRING : "expecting category string";
+                mapFromUnitToIndex.put(key.toString(), categories.size());
+                categories.add(value.toString());
             }
         }
-
-        public HashMap<String, String> getMapFromUnitToCategory() {
-            return mapFromUnitToCategory;
-        }
     }
 }
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 9a6a696..d1909fa 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
@@ -10,7 +10,6 @@
 import android.icu.impl.IllegalIcuArgumentException;
 import android.icu.impl.number.MicroProps;
 import android.icu.number.Precision;
-import android.icu.util.Measure;
 import android.icu.util.MeasureUnit;
 
 /**
@@ -49,13 +48,16 @@
     private ArrayList<MeasureUnit> outputUnits_ = new ArrayList<>();
     private ArrayList<ConverterPreference> converterPreferences_ = new ArrayList<>();
 
-    public UnitsRouter(MeasureUnitImpl inputUnitImpl, String region, String usage) {
+    public UnitsRouter(String inputUnitIdentifier, String region, String usage) {
+        this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), region, usage);
+    }
+
+    public UnitsRouter(MeasureUnitImpl inputUnit, String region, 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();
 
-        //MeasureUnitImpl inputUnitImpl = MeasureUnitImpl.forMeasureUnitMaybeCopy(inputUnit);
-        String category = data.getCategory(inputUnitImpl);
+        String category = data.getCategory(inputUnit);
         UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, region);
 
         for (int i = 0; i < unitPreferences.length; ++i) {
@@ -76,7 +78,7 @@
             }
 
             outputUnits_.add(complexTargetUnitImpl.build());
-            converterPreferences_.add(new ConverterPreference(inputUnitImpl, complexTargetUnitImpl,
+            converterPreferences_.add(new ConverterPreference(inputUnit, complexTargetUnitImpl,
                     preference.getGeq(), precision,
                     data.getConversionRates()));
         }
@@ -182,19 +184,15 @@
      * @hide Only a subset of ICU is exposed in Android
      */
     public class RouteResult {
-        // A list of measures: a single measure for single units, multiple measures
-        // for mixed units.
-        //
-        // TODO(icu-units/icu#21): figure out the right mixed unit API.
-        public final List<Measure> measures;
+        public final ComplexUnitsConverter.ComplexConverterResult complexConverterResult;
 
         // The output unit for this RouteResult. This may be a MIXED unit - for
         // example: "yard-and-foot-and-inch", for which `measures` will have three
         // elements.
         public final MeasureUnitImpl outputUnit;
 
-        RouteResult(List<Measure> measures, MeasureUnitImpl outputUnit) {
-            this.measures = measures;
+        RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit) {
+            this.complexConverterResult = complexConverterResult;
             this.outputUnit = outputUnit;
         }
     }
diff --git a/android_icu4j/src/main/java/android/icu/lang/CharacterProperties.java b/android_icu4j/src/main/java/android/icu/lang/CharacterProperties.java
index 9e57d12..d6c4d89 100644
--- a/android_icu4j/src/main/java/android/icu/lang/CharacterProperties.java
+++ b/android_icu4j/src/main/java/android/icu/lang/CharacterProperties.java
@@ -5,6 +5,7 @@
 package android.icu.lang;
 
 import android.icu.impl.CharacterPropertiesImpl;
+import android.icu.impl.EmojiProps;
 import android.icu.text.UnicodeSet;
 import android.icu.util.CodePointMap;
 import android.icu.util.CodePointTrie;
@@ -30,6 +31,15 @@
 
     private static UnicodeSet makeSet(int property) {
         UnicodeSet set = new UnicodeSet();
+        if (UProperty.BASIC_EMOJI <= property && property <= UProperty.RGI_EMOJI) {
+            // property of strings
+            EmojiProps.INSTANCE.addStrings(property, set);
+            if (property != UProperty.BASIC_EMOJI && property != UProperty.RGI_EMOJI) {
+                // property of _only_ strings
+                return set.freeze();
+            }
+        }
+
         UnicodeSet inclusions = CharacterPropertiesImpl.getInclusionsForProperty(property);
         int numRanges = inclusions.getRangeCount();
         int startHasProperty = -1;
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 1e32bc8..da7dda7 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
@@ -17,6 +17,7 @@
 import java.util.Map;
 
 import android.icu.impl.CaseMapImpl;
+import android.icu.impl.EmojiProps;
 import android.icu.impl.IllegalIcuArgumentException;
 import android.icu.impl.Trie2;
 import android.icu.impl.UBiDiProps;
@@ -85,9 +86,9 @@
  * For more information see
  * <a href="http://www.unicode/org/ucd/">"About the Unicode Character Database"</a>
  * (http://www.unicode.org/ucd/)
- * and the <a href="http://www.icu-project.org/userguide/properties.html">ICU
+ * and the <a href="https://unicode-org.github.io/icu/userguide/strings/properties">ICU
  * User Guide chapter on Properties</a>
- * (http://www.icu-project.org/userguide/properties.html).
+ * (https://unicode-org.github.io/icu/userguide/strings/properties).
  * <p>
  * There are also functions that provide easy migration from C/POSIX functions
  * like isblank(). Their use is generally discouraged because the C/POSIX
@@ -145,6 +146,28 @@
 
 public final class UCharacter implements ECharacterCategory, ECharacterDirection
 {
+    /**
+     * Lead surrogate bitmask
+     */
+    private static final int LEAD_SURROGATE_BITMASK = 0xFFFFFC00;
+
+    /**
+     * Trail surrogate bitmask
+     */
+    private static final int TRAIL_SURROGATE_BITMASK = 0xFFFFFC00;
+
+    /**
+     * Lead surrogate bits
+     */
+    private static final int LEAD_SURROGATE_BITS = 0xD800;
+
+    /**
+     * Trail surrogate bits
+     */
+    private static final int TRAIL_SURROGATE_BITS = 0xDC00;
+
+    private static final int U16_SURROGATE_OFFSET = ((0xd800 << 10) + 0xdc00 - 0x10000);
+
     // public inner classes ----------------------------------------------
 
     /**
@@ -1051,6 +1074,33 @@
         /***/
         public static final int YEZIDI_ID = 308; /*[10E80]*/
 
+        // New blocks in Unicode 14.0
+
+        /***/
+        public static final int ARABIC_EXTENDED_B_ID = 309; /*[0870]*/
+        /***/
+        public static final int CYPRO_MINOAN_ID = 310; /*[12F90]*/
+        /***/
+        public static final int ETHIOPIC_EXTENDED_B_ID = 311; /*[1E7E0]*/
+        /***/
+        public static final int KANA_EXTENDED_B_ID = 312; /*[1AFF0]*/
+        /***/
+        public static final int LATIN_EXTENDED_F_ID = 313; /*[10780]*/
+        /***/
+        public static final int LATIN_EXTENDED_G_ID = 314; /*[1DF00]*/
+        /***/
+        public static final int OLD_UYGHUR_ID = 315; /*[10F70]*/
+        /***/
+        public static final int TANGSA_ID = 316; /*[16A70]*/
+        /***/
+        public static final int TOTO_ID = 317; /*[1E290]*/
+        /***/
+        public static final int UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_A_ID = 318; /*[11AB0]*/
+        /***/
+        public static final int VITHKUQI_ID = 319; /*[10570]*/
+        /***/
+        public static final int ZNAMENNY_MUSICAL_NOTATION_ID = 320; /*[1CF00]*/
+
         /**
          * One more than the highest normal UnicodeBlock value.
          * The highest value is available via UCharacter.getIntPropertyMaxValue(UProperty.BLOCK).
@@ -1059,7 +1109,7 @@
          * @hide unsupported on Android
          */
         @Deprecated
-        public static final int COUNT = 309;
+        public static final int COUNT = 321;
 
         // blocks objects ---------------------------------------------------
 
@@ -2253,6 +2303,45 @@
         /***/
         public static final UnicodeBlock YEZIDI = new UnicodeBlock("YEZIDI", YEZIDI_ID); /*[10E80]*/
 
+        // New blocks in Unicode 14.0
+
+        /***/
+        public static final UnicodeBlock ARABIC_EXTENDED_B =
+                new UnicodeBlock("ARABIC_EXTENDED_B", ARABIC_EXTENDED_B_ID); /*[0870]*/
+        /***/
+        public static final UnicodeBlock CYPRO_MINOAN =
+                new UnicodeBlock("CYPRO_MINOAN", CYPRO_MINOAN_ID); /*[12F90]*/
+        /***/
+        public static final UnicodeBlock ETHIOPIC_EXTENDED_B =
+                new UnicodeBlock("ETHIOPIC_EXTENDED_B", ETHIOPIC_EXTENDED_B_ID); /*[1E7E0]*/
+        /***/
+        public static final UnicodeBlock KANA_EXTENDED_B =
+                new UnicodeBlock("KANA_EXTENDED_B", KANA_EXTENDED_B_ID); /*[1AFF0]*/
+        /***/
+        public static final UnicodeBlock LATIN_EXTENDED_F =
+                new UnicodeBlock("LATIN_EXTENDED_F", LATIN_EXTENDED_F_ID); /*[10780]*/
+        /***/
+        public static final UnicodeBlock LATIN_EXTENDED_G =
+                new UnicodeBlock("LATIN_EXTENDED_G", LATIN_EXTENDED_G_ID); /*[1DF00]*/
+        /***/
+        public static final UnicodeBlock OLD_UYGHUR =
+                new UnicodeBlock("OLD_UYGHUR", OLD_UYGHUR_ID); /*[10F70]*/
+        /***/
+        public static final UnicodeBlock TANGSA = new UnicodeBlock("TANGSA", TANGSA_ID); /*[16A70]*/
+        /***/
+        public static final UnicodeBlock TOTO = new UnicodeBlock("TOTO", TOTO_ID); /*[1E290]*/
+        /***/
+        public static final UnicodeBlock UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_A =
+                new UnicodeBlock("UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_A",
+                        UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED_A_ID); /*[11AB0]*/
+        /***/
+        public static final UnicodeBlock VITHKUQI =
+                new UnicodeBlock("VITHKUQI", VITHKUQI_ID); /*[10570]*/
+        /***/
+        public static final UnicodeBlock ZNAMENNY_MUSICAL_NOTATION =
+                new UnicodeBlock("ZNAMENNY_MUSICAL_NOTATION",
+                        ZNAMENNY_MUSICAL_NOTATION_ID); /*[1CF00]*/
+
         /**
          */
         public static final UnicodeBlock INVALID_CODE
@@ -2795,6 +2884,11 @@
         /***/
         public static final int HANIFI_ROHINGYA_PA = 101;
 
+        /***/
+        public static final int THIN_YEH = 102;
+        /***/
+        public static final int VERTICAL_TAIL = 103;
+
         /**
          * One more than the highest normal JoiningGroup value.
          * The highest value is available via UCharacter.getIntPropertyMaxValue(UProperty.JoiningGroup).
@@ -2803,7 +2897,7 @@
          * @hide unsupported on Android
          */
         @Deprecated
-        public static final int COUNT = 102;
+        public static final int COUNT = 104;
     }
 
     /**
@@ -3726,7 +3820,7 @@
      * one-to-one mappings; it also omits information about context-sensitive
      * case mappings.<br> For more information about Unicode case mapping
      * please refer to the
-     * <a href=http://www.unicode.org/unicode/reports/tr21/>Technical report
+     * <a href=https://www.unicode.org/reports/tr21/>Technical report
      * #21</a>.<br>
      * Up-to-date Unicode implementation of java.lang.Character.isLowerCase()
      * @param ch code point to determine if it is in lowercase
@@ -3802,7 +3896,7 @@
      * one-to-one mappings; it also omits information about context-sensitive
      * case mappings.<br>
      * For more information about Unicode case mapping please refer to the
-     * <a href=http://www.unicode.org/unicode/reports/tr21/>
+     * <a href=https://www.unicode.org/reports/tr21/>
      * Technical report #21</a>.<br>
      * Up-to-date Unicode implementation of java.lang.Character.isTitleCase().
      * @param ch code point to determine if it is in title case
@@ -3834,7 +3928,7 @@
      * </ul>
      * Up-to-date Unicode implementation of
      * java.lang.Character.isUnicodeIdentifierPart().<br>
-     * See <a href=http://www.unicode.org/unicode/reports/tr8/>UTR #8</a>.
+     * See <a href=https://www.unicode.org/reports/tr8/>UTR #8</a>.
      * @param ch code point to determine if is can be part of a Unicode
      *        identifier
      * @return true if code point is any character belonging a unicode
@@ -3872,7 +3966,7 @@
      * </ul>
      * Up-to-date Unicode implementation of
      * java.lang.Character.isUnicodeIdentifierStart().<br>
-     * See <a href=http://www.unicode.org/unicode/reports/tr8/>UTR #8</a>.
+     * See <a href=https://www.unicode.org/reports/tr8/>UTR #8</a>.
      * @param ch code point to determine if it can start a Unicode identifier
      * @return true if code point is the first character belonging a unicode
      *              identifier
@@ -3898,7 +3992,7 @@
      * U+0000..U+0008, U+000E..U+001B, U+007F..U+009F.<br>
      * Up-to-date Unicode implementation of
      * java.lang.Character.isIdentifierIgnorable().<br>
-     * See <a href=http://www.unicode.org/unicode/reports/tr8/>UTR #8</a>.
+     * See <a href=https://www.unicode.org/reports/tr8/>UTR #8</a>.
      * <p>Note that Unicode just recommends to ignore Cf (format controls).
      * @param ch code point to be determined if it can be ignored in a Unicode
      *        identifier.
@@ -3926,7 +4020,7 @@
      * For example, the case conversion for dot-less i and dotted I in Turkish,
      * or for final sigma in Greek.
      * For more information about Unicode case mapping please refer to the
-     * <a href=http://www.unicode.org/unicode/reports/tr21/>
+     * <a href=https://www.unicode.org/reports/tr21/>
      * Technical report #21</a>.<br>
      * Up-to-date Unicode implementation of java.lang.Character.isUpperCase().
      * @param ch code point to determine if it is in uppercase
@@ -3951,7 +4045,7 @@
      * Full case mappings are applied by the case mapping functions
      * that take String parameters rather than code points (int).
      * See also the User Guide chapter on C/POSIX migration:
-     * http://www.icu-project.org/userguide/posix.html#case_mappings
+     * https://unicode-org.github.io/icu/userguide/icu/posix#case-mappings
      *
      * @param ch code point whose lowercase equivalent is to be retrieved
      * @return the lowercase equivalent code point
@@ -3998,7 +4092,7 @@
      * Full case mappings are applied by the case mapping functions
      * that take String parameters rather than code points (int).
      * See also the User Guide chapter on C/POSIX migration:
-     * http://www.icu-project.org/userguide/posix.html#case_mappings
+     * https://unicode-org.github.io/icu/userguide/icu/posix#case-mappings
      *
      * @param ch code point  whose title case is to be retrieved
      * @return titlecase code point
@@ -4020,7 +4114,7 @@
      * Full case mappings are applied by the case mapping functions
      * that take String parameters rather than code points (int).
      * See also the User Guide chapter on C/POSIX migration:
-     * http://www.icu-project.org/userguide/posix.html#case_mappings
+     * https://unicode-org.github.io/icu/userguide/icu/posix#case-mappings
      *
      * @param ch code point whose uppercase is to be retrieved
      * @return uppercase code point
@@ -4241,7 +4335,7 @@
      * UCharacter.MIN_VALUE and UCharacter.MAX_VALUE or does not have a name.
      * <br>
      * Note calling any methods related to code point names, e.g. get*Name*()
-     * incurs a one-time initialisation cost to construct the name tables.
+     * incurs a one-time initialization cost to construct the name tables.
      * @param ch the code point for which to get the name
      * @return most current Unicode name
      */
@@ -4297,7 +4391,7 @@
      *      "&lt;codepoint_type-codepoint_hex_digits&gt;". E.g., &lt;noncharacter-fffe&gt;
      * </ul>
      * Note calling any methods related to code point names, e.g. get*Name*()
-     * incurs a one-time initialisation cost to construct the name tables.
+     * incurs a one-time initialization cost to construct the name tables.
      * @param ch the code point for which to get the name
      * @return a name for the argument codepoint
      */
@@ -4311,7 +4405,7 @@
      * UCharacter.MIN_VALUE and UCharacter.MAX_VALUE or does not have a name.
      * <br>
      * Note calling any methods related to code point names, e.g. get*Name*()
-     * incurs a one-time initialisation cost to construct the name tables.
+     * incurs a one-time initialization cost to construct the name tables.
      * @param ch the code point for which to get the name alias
      * @return Unicode name alias, or null
      */
@@ -4341,7 +4435,7 @@
      * <strong>[icu]</strong> <p>Finds a Unicode code point by its most current Unicode name and
      * return its code point value. All Unicode names are in uppercase.
      * Note calling any methods related to code point names, e.g. get*Name*()
-     * incurs a one-time initialisation cost to construct the name tables.
+     * incurs a one-time initialization cost to construct the name tables.
      * @param name most current Unicode character name whose code point is to
      *        be returned
      * @return code point or -1 if name is not found
@@ -4380,7 +4474,7 @@
      *      "&lt;codepoint_type-codepoint_hex_digits&gt;". E.g. &lt;noncharacter-FFFE&gt;
      * </ul>
      * Note calling any methods related to code point names, e.g. get*Name*()
-     * incurs a one-time initialisation cost to construct the name tables.
+     * incurs a one-time initialization cost to construct the name tables.
      * @param name codepoint name
      * @return code point associated with the name or -1 if the name is not
      *         found.
@@ -4394,7 +4488,7 @@
      * <strong>[icu]</strong> <p>Find a Unicode character by its corrected name alias and return
      * its code point value. All Unicode names are in uppercase.
      * Note calling any methods related to code point names, e.g. get*Name*()
-     * incurs a one-time initialisation cost to construct the name tables.
+     * incurs a one-time initialization cost to construct the name tables.
      * @param name Unicode name alias whose code point is to be returned
      * @return code point or -1 if name is not found
      */
@@ -4596,6 +4690,26 @@
     /**
      * <strong>[icu]</strong> Returns a code point corresponding to the two surrogate code units.
      *
+     * @param lead the lead unit
+     *        (In ICU 2.1-69 the type of both parameters was <code>char</code>.)
+     * @param trail the trail unit
+     * @return code point if lead and trail form a valid surrogate pair.
+     * @exception IllegalArgumentException thrown when the code units do
+     *            not form a valid surrogate pair
+     * @see #toCodePoint(int, int)
+     */
+    public static int getCodePoint(int lead, int trail)
+    {
+        if (isHighSurrogate(lead) && isLowSurrogate(trail)) {
+            return toCodePoint(lead, trail);
+        }
+        throw new IllegalArgumentException("Not a valid surrogate pair");
+    }
+
+    // BEGIN Android patch: Keep the `char` version on Android. See ICU-21655
+    /**
+     * <strong>[icu]</strong> Returns a code point corresponding to the two surrogate code units.
+     *
      * @param lead the lead char
      * @param trail the trail char
      * @return code point if surrogate characters are valid.
@@ -4604,11 +4718,9 @@
      */
     public static int getCodePoint(char lead, char trail)
     {
-        if (Character.isSurrogatePair(lead, trail)) {
-            return Character.toCodePoint(lead, trail);
-        }
-        throw new IllegalArgumentException("Illegal surrogate characters");
+        return getCodePoint((int) lead, (int) trail);
     }
+    // END Android patch: Keep the `char` version on Android. See ICU-21655
 
     /**
      * <strong>[icu]</strong> Returns the code point corresponding to the BMP code point.
@@ -4856,7 +4968,7 @@
      * Full case mappings are applied by the case mapping functions
      * that take String parameters rather than code points (int).
      * See also the User Guide chapter on C/POSIX migration:
-     * http://www.icu-project.org/userguide/posix.html#case_mappings
+     * https://unicode-org.github.io/icu/userguide/icu/posix#case-mappings
      *
      * @param ch             the character to be converted
      * @param defaultmapping Indicates whether the default mappings defined in
@@ -4923,7 +5035,7 @@
      * Full case mappings are applied by the case mapping functions
      * that take String parameters rather than code points (int).
      * See also the User Guide chapter on C/POSIX migration:
-     * http://www.icu-project.org/userguide/posix.html#case_mappings
+     * https://unicode-org.github.io/icu/userguide/icu/posix#case-mappings
      *
      * @param ch the character to be converted
      * @param options A bit set for special processing. Currently the recognised options
@@ -5201,6 +5313,43 @@
     }
 
     /**
+     * <strong>[icu]</strong> Returns true if the property is true for the string.
+     * Same as {@link #hasBinaryProperty(int, int)}
+     * if the string contains exactly one code point.
+     *
+     * <p>Most properties apply only to single code points.
+     * <a href="https://www.unicode.org/reports/tr51/#Emoji_Sets">UTS #51 Unicode Emoji</a>
+     * defines several properties of strings.
+     *
+     * @param s String to test.
+     * @param property UProperty selector constant, identifies which binary property to check.
+     *        Must be BINARY_START&lt;=which&lt;BINARY_LIMIT.
+     * @return true or false according to the binary Unicode property value for the string.
+     *         Also false if <code>property</code> is out of bounds or if the Unicode version
+     *         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();
+        if (length == 1) {
+            return hasBinaryProperty(s.charAt(0), property);  // single code point
+        } else if (length == 2) {
+            // first code point
+            int c = Character.codePointAt(s, 0);
+            if (Character.charCount(c) == length) {
+                return hasBinaryProperty(c, property);  // single code point
+            }
+        }
+        // Only call into EmojiProps for a relevant property,
+        // so that we not unnecessarily try to load its data file.
+        return UProperty.BASIC_EMOJI <= property && property <= UProperty.RGI_EMOJI &&
+            EmojiProps.INSTANCE.hasBinaryProperty(s, property);
+    }
+
+    /**
      * <strong>[icu]</strong> <p>Check if a code point has the Alphabetic Unicode property.
      * <p>Same as UCharacter.hasBinaryProperty(ch, UProperty.ALPHABETIC).
      * <p>Different from UCharacter.isLetter(ch)!
@@ -5456,25 +5605,67 @@
     }
 
     /**
-     * Same as {@link Character#isHighSurrogate}.
+     * Same as {@link Character#isHighSurrogate},
+     * except that the ICU version accepts <code>int</code> for code points.
+     *
+     * @param codePoint the code point to check
+     *        (In ICU 3.0-69 the type of this parameter was <code>char</code>.)
+     * @return true if codePoint is a high (lead) surrogate
+     */
+    public static boolean isHighSurrogate(int codePoint) {
+        return (codePoint & LEAD_SURROGATE_BITMASK) == LEAD_SURROGATE_BITS;
+    }
+
+    // BEGIN Android patch: Keep the `char` version on Android. See ICU-21655
+    /**
+     * Same as {@link Character#isHighSurrogate},
      *
      * @param ch the char to check
      * @return true if ch is a high (lead) surrogate
      */
     public static boolean isHighSurrogate(char ch) {
-        return Character.isHighSurrogate(ch);
+        return isHighSurrogate((int) ch);
     }
+    // END Android patch: Keep the `char` version on Android. See ICU-21655
 
     /**
-     * Same as {@link Character#isLowSurrogate}.
+     * Same as {@link Character#isLowSurrogate},
+     * except that the ICU version accepts <code>int</code> for code points.
+     *
+     * @param codePoint the code point to check
+     *        (In ICU 3.0-69 the type of this parameter was <code>char</code>.)
+     * @return true if codePoint is a low (trail) surrogate
+     */
+    public static boolean isLowSurrogate(int codePoint) {
+        return (codePoint & TRAIL_SURROGATE_BITMASK) == TRAIL_SURROGATE_BITS;
+    }
+
+    // BEGIN Android patch: Keep the `char` version on Android. See ICU-21655
+    /**
+     * Same as {@link Character#isLowSurrogate},
      *
      * @param ch the char to check
      * @return true if ch is a low (trail) surrogate
      */
     public static boolean isLowSurrogate(char ch) {
-        return Character.isLowSurrogate(ch);
+        return isLowSurrogate((int) ch);
+    }
+    // END Android patch: Keep the `char` version on Android. See ICU-21655
+
+    /**
+     * Same as {@link Character#isSurrogatePair},
+     * except that the ICU version accepts <code>int</code> for code points.
+     *
+     * @param high the high (lead) unit
+     *        (In ICU 3.0-69 the type of both parameters was <code>char</code>.)
+     * @param low the low (trail) unit
+     * @return true if high, low form a surrogate pair
+     */
+    public static final boolean isSurrogatePair(int high, int low) {
+        return isHighSurrogate(high) && isLowSurrogate(low);
     }
 
+    // BEGIN Android patch: Keep the `char` version on Android. See ICU-21655
     /**
      * Same as {@link Character#isSurrogatePair}.
      *
@@ -5483,8 +5674,9 @@
      * @return true if high, low form a surrogate pair
      */
     public static final boolean isSurrogatePair(char high, char low) {
-        return Character.isSurrogatePair(high, low);
+        return isSurrogatePair((int) high, (int) low);
     }
+    // END Android patch: Keep the `char` version on Android. See ICU-21655
 
     /**
      * Same as {@link Character#charCount}.
@@ -5499,6 +5691,24 @@
     }
 
     /**
+     * Same as {@link Character#toCodePoint},
+     * except that the ICU version accepts <code>int</code> for code points.
+     * Returns the code point represented by the two surrogate code units.
+     * This does not check the surrogate pair for validity.
+     *
+     * @param high the high (lead) surrogate
+     *        (In ICU 3.0-69 the type of both parameters was <code>char</code>.)
+     * @param low the low (trail) surrogate
+     * @return the code point formed by the surrogate pair
+     * @see #getCodePoint(int, int)
+     */
+    public static final int toCodePoint(int high, int low) {
+        // see ICU4C U16_GET_SUPPLEMENTARY()
+        return (high << 10) + low - U16_SURROGATE_OFFSET;
+    }
+
+    // BEGIN Android patch: Keep the `char` version on Android. See ICU-21655
+    /**
      * Same as {@link Character#toCodePoint}.
      * Returns the code point represented by the two surrogate code units.
      * This does not check the surrogate pair for validity.
@@ -5508,8 +5718,9 @@
      * @return the code point formed by the surrogate pair
      */
     public static final int toCodePoint(char high, char low) {
-        return Character.toCodePoint(high, low);
+        return toCodePoint((int) high, (int) low);
     }
+    // END Android patch: Keep the `char` version on Android. See ICU-21655
 
     /**
      * Same as {@link Character#codePointAt(CharSequence, int)}.
diff --git a/android_icu4j/src/main/java/android/icu/lang/UCharacterCategory.java b/android_icu4j/src/main/java/android/icu/lang/UCharacterCategory.java
index 179ffed..c4328ec 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UCharacterCategory.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UCharacterCategory.java
@@ -104,7 +104,7 @@
     // private constructor -----------------------------------------------
     ///CLOVER:OFF 
     /**
-     * Private constructor to prevent initialisation
+     * Private constructor to prevent initialization
      */
     private UCharacterCategory()
     {
diff --git a/android_icu4j/src/main/java/android/icu/lang/UCharacterDirection.java b/android_icu4j/src/main/java/android/icu/lang/UCharacterDirection.java
index e2390e2..3d6541c 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UCharacterDirection.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UCharacterDirection.java
@@ -26,7 +26,7 @@
     // private constructor =========================================
     ///CLOVER:OFF  
     /**
-     * Private constructor to prevent initialisation
+     * Private constructor to prevent initialization
      */
     private UCharacterDirection()
     {
diff --git a/android_icu4j/src/main/java/android/icu/lang/UCharacterEnums.java b/android_icu4j/src/main/java/android/icu/lang/UCharacterEnums.java
index 3b6bdce..64ebf7e 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UCharacterEnums.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UCharacterEnums.java
@@ -15,7 +15,7 @@
  */
 public class UCharacterEnums {
 
-    /** This is just a namespace, it is not instantiatable. */
+    /** This is just a namespace, it is not instantiable. */
     ///CLOVER:OFF
     private UCharacterEnums() {}
 
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 1be09f4..504d8d6 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UProperty.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UProperty.java
@@ -492,6 +492,55 @@
      * See http://www.unicode.org/reports/tr51/#Emoji_Properties
      */
     public static final int EXTENDED_PICTOGRAPHIC=64;
+    /**
+     * 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;
 
     /**
      * One more than the last constant for binary Unicode properties.
@@ -499,7 +548,7 @@
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int BINARY_LIMIT = 65;
+    public static final int BINARY_LIMIT = 72;
 
     /**
      * Enumerated property Bidi_Class.
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 a4ab1d9..2d95fab 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UScript.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UScript.java
@@ -864,6 +864,17 @@
     /***/
     public static final int YEZIDI = 192; /* Yezi */
 
+    /***/
+    public static final int CYPRO_MINOAN = 193; /* Cpmn */
+    /***/
+    public static final int OLD_UYGHUR = 194; /* Ougr */
+    /***/
+    public static final int TANGSA = 195; /* Tnsa */
+    /***/
+    public static final int TOTO = 196; /* Toto */
+    /***/
+    public static final int VITHKUQI = 197; /* Vith */
+
     /**
      * One more than the highest normal UScript code.
      * The highest value is available via UCharacter.getIntPropertyMaxValue(UProperty.SCRIPT).
@@ -872,7 +883,7 @@
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int CODE_LIMIT   = 193;
+    public static final int CODE_LIMIT   = 198;
 
     private static int[] getCodesFromLocale(ULocale locale) {
         // Multi-script languages, equivalent to the LocaleScript data
@@ -1338,6 +1349,11 @@
             0x1190C | EXCLUSION,  // Diak
             0x18C65 | EXCLUSION | LB_LETTERS,  // Kits
             0x10E88 | EXCLUSION | RTL,  // Yezi
+            0x12FE5 | EXCLUSION,  // Cpmn
+            0x10F7C | EXCLUSION | RTL,  // Ougr
+            0x16ABC | EXCLUSION,  // Tnsa
+            0x1E290 | EXCLUSION,  // Toto
+            0x10582 | EXCLUSION | CASED,  // Vith
             // End copy-paste from parsescriptmetadata.py
         };
 
diff --git a/android_icu4j/src/main/java/android/icu/lang/UScriptRun.java b/android_icu4j/src/main/java/android/icu/lang/UScriptRun.java
index 1c89c5c..abd529c 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UScriptRun.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UScriptRun.java
@@ -19,7 +19,7 @@
  * the same script, as defined in the <code>UScript</code> class.
  * It implements a simple iterator over an array of characters.
  * The iterator will assign <code>COMMON</code> and <code>INHERITED</code>
- * characters to the same script as the preceeding characters. If the
+ * characters to the same script as the preceding characters. If the
  * COMMON and INHERITED characters are first, they will be assigned to
  * the same script as the following characters.
  *
@@ -88,7 +88,7 @@
     
     /**
      * Construct a <code>UScriptRun</code> object which iterates over a subrange
-     * of the characetrs in the given string.
+     * of the characters in the given string.
      *
      * @param text the string of characters over which to iterate.
      * @param start the index of the first character over which to iterate
@@ -105,7 +105,7 @@
 
     /**
      * Construct a <code>UScriptRun</code> object which iterates over the given
-     * characetrs.
+     * characters.
      *
      * @param chars the array of characters over which to iterate.
      *
@@ -120,7 +120,7 @@
 
     /**
      * Construct a <code>UScriptRun</code> object which iterates over a subrange
-     * of the given characetrs.
+     * of the given characters.
      *
      * @param chars the array of characters over which to iterate.
      * @param start the index of the first character over which to iterate
diff --git a/android_icu4j/src/main/java/android/icu/math/BigDecimal.java b/android_icu4j/src/main/java/android/icu/math/BigDecimal.java
index 2eaf4a8..9909e66 100644
--- a/android_icu4j/src/main/java/android/icu/math/BigDecimal.java
+++ b/android_icu4j/src/main/java/android/icu/math/BigDecimal.java
@@ -91,7 +91,7 @@
 /*            constructor, no blanks in string constructor, add       */
 /*            offset and length version of char[] constructor;        */
 /*            add valueOf(double); drop booleanValue, charValue;      */
-/*            add ...Exact versions of remaining convertors           */
+/*            add ...Exact versions of remaining converters           */
 /* 1999.03.13 add toBigIntegerExact                                   */
 /* 1999.03.13 1.00 release to IBM Centre for Java Technology          */
 /* 1999.05.27 1.01 correct 0-0.2 bug under scaled arithmetic          */
diff --git a/android_icu4j/src/main/java/android/icu/number/CompactNotation.java b/android_icu4j/src/main/java/android/icu/number/CompactNotation.java
index 0faca2b..03c92fd 100644
--- a/android_icu4j/src/main/java/android/icu/number/CompactNotation.java
+++ b/android_icu4j/src/main/java/android/icu/number/CompactNotation.java
@@ -8,7 +8,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import android.icu.impl.StandardPlural;
 import android.icu.impl.number.CompactData;
 import android.icu.impl.number.CompactData.CompactType;
 import android.icu.impl.number.DecimalFormatProperties;
@@ -138,8 +137,7 @@
                 magnitude -= multiplier;
             }
 
-            StandardPlural plural = quantity.getStandardPlural(rules);
-            String patternString = data.getPattern(magnitude, plural);
+            String patternString = data.getPattern(magnitude, rules, quantity);
             if (patternString == null) {
                 // Use the default (non-compact) modifier.
                 // No need to take any action.
diff --git a/android_icu4j/src/main/java/android/icu/number/CurrencyPrecision.java b/android_icu4j/src/main/java/android/icu/number/CurrencyPrecision.java
index 4564171..db6b570 100644
--- a/android_icu4j/src/main/java/android/icu/number/CurrencyPrecision.java
+++ b/android_icu4j/src/main/java/android/icu/number/CurrencyPrecision.java
@@ -39,7 +39,9 @@
      */
     public Precision withCurrency(Currency currency) {
         if (currency != null) {
-            return constructFromCurrency(this, currency);
+            Precision retval = constructFromCurrency(this, currency);
+            retval.trailingZeroDisplay = trailingZeroDisplay;
+            return retval;
         } else {
             throw new IllegalArgumentException("Currency must not be null");
         }
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 9537cc1..0f10b8a 100644
--- a/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java
+++ b/android_icu4j/src/main/java/android/icu/number/FormattedNumber.java
@@ -28,10 +28,14 @@
     final DecimalQuantity fq;
     final MeasureUnit outputUnit;
 
-    FormattedNumber(FormattedStringBuilder nsb, DecimalQuantity fq, MeasureUnit outputUnit) {
+    // Grammatical gender of the formatted result.
+    final String gender;
+
+    FormattedNumber(FormattedStringBuilder nsb, DecimalQuantity fq, MeasureUnit outputUnit, String gender) {
         this.string = nsb;
         this.fq = fq;
         this.outputUnit = outputUnit;
+        this.gender = gender;
     }
 
     /**
@@ -111,13 +115,26 @@
      * as "foot-and-inch" or "hour-and-minute-and-second".
      *
      * @return `MeasureUnit`.
-     * @hide draft / provisional / internal are hidden on Android
      */
     public MeasureUnit getOutputUnit() {
         return this.outputUnit;
     }
 
     /**
+     * The gender of the formatted output.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String getGender() {
+        if (this.gender == null) {
+            return "";
+        }
+        return this.gender;
+    }
+
+    /**
      * @deprecated This API is ICU internal only.
      * @hide draft / provisional / internal are hidden on Android
      */
diff --git a/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java b/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java
index bae5b9b..06ed9d6 100644
--- a/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java
+++ b/android_icu4j/src/main/java/android/icu/number/FormattedNumberRange.java
@@ -164,8 +164,7 @@
         // FormattedStringBuilder and BigDecimal are mutable, so we can't call
         // #equals() or #hashCode() on them directly.
         FormattedNumberRange _other = (FormattedNumberRange) other;
-        return Arrays.equals(string.toCharArray(), _other.string.toCharArray())
-                && Arrays.equals(string.toFieldArray(), _other.string.toFieldArray())
+        return string.contentEquals(_other.string)
                 && quantity1.toBigDecimal().equals(_other.quantity1.toBigDecimal())
                 && quantity2.toBigDecimal().equals(_other.quantity2.toBigDecimal());
     }
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 ff4c28c..ced2a35 100644
--- a/android_icu4j/src/main/java/android/icu/number/FractionPrecision.java
+++ b/android_icu4j/src/main/java/android/icu/number/FractionPrecision.java
@@ -20,6 +20,36 @@
     }
 
     /**
+     * Override maximum fraction digits with maximum significant digits depending on the magnitude
+     * of the number. See UNumberRoundingPriority.
+     *
+     * @param minSignificantDigits
+     *            Pad trailing zeros to achieve this minimum number of significant digits.
+     * @param maxSignificantDigits
+     *            Round the number to achieve this maximum number of significant digits.
+     * @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,
+            int maxSignificantDigits,
+            NumberFormatter.RoundingPriority priority) {
+        if (maxSignificantDigits >= 1 &&
+                maxSignificantDigits >= minSignificantDigits &&
+                maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
+            return constructFractionSignificant(
+                this, minSignificantDigits, maxSignificantDigits, priority);
+        } else {
+            throw new IllegalArgumentException("Significant digits must be between 1 and "
+                    + RoundingUtils.MAX_INT_FRAC_SIG
+                    + " (inclusive)");
+        }
+    }
+
+    /**
      * Ensure that no less than this number of significant digits are retained when rounding according to
      * fraction rules.
      *
@@ -31,6 +61,9 @@
      * This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3",
      * not "3.0".
      *
+     * <p>
+     * This is equivalent to `withSignificantDigits(1, minSignificantDigits, RELAXED)`.
+     *
      * @param minSignificantDigits
      *            The number of significant figures to guarantee.
      * @return A Precision for chaining or passing to the NumberFormatter rounding() setter.
@@ -39,7 +72,8 @@
      */
     public Precision withMinDigits(int minSignificantDigits) {
         if (minSignificantDigits >= 1 && minSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
-            return constructFractionSignificant(this, minSignificantDigits, -1);
+            return constructFractionSignificant(
+                this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 1 and "
                     + RoundingUtils.MAX_INT_FRAC_SIG
@@ -59,6 +93,9 @@
      * This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2,
      * 123.4 would become "120.00".
      *
+     * <p>
+     * This is equivalent to `withSignificantDigits(1, maxSignificantDigits, STRICT)`.
+     *
      * @param maxSignificantDigits
      *            Round the number to no more than this number of significant figures.
      * @return A Precision for chaining or passing to the NumberFormatter rounding() setter.
@@ -67,7 +104,8 @@
      */
     public Precision withMaxDigits(int maxSignificantDigits) {
         if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
-            return constructFractionSignificant(this, -1, maxSignificantDigits);
+            return constructFractionSignificant(
+                this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT);
         } 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/LocalizedNumberFormatter.java b/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java
index c290a27..48a4e44 100644
--- a/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/number/LocalizedNumberFormatter.java
@@ -98,7 +98,7 @@
         MeasureUnit unit = input.getUnit();
         FormattedStringBuilder string = new FormattedStringBuilder();
         MicroProps micros = formatImpl(fq, unit, string);
-        return new FormattedNumber(string, fq, micros.outputUnit);
+        return new FormattedNumber(string, fq, micros.outputUnit, micros.gender);
     }
 
     /**
@@ -122,7 +122,7 @@
     private FormattedNumber format(DecimalQuantity fq) {
         FormattedStringBuilder string = new FormattedStringBuilder();
         MicroProps micros = formatImpl(fq, string);
-        return new FormattedNumber(string, fq, micros.outputUnit);
+        return new FormattedNumber(string, fq, micros.outputUnit, micros.gender);
     }
 
     /**
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 92f2027..101b06e 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatter.java
@@ -10,8 +10,9 @@
 import android.icu.util.ULocale;
 
 /**
- * The main entrypoint to the localized number formatting library introduced in ICU 60. Basic usage
- * examples:
+ * All-in-one formatter for localized numbers, currencies, and units.
+ *
+ * For a full list of options, see {@link NumberFormatterSettings}.
  *
  * <pre>
  * // Most basic usage:
@@ -68,6 +69,67 @@
     private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter();
 
     /**
+     * An enum declaring how to resolve conflicts between maximum fraction digits and maximum
+     * significant digits.
+     *
+     * <p>There are two modes, RELAXED and STRICT:
+     *
+     * <ul>
+     * <li> RELAXED: Relax one of the two constraints (fraction digits or significant digits) in order
+     *   to round the number to a higher level of precision.
+     * <li> STRICT: Enforce both constraints, resulting in the number being rounded to a lower
+     *   level of precision.
+     * </ul>
+     *
+     * <p>The default settings for compact notation rounding are Max-Fraction = 0 (round to the nearest
+     * integer), Max-Significant = 2 (round to 2 significant digits), and priority RELAXED (choose
+     * the constraint that results in more digits being displayed).
+     *
+     * <p>Conflicting *minimum* fraction and significant digits are always resolved in the direction that
+     * results in more trailing zeros.
+     *
+     * <p>Example 1: Consider the number 3.141, with various different settings:
+     *
+     * <ul>
+     * <li> Max-Fraction = 1: "3.1"
+     * <li> Max-Significant = 3: "3.14"
+     * </ul>
+     *
+     * <p>The rounding priority determines how to resolve the conflict when both Max-Fraction and
+     * Max-Significant are set. With RELAXED, the less-strict setting (the one that causes more digits
+     * to be displayed) will be used; Max-Significant wins. With STRICT, the more-strict setting (the
+     * one that causes fewer digits to be displayed) will be used; Max-Fraction wins.
+     *
+     * <p>Example 2: Consider the number 8317, with various different settings:
+     *
+     * <ul>
+     * <li> Max-Fraction = 1: "8317"
+     * <li> Max-Significant = 3: "8320"
+     * </ul>
+     *
+     * <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,
+    }
+
+    /**
      * An enum declaring how to render units, including currencies. Example outputs when formatting 123
      * USD and 123 meters in <em>en-CA</em>:
      *
@@ -147,7 +209,6 @@
          * Behavior of this option with non-currency units is not defined at this time.
          *
          * @see NumberFormatter
-         * @hide draft / provisional / internal are hidden on Android
          */
         FORMAL,
 
@@ -159,7 +220,6 @@
          * Behavior of this option with non-currency units is not defined at this time.
          *
          * @see NumberFormatter
-         * @hide draft / provisional / internal are hidden on Android
          */
         VARIANT,
 
@@ -287,6 +347,9 @@
          * Show the minus sign on negative numbers, and do not show the sign on positive numbers. This is
          * the default behavior.
          *
+         * If using this option, a sign will be displayed on negative zero, including negative numbers
+         * that round to zero. To hide the sign on negative zero, use the NEGATIVE option.
+         *
          * @see NumberFormatter
          */
         AUTO,
@@ -349,6 +412,20 @@
          * @see NumberFormatter
          */
         ACCOUNTING_EXCEPT_ZERO,
+
+        /**
+         * 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,
     }
 
     /**
@@ -380,6 +457,33 @@
     }
 
     /**
+     * An enum declaring how to render trailing zeros.
+     *
+     * <ul>
+     * <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,
+    }
+
+    /**
      * Use a default threshold of 3. This means that the third time .format() is called, the data
      * structures get built using the "safe" code path. The first two calls to .format() will trigger the
      * unsafe code path.
@@ -430,6 +534,9 @@
      * Call this method at the beginning of a NumberFormatter fluent chain to create an instance based
      * on a given number skeleton string.
      *
+     * For more information on number skeleton strings, see:
+     * https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html
+     *
      * @param skeleton
      *            The skeleton string off of which to base this NumberFormatter.
      * @return An {@link UnlocalizedNumberFormatter}, to be used for chaining.
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 566f90a..698d42a 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatterImpl.java
@@ -6,6 +6,7 @@
 import android.icu.impl.FormattedStringBuilder;
 import android.icu.impl.IllegalIcuArgumentException;
 import android.icu.impl.StandardPlural;
+import android.icu.impl.number.AffixPatternProvider;
 import android.icu.impl.number.CompactData.CompactType;
 import android.icu.impl.number.ConstantAffixModifier;
 import android.icu.impl.number.DecimalQuantity;
@@ -37,6 +38,7 @@
 import android.icu.util.Currency;
 import android.icu.util.MeasureUnit;
 
+
 /**
  * This is the "brain" of the number formatting pipeline. It ties all the pieces together, taking in a
  * MacroProps and a DecimalQuantity and outputting a properly formatted number string.
@@ -62,10 +64,10 @@
             MacroProps macros,
             DecimalQuantity inValue,
             FormattedStringBuilder outString) {
-        MicroProps micros = preProcessUnsafe(macros, inValue);
-        int length = writeNumber(micros, inValue, outString, 0);
-        writeAffixes(micros, outString, 0, length);
-        return micros;
+        MicroProps result = preProcessUnsafe(macros, inValue);
+        int length = writeNumber(result, inValue, outString, 0);
+        writeAffixes(result, outString, 0, length);
+        return result;
     }
 
     /**
@@ -93,10 +95,10 @@
      * Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
      */
     public MicroProps format(DecimalQuantity inValue, FormattedStringBuilder outString) {
-        MicroProps micros = preProcess(inValue);
-        int length = writeNumber(micros, inValue, outString, 0);
-        writeAffixes(micros, outString, 0, length);
-        return micros;
+        MicroProps result = preProcess(inValue);
+        int length = writeNumber(result, inValue, outString, 0);
+        writeAffixes(result, outString, 0, length);
+        return result;
     }
 
     /**
@@ -193,7 +195,8 @@
         boolean isCompactNotation = (macros.notation instanceof CompactNotation);
         boolean isAccounting = macros.sign == SignDisplay.ACCOUNTING
                 || macros.sign == SignDisplay.ACCOUNTING_ALWAYS
-                || macros.sign == SignDisplay.ACCOUNTING_EXCEPT_ZERO;
+                || macros.sign == SignDisplay.ACCOUNTING_EXCEPT_ZERO
+                || macros.sign == SignDisplay.ACCOUNTING_NEGATIVE;
         Currency currency = isCurrency ? (Currency) macros.unit : DEFAULT_CURRENCY;
         UnitWidth unitWidth = UnitWidth.SHORT;
         if (macros.unitWidth != null) {
@@ -226,6 +229,9 @@
         }
         micros.nsName = ns.getName();
 
+        // Default gender: none.
+        micros.gender = "";
+
         // Resolve the symbols. Do this here because currency may need to customize them.
         if (macros.symbols instanceof DecimalFormatSymbols) {
             micros.symbols = (DecimalFormatSymbols) macros.symbols;
@@ -275,9 +281,7 @@
             }
             chain = usagePrefsHandler = new UsagePrefsHandler(macros.loc, macros.unit, macros.usage, chain);
         } else if (isMixedUnit) {
-            // TODO(icu-units#97): The input unit should be the largest unit, not the first unit, in the identifier.
-            MeasureUnit inputUnit = macros.unit.splitToSingleUnits().get(0);
-            chain = new UnitConversionHandler(inputUnit, macros.unit, chain);
+            chain = new UnitConversionHandler(macros.unit, chain);
         }
 
         // Multiplier
@@ -359,8 +363,13 @@
         // Middle modifier (patterns, positive/negative, currency symbols, percent)
         // The default middle modifier is weak (thus the false argument).
         MutablePatternModifier patternMod = new MutablePatternModifier(false);
-        patternMod.setPatternInfo((macros.affixProvider != null) ? macros.affixProvider : patternInfo, null);
-        patternMod.setPatternAttributes(micros.sign, isPermille);
+        AffixPatternProvider affixProvider =
+            (macros.affixProvider != null)
+                ? macros.affixProvider
+                : patternInfo;
+        patternMod.setPatternInfo(affixProvider, null);
+        boolean approximately = (macros.approximately != null) ? macros.approximately : false;
+        patternMod.setPatternAttributes(micros.sign, isPermille, approximately);
         if (patternMod.needsPlurals()) {
             if (rules == null) {
                 // Lazily create PluralRules
@@ -375,8 +384,17 @@
             immPatternMod = patternMod.createImmutable();
         }
 
+        // currencyAsDecimal
+        if (affixProvider.currencyAsDecimal()) {
+            micros.currencyAsDecimal = patternMod.getCurrencySymbolForUnitWidth();
+        }
+
         // Outer modifier (CLDR units and currency long names)
         if (isCldrUnit) {
+            String unitDisplayCase = null;
+            if (macros.unitDisplayCase != null) {
+                unitDisplayCase = macros.unitDisplayCase;
+            }
             if (rules == null) {
                 // Lazily create PluralRules
                 rules = PluralRules.forLocale(macros.loc);
@@ -391,6 +409,7 @@
                         macros.loc,
                         usagePrefsHandler.getOutputUnits(),
                         unitWidth,
+                        unitDisplayCase,
                         pluralRules,
                         chain);
             } else if (isMixedUnit) {
@@ -398,13 +417,27 @@
                         macros.loc,
                         macros.unit,
                         unitWidth,
+                        unitDisplayCase,
                         pluralRules,
                         chain);
             } else {
-                chain = LongNameHandler.forMeasureUnit(macros.loc,
-                        macros.unit,
-                        macros.perUnit,
+                MeasureUnit unit = macros.unit;
+                if (macros.perUnit != null) {
+                    unit = unit.product(macros.perUnit.reciprocal());
+                    // This isn't strictly necessary, but was what we specced
+                    // out when perUnit became a backward-compatibility thing:
+                    // unit/perUnit use case is only valid if both units are
+                    // built-ins, or the product is a built-in.
+                    if (unit.getType() == null && (macros.unit.getType() == null || macros.perUnit.getType() == null)) {
+                        throw new UnsupportedOperationException(
+                            "perUnit() can only be used if unit and perUnit are both built-ins, or the combination is a built-in");
+                    }
+                }
+                chain = LongNameHandler.forMeasureUnit(
+                        macros.loc,
+                        unit,
                         unitWidth,
+                        unitDisplayCase,
                         pluralRules,
                         chain);
             }
@@ -491,10 +524,24 @@
             // Add the decimal point
             if (quantity.getLowerDisplayMagnitude() < 0
                     || micros.decimal == DecimalSeparatorDisplay.ALWAYS) {
-                length += string.insert(length + index,
-                        micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString()
-                                : micros.symbols.getDecimalSeparatorString(),
+                if (micros.currencyAsDecimal != null) {
+                    // Note: This unconditionally substitutes the standard short symbol.
+                    // TODO: Should we support narrow or other variants?
+                    length += string.insert(
+                        length + index,
+                        micros.currencyAsDecimal,
+                        NumberFormat.Field.CURRENCY);
+                } else if (micros.useCurrency) {
+                    length += string.insert(
+                        length + index,
+                        micros.symbols.getMonetaryDecimalSeparatorString(),
                         NumberFormat.Field.DECIMAL_SEPARATOR);
+                } else {
+                    length += string.insert(
+                        length + index,
+                        micros.symbols.getDecimalSeparatorString(),
+                        NumberFormat.Field.DECIMAL_SEPARATOR);
+                }
             }
 
             // Add the fraction digits
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 24d3af8..2efa445 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberFormatterSettings.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberFormatterSettings.java
@@ -45,7 +45,8 @@
     static final int KEY_THRESHOLD = 14;
     static final int KEY_PER_UNIT = 15;
     static final int KEY_USAGE = 16;
-    static final int KEY_MAX = 17;
+    static final int KEY_UNIT_DISPLAY_CASE = 17;
+    static final int KEY_MAX = 18;
 
     private final NumberFormatterSettings<?> parent;
     private final int key;
@@ -518,13 +519,29 @@
      * @param usage A usage parameter from the units resource.
      * @return The fluent chain
      * @throws IllegalArgumentException in case of Setting a usage string but not a correct input unit.
-     * @hide draft / provisional / internal are hidden on Android
      */
     public T usage(String usage) {
+        if (usage != null && usage.isEmpty()) {
+            return create(KEY_USAGE, null);
+        }
+
         return create(KEY_USAGE, usage);
     }
 
     /**
+     * Specifies the desired case for a unit formatter's output (e.g.
+     * accusative, dative, genitive).
+     *
+     * @return The fluent chain
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public T unitDisplayCase(String unitDisplayCase) {
+        return create(KEY_UNIT_DISPLAY_CASE, unitDisplayCase);
+    }
+
+    /**
      * Internal method to set a starting macros.
      *
      * @deprecated ICU 60 This API is ICU internal only.
@@ -568,6 +585,9 @@
      * <p>
      * The returned skeleton is in normalized form, such that two number formatters with equivalent
      * behavior should produce the same skeleton.
+     * <p>
+     * For more information on number skeleton strings, see:
+     * https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html
      *
      * @return A number skeleton string with behavior corresponding to this number formatter.
      * @throws UnsupportedOperationException
@@ -651,6 +671,9 @@
             case KEY_USAGE:
                 macros.usage = (String) current.value;
                 break;
+            case KEY_UNIT_DISPLAY_CASE:
+                macros.unitDisplayCase = (String) current.value;
+                break;
             default:
                 throw new AssertionError("Unknown key: " + current.key);
             }
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 343ea32..8ba138b 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberPropertyMapper.java
@@ -176,6 +176,9 @@
             if (PatternStringUtils.ignoreRoundingIncrement(roundingIncrement, maxFrac)) {
                 rounding = Precision.constructFraction(minFrac, maxFrac);
             } else {
+                if (minFrac > roundingIncrement.scale()) {
+                    roundingIncrement = roundingIncrement.setScale(minFrac);
+                }
                 rounding = Precision.constructIncrement(roundingIncrement);
             }
         } else if (explicitMinMaxSig) {
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 3f690a9..9618b5a 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatter.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatter.java
@@ -3,8 +3,10 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.number;
 
+import java.io.InvalidObjectException;
 import java.util.Locale;
 
+import android.icu.text.UFormat;
 import android.icu.util.ULocale;
 
 /**
@@ -139,6 +141,45 @@
         NOT_EQUAL
     }
 
+    /**
+     * 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;
+
+        /**
+         * The concrete field used for spans in FormattedNumberRange.
+         *
+         * 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");
+
+        private SpanField(String name) {
+            super(name);
+        }
+
+        /**
+         * Serialization method resolve instances to the constant
+         * NumberRangeFormatter.SpanField values
+         * @deprecated This API is ICU internal only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        @Override
+        protected Object readResolve() throws InvalidObjectException {
+            if (this.getName().equals(NUMBER_RANGE_SPAN.getName()))
+                return NUMBER_RANGE_SPAN;
+
+            throw new InvalidObjectException("An invalid object.");
+        }
+    }
+
     private static final UnlocalizedNumberRangeFormatter BASE = new UnlocalizedNumberRangeFormatter();
 
     /**
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 1cb5ddd..bff637e 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterImpl.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterImpl.java
@@ -6,6 +6,7 @@
 import java.util.MissingResourceException;
 
 import android.icu.impl.FormattedStringBuilder;
+import android.icu.impl.FormattedValueStringBuilderImpl;
 import android.icu.impl.ICUData;
 import android.icu.impl.ICUResourceBundle;
 import android.icu.impl.PatternProps;
@@ -13,6 +14,7 @@
 import android.icu.impl.StandardPlural;
 import android.icu.impl.UResource;
 import android.icu.impl.number.DecimalQuantity;
+import android.icu.impl.number.MacroProps;
 import android.icu.impl.number.MicroProps;
 import android.icu.impl.number.Modifier;
 import android.icu.impl.number.SimpleModifier;
@@ -38,10 +40,10 @@
     final NumberRangeFormatter.RangeCollapse fCollapse;
     final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
 
-    // Should be final, but they are set in a helper function, not the constructor proper.
-    // TODO: Clean up to make these fields actually final.
+    // Should be final, but it is set in a helper function, not the constructor proper.
+    // TODO: Clean up to make this field actually final.
     /* final */ String fRangePattern;
-    /* final */ SimpleModifier fApproximatelyModifier;
+    final NumberFormatterImpl fApproximatelyFormatter;
 
     final StandardPluralRanges fPluralRanges;
 
@@ -55,7 +57,8 @@
     private static final class NumberRangeDataSink extends UResource.Sink {
 
         String rangePattern;
-        String approximatelyPattern;
+        // Note: approximatelyPattern is unused since ICU 69.
+        // String approximatelyPattern;
 
         // For use with SimpleFormatterImpl
         StringBuilder sb;
@@ -72,10 +75,13 @@
                     String pattern = value.getString();
                     rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
                 }
+                /*
+                // Note: approximatelyPattern is unused since ICU 69.
                 if (key.contentEquals("approximately") && !hasApproxData()) {
                     String pattern = value.getString();
                     approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 1, 1); // 1 arg, as in "~{0}"
                 }
+                */
             }
         }
 
@@ -83,21 +89,26 @@
             return rangePattern != null;
         }
 
+        /*
+        // Note: approximatelyPattern is unused since ICU 69.
         private boolean hasApproxData() {
             return approximatelyPattern != null;
         }
+        */
 
         public boolean isComplete() {
-            return hasRangeData() && hasApproxData();
+            return hasRangeData() /* && hasApproxData() */;
         }
 
         public void fillInDefaults() {
             if (!hasRangeData()) {
                 rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}–{1}", sb, 2, 2);
             }
+            /*
             if (!hasApproxData()) {
                 approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1);
             }
+            */
         }
     }
 
@@ -127,16 +138,20 @@
         sink.fillInDefaults();
 
         out.fRangePattern = sink.rangePattern;
-        out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false);
+        // out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false);
     }
 
     ////////////////////
 
     public NumberRangeFormatterImpl(RangeMacroProps macros) {
-        formatterImpl1 = new NumberFormatterImpl(macros.formatter1 != null ? macros.formatter1.resolve()
-                : NumberFormatter.withLocale(macros.loc).resolve());
-        formatterImpl2 = new NumberFormatterImpl(macros.formatter2 != null ? macros.formatter2.resolve()
-                : NumberFormatter.withLocale(macros.loc).resolve());
+        LocalizedNumberFormatter formatter1 = macros.formatter1 != null
+            ? macros.formatter1.locale(macros.loc)
+            : NumberFormatter.withLocale(macros.loc);
+        LocalizedNumberFormatter formatter2 = macros.formatter2 != null
+            ? macros.formatter2.locale(macros.loc)
+            : NumberFormatter.withLocale(macros.loc);
+        formatterImpl1 = new NumberFormatterImpl(formatter1.resolve());
+        formatterImpl2 = new NumberFormatterImpl(formatter2.resolve());
         fSameFormatters = macros.sameFormatters != 0;
         fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO;
         fIdentityFallback = macros.identityFallback != null ? macros.identityFallback
@@ -148,6 +163,17 @@
         }
         getNumberRangeData(macros.loc, nsName, this);
 
+        if (fSameFormatters && (
+                fIdentityFallback == RangeIdentityFallback.APPROXIMATELY ||
+                fIdentityFallback == RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)) {
+            MacroProps approximatelyMacros = new MacroProps();
+            approximatelyMacros.approximately = true;
+            fApproximatelyFormatter = new NumberFormatterImpl(
+                formatter1.macros(approximatelyMacros).resolve());
+        } else {
+            fApproximatelyFormatter = null;
+        }
+
         // TODO: Get locale from PluralRules instead?
         fPluralRanges = StandardPluralRanges.forLocale(macros.loc);
     }
@@ -230,12 +256,14 @@
     private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string,
             MicroProps micros1, MicroProps micros2) {
         if (fSameFormatters) {
-            int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
+            // Re-format using the approximately formatter:
+            quantity1.resetExponent();
+            MicroProps microsAppx = fApproximatelyFormatter.preProcess(quantity1);
+            int length = NumberFormatterImpl.writeNumber(microsAppx, quantity1, string, 0);
             // HEURISTIC: Desired modifier order: inner, middle, approximately, outer.
-            length += micros1.modInner.apply(string, 0, length);
-            length += micros1.modMiddle.apply(string, 0, length);
-            length += fApproximatelyModifier.apply(string, 0, length);
-            micros1.modOuter.apply(string, 0, length);
+            length += microsAppx.modInner.apply(string, 0, length);
+            length += microsAppx.modMiddle.apply(string, 0, length);
+            microsAppx.modOuter.apply(string, 0, length);
         } else {
             formatRange(quantity1, quantity2, string, micros1, micros2);
         }
@@ -298,7 +326,7 @@
                 // INNER MODIFIER
                 collapseInner = micros1.modInner.semanticallyEquivalent(micros2.modInner);
 
-                // All done checking for collapsability.
+                // All done checking for collapsibility.
                 break;
             }
 
@@ -337,36 +365,56 @@
         }
 
         h.length1 += NumberFormatterImpl.writeNumber(micros1, quantity1, string, h.index0());
-        h.length2 += NumberFormatterImpl.writeNumber(micros2, quantity2, string, h.index2());
+        // ICU-21684: Write the second number to a temp string to avoid repeated insert operations
+        FormattedStringBuilder tempString = new FormattedStringBuilder();
+        NumberFormatterImpl.writeNumber(micros2, quantity2, tempString, 0);
+        h.length2 += string.insert(h.index2(), tempString);
 
         // TODO: Support padding?
 
         if (collapseInner) {
-            // Note: this is actually a mix of prefix and suffix, but adding to infix length works
             Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner);
-            h.lengthInfix += mod.apply(string, h.index0(), h.index3());
+            h.lengthSuffix += mod.apply(string, h.index0(), h.index4());
+            h.lengthPrefix += mod.getPrefixLength();
+            h.lengthSuffix -= mod.getPrefixLength();
         } else {
             h.length1 += micros1.modInner.apply(string, h.index0(), h.index1());
-            h.length2 += micros2.modInner.apply(string, h.index2(), h.index3());
+            h.length2 += micros2.modInner.apply(string, h.index2(), h.index4());
         }
 
         if (collapseMiddle) {
-            // Note: this is actually a mix of prefix and suffix, but adding to infix length works
             Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle);
-            h.lengthInfix += mod.apply(string, h.index0(), h.index3());
+            h.lengthSuffix += mod.apply(string, h.index0(), h.index4());
+            h.lengthPrefix += mod.getPrefixLength();
+            h.lengthSuffix -= mod.getPrefixLength();
         } else {
             h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1());
-            h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3());
+            h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index4());
         }
 
         if (collapseOuter) {
-            // Note: this is actually a mix of prefix and suffix, but adding to infix length works
             Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter);
-            h.lengthInfix += mod.apply(string, h.index0(), h.index3());
+            h.lengthSuffix += mod.apply(string, h.index0(), h.index4());
+            h.lengthPrefix += mod.getPrefixLength();
+            h.lengthSuffix -= mod.getPrefixLength();
         } else {
             h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1());
-            h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3());
+            h.length2 += micros2.modOuter.apply(string, h.index2(), h.index4());
         }
+
+        // Now that all pieces are added, save the span info.
+        FormattedValueStringBuilderImpl.applySpanRange(
+            string,
+            NumberRangeFormatter.SpanField.NUMBER_RANGE_SPAN,
+            0,
+            h.index0(),
+            h.index1());
+        FormattedValueStringBuilderImpl.applySpanRange(
+            string,
+            NumberRangeFormatter.SpanField.NUMBER_RANGE_SPAN,
+            1,
+            h.index2(),
+            h.index3());
     }
 
     Modifier resolveModifierPlurals(Modifier first, Modifier second) {
diff --git a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterSettings.java b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterSettings.java
index 226e75f..d545e8b 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterSettings.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberRangeFormatterSettings.java
@@ -144,41 +144,37 @@
         // of a MacroProps object at each step.
         // TODO: Remove the reference to the parent after the macros are resolved?
         RangeMacroProps macros = new RangeMacroProps();
+        // Bitmap: 1 if seen; 0 if unseen
+        long seen = 0;
         NumberRangeFormatterSettings<?> current = this;
         while (current != null) {
+            long keyBitmask = (1L << current.key);
+            if (0 != (seen & keyBitmask)) {
+                current = current.parent;
+                continue;
+            }
+            seen |= keyBitmask;
             switch (current.key) {
             case KEY_MACROS:
                 // ignored for now
                 break;
             case KEY_LOCALE:
-                if (macros.loc == null) {
-                    macros.loc = (ULocale) current.value;
-                }
+                macros.loc = (ULocale) current.value;
                 break;
             case KEY_FORMATTER_1:
-                if (macros.formatter1 == null) {
-                    macros.formatter1 = (UnlocalizedNumberFormatter) current.value;
-                }
+                macros.formatter1 = (UnlocalizedNumberFormatter) current.value;
                 break;
             case KEY_FORMATTER_2:
-                if (macros.formatter2 == null) {
-                    macros.formatter2 = (UnlocalizedNumberFormatter) current.value;
-                }
+                macros.formatter2 = (UnlocalizedNumberFormatter) current.value;
                 break;
             case KEY_SAME_FORMATTERS:
-                if (macros.sameFormatters == -1) {
-                    macros.sameFormatters = (boolean) current.value ? 1 : 0;
-                }
+                macros.sameFormatters = (boolean) current.value ? 1 : 0;
                 break;
             case KEY_COLLAPSE:
-                if (macros.collapse == null) {
-                    macros.collapse = (RangeCollapse) current.value;
-                }
+                macros.collapse = (RangeCollapse) current.value;
                 break;
             case KEY_IDENTITY_FALLBACK:
-                if (macros.identityFallback == null) {
-                    macros.identityFallback = (RangeIdentityFallback) current.value;
-                }
+                macros.identityFallback = (RangeIdentityFallback) current.value;
                 break;
             default:
                 throw new AssertionError("Unknown key: " + current.key);
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 2d7640d..6356939 100644
--- a/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java
+++ b/android_icu4j/src/main/java/android/icu/number/NumberSkeletonImpl.java
@@ -13,11 +13,11 @@
 import android.icu.impl.StringSegment;
 import android.icu.impl.number.MacroProps;
 import android.icu.impl.number.RoundingUtils;
-import android.icu.impl.units.MeasureUnitImpl;
-import android.icu.impl.units.SingleUnitImpl;
 import android.icu.number.NumberFormatter.DecimalSeparatorDisplay;
 import android.icu.number.NumberFormatter.GroupingStrategy;
+import android.icu.number.NumberFormatter.RoundingPriority;
 import android.icu.number.NumberFormatter.SignDisplay;
+import android.icu.number.NumberFormatter.TrailingZeroDisplay;
 import android.icu.number.NumberFormatter.UnitWidth;
 import android.icu.text.DecimalFormatSymbols;
 import android.icu.text.NumberingSystem;
@@ -53,6 +53,7 @@
         // Section 1: We might accept an option, but it is not required:
         STATE_SCIENTIFIC,
         STATE_FRACTION_PRECISION,
+        STATE_PRECISION,
 
         // Section 2: An option is required:
         STATE_INCREMENT_PRECISION,
@@ -96,6 +97,7 @@
         STEM_ROUNDING_MODE_HALF_DOWN,
         STEM_ROUNDING_MODE_HALF_UP,
         STEM_ROUNDING_MODE_UNNECESSARY,
+        STEM_INTEGER_WIDTH_TRUNC,
         STEM_GROUP_OFF,
         STEM_GROUP_MIN2,
         STEM_GROUP_AUTO,
@@ -116,6 +118,8 @@
         STEM_SIGN_ACCOUNTING_ALWAYS,
         STEM_SIGN_EXCEPT_ZERO,
         STEM_SIGN_ACCOUNTING_EXCEPT_ZERO,
+        STEM_SIGN_NEGATIVE,
+        STEM_SIGN_ACCOUNTING_NEGATIVE,
         STEM_DECIMAL_AUTO,
         STEM_DECIMAL_ALWAYS,
 
@@ -172,6 +176,7 @@
         b.add("rounding-mode-half-down", StemEnum.STEM_ROUNDING_MODE_HALF_DOWN.ordinal());
         b.add("rounding-mode-half-up", StemEnum.STEM_ROUNDING_MODE_HALF_UP.ordinal());
         b.add("rounding-mode-unnecessary", StemEnum.STEM_ROUNDING_MODE_UNNECESSARY.ordinal());
+        b.add("integer-width-trunc", StemEnum.STEM_INTEGER_WIDTH_TRUNC.ordinal());
         b.add("group-off", StemEnum.STEM_GROUP_OFF.ordinal());
         b.add("group-min2", StemEnum.STEM_GROUP_MIN2.ordinal());
         b.add("group-auto", StemEnum.STEM_GROUP_AUTO.ordinal());
@@ -192,6 +197,8 @@
         b.add("sign-accounting-always", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
         b.add("sign-except-zero", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal());
         b.add("sign-accounting-except-zero", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
+        b.add("sign-negative", StemEnum.STEM_SIGN_NEGATIVE.ordinal());
+        b.add("sign-accounting-negative", StemEnum.STEM_SIGN_ACCOUNTING_NEGATIVE.ordinal());
         b.add("decimal-auto", StemEnum.STEM_DECIMAL_AUTO.ordinal());
         b.add("decimal-always", StemEnum.STEM_DECIMAL_ALWAYS.ordinal());
 
@@ -220,6 +227,8 @@
         b.add("()!", StemEnum.STEM_SIGN_ACCOUNTING_ALWAYS.ordinal());
         b.add("+?", StemEnum.STEM_SIGN_EXCEPT_ZERO.ordinal());
         b.add("()?", StemEnum.STEM_SIGN_ACCOUNTING_EXCEPT_ZERO.ordinal());
+        b.add("+-", StemEnum.STEM_SIGN_NEGATIVE.ordinal());
+        b.add("()-", StemEnum.STEM_SIGN_ACCOUNTING_NEGATIVE.ordinal());
 
         // Build the CharsTrie
         // TODO: Use SLOW or FAST here?
@@ -354,6 +363,10 @@
                 return SignDisplay.EXCEPT_ZERO;
             case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
                 return SignDisplay.ACCOUNTING_EXCEPT_ZERO;
+            case STEM_SIGN_NEGATIVE:
+                return SignDisplay.NEGATIVE;
+            case STEM_SIGN_ACCOUNTING_NEGATIVE:
+                return SignDisplay.ACCOUNTING_NEGATIVE;
             default:
                 return null; // for objects, throw; for enums, return null
             }
@@ -481,6 +494,12 @@
             case ACCOUNTING_EXCEPT_ZERO:
                 sb.append("sign-accounting-except-zero");
                 break;
+            case NEGATIVE:
+                sb.append("sign-negative");
+                break;
+            case ACCOUNTING_NEGATIVE:
+                sb.append("sign-accounting-negative");
+                break;
             default:
                 throw new AssertionError();
             }
@@ -657,7 +676,7 @@
         case '@':
             checkNull(macros.precision, segment);
             BlueprintHelpers.parseDigitsStem(segment, macros);
-            return ParseState.STATE_NULL;
+            return ParseState.STATE_PRECISION;
         case 'E':
             checkNull(macros.notation, segment);
             BlueprintHelpers.parseScientificStem(segment, macros);
@@ -720,7 +739,7 @@
             case STEM_PRECISION_INTEGER:
                 return ParseState.STATE_FRACTION_PRECISION; // allows for "precision-integer/@##"
             default:
-                return ParseState.STATE_NULL;
+                return ParseState.STATE_PRECISION;
             }
 
         case STEM_ROUNDING_MODE_CEILING:
@@ -735,6 +754,11 @@
             macros.roundingMode = StemToObject.roundingMode(stem);
             return ParseState.STATE_NULL;
 
+        case STEM_INTEGER_WIDTH_TRUNC:
+            checkNull(macros.integerWidth, segment);
+            macros.integerWidth = IntegerWidth.zeroFillTo(0).truncateAt(0);
+            return ParseState.STATE_NULL;
+
         case STEM_GROUP_OFF:
         case STEM_GROUP_MIN2:
         case STEM_GROUP_AUTO:
@@ -767,6 +791,8 @@
         case STEM_SIGN_ACCOUNTING_ALWAYS:
         case STEM_SIGN_EXCEPT_ZERO:
         case STEM_SIGN_ACCOUNTING_EXCEPT_ZERO:
+        case STEM_SIGN_NEGATIVE:
+        case STEM_SIGN_ACCOUNTING_NEGATIVE:
             checkNull(macros.sign, segment);
             macros.sign = StemToObject.signDisplay(stem);
             return ParseState.STATE_NULL;
@@ -855,7 +881,7 @@
             return ParseState.STATE_NULL;
         case STATE_INCREMENT_PRECISION:
             BlueprintHelpers.parseIncrementOption(segment, macros);
-            return ParseState.STATE_NULL;
+            return ParseState.STATE_PRECISION;
         case STATE_INTEGER_WIDTH:
             BlueprintHelpers.parseIntegerWidthOption(segment, macros);
             return ParseState.STATE_NULL;
@@ -889,6 +915,19 @@
         switch (stem) {
         case STATE_FRACTION_PRECISION:
             if (BlueprintHelpers.parseFracSigOption(segment, macros)) {
+                return ParseState.STATE_PRECISION;
+            }
+            // If the fracSig option was not found, try normal precision options.
+            stem = ParseState.STATE_PRECISION;
+            break;
+        default:
+            break;
+        }
+
+        // Trailing zeros option
+        switch (stem) {
+        case STATE_PRECISION:
+            if (BlueprintHelpers.parseTrailingZeroOption(segment, macros)) {
                 return ParseState.STATE_NULL;
             }
             break;
@@ -950,6 +989,10 @@
             throw new UnsupportedOperationException(
                     "Cannot generate number skeleton with custom padder");
         }
+        if (macros.unitDisplayCase != null && !macros.unitDisplayCase.isEmpty()) {
+            throw new UnsupportedOperationException(
+                    "Cannot generate number skeleton with custom unit display case");
+        }
         if (macros.affixProvider != null) {
             throw new UnsupportedOperationException(
                     "Cannot generate number skeleton with custom affix provider");
@@ -1071,45 +1114,11 @@
          * specified via a "unit/" concise skeleton.
          */
         private static void parseIdentifierUnitOption(StringSegment segment, MacroProps macros) {
-            MeasureUnitImpl fullUnit;
             try {
-                fullUnit = MeasureUnitImpl.forIdentifier(segment.asString());
+                macros.unit = MeasureUnit.forIdentifier(segment.asString());
             } catch (IllegalArgumentException e) {
                 throw new SkeletonSyntaxException("Invalid unit stem", segment);
             }
-
-            // Mixed units can only be represented by full MeasureUnit instances, so we
-            // don't split the denominator into macros.perUnit.
-            if (fullUnit.getComplexity() == MeasureUnit.Complexity.MIXED) {
-                macros.unit = fullUnit.build();
-                return;
-            }
-
-            // When we have a built-in unit (e.g. meter-per-second), we don't split it up
-            MeasureUnit testBuiltin = fullUnit.build();
-            if (testBuiltin.getType() != null) {
-                macros.unit = testBuiltin;
-                return;
-            }
-
-            // TODO(ICU-20941): Clean this up.
-            for (SingleUnitImpl subUnit : fullUnit.getSingleUnits()) {
-                if (subUnit.getDimensionality() > 0) {
-                    if (macros.unit == null) {
-                        macros.unit = subUnit.build();
-                    } else {
-                        macros.unit = macros.unit.product(subUnit.build());
-                    }
-                } else {
-                    // It's okay to mutate fullUnit, we're throwing it away after this:
-                    subUnit.setDimensionality(subUnit.getDimensionality() * -1);
-                    if (macros.perUnit == null) {
-                        macros.perUnit = subUnit.build();
-                    } else {
-                        macros.perUnit = macros.perUnit.product(subUnit.build());
-                    }
-                }
-            }
         }
 
         private static void parseUnitUsageOption(StringSegment segment, MacroProps macros) {
@@ -1252,6 +1261,7 @@
                     } else if (segment.charAt(offset) == '?') {
                         signDisplay = SignDisplay.EXCEPT_ZERO;
                     } else {
+                        // NOTE: Other sign displays are not included because they aren't useful in this context
                         break block;
                     }
                     offset++;
@@ -1305,20 +1315,14 @@
                     break;
                 }
             }
-            // For the frac-sig option, there must be minSig or maxSig but not both.
-            // Valid: @+, @@+, @@@+
-            // Valid: @#, @##, @###
-            // Invalid: @, @@, @@@
-            // Invalid: @@#, @@##, @@@#
             if (offset < segment.length()) {
                 if (isWildcardChar(segment.charAt(offset))) {
+                    // @+, @@+, @@@+
                     maxSig = -1;
                     offset++;
-                } else if (minSig > 1) {
-                    // @@#, @@##, @@@#
-                    throw new SkeletonSyntaxException("Invalid digits option for fraction rounder",
-                            segment);
                 } else {
+                    // @#, @##, @###
+                    // @@#, @@##, @@@#
                     maxSig = minSig;
                     for (; offset < segment.length(); offset++) {
                         if (segment.charAt(offset) == '#') {
@@ -1330,21 +1334,55 @@
                 }
             } else {
                 // @, @@, @@@
-                throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+                maxSig = minSig;
             }
+            RoundingPriority priority;
             if (offset < segment.length()) {
-                throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+                if (maxSig == -1) {
+                    throw new SkeletonSyntaxException(
+                        "Invalid digits option: Wildcard character not allowed with the priority annotation", segment);
+                }
+                if (segment.codePointAt(offset) == 'r') {
+                    priority = RoundingPriority.RELAXED;
+                    offset++;
+                } else if (segment.codePointAt(offset) == 's') {
+                    priority = RoundingPriority.STRICT;
+                    offset++;
+                } else {
+                    assert offset < segment.length();
+                    priority = RoundingPriority.RELAXED; // make compiler happy (uninitialized variable)
+                }
+                if (offset < segment.length()) {
+                    throw new SkeletonSyntaxException(
+                        "Invalid digits option for fraction rounder", segment);
+                }
+            } else if (maxSig == -1) {
+                // withMinDigits
+                maxSig = minSig;
+                minSig = 1;
+                priority = RoundingPriority.RELAXED;
+            } else if (minSig == 1) {
+                // withMaxDigits
+                priority = RoundingPriority.STRICT;
+            } else {
+                throw new SkeletonSyntaxException(
+                    "Invalid digits option: Priority annotation required", segment);
             }
 
             FractionPrecision oldRounder = (FractionPrecision) macros.precision;
-            if (maxSig == -1) {
-                macros.precision = oldRounder.withMinDigits(minSig);
-            } else {
-                macros.precision = oldRounder.withMaxDigits(maxSig);
-            }
+            macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
             return true;
         }
 
+        /** @return Whether we successfully found and parsed a trailing zero option. */
+        private static boolean parseTrailingZeroOption(StringSegment segment, MacroProps macros) {
+            if (segment.contentEquals("w")) {
+                macros.precision = macros.precision.trailingZeroDisplay(TrailingZeroDisplay.HIDE_IF_WHOLE);
+                return true;
+            }
+            return false;
+        }
+
         private static void parseIncrementOption(StringSegment segment, MacroProps macros) {
             // Call segment.subSequence() because segment.toString() doesn't create a clean string.
             String str = segment.subSequence(0, segment.length()).toString();
@@ -1540,10 +1578,11 @@
                 Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
                 BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
                 sb.append('/');
-                if (impl.minSig == -1) {
-                    BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
+                BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
+                if (impl.priority == RoundingPriority.RELAXED) {
+                    sb.append('r');
                 } else {
-                    BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb);
+                    sb.append('s');
                 }
             } else if (macros.precision instanceof Precision.IncrementRounderImpl) {
                 Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
@@ -1559,6 +1598,10 @@
                 }
             }
 
+            if (macros.precision.trailingZeroDisplay == TrailingZeroDisplay.HIDE_IF_WHOLE) {
+                sb.append("/w");
+            }
+
             // NOTE: Always return true for rounding because the default value depends on other options.
             return true;
         }
@@ -1588,6 +1631,10 @@
             if (macros.integerWidth.equals(IntegerWidth.DEFAULT)) {
                 return false; // Default
             }
+            if (macros.integerWidth.minInt == 0 && macros.integerWidth.maxInt == 0) {
+                sb.append("integer-width-trunc");
+                return true;
+            }
             sb.append("integer-width/");
             BlueprintHelpers.generateIntegerWidthOption(macros.integerWidth.minInt,
                     macros.integerWidth.maxInt,
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 3f4e906..c892ad8 100644
--- a/android_icu4j/src/main/java/android/icu/number/Precision.java
+++ b/android_icu4j/src/main/java/android/icu/number/Precision.java
@@ -10,6 +10,9 @@
 import android.icu.impl.number.DecimalQuantity;
 import android.icu.impl.number.MultiplierProducer;
 import android.icu.impl.number.RoundingUtils;
+import android.icu.number.NumberFormatter.RoundingPriority;
+import android.icu.number.NumberFormatter.TrailingZeroDisplay;
+import android.icu.text.PluralRules.Operand;
 import android.icu.util.Currency;
 import android.icu.util.Currency.CurrencyUsage;
 
@@ -24,6 +27,7 @@
 public abstract class Precision {
 
     /* package-private final */ MathContext mathContext;
+    /* package-private final */ TrailingZeroDisplay trailingZeroDisplay;
 
     /* package-private */ Precision() {
         mathContext = RoundingUtils.DEFAULT_MATH_CONTEXT_UNLIMITED;
@@ -324,6 +328,19 @@
     }
 
     /**
+     * Configure how trailing zeros are displayed on numbers. For example, to hide trailing zeros
+     * 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();
+        result.trailingZeroDisplay = trailingZeroDisplay;
+        return result;
+    }
+
+    /**
      * Sets a MathContext to use on this Precision.
      *
      * @deprecated This API is ICU internal only.
@@ -370,7 +387,7 @@
     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, 2, -1);
+    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED);
 
     static final IncrementFiveRounderImpl NICKEL = new IncrementFiveRounderImpl(new BigDecimal("0.05"), 2, 2);
 
@@ -406,14 +423,16 @@
         }
     }
 
-    static Precision constructFractionSignificant(FractionPrecision base_, int minSig, int maxSig) {
+    static Precision constructFractionSignificant(
+            FractionPrecision base_, int minSig, int maxSig, RoundingPriority priority) {
         assert base_ instanceof FractionRounderImpl;
         FractionRounderImpl base = (FractionRounderImpl) base_;
         Precision returnValue;
-        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) {
+        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 &&
+                priority == RoundingPriority.RELAXED) {
             returnValue = COMPACT_STRATEGY;
         } else {
-            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig);
+            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority);
         }
         return returnValue.withMode(base.mathContext);
     }
@@ -597,7 +616,7 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToInfinity();
-            value.setMinFraction(0);
+            setResolvedMinFraction(value, 0);
         }
 
         @Override
@@ -620,7 +639,7 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToMagnitude(getRoundingMagnitudeFraction(maxFrac), mathContext);
-            value.setMinFraction(Math.max(0, -getDisplayMagnitudeFraction(minFrac)));
+            setResolvedMinFraction(value, Math.max(0, -getDisplayMagnitudeFraction(minFrac)));
         }
 
         @Override
@@ -643,7 +662,7 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToMagnitude(getRoundingMagnitudeSignificant(value, maxSig), mathContext);
-            value.setMinFraction(Math.max(0, -getDisplayMagnitudeSignificant(value, minSig)));
+            setResolvedMinFraction(value, Math.max(0, -getDisplayMagnitudeSignificant(value, minSig)));
             // Make sure that digits are displayed on zero.
             if (value.isZeroish() && minSig > 0) {
                 value.setMinInteger(1);
@@ -656,7 +675,7 @@
          */
         public void apply(DecimalQuantity quantity, int minInt) {
             assert quantity.isZeroish();
-            quantity.setMinFraction(minSig - minInt);
+            setResolvedMinFraction(quantity, minSig - minInt);
         }
 
         @Override
@@ -672,34 +691,37 @@
         final int maxFrac;
         final int minSig;
         final int maxSig;
+        final RoundingPriority priority;
 
-        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig) {
+        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority) {
             this.minFrac = minFrac;
             this.maxFrac = maxFrac;
             this.minSig = minSig;
             this.maxSig = maxSig;
+            this.priority = priority;
         }
 
         @Override
         public void apply(DecimalQuantity value) {
-            int displayMag = getDisplayMagnitudeFraction(minFrac);
-            int roundingMag = getRoundingMagnitudeFraction(maxFrac);
-            if (minSig == -1) {
-                // Max Sig override
-                int candidate = getRoundingMagnitudeSignificant(value, maxSig);
-                roundingMag = Math.max(roundingMag, candidate);
+            int roundingMag1 = getRoundingMagnitudeFraction(maxFrac);
+            int roundingMag2 = getRoundingMagnitudeSignificant(value, maxSig);
+            int roundingMag;
+            if (priority == RoundingPriority.RELAXED) {
+                roundingMag = Math.min(roundingMag1, roundingMag2);
             } else {
-                // Min Sig override
-                int candidate = getDisplayMagnitudeSignificant(value, minSig);
-                roundingMag = Math.min(roundingMag, candidate);
+                roundingMag = Math.max(roundingMag1, roundingMag2);
             }
             value.roundToMagnitude(roundingMag, mathContext);
-            value.setMinFraction(Math.max(0, -displayMag));
+
+            int displayMag1 = getDisplayMagnitudeFraction(minFrac);
+            int displayMag2 = getDisplayMagnitudeSignificant(value, minSig);
+            int displayMag = Math.min(displayMag1, displayMag2);
+            setResolvedMinFraction(value, Math.max(0, -displayMag));
         }
 
         @Override
         FracSigRounderImpl createCopy() {
-            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig);
+            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority);
             copy.mathContext = mathContext;
             return copy;
         }
@@ -718,7 +740,7 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToIncrement(increment, mathContext);
-            value.setMinFraction(increment.scale());
+            setResolvedMinFraction(value, increment.scale());
         }
 
         @Override
@@ -747,7 +769,7 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToMagnitude(-maxFrac, mathContext);
-            value.setMinFraction(minFrac);
+            setResolvedMinFraction(value, minFrac);
         }
 
         @Override
@@ -774,7 +796,7 @@
         @Override
         public void apply(DecimalQuantity value) {
             value.roundToNickel(-maxFrac, mathContext);
-            value.setMinFraction(minFrac);
+            setResolvedMinFraction(value, minFrac);
         }
 
         @Override
@@ -828,7 +850,17 @@
         return -minFrac;
     }
 
+    void setResolvedMinFraction(DecimalQuantity value, int resolvedMinFraction) {
+        if (trailingZeroDisplay == null ||
+                trailingZeroDisplay == TrailingZeroDisplay.AUTO ||
+                // PLURAL_OPERAND_T returns fraction digits as an integer
+                value.getPluralOperand(Operand.t) != 0) {
+            value.setMinFraction(resolvedMinFraction);
+        }
+    }
+
     private static int getDisplayMagnitudeSignificant(DecimalQuantity value, int minSig) {
+        // Question: Is it useful to look at trailingZeroDisplay here?
         int magnitude = value.isZeroish() ? 0 : value.getMagnitude();
         return magnitude - minSig + 1;
     }
diff --git a/android_icu4j/src/main/java/android/icu/text/Bidi.java b/android_icu4j/src/main/java/android/icu/text/Bidi.java
index a6319a7..e2adb46 100644
--- a/android_icu4j/src/main/java/android/icu/text/Bidi.java
+++ b/android_icu4j/src/main/java/android/icu/text/Bidi.java
@@ -45,7 +45,7 @@
  *
  * This is an implementation of the Unicode Bidirectional Algorithm. The
  * algorithm is defined in the <a
- * href="http://www.unicode.org/unicode/reports/tr9/">Unicode Standard Annex #9</a>.
+ * href="https://www.unicode.org/reports/tr9/">Unicode Standard Annex #9</a>.
  * <p>
  *
  * Note: Libraries that perform a bidirectional algorithm and reorder strings
@@ -4227,7 +4227,7 @@
 
     /**
      * Perform the Unicode Bidi algorithm on a given paragraph, as defined in the
-     * <a href="http://www.unicode.org/unicode/reports/tr9/">Unicode Standard Annex #9</a>,
+     * <a href="https://www.unicode.org/reports/tr9/">Unicode Standard Annex #9</a>,
      * version 13,
      * also described in The Unicode Standard, Version 4.0 .<p>
      *
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 e07b223..ef95239 100644
--- a/android_icu4j/src/main/java/android/icu/text/BidiLine.java
+++ b/android_icu4j/src/main/java/android/icu/text/BidiLine.java
@@ -25,7 +25,7 @@
      * text in a single paragraph or in a line of a single paragraph
      * which has already been processed according to
      * the Unicode 3.0 Bidi algorithm as defined in
-     * http://www.unicode.org/unicode/reports/tr9/ , version 13,
+     * https://www.unicode.org/reports/tr9/ , version 13,
      * also described in The Unicode Standard, Version 4.0.1 .
      *
      * This means that there is a Bidi object with a levels
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 8971dfc..e8dfb42 100644
--- a/android_icu4j/src/main/java/android/icu/text/BreakIteratorFactory.java
+++ b/android_icu4j/src/main/java/android/icu/text/BreakIteratorFactory.java
@@ -130,17 +130,25 @@
         //  Get the binary rules.
         //
         ByteBuffer bytes = null;
-        String typeKeyExt = null;
+        String typeKeyExt = "";
         if (kind == BreakIterator.KIND_LINE) {
-            String lbKeyValue = locale.getKeywordValue("lb");
-            if ( lbKeyValue != null && (lbKeyValue.equals("strict") || lbKeyValue.equals("normal") || lbKeyValue.equals("loose")) ) {
-                typeKeyExt = "_" + lbKeyValue;
+            String keyValue = locale.getKeywordValue("lb");
+            if ( keyValue != null && (keyValue.equals("strict") || keyValue.equals("normal") || keyValue.equals("loose")) ) {
+                typeKeyExt = "_" + keyValue;
+            }
+            String language = locale.getLanguage();
+            if (language != null && language.equals("ja")) {
+                keyValue = locale.getKeywordValue("lw");
+                if (keyValue != null && keyValue.equals("phrase")) {
+                    typeKeyExt += "_" + keyValue;
+                }
             }
         }
 
+        String brkfname;
         try {
-            String         typeKey       = (typeKeyExt == null)? KIND_NAMES[kind]: KIND_NAMES[kind] + typeKeyExt;
-            String         brkfname      = rb.getStringWithFallback("boundaries/" + typeKey);
+            String         typeKey       = typeKeyExt.isEmpty() ? KIND_NAMES[kind] : KIND_NAMES[kind] + typeKeyExt;
+                           brkfname      = rb.getStringWithFallback("boundaries/" + typeKey);
             String         rulesFileName = ICUData.ICU_BRKITR_NAME+ '/' + brkfname;
                            bytes         = ICUBinary.getData(rulesFileName);
         }
@@ -152,7 +160,8 @@
         // Create a normal RuleBasedBreakIterator.
         //
         try {
-            iter = RuleBasedBreakIterator.getInstanceFromCompiledRules(bytes);
+            boolean isPhraseBreaking = (brkfname != null) && brkfname.contains("phrase");
+            iter = RuleBasedBreakIterator.getInstanceFromCompiledRules(bytes, isPhraseBreaking);
         }
         catch (IOException e) {
             // Shouldn't be possible to get here.
diff --git a/android_icu4j/src/main/java/android/icu/text/BreakTransliterator.java b/android_icu4j/src/main/java/android/icu/text/BreakTransliterator.java
index be562ad..627e3a4 100644
--- a/android_icu4j/src/main/java/android/icu/text/BreakTransliterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/BreakTransliterator.java
@@ -93,7 +93,7 @@
 
         for(boundary = bi.first(); boundary != BreakIterator.DONE && boundary < pos.limit; boundary = bi.next()) {
             if (boundary == 0) continue;
-            // HACK: Check to see that preceeding item was a letter
+            // HACK: Check to see that preceding item was a letter
 
             int cp = UTF16.charAt(text, boundary-1);
             int type = UCharacter.getType(cp);
diff --git a/android_icu4j/src/main/java/android/icu/text/CanonicalIterator.java b/android_icu4j/src/main/java/android/icu/text/CanonicalIterator.java
index 3f20563..27d8c54 100644
--- a/android_icu4j/src/main/java/android/icu/text/CanonicalIterator.java
+++ b/android_icu4j/src/main/java/android/icu/text/CanonicalIterator.java
@@ -128,7 +128,7 @@
         int start = 0;
 
         // i should be the end of the first code point
-        // break up the string into segements
+        // break up the string into segments
 
         int i = UTF16.findOffsetFromCodePoint(source, 1);
 
diff --git a/android_icu4j/src/main/java/android/icu/text/CharsetDetector.java b/android_icu4j/src/main/java/android/icu/text/CharsetDetector.java
index e761d35..837c23b 100644
--- a/android_icu4j/src/main/java/android/icu/text/CharsetDetector.java
+++ b/android_icu4j/src/main/java/android/icu/text/CharsetDetector.java
@@ -381,7 +381,7 @@
         }
         
         //
-        // Tally up the byte occurence statistics.
+        // Tally up the byte occurrence statistics.
         //   These are available for use by the various detectors.
         //
         Arrays.fill(fByteStats, (short)0);
@@ -411,7 +411,7 @@
     
     short       fByteStats[] =      // byte frequency statistics for the input text.
                    new short[256];  //   Value is percent, not absolute.
-                                    //   Value is rounded up, so zero really means zero occurences.
+                                    //   Value is rounded up, so zero really means zero occurrences.
     
     boolean     fC1Bytes =          // True if any bytes in the range 0x80 - 0x9F are in the input;
                    false;
diff --git a/android_icu4j/src/main/java/android/icu/text/CharsetMatch.java b/android_icu4j/src/main/java/android/icu/text/CharsetMatch.java
index 6a46072..fe0d83f 100644
--- a/android_icu4j/src/main/java/android/icu/text/CharsetMatch.java
+++ b/android_icu4j/src/main/java/android/icu/text/CharsetMatch.java
@@ -76,7 +76,7 @@
      * the string will be trunctated to this length if necessary.  A limit value of
      * zero or less is ignored, and treated as no limit.
      *
-     * @param maxLength The maximium length of the String to be created when the
+     * @param maxLength The maximum length of the String to be created when the
      *                  source of the data is an input stream, or -1 for
      *                  unlimited length.
      * @return a String created from the converted input data.
diff --git a/android_icu4j/src/main/java/android/icu/text/CharsetRecog_2022.java b/android_icu4j/src/main/java/android/icu/text/CharsetRecog_2022.java
index 98ff2bb..a90464e 100644
--- a/android_icu4j/src/main/java/android/icu/text/CharsetRecog_2022.java
+++ b/android_icu4j/src/main/java/android/icu/text/CharsetRecog_2022.java
@@ -10,7 +10,7 @@
 package android.icu.text;
 
 /**
- *  class CharsetRecog_2022  part of the ICU charset detection imlementation.
+ *  class CharsetRecog_2022  part of the ICU charset detection implementation.
  *                           This is a superclass for the individual detectors for
  *                           each of the detectable members of the ISO 2022 family
  *                           of encodings.
@@ -75,7 +75,7 @@
         }
 
         //
-        // Initial quality is based on relative proportion of recongized vs.
+        // Initial quality is based on relative proportion of recognized vs.
         //   unrecognized escape sequences.
         //   All good:  quality = 100;
         //   half or less good: quality = 0;
diff --git a/android_icu4j/src/main/java/android/icu/text/CharsetRecog_UTF8.java b/android_icu4j/src/main/java/android/icu/text/CharsetRecog_UTF8.java
index bf5e17c..ce9d5a0 100644
--- a/android_icu4j/src/main/java/android/icu/text/CharsetRecog_UTF8.java
+++ b/android_icu4j/src/main/java/android/icu/text/CharsetRecog_UTF8.java
@@ -74,7 +74,7 @@
             }
         }
 
-        // Cook up some sort of confidence score, based on presense of a BOM
+        // Cook up some sort of confidence score, based on presence of a BOM
         //    and the existence of valid and/or invalid multi-byte sequences.
         confidence = 0;
         if (hasBOM && numInvalid==0) {
@@ -91,7 +91,7 @@
             // TODO: add plain ASCII as an explicitly detected type.
             confidence = 15;
         } else if (numValid > numInvalid*10) {
-            // Probably corruput utf-8 data.  Valid sequences aren't likely by chance.
+            // Probably corrupt utf-8 data.  Valid sequences aren't likely by chance.
             confidence = 25;
         }
         return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
diff --git a/android_icu4j/src/main/java/android/icu/text/CharsetRecog_mbcs.java b/android_icu4j/src/main/java/android/icu/text/CharsetRecog_mbcs.java
index b250c2c..bbecc7d 100644
--- a/android_icu4j/src/main/java/android/icu/text/CharsetRecog_mbcs.java
+++ b/android_icu4j/src/main/java/android/icu/text/CharsetRecog_mbcs.java
@@ -13,10 +13,10 @@
 import java.util.Arrays;
 
 /**
- * CharsetRecognizer implemenation for Asian  - double or multi-byte - charsets.
+ * CharsetRecognizer implementation for Asian  - double or multi-byte - charsets.
  *                   Match is determined mostly by the input data adhering to the
  *                   encoding scheme for the charset, and, optionally,
- *                   frequency-of-occurence of characters.
+ *                   frequency-of-occurrence of characters.
  * <p/>
  *                   Instances of this class are singletons, one per encoding
  *                   being recognized.  They are created in the main
@@ -87,7 +87,7 @@
                 if (doubleByteCharCount == 0 && totalCharCount < 10) {
                     // There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes.
                     // We don't have enough data to have any confidence.
-                    // Statistical analysis of single byte non-ASCII charcters would probably help here.
+                    // Statistical analysis of single byte non-ASCII characters would probably help here.
                     confidence = 0;
                 }
                 else {
@@ -109,7 +109,7 @@
             }
 
             if (commonChars == null) {
-                // We have no statistics on frequently occuring characters.
+                // We have no statistics on frequently occurring characters.
                 //  Assess confidence purely on having a reasonable number of
                 //  multi-byte characters (the more the better
                 confidence = 30 + doubleByteCharCount - 20*badCharCount;
@@ -118,7 +118,7 @@
                 }
             }else {
                 //
-                // Frequency of occurence statistics exist.
+                // Frequency of occurrence statistics exist.
                 //
                 double maxVal = Math.log((float)doubleByteCharCount / 4);
                 double scaleFactor = 90.0 / maxVal;
@@ -189,7 +189,7 @@
      static class CharsetRecog_sjis extends CharsetRecog_mbcs {
          static int [] commonChars =
              // TODO:  This set of data comes from the character frequency-
-             //        of-occurence analysis tool.  The data needs to be moved
+             //        of-occurrence analysis tool.  The data needs to be moved
              //        into a resource and loaded from there.
             {0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0,
              0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5,
@@ -251,7 +251,7 @@
      static class CharsetRecog_big5 extends CharsetRecog_mbcs {
          static int [] commonChars =
              // TODO:  This set of data comes from the character frequency-
-             //        of-occurence analysis tool.  The data needs to be moved
+             //        of-occurrence analysis tool.  The data needs to be moved
              //        into a resource and loaded from there.
             {0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446,
              0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3,
@@ -390,7 +390,7 @@
          static class CharsetRecog_euc_jp extends CharsetRecog_euc {
              static int [] commonChars =
                  // TODO:  This set of data comes from the character frequency-
-                 //        of-occurence analysis tool.  The data needs to be moved
+                 //        of-occurrence analysis tool.  The data needs to be moved
                  //        into a resource and loaded from there.
                 {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2,
                  0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3,
@@ -427,7 +427,7 @@
          static class CharsetRecog_euc_kr extends CharsetRecog_euc {
              static int [] commonChars =
                  // TODO:  This set of data comes from the character frequency-
-                 //        of-occurence analysis tool.  The data needs to be moved
+                 //        of-occurrence analysis tool.  The data needs to be moved
                  //        into a resource and loaded from there.
                 {0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc,
                  0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9,
@@ -527,7 +527,7 @@
 
          static int [] commonChars =
              // TODO:  This set of data comes from the character frequency-
-             //        of-occurence analysis tool.  The data needs to be moved
+             //        of-occurrence analysis tool.  The data needs to be moved
              //        into a resource and loaded from there.
             {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac,
              0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4,
diff --git a/android_icu4j/src/main/java/android/icu/text/CollationKey.java b/android_icu4j/src/main/java/android/icu/text/CollationKey.java
index dea2abe..fb69e5b 100644
--- a/android_icu4j/src/main/java/android/icu/text/CollationKey.java
+++ b/android_icu4j/src/main/java/android/icu/text/CollationKey.java
@@ -49,7 +49,7 @@
  *
  * <p>More information about the composition of the bit sequence can
  * be found in the
- * <a href="http://www.icu-project.org/userguide/Collate_ServiceArchitecture.html">
+ * <a href="https://unicode-org.github.io/icu/userguide/collation/architecture">
  * user guide</a>.</p>
  *
  * <p>The following example shows how <code>CollationKey</code>s can be used
@@ -373,7 +373,7 @@
      * also match "Smithsonian" and similar.
      * <p>
      * For more on usage, see example in test procedure
-     * <a href="http://source.icu-project.org/repos/icu/icu4j/trunk/src/com/ibm/icu/dev/test/collator/CollationAPITest.java">
+     * <a href="https://github.com/unicode-org/icu/blob/main/icu4j/main/tests/collate/src/com/ibm/icu/dev/test/collator/CollationAPITest.java">
      * src/com/ibm/icu/dev/test/collator/CollationAPITest/TestBounds.
      * </a>
      * <p>
diff --git a/android_icu4j/src/main/java/android/icu/text/Collator.java b/android_icu4j/src/main/java/android/icu/text/Collator.java
index dda667b..ccfb38d 100644
--- a/android_icu4j/src/main/java/android/icu/text/Collator.java
+++ b/android_icu4j/src/main/java/android/icu/text/Collator.java
@@ -42,7 +42,7 @@
 *
 * <p>Following the <a href=http://www.unicode.org>Unicode
 * Consortium</a>'s specifications for the
-* <a href="http://www.unicode.org/unicode/reports/tr10/">Unicode Collation
+* <a href="https://www.unicode.org/reports/tr10/">Unicode Collation
 * Algorithm (UCA)</a>, there are 5 different levels of strength used
 * in comparisons:
 *
@@ -77,7 +77,7 @@
 *     When all other strengths are equal, the IDENTICAL strength is used as a
 *     tiebreaker. The Unicode code point values of the NFD form of each string
 *     are compared, just in case there is no difference.
-*     For example, Hebrew cantellation marks are only distinguished at this
+*     For example, Hebrew cantillation marks are only distinguished at this
 *     strength. This strength should be used sparingly, as only code point
 *     value differences between two strings is an extremely rare occurrence.
 *     Using this strength substantially decreases the performance for both
@@ -216,7 +216,7 @@
      * will be decomposed for collation.
      *
      * <p>CANONICAL_DECOMPOSITION corresponds to Normalization Form D as
-     * described in <a href="http://www.unicode.org/unicode/reports/tr15/">
+     * described in <a href="https://www.unicode.org/reports/tr15/">
      * Unicode Technical Report #15</a>.
      *
      * @see #NO_DECOMPOSITION
diff --git a/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java b/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java
index 82be12c..fc253cf 100644
--- a/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/CompactDecimalFormat.java
@@ -21,6 +21,12 @@
 /**
  * Formats numbers in compact (abbreviated) notation, like "1.2K" instead of "1200".
  *
+ * <p>
+ * <strong>IMPORTANT:</strong> New users are strongly encouraged to see if
+ * {@link NumberFormatter} fits their use case.  Although not deprecated, this
+ * class, CompactDecimalFormat, is provided for backwards compatibility only.
+ * <hr>
+ *
  * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will
  * limited real estate. For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will
  * be appropriate for the given language, such as "1,2 Mrd." for German.
@@ -67,6 +73,9 @@
   }
 
   /**
+   * <strong>NOTE:</strong> New users are strongly encouraged to use
+   * {@link NumberFormatter} instead of NumberFormat.
+   * <hr>
    * Creates a CompactDecimalFormat appropriate for a locale. The result may be affected by the
    * number system in the locale, such as ar-u-nu-latn.
    *
@@ -78,6 +87,9 @@
   }
 
   /**
+   * <strong>NOTE:</strong> New users are strongly encouraged to use
+   * {@link NumberFormatter} instead of NumberFormat.
+   * <hr>
    * Creates a CompactDecimalFormat appropriate for a locale. The result may be affected by the
    * number system in the locale, such as ar-u-nu-latn.
    *
diff --git a/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java b/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java
index 0f1dff0..0af43d0 100644
--- a/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java
+++ b/android_icu4j/src/main/java/android/icu/text/CurrencyDisplayNames.java
@@ -143,7 +143,6 @@
      *
      * @param isoCode the three-letter ISO code.
      * @return the formal symbol.
-     * @hide draft / provisional / internal are hidden on Android
      */
     public abstract String getFormalSymbol(String isoCode);
 
@@ -158,7 +157,6 @@
      *
      * @param isoCode the three-letter ISO code.
      * @return the variant symbol.
-     * @hide draft / provisional / internal are hidden on Android
      */
     public abstract String getVariantSymbol(String isoCode);
 
diff --git a/android_icu4j/src/main/java/android/icu/text/DateFormat.java b/android_icu4j/src/main/java/android/icu/text/DateFormat.java
index f814af0..ef189e6 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateFormat.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateFormat.java
@@ -493,31 +493,25 @@
 
     /**
      * Hour Cycle
-     * @hide Only a subset of ICU is exposed in Android
-     * @hide draft / provisional / internal are hidden on Android
      */
     public enum HourCycle {
         /**
          * hour in am/pm (0~11)
-         * @hide draft / provisional / internal are hidden on Android
          */
         HOUR_CYCLE_11,
 
         /**
          * hour in am/pm (1~12)
-         * @hide draft / provisional / internal are hidden on Android
          */
         HOUR_CYCLE_12,
 
         /**
          * hour in day (0~23)
-         * @hide draft / provisional / internal are hidden on Android
          */
         HOUR_CYCLE_23,
 
         /**
          * hour in day (1~24)
-         * @hide draft / provisional / internal are hidden on Android
          */
         HOUR_CYCLE_24;
     };
@@ -1798,7 +1792,7 @@
     //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
     //-------------------------------------------------------------------------
-    // Public static interface for creating custon DateFormats for different
+    // Public static interface for creating custom DateFormats for different
     // types of Calendars.
     //-------------------------------------------------------------------------
 
@@ -1938,7 +1932,6 @@
      *
      * @param cal   The calendar system for which a date/time format is desired.
      * @param locale The locale for which the date/time format is desired.
-     * @hide draft / provisional / internal are hidden on Android
      */
     static final public DateFormat getInstance(Calendar cal, ULocale locale) {
         return getDateTimeInstance(cal, SHORT, SHORT, locale);
diff --git a/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java b/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java
index 9f8b781..3f25b39 100644
--- a/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java
+++ b/android_icu4j/src/main/java/android/icu/text/DateFormatSymbols.java
@@ -505,6 +505,13 @@
     String shortQuarters[] = null;
 
     /**
+     * Narrow quarter names. For example: "1", "2", "3", "4". An array
+     * of 4 strings indexed by the month divided by 3.
+     * @serial
+     */
+    String narrowQuarters[] = null;
+
+    /**
      * Full quarter names. For example: "1st Quarter", "2nd Quarter", "3rd Quarter",
      * "4th Quarter". An array of 4 strings, indexed by the month divided by 3.
      * @serial
@@ -519,6 +526,13 @@
     String standaloneShortQuarters[] = null;
 
     /**
+     * Standalone narrow quarter names. For example: "1", "2", "3", "4". An array
+     * of 4 strings indexed by the month divided by 3.
+     * @serial
+     */
+    String standaloneNarrowQuarters[] = null;
+
+    /**
      * Standalone full quarter names. For example: "1st Quarter", "2nd Quarter", "3rd Quarter",
      * "4th Quarter". An array of 4 strings, indexed by the month divided by 3.
      * @serial
@@ -1013,7 +1027,7 @@
      * <strong>[icu]</strong> Returns quarter strings. For example: "1st Quarter", "2nd Quarter", etc.
      * @param context    The quarter context, FORMAT or STANDALONE.
      * @param width      The width or the returned quarter string,
-     *                   either WIDE or ABBREVIATED. There are no NARROW quarters.
+     *                   WIDE, NARROW, or ABBREVIATED.
      * @return the quarter strings.
      */
     public String[] getQuarters(int context, int width) {
@@ -1029,7 +1043,7 @@
                     returnValue = shortQuarters;
                     break;
                  case NARROW :
-                     returnValue = null;
+                     returnValue = narrowQuarters;
                      break;
               }
               break;
@@ -1044,7 +1058,7 @@
                     returnValue = standaloneShortQuarters;
                     break;
                  case NARROW:
-                     returnValue = null;
+                     returnValue = standaloneNarrowQuarters;
                      break;
               }
               break;
@@ -1060,7 +1074,7 @@
      * @param newQuarters the new quarter strings.
      * @param context    The formatting context, FORMAT or STANDALONE.
      * @param width      The width of the quarter string,
-     *                   either WIDE or ABBREVIATED. There are no NARROW quarters.
+     *                   WIDE, NARROW, or ABBREVIATED.
      */
     public void setQuarters(String[] newQuarters, int context, int width) {
         switch (context) {
@@ -1073,7 +1087,7 @@
                     shortQuarters = duplicate(newQuarters);
                     break;
                  case NARROW :
-                    //narrowQuarters = duplicate(newQuarters);
+                    narrowQuarters = duplicate(newQuarters);
                     break;
                  default : // HANDLE SHORT, etc.
                     break;
@@ -1088,7 +1102,7 @@
                     standaloneShortQuarters = duplicate(newQuarters);
                     break;
                  case NARROW :
-                    //standaloneNarrowQuarters = duplicate(newQuarters);
+                    standaloneNarrowQuarters = duplicate(newQuarters);
                     break;
                  default : // HANDLE SHORT, etc.
                     break;
@@ -1472,7 +1486,7 @@
                 && arrayOfArrayEquals(zoneStrings, that.zoneStrings)
                 // getDiplayName maps deprecated country and language codes to the current ones
                 // too bad there is no way to get the current codes!
-                // I thought canolicalize() would map the codes but .. alas! it doesn't.
+                // I thought canonicalize() would map the codes but .. alas! it doesn't.
                 && requestedLocale.getDisplayName().equals(that.requestedLocale.getDisplayName())
                 && Utility.arrayEquals(localPatternChars,
                                        that.localPatternChars));
@@ -1636,8 +1650,10 @@
         this.ampmsNarrow = dfs.ampmsNarrow;
         this.timeSeparator = dfs.timeSeparator;
         this.shortQuarters = dfs.shortQuarters;
+        this.narrowQuarters = dfs.narrowQuarters;
         this.quarters = dfs.quarters;
         this.standaloneShortQuarters = dfs.standaloneShortQuarters;
+        this.standaloneNarrowQuarters = dfs.standaloneNarrowQuarters;
         this.standaloneQuarters = dfs.standaloneQuarters;
         this.leapMonthPatterns = dfs.leapMonthPatterns;
         this.shortYearNames = dfs.shortYearNames;
@@ -2052,9 +2068,11 @@
 
         quarters = arrays.get("quarters/format/wide");
         shortQuarters = arrays.get("quarters/format/abbreviated");
+        narrowQuarters = arrays.get("quarters/format/narrow");
 
         standaloneQuarters = arrays.get("quarters/stand-alone/wide");
         standaloneShortQuarters = arrays.get("quarters/stand-alone/abbreviated");
+        standaloneNarrowQuarters = arrays.get("quarters/stand-alone/narrow");
 
         // BEGIN Android-changed: Load narrow quarters needed for the Q/q symbols in DateTimeFormatter.
         if (aospExtendedDateFormatSymbols != null) {
@@ -2298,20 +2316,6 @@