Merge "Disable from-text stub generation for i18n.module.intra.core.api" into main
diff --git a/Android.bp b/Android.bp
index 8abda73..39811ec 100644
--- a/Android.bp
+++ b/Android.bp
@@ -41,11 +41,11 @@
         "SPDX-license-identifier-ICU",
         "SPDX-license-identifier-ISC",
         "SPDX-license-identifier-MIT",
-        "SPDX-license-identifier-Unicode-DFS",
+        "SPDX-license-identifier-Unicode-3.0",
         "legacy_unencumbered",
     ],
     license_text: [
-        "NOTICE",
+        "LICENSE",
     ],
 }
 
@@ -54,7 +54,7 @@
     visibility: [
         "//external/icu:__subpackages__",
     ],
-    srcs: ["NOTICE"],
+    srcs: ["LICENSE"],
 }
 
 cc_defaults {
@@ -101,6 +101,7 @@
     libs: [
         "fake_device_config",
         "aconfig-annotations-lib-sdk-none",
+        "aconfig_storage_reader_java_none",
         "unsupportedappusage-sdk-none",
     ],
     apex_available: [
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6fdb4aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,717 @@
+UNICODE LICENSE V3
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 2016-2023 Unicode, Inc.
+
+NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
+SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
+TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
+DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of data files and any associated documentation (the "Data Files") or
+software and any associated documentation (the "Software") to deal in the
+Data Files or Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, and/or sell
+copies of the Data Files or Software, and to permit persons to whom the
+Data Files or Software are furnished to do so, provided that either (a)
+this copyright and permission notice appear with all copies of the Data
+Files or Software, or (b) this copyright and permission notice appear in
+associated Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+THIRD PARTY RIGHTS.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
+BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
+FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall
+not be used in advertising or otherwise to promote the sale, use or other
+dealings in these Data Files or Software without prior written
+authorization of the copyright holder.
+
+----------------------------------------------------------------------
+
+Third-Party Software Licenses
+
+This section contains third-party software notices and/or additional
+terms for licensed third-party software components included within ICU
+libraries.
+
+----------------------------------------------------------------------
+
+ICU License - ICU 1.8.1 to ICU 57.1
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1995-2016 International Business Machines Corporation and others
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, provided that the above
+copyright notice(s) and this permission notice appear in all copies of
+the Software and that both the above copyright notice(s) and this
+permission notice appear in supporting documentation.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
+SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
+RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale, use
+or other dealings in this Software without prior written authorization
+of the copyright holder.
+
+All trademarks and registered trademarks mentioned herein are the
+property of their respective owners.
+
+----------------------------------------------------------------------
+
+Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
+
+ #     The Google Chrome software developed by Google is licensed under
+ # the BSD license. Other software included in this distribution is
+ # provided under other licenses, as set forth below.
+ #
+ #  The BSD License
+ #  http://opensource.org/licenses/bsd-license.php
+ #  Copyright (C) 2006-2008, Google Inc.
+ #
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ # modification, are permitted provided that the following conditions are met:
+ #
+ #  Redistributions of source code must retain the above copyright notice,
+ # this list of conditions and the following disclaimer.
+ #  Redistributions in binary form must reproduce the above
+ # copyright notice, this list of conditions and the following
+ # disclaimer in the documentation and/or other materials provided with
+ # the distribution.
+ #  Neither the name of  Google Inc. nor the names of its
+ # contributors may be used to endorse or promote products derived from
+ # this software without specific prior written permission.
+ #
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #
+ #
+ #  The word list in cjdict.txt are generated by combining three word lists
+ # listed below with further processing for compound word breaking. The
+ # frequency is generated with an iterative training against Google web
+ # corpora.
+ #
+ #  * Libtabe (Chinese)
+ #    - https://sourceforge.net/project/?group_id=1519
+ #    - Its license terms and conditions are shown below.
+ #
+ #  * IPADIC (Japanese)
+ #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html
+ #    - Its license terms and conditions are shown below.
+ #
+ #  ---------COPYING.libtabe ---- BEGIN--------------------
+ #
+ #  /*
+ #   * Copyright (c) 1999 TaBE Project.
+ #   * Copyright (c) 1999 Pai-Hsiang Hsiao.
+ #   * All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the TaBE Project nor the names of its
+ #   *   contributors may be used to endorse or promote products derived
+ #   *   from this software without specific prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  /*
+ #   * Copyright (c) 1999 Computer Systems and Communication Lab,
+ #   *                    Institute of Information Science, Academia
+ #       *                    Sinica. All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the Computer Systems and Communication Lab
+ #   *   nor the names of its contributors may be used to endorse or
+ #   *   promote products derived from this software without specific
+ #   *   prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
+ #      University of Illinois
+ #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4
+ #
+ #  ---------------COPYING.libtabe-----END--------------------------------
+ #
+ #
+ #  ---------------COPYING.ipadic-----BEGIN-------------------------------
+ #
+ #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
+ #  and Technology.  All Rights Reserved.
+ #
+ #  Use, reproduction, and distribution of this software is permitted.
+ #  Any copy of this software, whether in its original form or modified,
+ #  must include both the above copyright notice and the following
+ #  paragraphs.
+ #
+ #  Nara Institute of Science and Technology (NAIST),
+ #  the copyright holders, disclaims all warranties with regard to this
+ #  software, including all implied warranties of merchantability and
+ #  fitness, in no event shall NAIST be liable for
+ #  any special, indirect or consequential damages or any damages
+ #  whatsoever resulting from loss of use, data or profits, whether in an
+ #  action of contract, negligence or other tortuous action, arising out
+ #  of or in connection with the use or performance of this software.
+ #
+ #  A large portion of the dictionary entries
+ #  originate from ICOT Free Software.  The following conditions for ICOT
+ #  Free Software applies to the current dictionary as well.
+ #
+ #  Each User may also freely distribute the Program, whether in its
+ #  original form or modified, to any third party or parties, PROVIDED
+ #  that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
+ #  on, or be attached to, the Program, which is distributed substantially
+ #  in the same form as set out herein and that such intended
+ #  distribution, if actually made, will neither violate or otherwise
+ #  contravene any of the laws and regulations of the countries having
+ #  jurisdiction over the User or the intended distribution itself.
+ #
+ #  NO WARRANTY
+ #
+ #  The program was produced on an experimental basis in the course of the
+ #  research and development conducted during the project and is provided
+ #  to users as so produced on an experimental basis.  Accordingly, the
+ #  program is provided without any warranty whatsoever, whether express,
+ #  implied, statutory or otherwise.  The term "warranty" used herein
+ #  includes, but is not limited to, any warranty of the quality,
+ #  performance, merchantability and fitness for a particular purpose of
+ #  the program and the nonexistence of any infringement or violation of
+ #  any right of any third party.
+ #
+ #  Each user of the program will agree and understand, and be deemed to
+ #  have agreed and understood, that there is no warranty whatsoever for
+ #  the program and, accordingly, the entire risk arising from or
+ #  otherwise connected with the program is assumed by the user.
+ #
+ #  Therefore, neither ICOT, the copyright holder, or any other
+ #  organization that participated in or was otherwise related to the
+ #  development of the program and their respective officials, directors,
+ #  officers and other employees shall be held liable for any and all
+ #  damages, including, without limitation, general, special, incidental
+ #  and consequential damages, arising out of or otherwise in connection
+ #  with the use or inability to use the program or any product, material
+ #  or result produced or otherwise obtained by using the program,
+ #  regardless of whether they have been advised of, or otherwise had
+ #  knowledge of, the possibility of such damages at any time during the
+ #  project or thereafter.  Each user will be deemed to have agreed to the
+ #  foregoing by his or her commencement of use of the program.  The term
+ #  "use" as used herein includes, but is not limited to, the use,
+ #  modification, copying and distribution of the program and the
+ #  production of secondary products from the program.
+ #
+ #  In the case where the program, whether in its original form or
+ #  modified, was distributed or delivered to or received by a user from
+ #  any person, organization or entity other than ICOT, unless it makes or
+ #  grants independently of ICOT any specific warranty to the user in
+ #  writing, such person, organization or entity, will also be exempted
+ #  from and not be held liable to the user for any such damages as noted
+ #  above as far as the program is concerned.
+ #
+ #  ---------------COPYING.ipadic-----END----------------------------------
+
+----------------------------------------------------------------------
+
+Lao Word Break Dictionary Data (laodict.txt)
+
+ # Copyright (C) 2016 and later: Unicode, Inc. and others.
+ # License & terms of use: http://www.unicode.org/copyright.html
+ # Copyright (c) 2015 International Business Machines Corporation
+ # and others. All Rights Reserved.
+ #
+ # Project: https://github.com/rober42539/lao-dictionary
+ # Dictionary: https://github.com/rober42539/lao-dictionary/laodict.txt
+ # License: https://github.com/rober42539/lao-dictionary/LICENSE.txt
+ #          (copied below)
+ #
+ #	This file is derived from the above dictionary version of Nov 22, 2020
+ #  ----------------------------------------------------------------------
+ #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification, are permitted provided that the following conditions are met:
+ #
+ #  Redistributions of source code must retain the above copyright notice, this
+ #  list of conditions and the following disclaimer. Redistributions in binary
+ #  form must reproduce the above copyright notice, this list of conditions and
+ #  the following disclaimer in the documentation and/or other materials
+ #  provided with the distribution.
+ #
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+----------------------------------------------------------------------
+
+Burmese Word Break Dictionary Data (burmesedict.txt)
+
+ #  Copyright (c) 2014 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ #  This list is part of a project hosted at:
+ #    github.com/kanyawtech/myanmar-karen-word-lists
+ #
+ #  --------------------------------------------------------------------------
+ #  Copyright (c) 2013, LeRoy Benjamin Sharon
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification, are permitted provided that the following conditions
+ #  are met: Redistributions of source code must retain the above
+ #  copyright notice, this list of conditions and the following
+ #  disclaimer.  Redistributions in binary form must reproduce the
+ #  above copyright notice, this list of conditions and the following
+ #  disclaimer in the documentation and/or other materials provided
+ #  with the distribution.
+ #
+ #    Neither the name Myanmar Karen Word Lists, nor the names of its
+ #    contributors may be used to endorse or promote products derived
+ #    from this software without specific prior written permission.
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ #  SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+----------------------------------------------------------------------
+
+Time Zone Database
+
+  ICU uses the public domain data and code derived from Time Zone
+Database for its time zone support. The ownership of the TZ database
+is explained in BCP 175: Procedure for Maintaining the Time Zone
+Database section 7.
+
+ # 7.  Database Ownership
+ #
+ #    The TZ database itself is not an IETF Contribution or an IETF
+ #    document.  Rather it is a pre-existing and regularly updated work
+ #    that is in the public domain, and is intended to remain in the
+ #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
+ #    not apply to the TZ Database or contributions that individuals make
+ #    to it.  Should any claims be made and substantiated against the TZ
+ #    Database, the organization that is providing the IANA
+ #    Considerations defined in this RFC, under the memorandum of
+ #    understanding with the IETF, currently ICANN, may act in accordance
+ #    with all competent court orders.  No ownership claims will be made
+ #    by ICANN or the IETF Trust on the database or the code.  Any person
+ #    making a contribution to the database or code waives all rights to
+ #    future claims in that contribution or in the TZ Database.
+
+----------------------------------------------------------------------
+
+Google double-conversion
+
+Copyright 2006-2011, the V8 project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----------------------------------------------------------------------
+
+File: aclocal.m4 (only for ICU4C)
+Section: pkg.m4 - Macros to locate and utilise pkg-config.
+
+
+Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
+Copyright © 2012-2015 Dan Nicholson <dbn.lists@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+02111-1307, USA.
+
+As a special exception to the GNU General Public License, if you
+distribute this file as part of a program that contains a
+configuration script generated by Autoconf, you may include it under
+the same distribution terms that you use for the rest of that
+program.
+
+
+(The condition for the exception is fulfilled because
+ICU4C includes a configuration script generated by Autoconf,
+namely the `configure` script.)
+
+----------------------------------------------------------------------
+
+File: config.guess (only for ICU4C)
+
+
+This file is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses/>.
+
+As a special exception to the GNU General Public License, if you
+distribute this file as part of a program that contains a
+configuration script generated by Autoconf, you may include it under
+the same distribution terms that you use for the rest of that
+program.  This Exception is an additional permission under section 7
+of the GNU General Public License, version 3 ("GPLv3").
+
+
+(The condition for the exception is fulfilled because
+ICU4C includes a configuration script generated by Autoconf,
+namely the `configure` script.)
+
+----------------------------------------------------------------------
+
+File: install-sh (only for ICU4C)
+
+
+Copyright 1991 by the Massachusetts Institute of Technology
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation, and that the name of M.I.T. not be used in advertising or
+publicity pertaining to distribution of the software without specific,
+written prior permission.  M.I.T. makes no representations about the
+suitability of this software for any purpose.  It is provided "as is"
+without express or implied warranty.
+
+----------------------------------------------------------------------
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2024 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index 25b6eb9..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,414 +0,0 @@
-COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later)
-
-Copyright © 1991-2018 Unicode, Inc. All rights reserved.
-Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Unicode data files and any associated documentation
-(the "Data Files") or Unicode software and any associated documentation
-(the "Software") to deal in the Data Files or Software
-without restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, and/or sell copies of
-the Data Files or Software, and to permit persons to whom the Data Files
-or Software are furnished to do so, provided that either
-(a) this copyright and permission notice appear with all copies
-of the Data Files or Software, or
-(b) this copyright and permission notice appear in associated
-Documentation.
-
-THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
-ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT OF THIRD PARTY RIGHTS.
-IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
-NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
-DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
-DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
-TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THE DATA FILES OR SOFTWARE.
-
-Except as contained in this notice, the name of a copyright holder
-shall not be used in advertising or otherwise to promote the sale,
-use or other dealings in these Data Files or Software without prior
-written authorization of the copyright holder.
-
----------------------
-
-Third-Party Software Licenses
-
-This section contains third-party software notices and/or additional
-terms for licensed third-party software components included within ICU
-libraries.
-
-1. ICU License - ICU 1.8.1 to ICU 57.1
-
-COPYRIGHT AND PERMISSION NOTICE
-
-Copyright (c) 1995-2016 International Business Machines Corporation and others
-All rights reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, and/or sell copies of the Software, and to permit persons
-to whom the Software is furnished to do so, provided that the above
-copyright notice(s) and this permission notice appear in all copies of
-the Software and that both the above copyright notice(s) and this
-permission notice appear in supporting documentation.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
-HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
-SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
-RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
-CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-Except as contained in this notice, the name of a copyright holder
-shall not be used in advertising or otherwise to promote the sale, use
-or other dealings in this Software without prior written authorization
-of the copyright holder.
-
-All trademarks and registered trademarks mentioned herein are the
-property of their respective owners.
-
-2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
-
- #     The Google Chrome software developed by Google is licensed under
- # the BSD license. Other software included in this distribution is
- # provided under other licenses, as set forth below.
- #
- #  The BSD License
- #  http://opensource.org/licenses/bsd-license.php
- #  Copyright (C) 2006-2008, Google Inc.
- #
- #  All rights reserved.
- #
- #  Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are met:
- #
- #  Redistributions of source code must retain the above copyright notice,
- # this list of conditions and the following disclaimer.
- #  Redistributions in binary form must reproduce the above
- # copyright notice, this list of conditions and the following
- # disclaimer in the documentation and/or other materials provided with
- # the distribution.
- #  Neither the name of  Google Inc. nor the names of its
- # contributors may be used to endorse or promote products derived from
- # this software without specific prior written permission.
- #
- #
- #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
- # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- #
- #
- #  The word list in cjdict.txt are generated by combining three word lists
- # listed below with further processing for compound word breaking. The
- # frequency is generated with an iterative training against Google web
- # corpora.
- #
- #  * Libtabe (Chinese)
- #    - https://sourceforge.net/project/?group_id=1519
- #    - Its license terms and conditions are shown below.
- #
- #  * IPADIC (Japanese)
- #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html
- #    - Its license terms and conditions are shown below.
- #
- #  ---------COPYING.libtabe ---- BEGIN--------------------
- #
- #  /*
- #   * Copyright (c) 1999 TaBE Project.
- #   * Copyright (c) 1999 Pai-Hsiang Hsiao.
- #   * All rights reserved.
- #   *
- #   * Redistribution and use in source and binary forms, with or without
- #   * modification, are permitted provided that the following conditions
- #   * are met:
- #   *
- #   * . Redistributions of source code must retain the above copyright
- #   *   notice, this list of conditions and the following disclaimer.
- #   * . Redistributions in binary form must reproduce the above copyright
- #   *   notice, this list of conditions and the following disclaimer in
- #   *   the documentation and/or other materials provided with the
- #   *   distribution.
- #   * . Neither the name of the TaBE Project nor the names of its
- #   *   contributors may be used to endorse or promote products derived
- #   *   from this software without specific prior written permission.
- #   *
- #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
- #   * OF THE POSSIBILITY OF SUCH DAMAGE.
- #   */
- #
- #  /*
- #   * Copyright (c) 1999 Computer Systems and Communication Lab,
- #   *                    Institute of Information Science, Academia
- #       *                    Sinica. All rights reserved.
- #   *
- #   * Redistribution and use in source and binary forms, with or without
- #   * modification, are permitted provided that the following conditions
- #   * are met:
- #   *
- #   * . Redistributions of source code must retain the above copyright
- #   *   notice, this list of conditions and the following disclaimer.
- #   * . Redistributions in binary form must reproduce the above copyright
- #   *   notice, this list of conditions and the following disclaimer in
- #   *   the documentation and/or other materials provided with the
- #   *   distribution.
- #   * . Neither the name of the Computer Systems and Communication Lab
- #   *   nor the names of its contributors may be used to endorse or
- #   *   promote products derived from this software without specific
- #   *   prior written permission.
- #   *
- #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
- #   * OF THE POSSIBILITY OF SUCH DAMAGE.
- #   */
- #
- #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
- #      University of Illinois
- #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4
- #
- #  ---------------COPYING.libtabe-----END--------------------------------
- #
- #
- #  ---------------COPYING.ipadic-----BEGIN-------------------------------
- #
- #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
- #  and Technology.  All Rights Reserved.
- #
- #  Use, reproduction, and distribution of this software is permitted.
- #  Any copy of this software, whether in its original form or modified,
- #  must include both the above copyright notice and the following
- #  paragraphs.
- #
- #  Nara Institute of Science and Technology (NAIST),
- #  the copyright holders, disclaims all warranties with regard to this
- #  software, including all implied warranties of merchantability and
- #  fitness, in no event shall NAIST be liable for
- #  any special, indirect or consequential damages or any damages
- #  whatsoever resulting from loss of use, data or profits, whether in an
- #  action of contract, negligence or other tortuous action, arising out
- #  of or in connection with the use or performance of this software.
- #
- #  A large portion of the dictionary entries
- #  originate from ICOT Free Software.  The following conditions for ICOT
- #  Free Software applies to the current dictionary as well.
- #
- #  Each User may also freely distribute the Program, whether in its
- #  original form or modified, to any third party or parties, PROVIDED
- #  that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
- #  on, or be attached to, the Program, which is distributed substantially
- #  in the same form as set out herein and that such intended
- #  distribution, if actually made, will neither violate or otherwise
- #  contravene any of the laws and regulations of the countries having
- #  jurisdiction over the User or the intended distribution itself.
- #
- #  NO WARRANTY
- #
- #  The program was produced on an experimental basis in the course of the
- #  research and development conducted during the project and is provided
- #  to users as so produced on an experimental basis.  Accordingly, the
- #  program is provided without any warranty whatsoever, whether express,
- #  implied, statutory or otherwise.  The term "warranty" used herein
- #  includes, but is not limited to, any warranty of the quality,
- #  performance, merchantability and fitness for a particular purpose of
- #  the program and the nonexistence of any infringement or violation of
- #  any right of any third party.
- #
- #  Each user of the program will agree and understand, and be deemed to
- #  have agreed and understood, that there is no warranty whatsoever for
- #  the program and, accordingly, the entire risk arising from or
- #  otherwise connected with the program is assumed by the user.
- #
- #  Therefore, neither ICOT, the copyright holder, or any other
- #  organization that participated in or was otherwise related to the
- #  development of the program and their respective officials, directors,
- #  officers and other employees shall be held liable for any and all
- #  damages, including, without limitation, general, special, incidental
- #  and consequential damages, arising out of or otherwise in connection
- #  with the use or inability to use the program or any product, material
- #  or result produced or otherwise obtained by using the program,
- #  regardless of whether they have been advised of, or otherwise had
- #  knowledge of, the possibility of such damages at any time during the
- #  project or thereafter.  Each user will be deemed to have agreed to the
- #  foregoing by his or her commencement of use of the program.  The term
- #  "use" as used herein includes, but is not limited to, the use,
- #  modification, copying and distribution of the program and the
- #  production of secondary products from the program.
- #
- #  In the case where the program, whether in its original form or
- #  modified, was distributed or delivered to or received by a user from
- #  any person, organization or entity other than ICOT, unless it makes or
- #  grants independently of ICOT any specific warranty to the user in
- #  writing, such person, organization or entity, will also be exempted
- #  from and not be held liable to the user for any such damages as noted
- #  above as far as the program is concerned.
- #
- #  ---------------COPYING.ipadic-----END----------------------------------
-
-3. Lao Word Break Dictionary Data (laodict.txt)
-
- #  Copyright (c) 2013 International Business Machines Corporation
- #  and others. All Rights Reserved.
- #
- # Project: http://code.google.com/p/lao-dictionary/
- # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt
- # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt
- #              (copied below)
- #
- #  This file is derived from the above dictionary, with slight
- #  modifications.
- #  ----------------------------------------------------------------------
- #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
- #  All rights reserved.
- #
- #  Redistribution and use in source and binary forms, with or without
- #  modification,
- #  are permitted provided that the following conditions are met:
- #
- #
- # Redistributions of source code must retain the above copyright notice, this
- #  list of conditions and the following disclaimer. Redistributions in
- #  binary form must reproduce the above copyright notice, this list of
- #  conditions and the following disclaimer in the documentation and/or
- #  other materials provided with the distribution.
- #
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
- # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
- # OF THE POSSIBILITY OF SUCH DAMAGE.
- #  --------------------------------------------------------------------------
-
-4. Burmese Word Break Dictionary Data (burmesedict.txt)
-
- #  Copyright (c) 2014 International Business Machines Corporation
- #  and others. All Rights Reserved.
- #
- #  This list is part of a project hosted at:
- #    github.com/kanyawtech/myanmar-karen-word-lists
- #
- #  --------------------------------------------------------------------------
- #  Copyright (c) 2013, LeRoy Benjamin Sharon
- #  All rights reserved.
- #
- #  Redistribution and use in source and binary forms, with or without
- #  modification, are permitted provided that the following conditions
- #  are met: Redistributions of source code must retain the above
- #  copyright notice, this list of conditions and the following
- #  disclaimer.  Redistributions in binary form must reproduce the
- #  above copyright notice, this list of conditions and the following
- #  disclaimer in the documentation and/or other materials provided
- #  with the distribution.
- #
- #    Neither the name Myanmar Karen Word Lists, nor the names of its
- #    contributors may be used to endorse or promote products derived
- #    from this software without specific prior written permission.
- #
- #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
- #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
- #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- #  SUCH DAMAGE.
- #  --------------------------------------------------------------------------
-
-5. Time Zone Database
-
-  ICU uses the public domain data and code derived from Time Zone
-Database for its time zone support. The ownership of the TZ database
-is explained in BCP 175: Procedure for Maintaining the Time Zone
-Database section 7.
-
- # 7.  Database Ownership
- #
- #    The TZ database itself is not an IETF Contribution or an IETF
- #    document.  Rather it is a pre-existing and regularly updated work
- #    that is in the public domain, and is intended to remain in the
- #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
- #    not apply to the TZ Database or contributions that individuals make
- #    to it.  Should any claims be made and substantiated against the TZ
- #    Database, the organization that is providing the IANA
- #    Considerations defined in this RFC, under the memorandum of
- #    understanding with the IETF, currently ICANN, may act in accordance
- #    with all competent court orders.  No ownership claims will be made
- #    by ICANN or the IETF Trust on the database or the code.  Any person
- #    making a contribution to the database or code waives all rights to
- #    future claims in that contribution or in the TZ Database.
-
-6. Google double-conversion
-
-Copyright 2006-2011, the V8 project authors. All rights reserved.
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-    * Redistributions of source code must retain the above copyright
-      notice, this list of conditions and the following disclaimer.
-    * Redistributions in binary form must reproduce the above
-      copyright notice, this list of conditions and the following
-      disclaimer in the documentation and/or other materials provided
-      with the distribution.
-    * Neither the name of Google Inc. nor the names of its
-      contributors may be used to endorse or promote products derived
-      from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/OWNERS b/OWNERS
index 2a49be3..ec19bb4 100644
--- a/OWNERS
+++ b/OWNERS
@@ -10,3 +10,5 @@
 # g2.corp.android-icu-maintainers@google.com
 # android-libcore-team+review@google.com
 
+per-file *ravenwood* = file:platform/frameworks/base:/ravenwood/OWNERS
+per-file *Ravenwood* = file:platform/frameworks/base:/ravenwood/OWNERS
diff --git a/README-ravenwood.md b/README-ravenwood.md
new file mode 100644
index 0000000..bfc6b62
--- /dev/null
+++ b/README-ravenwood.md
@@ -0,0 +1,31 @@
+# ICU on Ravenwood
+
+# What APIs are enabled
+As of 2024-06-19, Ravenwood uses the host side JVM, not ART, so it doesn't use `libcore` either.
+
+To support ICU on Ravenwood, we include the following jar files in the
+Ravenwood classpath.
+- `core-icu4j-for-host.ravenwood`
+- `icu4j-icudata-jarjar`
+- `icu4j-icutzdata-jarjar`
+
+`core-icu4j-for-host.ravenwood` is made from `core-icu4j-for-host.ravenwood`
+with `hoststubgen` to make the following modifications.
+- Enable `android.icu` APIs on Ravenwood.
+- But all other APIs -- i.e. all `libcore_bridge` APIS -- will throw at runtime.
+
+This "policy" is defined in android_icu4j/icu-ravenwood-policies.txt.
+
+As a result, on Ravenwood, all `android.icu` APIs will work, but none of the `libcore_bridge` APIs.
+
+# CTS
+
+ICU's CTS is `CtsIcuTestCases`, which contains the tests under
+android_icu4j/src/main/tests/, which are the tests from the upstream ICU, and
+android_icu4j/testing/, which are android specific tests, which depends
+on `libcore_bridge`.
+
+On Ravenwood, android_icu4j/src/main/tests/ will pass, but not android_icu4j/testing/.
+
+So we have `CtsIcuTestCasesRavenwood-core-only`, which only contains the
+tests from the upstream. You can run this with `atest CtsIcuTestCasesRavenwood-core-only`.
diff --git a/README.version b/README.version
index 3939e1b..1832b36 100644
--- a/README.version
+++ b/README.version
@@ -1,3 +1,3 @@
 URL: https://github.com/unicode-org/icu
-Version: 72.1
+Version: 75.1
 BugComponent: 23970
diff --git a/TEST_MAPPING b/TEST_MAPPING
index de82f9f..30e6eb8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -6,5 +6,10 @@
     {
       "name": "minikin_tests"
     }
+  ],
+  "postsubmit": [
+    {
+      "name": "CtsIcuTestCasesRavenwood"
+    }
   ]
 }
diff --git a/android_icu4j/Android.bp b/android_icu4j/Android.bp
index 88ecd4c..5818b50 100644
--- a/android_icu4j/Android.bp
+++ b/android_icu4j/Android.bp
@@ -30,6 +30,10 @@
     default_applicable_licenses: ["external_icu_license"],
 }
 
+build = [
+    "Ravenwood.bp",
+]
+
 //==========================================================
 // build repackaged ICU for target
 //
@@ -128,6 +132,7 @@
     },
     lint: {
         warning_checks: ["SuspiciousIndentation"],
+        baseline_filename: "lint-baseline.xml",
     },
 }
 
@@ -162,7 +167,7 @@
     dxflags: ["--core-library"],
 }
 
-// Java library for use on host, e.g. by robolectric.
+// Java library for use on host, e.g. by ravenwood, robolectric.
 java_library {
     name: "core-icu4j-for-host",
     visibility: [
@@ -170,6 +175,7 @@
         "//external/robolectric",
         "//external/robolectric-shadows",
         "//frameworks/layoutlib",
+        "//frameworks/base/ravenwood:__subpackages__",
         "//packages/modules/RuntimeI18n/apex",
     ],
     static_libs: [
@@ -301,7 +307,7 @@
 
 // Referenced implicitly from i18n.module.intra.core.api.
 filegroup {
-    name: "i18n.module.intra.core.api.api.public.latest",
+    name: "i18n.module.intra.core.api.api.combined.public.latest",
     srcs: [
         "api/intra/last-api.txt",
     ],
@@ -309,7 +315,7 @@
 
 // Referenced implicitly from i18n.module.intra.core.api.
 filegroup {
-    name: "i18n.module.intra.core.api-removed.api.public.latest",
+    name: "i18n.module.intra.core.api-removed.api.combined.public.latest",
     srcs: [
         "api/intra/last-removed.txt",
     ],
@@ -325,7 +331,7 @@
 
 // Referenced implicitly from legacy.i18n.module.platform.api.
 filegroup {
-    name: "legacy.i18n.module.platform.api.api.public.latest",
+    name: "legacy.i18n.module.platform.api.api.combined.public.latest",
     srcs: [
         "api/legacy_platform/last-api.txt",
     ],
@@ -333,7 +339,7 @@
 
 // Referenced implicitly from legacy.i18n.module.platform.api.
 filegroup {
-    name: "legacy.i18n.module.platform.api-removed.api.public.latest",
+    name: "legacy.i18n.module.platform.api-removed.api.combined.public.latest",
     srcs: [
         "api/legacy_platform/last-removed.txt",
     ],
@@ -349,7 +355,7 @@
 
 // Referenced implicitly from stable.i18n.module.platform.api.
 filegroup {
-    name: "stable.i18n.module.platform.api.api.public.latest",
+    name: "stable.i18n.module.platform.api.api.combined.public.latest",
     srcs: [
         "api/stable_platform/last-api.txt",
     ],
@@ -357,7 +363,7 @@
 
 // Referenced implicitly from stable.i18n.module.platform.api.
 filegroup {
-    name: "stable.i18n.module.platform.api-removed.api.public.latest",
+    name: "stable.i18n.module.platform.api-removed.api.combined.public.latest",
     srcs: [
         "api/stable_platform/last-removed.txt",
     ],
@@ -456,16 +462,11 @@
 // repackaged android.icu classes and methods and not just the ones available
 // through the Android API.
 //==========================================================
-java_test {
-    name: "android-icu4j-tests",
+java_defaults {
+    name: "android-icu4j-tests-default",
     visibility: [
         "//cts/tests/tests/icu",
     ],
-
-    srcs: [
-        "src/main/tests/**/*.java",
-        "testing/src/**/*.java",
-    ],
     java_resource_dirs: [
         "src/main/tests",
         "testing/src",
@@ -474,6 +475,7 @@
         "core-icu4j",
     ],
     static_libs: [
+        "gson",
         "junit",
         "junit-params",
         "tzdata-testing",
@@ -496,3 +498,39 @@
         ],
     },
 }
+
+java_test {
+    name: "android-icu4j-tests",
+    defaults: ["android-icu4j-tests-default"],
+    visibility: [
+        "//cts/tests/tests/icu",
+    ],
+    srcs: [
+        "src/main/tests/**/*.java",
+        "testing/src/**/*.java",
+    ],
+}
+
+// Equivalent to android-icu4j-tests, excluding the tests under testing/.
+// We run this as ICU CTS on Ravenwood, where the testing/ tests won't pass due to lack of
+// libcore.
+java_test {
+    name: "android-icu4j-tests-core-only",
+    defaults: ["android-icu4j-tests-default"],
+    visibility: [
+        "//cts/tests/tests/icu",
+    ],
+    srcs: [
+        "src/main/tests/**/*.java",
+        "testing/src/android/icu/testsharding/**/*.java",
+    ],
+    // TODO(b/340889954) Un-excluide the excluded tests.
+    exclude_srcs: [
+        // This class has a "known-failure", which we can't exclude on Ravenwood without
+        // modifying this file, so let's just exclude the whole class for now.
+        "src/main/tests/android/icu/dev/test/format/NumberFormatRegressionTest.java",
+
+        // This test takes too much time and hits the timeout.
+        "src/main/tests/android/icu/dev/test/rbbi/RBBIMonkeyTest.java",
+    ],
+}
diff --git a/android_icu4j/OWNERS b/android_icu4j/OWNERS
new file mode 100644
index 0000000..cb5dcc2
--- /dev/null
+++ b/android_icu4j/OWNERS
@@ -0,0 +1,2 @@
+per-file *ravenwood* = file:platform/frameworks/base:/ravenwood/OWNERS
+per-file *Ravenwood* = file:platform/frameworks/base:/ravenwood/OWNERS
diff --git a/android_icu4j/Ravenwood.bp b/android_icu4j/Ravenwood.bp
new file mode 100644
index 0000000..2839392
--- /dev/null
+++ b/android_icu4j/Ravenwood.bp
@@ -0,0 +1,69 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// For ravenwood.
+// TODO(b/340889954) Enable --supported-api-list-file, once AOSP gets this feature.
+java_genrule {
+    name: "core-icu4j-for-host.ravenwood-base",
+    tools: ["hoststubgen"],
+    cmd: "$(location hoststubgen) " +
+        "@$(location :ravenwood-standard-options) " +
+
+        "--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
+        "--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
+        // "--supported-api-list-file $(location hoststubgen_core-icu4j-for-host_apis.csv) " +
+
+        "--out-impl-jar $(location ravenwood.jar) " +
+
+        "--gen-keep-all-file $(location hoststubgen_core-icu4j-for-host_keep_all.txt) " +
+        "--gen-input-dump-file $(location hoststubgen_core-icu4j-for-host_dump.txt) " +
+
+        "--in-jar $(location :core-icu4j-for-host) " +
+        "--policy-override-file $(location icu-ravenwood-policies.txt) " +
+        "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+    srcs: [
+        ":core-icu4j-for-host",
+
+        "icu-ravenwood-policies.txt",
+        ":ravenwood-standard-options",
+        ":ravenwood-annotation-allowed-classes",
+    ],
+    out: [
+        "ravenwood.jar",
+
+        // Following files are created just as FYI.
+        "hoststubgen_core-icu4j-for-host_keep_all.txt",
+        "hoststubgen_core-icu4j-for-host_dump.txt",
+
+        "hoststubgen_core-icu4j-for-host.log",
+        "hoststubgen_core-icu4j-for-host_stats.csv",
+        // "hoststubgen_core-icu4j-for-host_apis.csv",
+    ],
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+}
+
+// Extract the impl jar from "core-icu4j-for-host.ravenwood-base" for subsequent build rules.
+// Note this emits a "device side" output, so that ravenwood tests can (implicitly)
+// depend on it.
+java_genrule {
+    name: "core-icu4j-for-host.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":core-icu4j-for-host.ravenwood-base{ravenwood.jar}",
+    ],
+    out: [
+        "core-icu4j-for-host.ravenwood.jar",
+    ],
+}
diff --git a/android_icu4j/api/public/current.txt b/android_icu4j/api/public/current.txt
index ca5a60c..6125d38 100644
--- a/android_icu4j/api/public/current.txt
+++ b/android_icu4j/api/public/current.txt
@@ -4034,7 +4034,7 @@
     method public String toPattern(boolean);
     field public static final int ADD_CASE_MAPPINGS = 4; // 0x4
     field public static final android.icu.text.UnicodeSet ALL_CODE_POINTS;
-    field public static final int CASE = 2; // 0x2
+    field @Deprecated public static final int CASE = 2; // 0x2
     field public static final int CASE_INSENSITIVE = 2; // 0x2
     field public static final android.icu.text.UnicodeSet EMPTY;
     field public static final int IGNORE_SPACE = 1; // 0x1
@@ -4236,7 +4236,6 @@
     field public static final int AM_PM = 9; // 0x9
     field public static final int APRIL = 3; // 0x3
     field public static final int AUGUST = 7; // 0x7
-    field @Deprecated protected static final int BASE_FIELD_COUNT = 23; // 0x17
     field public static final int DATE = 5; // 0x5
     field public static final int DAY_OF_MONTH = 5; // 0x5
     field public static final int DAY_OF_WEEK = 7; // 0x7
diff --git a/android_icu4j/api/public/removed.txt b/android_icu4j/api/public/removed.txt
index e73b663..1da85f6 100644
--- a/android_icu4j/api/public/removed.txt
+++ b/android_icu4j/api/public/removed.txt
@@ -1,6 +1,10 @@
 // Signature format: 2.0
 package android.icu.util {
 
+  public abstract class Calendar implements java.lang.Cloneable java.lang.Comparable<android.icu.util.Calendar> java.io.Serializable {
+    field @Deprecated protected static final int BASE_FIELD_COUNT;
+  }
+
   public class JapaneseCalendar extends android.icu.util.GregorianCalendar {
     field @Deprecated public static final int CURRENT_ERA;
   }
diff --git a/android_icu4j/icu-ravenwood-policies.txt b/android_icu4j/icu-ravenwood-policies.txt
new file mode 100644
index 0000000..208b856
--- /dev/null
+++ b/android_icu4j/icu-ravenwood-policies.txt
@@ -0,0 +1,614 @@
+# Ravenwood policy file to expose APIs under android.icu, (which is under src/main/java)
+# We do not expose APIs under com.android, which is under libcore_bridge.
+
+# This file is generated with the following:
+# $ jar tvf $ANDROID_BUILD_TOP/out/host/linux-x86/testcases/ravenwood-runtime/core-icu4j-for-host.ravenwood.jar | sed -ne 's!^.* !! ; \!\$!d ; y!/!.!; s!\.class$!!p' | grep '^android\.icu' | sed -e 's!^!class ! ; s!$! keepclass!'
+
+# On goog/master, or once AOSP gets ravenwood-stats-collector.sh, we can use the following command
+# instead.
+# $ $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/ravenwood-stats-collector.sh
+# $ sed -ne '\!\$!d ; s/ keep$/ keepclass/ ; /^class android\.icu/p' /tmp/ravenwood/ravenwood-keep-all/hoststubgen_core-icu4j-for-host_keep_all.txt
+
+# TODO(b/353573764): Switch to `package` once it's supported on aosp/main.
+
+class android.icu.impl.Assert keepclass
+class android.icu.impl.BMPSet keepclass
+class android.icu.impl.CSCharacterIterator keepclass
+class android.icu.impl.CacheBase keepclass
+class android.icu.impl.CacheValue keepclass
+class android.icu.impl.CalType keepclass
+class android.icu.impl.CalendarAstronomer keepclass
+class android.icu.impl.CalendarCache keepclass
+class android.icu.impl.CalendarUtil keepclass
+class android.icu.impl.CaseMapImpl keepclass
+class android.icu.impl.CharTrie keepclass
+class android.icu.impl.CharacterIteration keepclass
+class android.icu.impl.CharacterIteratorWrapper keepclass
+class android.icu.impl.CharacterPropertiesImpl keepclass
+class android.icu.impl.ClassLoaderUtil keepclass
+class android.icu.impl.CollectionSet keepclass
+class android.icu.impl.CurrencyData keepclass
+class android.icu.impl.DateNumberFormat keepclass
+class android.icu.impl.DayPeriodRules keepclass
+class android.icu.impl.DontCareFieldPosition keepclass
+class android.icu.impl.EmojiProps keepclass
+class android.icu.impl.EraRules keepclass
+class android.icu.impl.FormattedStringBuilder keepclass
+class android.icu.impl.FormattedValueFieldPositionIteratorImpl keepclass
+class android.icu.impl.FormattedValueStringBuilderImpl keepclass
+class android.icu.impl.Grego keepclass
+class android.icu.impl.ICUBinary keepclass
+class android.icu.impl.ICUCache keepclass
+class android.icu.impl.ICUConfig keepclass
+class android.icu.impl.ICUCurrencyDisplayInfoProvider keepclass
+class android.icu.impl.ICUCurrencyMetaInfo keepclass
+class android.icu.impl.ICUData keepclass
+class android.icu.impl.ICUDataVersion keepclass
+class android.icu.impl.ICUDebug keepclass
+class android.icu.impl.ICULangDataTables keepclass
+class android.icu.impl.ICULocaleService keepclass
+class android.icu.impl.ICUNotifier keepclass
+class android.icu.impl.ICURWLock keepclass
+class android.icu.impl.ICURegionDataTables keepclass
+class android.icu.impl.ICUResourceBundle keepclass
+class android.icu.impl.ICUResourceBundleImpl keepclass
+class android.icu.impl.ICUResourceBundleReader keepclass
+class android.icu.impl.ICUResourceTableAccess keepclass
+class android.icu.impl.ICUService keepclass
+class android.icu.impl.IDNA2003 keepclass
+class android.icu.impl.IllegalIcuArgumentException keepclass
+class android.icu.impl.IntTrie keepclass
+class android.icu.impl.IntTrieBuilder keepclass
+class android.icu.impl.InvalidFormatException keepclass
+class android.icu.impl.IterableComparator keepclass
+class android.icu.impl.JavaTimeZone keepclass
+class android.icu.impl.LocaleDisplayNamesImpl keepclass
+class android.icu.impl.LocaleFallbackData keepclass
+class android.icu.impl.LocaleIDParser keepclass
+class android.icu.impl.LocaleIDs keepclass
+class android.icu.impl.LocaleUtility keepclass
+class android.icu.impl.Norm2AllModes keepclass
+class android.icu.impl.Normalizer2Impl keepclass
+class android.icu.impl.OlsonTimeZone keepclass
+class android.icu.impl.PVecToTrieCompactHandler keepclass
+class android.icu.impl.Pair keepclass
+class android.icu.impl.PatternProps keepclass
+class android.icu.impl.PatternTokenizer keepclass
+class android.icu.impl.PluralRulesLoader keepclass
+class android.icu.impl.PropsVectors keepclass
+class android.icu.impl.Punycode keepclass
+class android.icu.impl.RBBIDataWrapper keepclass
+class android.icu.impl.Relation keepclass
+class android.icu.impl.RelativeDateFormat keepclass
+class android.icu.impl.ReplaceableUCharacterIterator keepclass
+class android.icu.impl.ResourceBundleWrapper keepclass
+class android.icu.impl.Row keepclass
+class android.icu.impl.RuleCharacterIterator keepclass
+class android.icu.impl.SimpleCache keepclass
+class android.icu.impl.SimpleFilteredSentenceBreakIterator keepclass
+class android.icu.impl.SimpleFormatterImpl keepclass
+class android.icu.impl.SoftCache keepclass
+class android.icu.impl.SortedSetRelation keepclass
+class android.icu.impl.StandardPlural keepclass
+class android.icu.impl.StaticUnicodeSets keepclass
+class android.icu.impl.StringPrepDataReader keepclass
+class android.icu.impl.StringRange keepclass
+class android.icu.impl.StringSegment keepclass
+class android.icu.impl.TZDBTimeZoneNames keepclass
+class android.icu.impl.TextTrieMap keepclass
+class android.icu.impl.TimeZoneAdapter keepclass
+class android.icu.impl.TimeZoneGenericNames keepclass
+class android.icu.impl.TimeZoneNamesFactoryImpl keepclass
+class android.icu.impl.TimeZoneNamesImpl keepclass
+class android.icu.impl.Trie keepclass
+class android.icu.impl.Trie2 keepclass
+class android.icu.impl.Trie2Writable keepclass
+class android.icu.impl.Trie2_16 keepclass
+class android.icu.impl.Trie2_32 keepclass
+class android.icu.impl.TrieBuilder keepclass
+class android.icu.impl.TrieIterator keepclass
+class android.icu.impl.UBiDiProps keepclass
+class android.icu.impl.UCaseProps keepclass
+class android.icu.impl.UCharArrayIterator keepclass
+class android.icu.impl.UCharacterIteratorWrapper keepclass
+class android.icu.impl.UCharacterName keepclass
+class android.icu.impl.UCharacterNameChoice keepclass
+class android.icu.impl.UCharacterNameReader keepclass
+class android.icu.impl.UCharacterProperty keepclass
+class android.icu.impl.UCharacterUtility keepclass
+class android.icu.impl.UPropertyAliases keepclass
+class android.icu.impl.URLHandler keepclass
+class android.icu.impl.UResource keepclass
+class android.icu.impl.USerializedSet keepclass
+class android.icu.impl.UTS46 keepclass
+class android.icu.impl.UnicodeRegex keepclass
+class android.icu.impl.UnicodeSetStringSpan keepclass
+class android.icu.impl.Utility keepclass
+class android.icu.impl.UtilityExtensions keepclass
+class android.icu.impl.ValidIdentifiers keepclass
+class android.icu.impl.ZoneMeta keepclass
+class android.icu.impl.breakiter.BurmeseBreakEngine keepclass
+class android.icu.impl.breakiter.BytesDictionaryMatcher keepclass
+class android.icu.impl.breakiter.CharsDictionaryMatcher keepclass
+class android.icu.impl.breakiter.CjkBreakEngine keepclass
+class android.icu.impl.breakiter.DictionaryBreakEngine keepclass
+class android.icu.impl.breakiter.DictionaryData keepclass
+class android.icu.impl.breakiter.DictionaryMatcher keepclass
+class android.icu.impl.breakiter.KhmerBreakEngine keepclass
+class android.icu.impl.breakiter.LSTMBreakEngine keepclass
+class android.icu.impl.breakiter.LanguageBreakEngine keepclass
+class android.icu.impl.breakiter.LaoBreakEngine keepclass
+class android.icu.impl.breakiter.MlBreakEngine keepclass
+class android.icu.impl.breakiter.ModelIndex keepclass
+class android.icu.impl.breakiter.ThaiBreakEngine keepclass
+class android.icu.impl.breakiter.UnhandledBreakEngine keepclass
+class android.icu.impl.coll.BOCSU keepclass
+class android.icu.impl.coll.Collation keepclass
+class android.icu.impl.coll.CollationBuilder keepclass
+class android.icu.impl.coll.CollationCompare keepclass
+class android.icu.impl.coll.CollationData keepclass
+class android.icu.impl.coll.CollationDataBuilder keepclass
+class android.icu.impl.coll.CollationDataReader keepclass
+class android.icu.impl.coll.CollationFCD keepclass
+class android.icu.impl.coll.CollationFastLatin keepclass
+class android.icu.impl.coll.CollationFastLatinBuilder keepclass
+class android.icu.impl.coll.CollationIterator keepclass
+class android.icu.impl.coll.CollationKeys keepclass
+class android.icu.impl.coll.CollationLoader keepclass
+class android.icu.impl.coll.CollationRoot keepclass
+class android.icu.impl.coll.CollationRootElements keepclass
+class android.icu.impl.coll.CollationRuleParser keepclass
+class android.icu.impl.coll.CollationSettings keepclass
+class android.icu.impl.coll.CollationTailoring keepclass
+class android.icu.impl.coll.CollationWeights keepclass
+class android.icu.impl.coll.ContractionsAndExpansions keepclass
+class android.icu.impl.coll.FCDIterCollationIterator keepclass
+class android.icu.impl.coll.FCDUTF16CollationIterator keepclass
+class android.icu.impl.coll.IterCollationIterator keepclass
+class android.icu.impl.coll.SharedObject keepclass
+class android.icu.impl.coll.TailoredSet keepclass
+class android.icu.impl.coll.UTF16CollationIterator keepclass
+class android.icu.impl.coll.UVector32 keepclass
+class android.icu.impl.coll.UVector64 keepclass
+class android.icu.impl.data.HolidayBundle keepclass
+class android.icu.impl.data.HolidayBundle_da keepclass
+class android.icu.impl.data.HolidayBundle_da_DK keepclass
+class android.icu.impl.data.HolidayBundle_de keepclass
+class android.icu.impl.data.HolidayBundle_de_AT keepclass
+class android.icu.impl.data.HolidayBundle_de_DE keepclass
+class android.icu.impl.data.HolidayBundle_el keepclass
+class android.icu.impl.data.HolidayBundle_el_GR keepclass
+class android.icu.impl.data.HolidayBundle_en keepclass
+class android.icu.impl.data.HolidayBundle_en_CA keepclass
+class android.icu.impl.data.HolidayBundle_en_GB keepclass
+class android.icu.impl.data.HolidayBundle_en_US keepclass
+class android.icu.impl.data.HolidayBundle_es keepclass
+class android.icu.impl.data.HolidayBundle_es_MX keepclass
+class android.icu.impl.data.HolidayBundle_fr keepclass
+class android.icu.impl.data.HolidayBundle_fr_CA keepclass
+class android.icu.impl.data.HolidayBundle_fr_FR keepclass
+class android.icu.impl.data.HolidayBundle_it keepclass
+class android.icu.impl.data.HolidayBundle_it_IT keepclass
+class android.icu.impl.data.HolidayBundle_iw keepclass
+class android.icu.impl.data.HolidayBundle_iw_IL keepclass
+class android.icu.impl.data.HolidayBundle_ja_JP keepclass
+class android.icu.impl.data.ResourceReader keepclass
+class android.icu.impl.data.TokenIterator keepclass
+class android.icu.impl.duration.BasicDurationFormat keepclass
+class android.icu.impl.duration.BasicDurationFormatter keepclass
+class android.icu.impl.duration.BasicDurationFormatterFactory keepclass
+class android.icu.impl.duration.BasicPeriodBuilderFactory keepclass
+class android.icu.impl.duration.BasicPeriodFormatter keepclass
+class android.icu.impl.duration.BasicPeriodFormatterFactory keepclass
+class android.icu.impl.duration.BasicPeriodFormatterService keepclass
+class android.icu.impl.duration.DateFormatter keepclass
+class android.icu.impl.duration.DurationFormatter keepclass
+class android.icu.impl.duration.DurationFormatterFactory keepclass
+class android.icu.impl.duration.FixedUnitBuilder keepclass
+class android.icu.impl.duration.MultiUnitBuilder keepclass
+class android.icu.impl.duration.OneOrTwoUnitBuilder keepclass
+class android.icu.impl.duration.Period keepclass
+class android.icu.impl.duration.PeriodBuilder keepclass
+class android.icu.impl.duration.PeriodBuilderFactory keepclass
+class android.icu.impl.duration.PeriodBuilderImpl keepclass
+class android.icu.impl.duration.PeriodFormatter keepclass
+class android.icu.impl.duration.PeriodFormatterFactory keepclass
+class android.icu.impl.duration.PeriodFormatterService keepclass
+class android.icu.impl.duration.SingleUnitBuilder keepclass
+class android.icu.impl.duration.TimeUnit keepclass
+class android.icu.impl.duration.TimeUnitConstants keepclass
+class android.icu.impl.duration.impl.DataRecord keepclass
+class android.icu.impl.duration.impl.PeriodFormatterData keepclass
+class android.icu.impl.duration.impl.PeriodFormatterDataService keepclass
+class android.icu.impl.duration.impl.RecordReader keepclass
+class android.icu.impl.duration.impl.RecordWriter keepclass
+class android.icu.impl.duration.impl.ResourceBasedPeriodFormatterDataService keepclass
+class android.icu.impl.duration.impl.Utils keepclass
+class android.icu.impl.duration.impl.XMLRecordReader keepclass
+class android.icu.impl.duration.impl.XMLRecordWriter keepclass
+class android.icu.impl.locale.AsciiUtil keepclass
+class android.icu.impl.locale.BaseLocale keepclass
+class android.icu.impl.locale.Extension keepclass
+class android.icu.impl.locale.InternalLocaleBuilder keepclass
+class android.icu.impl.locale.KeyTypeData keepclass
+class android.icu.impl.locale.LSR keepclass
+class android.icu.impl.locale.LanguageTag keepclass
+class android.icu.impl.locale.LikelySubtags keepclass
+class android.icu.impl.locale.LocaleDistance keepclass
+class android.icu.impl.locale.LocaleExtensions keepclass
+class android.icu.impl.locale.LocaleObjectCache keepclass
+class android.icu.impl.locale.LocaleSyntaxException keepclass
+class android.icu.impl.locale.LocaleValidityChecker keepclass
+class android.icu.impl.locale.ParseStatus keepclass
+class android.icu.impl.locale.StringTokenIterator keepclass
+class android.icu.impl.locale.UnicodeLocaleExtension keepclass
+class android.icu.impl.locale.XCldrStub keepclass
+class android.icu.impl.number.AdoptingModifierStore keepclass
+class android.icu.impl.number.AffixPatternProvider keepclass
+class android.icu.impl.number.AffixUtils keepclass
+class android.icu.impl.number.CompactData keepclass
+class android.icu.impl.number.ConstantAffixModifier keepclass
+class android.icu.impl.number.ConstantMultiFieldModifier keepclass
+class android.icu.impl.number.CurrencyPluralInfoAffixProvider keepclass
+class android.icu.impl.number.CurrencySpacingEnabledModifier keepclass
+class android.icu.impl.number.CustomSymbolCurrency keepclass
+class android.icu.impl.number.DecimalFormatProperties keepclass
+class android.icu.impl.number.DecimalQuantity keepclass
+class android.icu.impl.number.DecimalQuantity_AbstractBCD keepclass
+class android.icu.impl.number.DecimalQuantity_DualStorageBCD keepclass
+class android.icu.impl.number.Grouper keepclass
+class android.icu.impl.number.LocalizedNumberFormatterAsFormat keepclass
+class android.icu.impl.number.LongNameHandler keepclass
+class android.icu.impl.number.LongNameMultiplexer keepclass
+class android.icu.impl.number.MacroProps keepclass
+class android.icu.impl.number.MicroProps keepclass
+class android.icu.impl.number.MicroPropsGenerator keepclass
+class android.icu.impl.number.MicroPropsMutator keepclass
+class android.icu.impl.number.MixedUnitLongNameHandler keepclass
+class android.icu.impl.number.Modifier keepclass
+class android.icu.impl.number.ModifierStore keepclass
+class android.icu.impl.number.MultiplierFormatHandler keepclass
+class android.icu.impl.number.MultiplierProducer keepclass
+class android.icu.impl.number.MutablePatternModifier keepclass
+class android.icu.impl.number.Padder keepclass
+class android.icu.impl.number.PatternStringParser keepclass
+class android.icu.impl.number.PatternStringUtils keepclass
+class android.icu.impl.number.Properties keepclass
+class android.icu.impl.number.PropertiesAffixPatternProvider keepclass
+class android.icu.impl.number.RoundingUtils keepclass
+class android.icu.impl.number.SimpleModifier keepclass
+class android.icu.impl.number.UnitConversionHandler keepclass
+class android.icu.impl.number.UsagePrefsHandler keepclass
+class android.icu.impl.number.parse.AffixMatcher keepclass
+class android.icu.impl.number.parse.AffixPatternMatcher keepclass
+class android.icu.impl.number.parse.AffixTokenMatcherFactory keepclass
+class android.icu.impl.number.parse.CodePointMatcher keepclass
+class android.icu.impl.number.parse.CombinedCurrencyMatcher keepclass
+class android.icu.impl.number.parse.DecimalMatcher keepclass
+class android.icu.impl.number.parse.IgnorablesMatcher keepclass
+class android.icu.impl.number.parse.InfinityMatcher keepclass
+class android.icu.impl.number.parse.MinusSignMatcher keepclass
+class android.icu.impl.number.parse.MultiplierParseHandler keepclass
+class android.icu.impl.number.parse.NanMatcher keepclass
+class android.icu.impl.number.parse.NumberParseMatcher keepclass
+class android.icu.impl.number.parse.NumberParserImpl keepclass
+class android.icu.impl.number.parse.PaddingMatcher keepclass
+class android.icu.impl.number.parse.ParsedNumber keepclass
+class android.icu.impl.number.parse.ParsingUtils keepclass
+class android.icu.impl.number.parse.PercentMatcher keepclass
+class android.icu.impl.number.parse.PermilleMatcher keepclass
+class android.icu.impl.number.parse.PlusSignMatcher keepclass
+class android.icu.impl.number.parse.RequireAffixValidator keepclass
+class android.icu.impl.number.parse.RequireCurrencyValidator keepclass
+class android.icu.impl.number.parse.RequireDecimalSeparatorValidator keepclass
+class android.icu.impl.number.parse.RequireNumberValidator keepclass
+class android.icu.impl.number.parse.ScientificMatcher keepclass
+class android.icu.impl.number.parse.SeriesMatcher keepclass
+class android.icu.impl.number.parse.SymbolMatcher keepclass
+class android.icu.impl.number.parse.ValidationMatcher keepclass
+class android.icu.impl.number.range.PrefixInfixSuffixLengthHelper keepclass
+class android.icu.impl.number.range.RangeMacroProps keepclass
+class android.icu.impl.number.range.StandardPluralRanges keepclass
+class android.icu.impl.personname.FieldModifierImpl keepclass
+class android.icu.impl.personname.PersonNameFormatterImpl keepclass
+class android.icu.impl.personname.PersonNamePattern keepclass
+class android.icu.impl.text.RbnfScannerProviderImpl keepclass
+class android.icu.impl.units.ComplexUnitsConverter keepclass
+class android.icu.impl.units.ConversionRates keepclass
+class android.icu.impl.units.MeasureUnitImpl keepclass
+class android.icu.impl.units.SingleUnitImpl keepclass
+class android.icu.impl.units.UnitPreferences keepclass
+class android.icu.impl.units.UnitsConverter keepclass
+class android.icu.impl.units.UnitsData keepclass
+class android.icu.impl.units.UnitsRouter keepclass
+class android.icu.lang.CharSequences keepclass
+class android.icu.lang.CharacterProperties keepclass
+class android.icu.lang.UCharacter keepclass
+class android.icu.lang.UCharacterCategory keepclass
+class android.icu.lang.UCharacterDirection keepclass
+class android.icu.lang.UCharacterEnums keepclass
+class android.icu.lang.UCharacterNameIterator keepclass
+class android.icu.lang.UProperty keepclass
+class android.icu.lang.UScript keepclass
+class android.icu.lang.UScriptRun keepclass
+class android.icu.math.BigDecimal keepclass
+class android.icu.math.MathContext keepclass
+class android.icu.message2.DateTimeFormatterFactory keepclass
+class android.icu.message2.FormattedMessage keepclass
+class android.icu.message2.FormattedPlaceholder keepclass
+class android.icu.message2.Formatter keepclass
+class android.icu.message2.FormatterFactory keepclass
+class android.icu.message2.IdentityFormatterFactory keepclass
+class android.icu.message2.InputSource keepclass
+class android.icu.message2.MFDataModel keepclass
+class android.icu.message2.MFDataModelFormatter keepclass
+class android.icu.message2.MFDataModelValidator keepclass
+class android.icu.message2.MFFunctionRegistry keepclass
+class android.icu.message2.MFParseException keepclass
+class android.icu.message2.MFParser keepclass
+class android.icu.message2.MFSerializer keepclass
+class android.icu.message2.MessageFormatter keepclass
+class android.icu.message2.NumberFormatterFactory keepclass
+class android.icu.message2.OptUtils keepclass
+class android.icu.message2.PlainStringFormattedValue keepclass
+class android.icu.message2.Selector keepclass
+class android.icu.message2.SelectorFactory keepclass
+class android.icu.message2.StringUtils keepclass
+class android.icu.message2.StringView keepclass
+class android.icu.message2.TextSelectorFactory keepclass
+class android.icu.number.CompactNotation keepclass
+class android.icu.number.CurrencyPrecision keepclass
+class android.icu.number.FormattedNumber keepclass
+class android.icu.number.FormattedNumberRange keepclass
+class android.icu.number.FractionPrecision keepclass
+class android.icu.number.IntegerWidth keepclass
+class android.icu.number.LocalizedNumberFormatter keepclass
+class android.icu.number.LocalizedNumberRangeFormatter keepclass
+class android.icu.number.Notation keepclass
+class android.icu.number.NumberFormatter keepclass
+class android.icu.number.NumberFormatterImpl keepclass
+class android.icu.number.NumberFormatterSettings keepclass
+class android.icu.number.NumberPropertyMapper keepclass
+class android.icu.number.NumberRangeFormatter keepclass
+class android.icu.number.NumberRangeFormatterImpl keepclass
+class android.icu.number.NumberRangeFormatterSettings keepclass
+class android.icu.number.NumberSkeletonImpl keepclass
+class android.icu.number.Precision keepclass
+class android.icu.number.Scale keepclass
+class android.icu.number.ScientificNotation keepclass
+class android.icu.number.SimpleNotation keepclass
+class android.icu.number.SkeletonSyntaxException keepclass
+class android.icu.number.UnlocalizedNumberFormatter keepclass
+class android.icu.number.UnlocalizedNumberRangeFormatter keepclass
+class android.icu.platform.AndroidDataFiles keepclass
+class android.icu.text.AbsoluteValueSubstitution keepclass
+class android.icu.text.AlphabeticIndex keepclass
+class android.icu.text.AnyTransliterator keepclass
+class android.icu.text.ArabicShaping keepclass
+class android.icu.text.ArabicShapingException keepclass
+class android.icu.text.Bidi keepclass
+class android.icu.text.BidiClassifier keepclass
+class android.icu.text.BidiLine keepclass
+class android.icu.text.BidiRun keepclass
+class android.icu.text.BidiTransform keepclass
+class android.icu.text.BidiWriter keepclass
+class android.icu.text.BreakIterator keepclass
+class android.icu.text.BreakIteratorFactory keepclass
+class android.icu.text.BreakTransliterator keepclass
+class android.icu.text.CanonicalIterator keepclass
+class android.icu.text.CaseFoldTransliterator keepclass
+class android.icu.text.CaseMap keepclass
+class android.icu.text.CharsetDetector keepclass
+class android.icu.text.CharsetMatch keepclass
+class android.icu.text.CharsetRecog_2022 keepclass
+class android.icu.text.CharsetRecog_UTF8 keepclass
+class android.icu.text.CharsetRecog_Unicode keepclass
+class android.icu.text.CharsetRecog_mbcs keepclass
+class android.icu.text.CharsetRecog_sbcs keepclass
+class android.icu.text.CharsetRecognizer keepclass
+class android.icu.text.ChineseDateFormat keepclass
+class android.icu.text.ChineseDateFormatSymbols keepclass
+class android.icu.text.CollationElementIterator keepclass
+class android.icu.text.CollationKey keepclass
+class android.icu.text.Collator keepclass
+class android.icu.text.CollatorServiceShim keepclass
+class android.icu.text.CompactDecimalFormat keepclass
+class android.icu.text.ComposedCharIter keepclass
+class android.icu.text.CompoundTransliterator keepclass
+class android.icu.text.ConstrainedFieldPosition keepclass
+class android.icu.text.CurrencyDisplayNames keepclass
+class android.icu.text.CurrencyFormat keepclass
+class android.icu.text.CurrencyMetaInfo keepclass
+class android.icu.text.CurrencyPluralInfo keepclass
+class android.icu.text.DateFormat keepclass
+class android.icu.text.DateFormatSymbols keepclass
+class android.icu.text.DateIntervalFormat keepclass
+class android.icu.text.DateIntervalInfo keepclass
+class android.icu.text.DateTimePatternGenerator keepclass
+class android.icu.text.DecimalFormat keepclass
+class android.icu.text.DecimalFormatSymbols keepclass
+class android.icu.text.DisplayContext keepclass
+class android.icu.text.DisplayOptions keepclass
+class android.icu.text.DurationFormat keepclass
+class android.icu.text.Edits keepclass
+class android.icu.text.EscapeTransliterator keepclass
+class android.icu.text.FilteredBreakIteratorBuilder keepclass
+class android.icu.text.FilteredNormalizer2 keepclass
+class android.icu.text.FormattedValue keepclass
+class android.icu.text.FractionalPartSubstitution keepclass
+class android.icu.text.FunctionReplacer keepclass
+class android.icu.text.IDNA keepclass
+class android.icu.text.IntegralPartSubstitution keepclass
+class android.icu.text.ListFormatter keepclass
+class android.icu.text.LocaleDisplayNames keepclass
+class android.icu.text.LowercaseTransliterator keepclass
+class android.icu.text.MeasureFormat keepclass
+class android.icu.text.MessageFormat keepclass
+class android.icu.text.MessagePattern keepclass
+class android.icu.text.MessagePatternUtil keepclass
+class android.icu.text.ModulusSubstitution keepclass
+class android.icu.text.MultiplierSubstitution keepclass
+class android.icu.text.NFRule keepclass
+class android.icu.text.NFRuleSet keepclass
+class android.icu.text.NFSubstitution keepclass
+class android.icu.text.NameUnicodeTransliterator keepclass
+class android.icu.text.NormalizationTransliterator keepclass
+class android.icu.text.Normalizer keepclass
+class android.icu.text.Normalizer2 keepclass
+class android.icu.text.NullTransliterator keepclass
+class android.icu.text.NumberFormat keepclass
+class android.icu.text.NumberFormatServiceShim keepclass
+class android.icu.text.NumberingSystem keepclass
+class android.icu.text.NumeratorSubstitution keepclass
+class android.icu.text.PersonName keepclass
+class android.icu.text.PersonNameFormatter keepclass
+class android.icu.text.PluralFormat keepclass
+class android.icu.text.PluralRules keepclass
+class android.icu.text.PluralRulesSerialProxy keepclass
+class android.icu.text.Quantifier keepclass
+class android.icu.text.QuantityFormatter keepclass
+class android.icu.text.RBBINode keepclass
+class android.icu.text.RBBIRuleBuilder keepclass
+class android.icu.text.RBBIRuleParseTable keepclass
+class android.icu.text.RBBIRuleScanner keepclass
+class android.icu.text.RBBISetBuilder keepclass
+class android.icu.text.RBBISymbolTable keepclass
+class android.icu.text.RBBITableBuilder keepclass
+class android.icu.text.RBNFChinesePostProcessor keepclass
+class android.icu.text.RBNFPostProcessor keepclass
+class android.icu.text.RawCollationKey keepclass
+class android.icu.text.RbnfLenientScanner keepclass
+class android.icu.text.RbnfLenientScannerProvider keepclass
+class android.icu.text.RelativeDateTimeFormatter keepclass
+class android.icu.text.RemoveTransliterator keepclass
+class android.icu.text.Replaceable keepclass
+class android.icu.text.ReplaceableContextIterator keepclass
+class android.icu.text.ReplaceableString keepclass
+class android.icu.text.RuleBasedBreakIterator keepclass
+class android.icu.text.RuleBasedCollator keepclass
+class android.icu.text.RuleBasedNumberFormat keepclass
+class android.icu.text.RuleBasedTransliterator keepclass
+class android.icu.text.SCSU keepclass
+class android.icu.text.SameValueSubstitution keepclass
+class android.icu.text.ScientificNumberFormatter keepclass
+class android.icu.text.SearchIterator keepclass
+class android.icu.text.SelectFormat keepclass
+class android.icu.text.SimpleDateFormat keepclass
+class android.icu.text.SimpleFormatter keepclass
+class android.icu.text.SimplePersonName keepclass
+class android.icu.text.SourceTargetUtility keepclass
+class android.icu.text.SpoofChecker keepclass
+class android.icu.text.StringCharacterIterator keepclass
+class android.icu.text.StringMatcher keepclass
+class android.icu.text.StringPrep keepclass
+class android.icu.text.StringPrepParseException keepclass
+class android.icu.text.StringReplacer keepclass
+class android.icu.text.StringSearch keepclass
+class android.icu.text.StringTransform keepclass
+class android.icu.text.SymbolTable keepclass
+class android.icu.text.TimeUnitFormat keepclass
+class android.icu.text.TimeZoneFormat keepclass
+class android.icu.text.TimeZoneNames keepclass
+class android.icu.text.TitlecaseTransliterator keepclass
+class android.icu.text.Transform keepclass
+class android.icu.text.TransliterationRule keepclass
+class android.icu.text.TransliterationRuleSet keepclass
+class android.icu.text.Transliterator keepclass
+class android.icu.text.TransliteratorIDParser keepclass
+class android.icu.text.TransliteratorParser keepclass
+class android.icu.text.TransliteratorRegistry keepclass
+class android.icu.text.UCharacterIterator keepclass
+class android.icu.text.UFieldPosition keepclass
+class android.icu.text.UFormat keepclass
+class android.icu.text.UForwardCharacterIterator keepclass
+class android.icu.text.UTF16 keepclass
+class android.icu.text.UnescapeTransliterator keepclass
+class android.icu.text.UnicodeCompressor keepclass
+class android.icu.text.UnicodeDecompressor keepclass
+class android.icu.text.UnicodeFilter keepclass
+class android.icu.text.UnicodeMatcher keepclass
+class android.icu.text.UnicodeNameTransliterator keepclass
+class android.icu.text.UnicodeReplacer keepclass
+class android.icu.text.UnicodeSet keepclass
+class android.icu.text.UnicodeSetIterator keepclass
+class android.icu.text.UnicodeSetSpanner keepclass
+class android.icu.text.UppercaseTransliterator keepclass
+class android.icu.util.AnnualTimeZoneRule keepclass
+class android.icu.util.BasicTimeZone keepclass
+class android.icu.util.BuddhistCalendar keepclass
+class android.icu.util.ByteArrayWrapper keepclass
+class android.icu.util.BytesTrie keepclass
+class android.icu.util.BytesTrieBuilder keepclass
+class android.icu.util.CECalendar keepclass
+class android.icu.util.Calendar keepclass
+class android.icu.util.CaseInsensitiveString keepclass
+class android.icu.util.CharsTrie keepclass
+class android.icu.util.CharsTrieBuilder keepclass
+class android.icu.util.ChineseCalendar keepclass
+class android.icu.util.CodePointMap keepclass
+class android.icu.util.CodePointTrie keepclass
+class android.icu.util.CompactByteArray keepclass
+class android.icu.util.CompactCharArray keepclass
+class android.icu.util.CopticCalendar keepclass
+class android.icu.util.Currency keepclass
+class android.icu.util.CurrencyAmount keepclass
+class android.icu.util.CurrencyServiceShim keepclass
+class android.icu.util.DangiCalendar keepclass
+class android.icu.util.DateInterval keepclass
+class android.icu.util.DateRule keepclass
+class android.icu.util.DateTimeRule keepclass
+class android.icu.util.EasterHoliday keepclass
+class android.icu.util.EasterRule keepclass
+class android.icu.util.EthiopicCalendar keepclass
+class android.icu.util.Freezable keepclass
+class android.icu.util.GenderInfo keepclass
+class android.icu.util.GlobalizationPreferences keepclass
+class android.icu.util.GregorianCalendar keepclass
+class android.icu.util.HebrewCalendar keepclass
+class android.icu.util.HebrewHoliday keepclass
+class android.icu.util.Holiday keepclass
+class android.icu.util.ICUCloneNotSupportedException keepclass
+class android.icu.util.ICUException keepclass
+class android.icu.util.ICUInputTooLongException keepclass
+class android.icu.util.ICUUncheckedIOException keepclass
+class android.icu.util.IllformedLocaleException keepclass
+class android.icu.util.IndianCalendar keepclass
+class android.icu.util.InitialTimeZoneRule keepclass
+class android.icu.util.IslamicCalendar keepclass
+class android.icu.util.JapaneseCalendar keepclass
+class android.icu.util.LocaleData keepclass
+class android.icu.util.LocaleMatcher keepclass
+class android.icu.util.LocalePriorityList keepclass
+class android.icu.util.Measure keepclass
+class android.icu.util.MeasureUnit keepclass
+class android.icu.util.MutableCodePointTrie keepclass
+class android.icu.util.NoUnit keepclass
+class android.icu.util.Output keepclass
+class android.icu.util.OutputInt keepclass
+class android.icu.util.PersianCalendar keepclass
+class android.icu.util.Range keepclass
+class android.icu.util.RangeDateRule keepclass
+class android.icu.util.RangeValueIterator keepclass
+class android.icu.util.Region keepclass
+class android.icu.util.RuleBasedTimeZone keepclass
+class android.icu.util.STZInfo keepclass
+class android.icu.util.SimpleDateRule keepclass
+class android.icu.util.SimpleHoliday keepclass
+class android.icu.util.SimpleTimeZone keepclass
+class android.icu.util.StringTokenizer keepclass
+class android.icu.util.StringTrieBuilder keepclass
+class android.icu.util.TaiwanCalendar keepclass
+class android.icu.util.TimeArrayTimeZoneRule keepclass
+class android.icu.util.TimeUnit keepclass
+class android.icu.util.TimeUnitAmount keepclass
+class android.icu.util.TimeZone keepclass
+class android.icu.util.TimeZoneRule keepclass
+class android.icu.util.TimeZoneTransition keepclass
+class android.icu.util.ULocale keepclass
+class android.icu.util.UResourceBundle keepclass
+class android.icu.util.UResourceBundleIterator keepclass
+class android.icu.util.UResourceTypeMismatchException keepclass
+class android.icu.util.UniversalTimeScale keepclass
+class android.icu.util.VTimeZone keepclass
+class android.icu.util.ValueIterator keepclass
+class android.icu.util.VersionInfo keepclass
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/system/ZygoteHooks.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/system/ZygoteHooks.java
index 166389f..9be61a9 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/system/ZygoteHooks.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/system/ZygoteHooks.java
@@ -27,6 +27,8 @@
 import android.icu.util.TimeZone;
 import android.icu.util.ULocale;
 
+import com.android.icu.util.UResourceBundleNative;
+
 import dalvik.annotation.compat.VersionCodes;
 
 /**
@@ -85,6 +87,10 @@
         // It's in the end of preload because preload and ICU4J initialization should succeed
         // without this property. Otherwise, it indicates that the Android patch is not working.
         System.setProperty(PROP_ICUBINARY_DATA_PATH, AndroidDataFiles.generateIcuDataPath());
+
+        // Cache the timezone bundles, e.g. metaZones.res, in Zygote due to app compat.
+        // http://b/339899412
+        UResourceBundleNative.cacheTimeZoneBundles();
     }
 
     /**
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java
index 6f3b16e..da6ab4b 100644
--- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java
+++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TimeZoneDataFiles.java
@@ -39,7 +39,10 @@
      * Returns time zone file paths for the specified file name in an array in the order they
      * should be tried. See {@link AndroidDataFiles#generateIcuDataPath()} for ICU files instead.
      * <ul>
-     * <li>[0] - the location of the file from the time zone module under /apex (must exist).</li>
+     * <li>[0] - the location of the versioned file from the time zone module under /apex
+     * (must exist).</li>
+     * <li>[1] - old, unversioned location of the file from the time zone module under /apex. Will
+     * be removed once prebuilts are updated.</>
      * </ul>
      */
     // VisibleForTesting
@@ -48,7 +51,8 @@
     }
 
     public static String getTimeZoneModuleTzFile(String fileName) {
-        return getTimeZoneModuleFile("tz/" + fileName);
+        return getTimeZoneModuleFile("tz/versioned/"
+                + TzDataSetVersion.currentFormatMajorVersion() + "/" + fileName);
     }
 
     // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797
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 4c07f84..e3dd275 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,9 @@
      * 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 = 7; // Android U
+    // LINT.IfChange
+    public static final int CURRENT_FORMAT_MAJOR_VERSION = 8; // Android V
+    // LINT.ThenChange(external/icu/android_icu4j/src/main/java/android/icu/platform/AndroidDataFiles.java)
 
     /**
      * Returns the major tz data format version supported by this device.
@@ -86,12 +88,9 @@
         return CURRENT_FORMAT_MINOR_VERSION;
     }
 
-    /** The full major + minor tz data format version for this device. */
-    private static final String FULL_CURRENT_FORMAT_VERSION_STRING =
-            toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION);
-
-    private static final int FORMAT_VERSION_STRING_LENGTH =
-            FULL_CURRENT_FORMAT_VERSION_STRING.length();
+    /** The full major + minor tz data format version's length for this device. */
+    // @VisibleForTesting
+    public static final int FORMAT_VERSION_STRING_LENGTH = 7;
     private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})");
 
     /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */
@@ -287,7 +286,8 @@
         return value;
     }
 
-    private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) {
+    // @VisibleForTesting
+    public static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) {
         return to3DigitVersionString(majorFormatVersion)
                 + "." + to3DigitVersionString(minorFormatVersion);
     }
diff --git a/android_icu4j/libcore_bridge/src/java/com/android/icu/util/UResourceBundleNative.java b/android_icu4j/libcore_bridge/src/java/com/android/icu/util/UResourceBundleNative.java
new file mode 100644
index 0000000..eae5cf6
--- /dev/null
+++ b/android_icu4j/libcore_bridge/src/java/com/android/icu/util/UResourceBundleNative.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.icu.util;
+
+/**
+ * @hide
+ */
+public class UResourceBundleNative {
+    public static native void cacheTimeZoneBundles();
+}
diff --git a/android_icu4j/libcore_bridge/src/native/Register.cpp b/android_icu4j/libcore_bridge/src/native/Register.cpp
index ad7c9c1..25d53f3 100644
--- a/android_icu4j/libcore_bridge/src/native/Register.cpp
+++ b/android_icu4j/libcore_bridge/src/native/Register.cpp
@@ -53,6 +53,7 @@
     REGISTER(register_com_android_icu_util_CaseMapperNative);
     REGISTER(register_com_android_icu_util_Icu4cMetadata);
     REGISTER(register_com_android_icu_util_LocaleNative);
+    REGISTER(register_com_android_icu_util_UResourceBundleNative);
     REGISTER(register_com_android_icu_util_regex_PatternNative);
     REGISTER(register_com_android_icu_util_regex_MatcherNative);
     REGISTER(register_com_android_icu_util_charset_NativeConverter);
diff --git a/android_icu4j/libcore_bridge/src/native/com_android_icu_util_UResourceBundleNative.cpp b/android_icu4j/libcore_bridge/src/native/com_android_icu_util_UResourceBundleNative.cpp
new file mode 100644
index 0000000..c1615aa
--- /dev/null
+++ b/android_icu4j/libcore_bridge/src/native/com_android_icu_util_UResourceBundleNative.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "UResourceBundleNative"
+
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/jni_macros.h>
+
+#include <log/log.h>
+#include "unicode/ures.h"
+#include "unicode/utypes.h"
+
+static inline void openDirectAndCloseRes(const char* res_name) {
+    UErrorCode status = U_ZERO_ERROR;
+    UResourceBundle *res = ures_openDirect(nullptr, res_name, &status);
+    if (U_FAILURE(status)) {
+        ALOGE("Failed to load ICU resource '%s': %s", res_name, u_errorName(status));
+        return;
+    }
+
+    ures_close(res);
+}
+
+static void UResourceBundleNative_cacheTimeZoneBundles(JNIEnv* env, jclass) {
+    openDirectAndCloseRes("zoneinfo64");
+    openDirectAndCloseRes("timezoneTypes");
+    openDirectAndCloseRes("metaZones");
+    openDirectAndCloseRes("windowsZones");
+}
+
+static JNINativeMethod gMethods[] = {
+    NATIVE_METHOD(UResourceBundleNative, cacheTimeZoneBundles, "()V"),
+};
+
+void register_com_android_icu_util_UResourceBundleNative(JNIEnv* env) {
+    jniRegisterNativeMethods(env, "com/android/icu/util/UResourceBundleNative", gMethods, NELEM(gMethods));
+}
diff --git a/android_icu4j/lint-baseline.xml b/android_icu4j/lint-baseline.xml
new file mode 100644
index 0000000..159b768
--- /dev/null
+++ b/android_icu4j/lint-baseline.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="Field `APPROXIMATELY_SIGN` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `getFieldForType` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="            return NumberFormat.Field.APPROXIMATELY_SIGN;"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/impl/number/AffixUtils.java"
+            line="282"
+            column="39"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setDateTimeFormat()` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `setDateTimeFromCalendar` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="            setDateTimeFormat(style, dateTimeFormat);"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java"
+            line="315"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getDateTimeFormat()` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `getBestPattern` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="                getDateTimeFormat(style), 2, 2, timePattern, datePattern);"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java"
+            line="702"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setDateTimeFormat()` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `setDateTimeFormat` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="            setDateTimeFormat(style, dateTimeFormat);"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java"
+            line="1048"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getDateTimeFormat()` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `getDateTimeFormat` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="        return getDateTimeFormat(DateFormat.MEDIUM);"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/text/DateTimePatternGenerator.java"
+            line="1063"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `UNICODE_15_1` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="        UNICODE_15_1   = getInstance(15, 1, 0, 0);"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/util/VersionInfo.java"
+            line="528"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `UNICODE_15_1` is a flagged API and should be inside an `if (Flags.icuVApi())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.FLAG_ICU_V_API) to transfer requirement to caller`)"
+        errorLine1="        UNICODE_VERSION = UNICODE_15_1;"
+        errorLine2="                          ~~~~~~~~~~~~">
+        <location
+            file="external/icu/android_icu4j/src/main/java/android/icu/util/VersionInfo.java"
+            line="532"
+            column="27"/>
+    </issue>
+
+</issues>
diff --git a/android_icu4j/src/icu74/Android.bp b/android_icu4j/src/icu74/Android.bp
new file mode 100644
index 0000000..e908532
--- /dev/null
+++ b/android_icu4j/src/icu74/Android.bp
@@ -0,0 +1,46 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_team: "trendy_team_java_core_libraries",
+    default_visibility: ["//visibility:private"],
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "external_icu_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    //   SPDX-license-identifier-BSD
+    //   SPDX-license-identifier-ICU
+    //   SPDX-license-identifier-MIT
+    //   SPDX-license-identifier-Unicode-DFS
+    //   legacy_unencumbered
+    default_applicable_licenses: ["external_icu_license"],
+}
+
+// Small static library used by TwilightService in the system server and WallpaperPicker2 app. To
+// avoid @CorePlaformApi, the system server doesn't use CalendarAstronomer in android.icu.
+// Don't link this in boot classpath or Zygote to avoid class collision with the
+// com.ibm.icu.impl.CalendarAstronomer in the app classloader.
+java_library_static {
+    name: "icu4j_calendar_astronomer",
+    host_supported: false,
+    sdk_version: "core_current",
+    srcs: ["main/java/com/ibm/icu/impl/CalendarAstronomer.java"],
+    visibility: [
+        "//frameworks/base/services/core",
+        "//packages/apps/WallpaperPicker2",
+    ],
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarAstronomer.java b/android_icu4j/src/icu74/main/java/com/ibm/icu/impl/CalendarAstronomer.java
similarity index 100%
rename from icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarAstronomer.java
rename to android_icu4j/src/icu74/main/java/com/ibm/icu/impl/CalendarAstronomer.java
diff --git a/android_icu4j/src/main/java/android/icu/impl/CalType.java b/android_icu4j/src/main/java/android/icu/impl/CalType.java
index 52ad03e..e860bf9 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CalType.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CalType.java
@@ -28,9 +28,7 @@
     ISLAMIC_UMALQURA("islamic-umalqura"),
     JAPANESE("japanese"),
     PERSIAN("persian"),
-    ROC("roc"),
-
-    UNKNOWN("unknown");
+    ROC("roc");
 
     String id;
 
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 088100d..e616c9c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CalendarAstronomer.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CalendarAstronomer.java
@@ -11,7 +11,6 @@
 package android.icu.impl;
 
 import java.util.Date;
-import java.util.TimeZone;
 
 /**
  * <code>CalendarAstronomer</code> is a class that can perform the calculations to
@@ -207,15 +206,6 @@
 
     /**
      * Construct a new <code>CalendarAstronomer</code> object that is initialized to
-     * the specified date and time.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public CalendarAstronomer(Date d) {
-        this(d.getTime());
-    }
-
-    /**
-     * Construct a new <code>CalendarAstronomer</code> object that is initialized to
      * the specified time.  The time is expressed as a number of milliseconds since
      * January 1, 1970 AD (Gregorian).
      *
@@ -226,32 +216,9 @@
         time = aTime;
     }
 
-    /**
-     * Construct a new <code>CalendarAstronomer</code> object with the given
-     * latitude and longitude.  The object's time is set to the current
-     * date and time.
-     * <p>
-     * @param longitude The desired longitude, in <em>degrees</em> east of
-     *                  the Greenwich meridian.
-     *
-     * @param latitude  The desired latitude, in <em>degrees</em>.  Positive
-     *                  values signify North, negative South.
-     *
-     * @see java.util.Date#getTime()
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public CalendarAstronomer(double longitude, double latitude) {
-        this();
-        fLongitude = normPI(longitude * DEG_RAD);
-        fLatitude  = normPI(latitude  * DEG_RAD);
-        fGmtOffset = (long)(fLongitude * 24 * HOUR_MS / PI2);
-    }
-
-
     //-------------------------------------------------------------------------
     // Time and date getters and setters
     //-------------------------------------------------------------------------
-
     /**
      * Set the current date and time of this <code>CalendarAstronomer</code> object.  All
      * astronomical calculations are performed based on this time setting.
@@ -268,19 +235,6 @@
         clearCache();
     }
 
-    /**
-     * Set the current date and time of this <code>CalendarAstronomer</code> object.  All
-     * astronomical calculations are performed based on this time setting.
-     *
-     * @param date the time and date, expressed as a <code>Date</code> object.
-     *
-     * @see #setTime
-     * @see #getDate
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public void setDate(Date date) {
-        setTime(date.getTime());
-    }
 
     /**
      * Set the current date and time of this <code>CalendarAstronomer</code> object.  All
@@ -343,77 +297,6 @@
         return julianDay;
     }
 
-    /**
-     * Return this object's time expressed in julian centuries:
-     * the number of centuries after 1/1/1900 AD, 12:00 GMT
-     *
-     * @see #getJulianDay
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public double getJulianCentury() {
-        if (julianCentury == INVALID) {
-            julianCentury = (getJulianDay() - 2415020.0) / 36525;
-        }
-        return julianCentury;
-    }
-
-    /**
-     * Returns the current Greenwich sidereal time, measured in hours
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public double getGreenwichSidereal() {
-        if (siderealTime == INVALID) {
-            // 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);
-
-            siderealTime = normalize(getSiderealOffset() + UT*1.002737909, 24);
-        }
-        return siderealTime;
-    }
-
-    private double getSiderealOffset() {
-        if (siderealT0 == INVALID) {
-            double JD  = Math.floor(getJulianDay() - 0.5) + 0.5;
-            double S   = JD - 2451545.0;
-            double T   = S / 36525.0;
-            siderealT0 = normalize(6.697374558 + 2400.051336*T + 0.000025862*T*T, 24);
-        }
-        return siderealT0;
-    }
-
-    /**
-     * Returns the current local sidereal time, measured in hours
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public double getLocalSidereal() {
-        return normalize(getGreenwichSidereal() + (double)fGmtOffset/HOUR_MS, 24);
-    }
-
-    /**
-     * Converts local sidereal time to Universal Time.
-     *
-     * @param lst   The Local Sidereal Time, in hours since sidereal midnight
-     *              on this object's current date.
-     *
-     * @return      The corresponding Universal Time, in milliseconds since
-     *              1 Jan 1970, GMT.
-     */
-    private long lstToUT(double lst) {
-        // Convert to local mean time
-        double lt = normalize((lst - getSiderealOffset()) * 0.9972695663, 24);
-
-        // Then find local midnight on this day
-        long base = DAY_MS * ((time + fGmtOffset)/DAY_MS) - fGmtOffset;
-
-        //out("    lt  =" + lt + " hours");
-        //out("    base=" + new Date(base));
-
-        return base + (long)(lt * HOUR_MS);
-    }
-
-
     //-------------------------------------------------------------------------
     // Coordinate transformations, all based on the current time of this object
     //-------------------------------------------------------------------------
@@ -421,18 +304,6 @@
     /**
      * Convert from ecliptic to equatorial coordinates.
      *
-     * @param ecliptic  A point in the sky in ecliptic coordinates.
-     * @return          The corresponding point in equatorial coordinates.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public final Equatorial eclipticToEquatorial(Ecliptic ecliptic)
-    {
-        return eclipticToEquatorial(ecliptic.longitude, ecliptic.latitude);
-    }
-
-    /**
-     * Convert from ecliptic to equatorial coordinates.
-     *
      * @param eclipLong     The ecliptic longitude
      * @param eclipLat      The ecliptic latitude
      *
@@ -459,42 +330,6 @@
                                Math.asin(sinB*cosE + cosB*sinE*sinL) );
     }
 
-    /**
-     * Convert from ecliptic longitude to equatorial coordinates.
-     *
-     * @param eclipLong     The ecliptic longitude
-     *
-     * @return              The corresponding point in equatorial coordinates.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public final Equatorial eclipticToEquatorial(double eclipLong)
-    {
-        return eclipticToEquatorial(eclipLong, 0);  // TODO: optimize
-    }
-
-    /**
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public Horizon eclipticToHorizon(double eclipLong)
-    {
-        Equatorial equatorial = eclipticToEquatorial(eclipLong);
-
-        double H = getLocalSidereal()*PI/12 - equatorial.ascension;     // Hour-angle
-
-        double sinH = Math.sin(H);
-        double cosH = Math.cos(H);
-        double sinD = Math.sin(equatorial.declination);
-        double cosD = Math.cos(equatorial.declination);
-        double sinL = Math.sin(fLatitude);
-        double cosL = Math.cos(fLatitude);
-
-        double altitude = Math.asin(sinD*sinL + cosD*cosL*cosH);
-        double azimuth  = Math.atan2(-cosD*cosL*sinH, sinD - sinL * Math.sin(altitude));
-
-        return new Horizon(azimuth, altitude);
-    }
-
-
     //-------------------------------------------------------------------------
     // The Sun
     //-------------------------------------------------------------------------
@@ -608,45 +443,12 @@
         };
     }
 
-    /**
-     * The position of the sun at this object's current date and time,
-     * in equatorial coordinates.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public Equatorial getSunPosition() {
-        return eclipticToEquatorial(getSunLongitude(), 0);
-    }
-
     private static class SolarLongitude {
         double value;
         SolarLongitude(double val) { value = val; }
     }
 
     /**
-     * Constant representing the vernal equinox.
-     * For use with {@link #getSunTime(SolarLongitude, boolean) getSunTime}.
-     * Note: In this case, "vernal" refers to the northern hemisphere's seasons.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final SolarLongitude VERNAL_EQUINOX  = new SolarLongitude(0);
-
-    /**
-     * Constant representing the summer solstice.
-     * For use with {@link #getSunTime(SolarLongitude, boolean) getSunTime}.
-     * Note: In this case, "summer" refers to the northern hemisphere's seasons.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final SolarLongitude SUMMER_SOLSTICE = new SolarLongitude(PI/2);
-
-    /**
-     * Constant representing the autumnal equinox.
-     * For use with {@link #getSunTime(SolarLongitude, boolean) getSunTime}.
-     * Note: In this case, "autumn" refers to the northern hemisphere's seasons.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final SolarLongitude AUTUMN_EQUINOX  = new SolarLongitude(PI);
-
-    /**
      * Constant representing the winter solstice.
      * For use with {@link #getSunTime(SolarLongitude, boolean) getSunTime}.
      * Note: In this case, "winter" refers to the northern hemisphere's seasons.
@@ -678,312 +480,6 @@
         return getSunTime(desired.value, next);
     }
 
-    /**
-     * Returns the time (GMT) of sunrise or sunset on the local date to which
-     * this calendar is currently set.
-     *
-     * NOTE: This method only works well if this object is set to a
-     * time near local noon.  Because of variations between the local
-     * official time zone and the geographic longitude, the
-     * computation can flop over into an adjacent day if this object
-     * is set to a time near local midnight.
-     *
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public long getSunRiseSet(boolean rise) {
-        long t0 = time;
-
-        // Make a rough guess: 6am or 6pm local time on the current day
-        long noon = ((time + fGmtOffset)/DAY_MS)*DAY_MS - fGmtOffset + 12*HOUR_MS;
-
-        setTime(noon + (rise ? -6L : 6L) * HOUR_MS);
-
-        long t = riseOrSet(new CoordFunc() {
-            @Override
-            public Equatorial eval() { return getSunPosition(); }
-            },
-                rise,
-                .533 * DEG_RAD,        // Angular Diameter
-                34 /60.0 * DEG_RAD,    // Refraction correction
-                MINUTE_MS / 12);       // Desired accuracy
-
-            setTime(t0);
-            return t;
-        }
-
-// Commented out - currently unused. ICU 2.6, Alan
-//    //-------------------------------------------------------------------------
-//    // Alternate Sun Rise/Set
-//    // See Duffett-Smith p.93
-//    //-------------------------------------------------------------------------
-//
-//    // This yields worse results (as compared to USNO data) than getSunRiseSet().
-//    /**
-//     * TODO Make this public when the entire class is package-private.
-//     */
-//    /*public*/ long getSunRiseSet2(boolean rise) {
-//        // 1. Calculate coordinates of the sun's center for midnight
-//        double jd = Math.floor(getJulianDay() - 0.5) + 0.5;
-//        double[] sl = getSunLongitude(jd);
-//        double lambda1 = sl[0];
-//        Equatorial pos1 = eclipticToEquatorial(lambda1, 0);
-//
-//        // 2. Add ... to lambda to get position 24 hours later
-//        double lambda2 = lambda1 + 0.985647*DEG_RAD;
-//        Equatorial pos2 = eclipticToEquatorial(lambda2, 0);
-//
-//        // 3. Calculate LSTs of rising and setting for these two positions
-//        double tanL = Math.tan(fLatitude);
-//        double H = Math.acos(-tanL * Math.tan(pos1.declination));
-//        double lst1r = (PI2 + pos1.ascension - H) * 24 / PI2;
-//        double lst1s = (pos1.ascension + H) * 24 / PI2;
-//               H = Math.acos(-tanL * Math.tan(pos2.declination));
-//        double lst2r = (PI2-H + pos2.ascension ) * 24 / PI2;
-//        double lst2s = (H + pos2.ascension ) * 24 / PI2;
-//        if (lst1r > 24) lst1r -= 24;
-//        if (lst1s > 24) lst1s -= 24;
-//        if (lst2r > 24) lst2r -= 24;
-//        if (lst2s > 24) lst2s -= 24;
-//
-//        // 4. Convert LSTs to GSTs.  If GST1 > GST2, add 24 to GST2.
-//        double gst1r = lstToGst(lst1r);
-//        double gst1s = lstToGst(lst1s);
-//        double gst2r = lstToGst(lst2r);
-//        double gst2s = lstToGst(lst2s);
-//        if (gst1r > gst2r) gst2r += 24;
-//        if (gst1s > gst2s) gst2s += 24;
-//
-//        // 5. Calculate GST at 0h UT of this date
-//        double t00 = utToGst(0);
-//
-//        // 6. Calculate GST at 0h on the observer's longitude
-//        double offset = Math.round(fLongitude*12/PI); // p.95 step 6; he _rounds_ to nearest 15 deg.
-//        double t00p = t00 - offset*1.002737909;
-//        if (t00p < 0) t00p += 24; // do NOT normalize
-//
-//        // 7. Adjust
-//        if (gst1r < t00p) {
-//            gst1r += 24;
-//            gst2r += 24;
-//        }
-//        if (gst1s < t00p) {
-//            gst1s += 24;
-//            gst2s += 24;
-//        }
-//
-//        // 8.
-//        double gstr = (24.07*gst1r-t00*(gst2r-gst1r))/(24.07+gst1r-gst2r);
-//        double gsts = (24.07*gst1s-t00*(gst2s-gst1s))/(24.07+gst1s-gst2s);
-//
-//        // 9. Correct for parallax, refraction, and sun's diameter
-//        double dec = (pos1.declination + pos2.declination) / 2;
-//        double psi = Math.acos(Math.sin(fLatitude) / Math.cos(dec));
-//        double x = 0.830725 * DEG_RAD; // parallax+refraction+diameter
-//        double y = Math.asin(Math.sin(x) / Math.sin(psi)) * RAD_DEG;
-//        double delta_t = 240 * y / Math.cos(dec) / 3600; // hours
-//
-//        // 10. Add correction to GSTs, subtract from GSTr
-//        gstr -= delta_t;
-//        gsts += delta_t;
-//
-//        // 11. Convert GST to UT and then to local civil time
-//        double ut = gstToUt(rise ? gstr : gsts);
-//        //System.out.println((rise?"rise=":"set=") + ut + ", delta_t=" + delta_t);
-//        long midnight = DAY_MS * (time / DAY_MS); // Find UT midnight on this day
-//        return midnight + (long) (ut * 3600000);
-//    }
-
-// Commented out - currently unused. ICU 2.6, Alan
-//    /**
-//     * Convert local sidereal time to Greenwich sidereal time.
-//     * Section 15.  Duffett-Smith p.21
-//     * @param lst in hours (0..24)
-//     * @return GST in hours (0..24)
-//     */
-//    double lstToGst(double lst) {
-//        double delta = fLongitude * 24 / PI2;
-//        return normalize(lst - delta, 24);
-//    }
-
-// Commented out - currently unused. ICU 2.6, Alan
-//    /**
-//     * Convert UT to GST on this date.
-//     * Section 12.  Duffett-Smith p.17
-//     * @param ut in hours
-//     * @return GST in hours
-//     */
-//    double utToGst(double ut) {
-//        return normalize(getT0() + ut*1.002737909, 24);
-//    }
-
-// Commented out - currently unused. ICU 2.6, Alan
-//    /**
-//     * Convert GST to UT on this date.
-//     * Section 13.  Duffett-Smith p.18
-//     * @param gst in hours
-//     * @return UT in hours
-//     */
-//    double gstToUt(double gst) {
-//        return normalize(gst - getT0(), 24) * 0.9972695663;
-//    }
-
-// Commented out - currently unused. ICU 2.6, Alan
-//    double getT0() {
-//        // Common computation for UT <=> GST
-//
-//        // Find JD for 0h UT
-//        double jd = Math.floor(getJulianDay() - 0.5) + 0.5;
-//
-//        double s = jd - 2451545.0;
-//        double t = s / 36525.0;
-//        double t0 = 6.697374558 + (2400.051336 + 0.000025862*t)*t;
-//        return t0;
-//    }
-
-// Commented out - currently unused. ICU 2.6, Alan
-//    //-------------------------------------------------------------------------
-//    // Alternate Sun Rise/Set
-//    // See sci.astro FAQ
-//    // http://www.faqs.org/faqs/astronomy/faq/part3/section-5.html
-//    //-------------------------------------------------------------------------
-//
-//    // Note: This method appears to produce inferior accuracy as
-//    // compared to getSunRiseSet().
-//
-//    /**
-//     * TODO Make this public when the entire class is package-private.
-//     */
-//    /*public*/ long getSunRiseSet3(boolean rise) {
-//
-//        // Compute day number for 0.0 Jan 2000 epoch
-//        double d = (double)(time - EPOCH_2000_MS) / DAY_MS;
-//
-//        // Now compute the Local Sidereal Time, LST:
-//        //
-//        double LST  =  98.9818  +  0.985647352 * d  +  /*UT*15  +  long*/
-//            fLongitude*RAD_DEG;
-//        //
-//        // (east long. positive).  Note that LST is here expressed in degrees,
-//        // where 15 degrees corresponds to one hour.  Since LST really is an angle,
-//        // it's convenient to use one unit---degrees---throughout.
-//
-//        //     COMPUTING THE SUN'S POSITION
-//        //     ----------------------------
-//        //
-//        // To be able to compute the Sun's rise/set times, you need to be able to
-//        // compute the Sun's position at any time.  First compute the "day
-//        // number" d as outlined above, for the desired moment.  Next compute:
-//        //
-//        double oblecl = 23.4393 - 3.563E-7 * d;
-//        //
-//        double w  =  282.9404  +  4.70935E-5   * d;
-//        double M  =  356.0470  +  0.9856002585 * d;
-//        double e  =  0.016709  -  1.151E-9     * d;
-//        //
-//        // This is the obliquity of the ecliptic, plus some of the elements of
-//        // the Sun's apparent orbit (i.e., really the Earth's orbit): w =
-//        // argument of perihelion, M = mean anomaly, e = eccentricity.
-//        // Semi-major axis is here assumed to be exactly 1.0 (while not strictly
-//        // true, this is still an accurate approximation).  Next compute E, the
-//        // eccentric anomaly:
-//        //
-//        double E = M + e*(180/PI) * Math.sin(M*DEG_RAD) * ( 1.0 + e*Math.cos(M*DEG_RAD) );
-//        //
-//        // where E and M are in degrees.  This is it---no further iterations are
-//        // needed because we know e has a sufficiently small value.  Next compute
-//        // the true anomaly, v, and the distance, r:
-//        //
-//        /*      r * cos(v)  =  */ double A  =  Math.cos(E*DEG_RAD) - e;
-//        /*      r * sin(v)  =  */ double B  =  Math.sqrt(1 - e*e) * Math.sin(E*DEG_RAD);
-//        //
-//        // and
-//        //
-//        //      r  =  sqrt( A*A + B*B )
-//        double v  =  Math.atan2( B, A )*RAD_DEG;
-//        //
-//        // The Sun's true longitude, slon, can now be computed:
-//        //
-//        double slon  =  v + w;
-//        //
-//        // Since the Sun is always at the ecliptic (or at least very very close to
-//        // it), we can use simplified formulae to convert slon (the Sun's ecliptic
-//        // longitude) to sRA and sDec (the Sun's RA and Dec):
-//        //
-//        //                   sin(slon) * cos(oblecl)
-//        //     tan(sRA)  =  -------------------------
-//        //             cos(slon)
-//        //
-//        //     sin(sDec) =  sin(oblecl) * sin(slon)
-//        //
-//        // As was the case when computing az, the Azimuth, if possible use an
-//        // atan2() function to compute sRA.
-//
-//        double sRA = Math.atan2(Math.sin(slon*DEG_RAD) * Math.cos(oblecl*DEG_RAD), Math.cos(slon*DEG_RAD))*RAD_DEG;
-//
-//        double sin_sDec = Math.sin(oblecl*DEG_RAD) * Math.sin(slon*DEG_RAD);
-//        double sDec = Math.asin(sin_sDec)*RAD_DEG;
-//
-//        //     COMPUTING RISE AND SET TIMES
-//        //     ----------------------------
-//        //
-//        // To compute when an object rises or sets, you must compute when it
-//        // passes the meridian and the HA of rise/set.  Then the rise time is
-//        // the meridian time minus HA for rise/set, and the set time is the
-//        // meridian time plus the HA for rise/set.
-//        //
-//        // To find the meridian time, compute the Local Sidereal Time at 0h local
-//        // time (or 0h UT if you prefer to work in UT) as outlined above---name
-//        // that quantity LST0.  The Meridian Time, MT, will now be:
-//        //
-//        //     MT  =  RA - LST0
-//        double MT = normalize(sRA - LST, 360);
-//        //
-//        // where "RA" is the object's Right Ascension (in degrees!).  If negative,
-//        // add 360 deg to MT.  If the object is the Sun, leave the time as it is,
-//        // but if it's stellar, multiply MT by 365.2422/366.2422, to convert from
-//        // sidereal to solar time.  Now, compute HA for rise/set, name that
-//        // quantity HA0:
-//        //
-//        //                 sin(h0)  -  sin(lat) * sin(Dec)
-//        // cos(HA0)  =  ---------------------------------
-//        //                      cos(lat) * cos(Dec)
-//        //
-//        // where h0 is the altitude selected to represent rise/set.  For a purely
-//        // mathematical horizon, set h0 = 0 and simplify to:
-//        //
-//        //     cos(HA0)  =  - tan(lat) * tan(Dec)
-//        //
-//        // If you want to account for refraction on the atmosphere, set h0 = -35/60
-//        // degrees (-35 arc minutes), and if you want to compute the rise/set times
-//        // for the Sun's upper limb, set h0 = -50/60 (-50 arc minutes).
-//        //
-//        double h0 = -50/60 * DEG_RAD;
-//
-//        double HA0 = Math.acos(
-//          (Math.sin(h0) - Math.sin(fLatitude) * sin_sDec) /
-//          (Math.cos(fLatitude) * Math.cos(sDec*DEG_RAD)))*RAD_DEG;
-//
-//        // When HA0 has been computed, leave it as it is for the Sun but multiply
-//        // by 365.2422/366.2422 for stellar objects, to convert from sidereal to
-//        // solar time.  Finally compute:
-//        //
-//        //    Rise time  =  MT - HA0
-//        //    Set  time  =  MT + HA0
-//        //
-//        // convert the times from degrees to hours by dividing by 15.
-//        //
-//        // If you'd like to check that your calculations are accurate or just
-//        // need a quick result, check the USNO's Sun or Moon Rise/Set Table,
-//        // <URL:http://aa.usno.navy.mil/AA/data/docs/RS_OneYear.html>.
-//
-//        double result = MT + (rise ? -HA0 : HA0); // in degrees
-//
-//        // Find UT midnight on this day
-//        long midnight = DAY_MS * (time / DAY_MS);
-//
-//        return midnight + (long) (result * 3600000 / 15);
-//    }
-
     //-------------------------------------------------------------------------
     // The Moon
     //-------------------------------------------------------------------------
@@ -1050,7 +546,7 @@
             double a4 =     0.2140*PI/180 * Math.sin(2 * meanAnomalyMoon);
 
             // Now find the moon's corrected longitude
-            moonLongitude = meanLongitude + evection + center - annual + a4;
+            double moonLongitude = meanLongitude + evection + center - annual + a4;
 
             //
             // And finally, find the variation, caused by the fact that the sun's
@@ -1104,26 +600,6 @@
         return norm2PI(moonEclipLong - sunLongitude);
     }
 
-    /**
-     * Calculate the phase of the moon at the time set in this object.
-     * The returned phase is a <code>double</code> in the range
-     * <code>0 <= phase < 1</code>, interpreted as follows:
-     * <ul>
-     * <li>0.00: New moon
-     * <li>0.25: First quarter
-     * <li>0.50: Full moon
-     * <li>0.75: Last quarter
-     * </ul>
-     *
-     * @see #getMoonAge
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public double getMoonPhase() {
-        // 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()));
-    }
-
     private static class MoonAge {
         double value;
         MoonAge(double val) { value = val; }
@@ -1137,27 +613,6 @@
     public static final MoonAge NEW_MOON      = new MoonAge(0);
 
     /**
-     * Constant representing the moon's first quarter.
-     * For use with {@link #getMoonTime(MoonAge, boolean) getMoonTime}
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final MoonAge FIRST_QUARTER = new MoonAge(PI/2);
-
-    /**
-     * Constant representing a full moon.
-     * For use with {@link #getMoonTime(MoonAge, boolean) getMoonTime}
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final MoonAge FULL_MOON     = new MoonAge(PI);
-
-    /**
-     * Constant representing the moon's last quarter.
-     * For use with {@link #getMoonTime(MoonAge, boolean) getMoonTime}
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final MoonAge LAST_QUARTER  = new MoonAge((PI*3)/2);
-
-    /**
      * Find the next or previous time at which the Moon's ecliptic
      * longitude will have the desired value.
      * <p>
@@ -1190,23 +645,6 @@
         return getMoonTime(desired.value, next);
     }
 
-    /**
-     * Returns the time (GMT) of sunrise or sunset on the local date to which
-     * this calendar is currently set.
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public long getMoonRiseSet(boolean rise)
-    {
-        return riseOrSet(new CoordFunc() {
-                            @Override
-                            public Equatorial eval() { return getMoonPosition(); }
-                         },
-                         rise,
-                         .533 * DEG_RAD,        // Angular Diameter
-                         34 /60.0 * DEG_RAD,    // Refraction correction
-                         MINUTE_MS);            // Desired accuracy
-    }
-
     //-------------------------------------------------------------------------
     // Interpolation methods for finding the time at which a given event occurs
     //-------------------------------------------------------------------------
@@ -1283,48 +721,6 @@
         return time;
     }
 
-    private interface CoordFunc {
-        public Equatorial eval();
-    }
-
-    private long riseOrSet(CoordFunc func, boolean rise,
-                           double diameter, double refraction,
-                           long epsilon)
-    {
-        Equatorial  pos = null;
-        double      tanL   = Math.tan(fLatitude);
-        long        deltaT = Long.MAX_VALUE;
-        int         count = 0;
-
-        //
-        // Calculate the object's position at the current time, then use that
-        // position to calculate the time of rising or setting.  The position
-        // will be different at that time, so iterate until the error is allowable.
-        //
-        do {
-            // See "Practical Astronomy With Your Calculator, section 33.
-            pos = func.eval();
-            double angle = Math.acos(-tanL * Math.tan(pos.declination));
-            double lst = ((rise ? PI2-angle : angle) + pos.ascension ) * 24 / PI2;
-
-            // Convert from LST to Universal Time.
-            long newTime = lstToUT( lst );
-
-            deltaT = newTime - time;
-            setTime(newTime);
-        }
-        while (++ count < 5 && Math.abs(deltaT) > epsilon);
-
-        // Calculate the correction due to refraction and the object's angular diameter
-        double cosD  = Math.cos(pos.declination);
-        double psi   = Math.acos(Math.sin(fLatitude) / cosD);
-        double x     = diameter / 2 + refraction;
-        double y     = Math.asin(Math.sin(x) / Math.sin(psi));
-        long  delta  = (long)((240 * y * RAD_DEG / cosD)*SECOND_MS);
-
-        return time + (rise ? -delta : delta);
-    }
-
     //-------------------------------------------------------------------------
     // Other utility methods
     //-------------------------------------------------------------------------
@@ -1391,19 +787,16 @@
      *          measured in radians.
      */
     private double eclipticObliquity() {
-        if (eclipObliquity == INVALID) {
-            final double epoch = 2451545.0;     // 2000 AD, January 1.5
+        final double epoch = 2451545.0;     // 2000 AD, January 1.5
 
-            double T = (getJulianDay() - epoch) / 36525;
+        double T = (getJulianDay() - epoch) / 36525;
 
-            eclipObliquity = 23.439292
+        double eclipObliquity = 23.439292
                            - 46.815/3600 * T
                            - 0.0006/3600 * T*T
                            + 0.00181/3600 * T*T*T;
 
-            eclipObliquity *= DEG_RAD;
-        }
-        return eclipObliquity;
+        return eclipObliquity * DEG_RAD;
     }
 
 
@@ -1417,13 +810,6 @@
      */
     private long time;
 
-    /* These aren't used yet, but they'll be needed for sunset calculations
-     * and equatorial to horizon coordinate conversions
-     */
-    private double fLongitude = 0.0;
-    private double fLatitude  = 0.0;
-    private long   fGmtOffset = 0;
-
     //
     // The following fields are used to cache calculated results for improved
     // performance.  These values all depend on the current time setting
@@ -1432,52 +818,20 @@
     static final private double INVALID = Double.MIN_VALUE;
 
     private transient double    julianDay       = INVALID;
-    private transient double    julianCentury   = INVALID;
     private transient double    sunLongitude    = INVALID;
     private transient double    meanAnomalySun  = INVALID;
-    private transient double    moonLongitude   = INVALID;
     private transient double    moonEclipLong   = INVALID;
-    //private transient double    meanAnomalyMoon = INVALID;
-    private transient double    eclipObliquity  = INVALID;
-    private transient double    siderealT0      = INVALID;
-    private transient double    siderealTime    = INVALID;
 
     private transient Equatorial  moonPosition = null;
 
     private void clearCache() {
         julianDay       = INVALID;
-        julianCentury   = INVALID;
         sunLongitude    = INVALID;
         meanAnomalySun  = INVALID;
-        moonLongitude   = INVALID;
         moonEclipLong   = INVALID;
-        //meanAnomalyMoon = INVALID;
-        eclipObliquity  = INVALID;
-        siderealTime    = INVALID;
-        siderealT0      = INVALID;
         moonPosition    = null;
     }
 
-    //private static void out(String s) {
-    //    System.out.println(s);
-    //}
-
-    //private static String deg(double rad) {
-    //    return Double.toString(rad * RAD_DEG);
-    //}
-
-    //private static String hours(long ms) {
-    //    return Double.toString((double)ms / HOUR_MS) + " hours";
-    //}
-
-    /**
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public String local(long localMillis) {
-        return new Date(localMillis - TimeZone.getDefault().getRawOffset()).toString();
-    }
-
-
     /**
      * Represents the position of an object in the sky relative to the ecliptic,
      * the plane of the earth's orbit around the Sun.
@@ -1492,7 +846,6 @@
      * value without worrying about whether other code will modify them.
      *
      * @see CalendarAstronomer.Equatorial
-     * @see CalendarAstronomer.Horizon
      * @hide Only a subset of ICU is exposed in Android
      * @hide draft / provisional / internal are hidden on Android
      */
@@ -1553,7 +906,6 @@
      * value without worrying about whether other code will modify them.
      *
      * @see CalendarAstronomer.Ecliptic
-     * @see CalendarAstronomer.Horizon
      * @hide Only a subset of ICU is exposed in Android
      * @hide draft / provisional / internal are hidden on Android
      */
@@ -1607,60 +959,6 @@
         public final double declination;
     }
 
-    /**
-     * Represents the position of an  object in the sky relative to
-     * the local horizon.
-     * The <i>Altitude</i> represents the object's elevation above the horizon,
-     * with objects below the horizon having a negative altitude.
-     * The <i>Azimuth</i> is the geographic direction of the object from the
-     * observer's position, with 0 representing north.  The azimuth increases
-     * clockwise from north.
-     * <p>
-     * Note that Horizon objects are immutable and cannot be modified
-     * once they are constructed.  This allows them to be passed and returned by
-     * value without worrying about whether other code will modify them.
-     *
-     * @see CalendarAstronomer.Ecliptic
-     * @see CalendarAstronomer.Equatorial
-     * @hide Only a subset of ICU is exposed in Android
-     * @hide draft / provisional / internal are hidden on Android
-     */
-    public static final class Horizon {
-        /**
-         * Constructs a Horizon coordinate object.
-         * <p>
-         * @param alt  The altitude, measured in radians above the horizon.
-         * @param azim The azimuth, measured in radians clockwise from north.
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        public Horizon(double alt, double azim) {
-            altitude = alt;
-            azimuth = azim;
-        }
-
-        /**
-         * Return a string representation of this object, with the
-         * angles measured in degrees.
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        @Override
-        public String toString() {
-            return Double.toString(altitude*RAD_DEG) + "," + (azimuth*RAD_DEG);
-        }
-
-        /**
-         * The object's altitude above the horizon, in radians.
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        public final double altitude;
-
-        /**
-         * The object's direction, in radians clockwise from north.
-         * @hide draft / provisional / internal are hidden on Android
-         */
-        public final double azimuth;
-    }
-
     static private String radToHms(double angle) {
         int hrs = (int) (angle*RAD_HOUR);
         int min = (int)((angle*RAD_HOUR - hrs) * 60);
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 24c5487..d87fbe1 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CaseMapImpl.java
@@ -911,7 +911,8 @@
 
         // State bits.
         private static final int AFTER_CASED = 1;
-        private static final int AFTER_VOWEL_WITH_ACCENT = 2;
+        private static final int AFTER_VOWEL_WITH_COMBINING_ACCENT = 2;
+        private static final int AFTER_VOWEL_WITH_PRECOMPOSED_ACCENT = 4;
 
         // Data generated by prototype code, see
         // https://icu.unicode.org/design/case/greek-upper
@@ -1421,14 +1422,17 @@
                     // Adding one only to the final vowel in a longer sequence
                     // (which does not occur in normal writing) would require lookahead.
                     // Set the same flag as for preserving an existing dialytika.
-                    if ((data & HAS_VOWEL) != 0 && (state & AFTER_VOWEL_WITH_ACCENT) != 0 &&
-                            (upper == 'Ι' || upper == 'Υ')) {
-                        data |= HAS_DIALYTIKA;
+                    if ((data & HAS_VOWEL) != 0
+                            && (state & (AFTER_VOWEL_WITH_PRECOMPOSED_ACCENT | AFTER_VOWEL_WITH_COMBINING_ACCENT)) != 0
+                            && (upper == 'Ι' || upper == 'Υ')) {
+                        data |= (state & AFTER_VOWEL_WITH_PRECOMPOSED_ACCENT) != 0 ? HAS_DIALYTIKA
+                                : HAS_COMBINING_DIALYTIKA;
                     }
                     int numYpogegrammeni = 0;  // Map each one to a trailing, spacing, capital iota.
                     if ((data & HAS_YPOGEGRAMMENI) != 0) {
                         numYpogegrammeni = 1;
                     }
+                    final boolean hasPrecomposedAccent = (data & HAS_ACCENT) != 0;
                     // Skip combining diacritics after this Greek letter.
                     while (nextIndex < src.length()) {
                         int diacriticData = getDiacriticData(src.charAt(nextIndex));
@@ -1443,7 +1447,8 @@
                         }
                     }
                     if ((data & HAS_VOWEL_AND_ACCENT_AND_DIALYTIKA) == HAS_VOWEL_AND_ACCENT) {
-                        nextState |= AFTER_VOWEL_WITH_ACCENT;
+                        nextState |= hasPrecomposedAccent ? AFTER_VOWEL_WITH_PRECOMPOSED_ACCENT
+                                                          : AFTER_VOWEL_WITH_COMBINING_ACCENT;
                     }
                     // Map according to Greek rules.
                     boolean addTonos = false;
@@ -1454,7 +1459,7 @@
                             !isFollowedByCasedLetter(src, nextIndex)) {
                         // Keep disjunctive "or" with (only) a tonos.
                         // We use the same "word boundary" conditions as for the Final_Sigma test.
-                        if (i == nextIndex) {
+                        if (hasPrecomposedAccent) {
                             upper = 'Ή';  // Preserve the precomposed form.
                         } else {
                             addTonos = true;
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 ffbef2c..29353e4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/CharacterPropertiesImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/CharacterPropertiesImpl.java
@@ -76,6 +76,14 @@
                 EmojiProps.INSTANCE.addPropertyStarts(incl);
                 break;
             }
+            case UCharacterProperty.SRC_IDSU:
+                // New in Unicode 15.1 for just two characters.
+                incl.add(0x2FFE);
+                incl.add(0x2FFF + 1);
+                break;
+            case UCharacterProperty.SRC_ID_COMPAT_MATH:
+                UCharacterProperty.mathCompat_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 f160e78..4d30687 100644
--- a/android_icu4j/src/main/java/android/icu/impl/DateNumberFormat.java
+++ b/android_icu4j/src/main/java/android/icu/impl/DateNumberFormat.java
@@ -247,7 +247,7 @@
         Number result = null;
         if (sawNumber) {
             num = negative ? num * (-1) : num;
-            result = Long.valueOf(num);
+            result = num;
             parsePosition.setIndex(base + offset);
         }
         return result;
diff --git a/android_icu4j/src/main/java/android/icu/impl/DayPeriodRules.java b/android_icu4j/src/main/java/android/icu/impl/DayPeriodRules.java
index 56d77f4..348a5dd 100644
--- a/android_icu4j/src/main/java/android/icu/impl/DayPeriodRules.java
+++ b/android_icu4j/src/main/java/android/icu/impl/DayPeriodRules.java
@@ -246,7 +246,7 @@
      * Get a DayPeriodRules object given a locale.
      * If data hasn't been loaded, it will be loaded for all locales at once.
      * @param locale locale for which the DayPeriodRules object is requested.
-     * @return a DayPeriodRules object for `locale`.
+     * @return a DayPeriodRules object for {@code locale}.
      */
     public static DayPeriodRules getInstance(ULocale locale) {
         String localeCode = locale.getBaseName();
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUBinary.java b/android_icu4j/src/main/java/android/icu/impl/ICUBinary.java
index b1f9a72..f5f0c6a 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUBinary.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUBinary.java
@@ -225,11 +225,15 @@
         abstract void addBaseNamesInFolder(String folder, String suffix, Set<String> names);
     }
 
+    // BEGIN Android-changed: Map file only once during ICUBinary initialization. Attempt to fix
+    // some apps not seeing metazones.res file. See b/339899412.
     private static final class SingleDataFile extends DataFile {
+        private final ByteBuffer bytes;
         private final File path;
 
         SingleDataFile(String item, File path) {
             super(item);
+            this.bytes = mapFile(path);
             this.path = path;
         }
         @Override
@@ -240,12 +244,13 @@
         @Override
         ByteBuffer getData(String requestedPath) {
             if (requestedPath.equals(itemPath)) {
-                return mapFile(path);
+                return bytes.duplicate();
             } else {
                 return null;
             }
         }
-
+    // END Android-changed: Map file only once during ICUBinary initialization. Attempt to fix
+    // some apps not seeing metazones.res files in b/339899412.
         @Override
         void addBaseNamesInFolder(String folder, String suffix, Set<String> names) {
             if (itemPath.length() > folder.length() + suffix.length() &&
diff --git a/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java b/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java
index 5ba1c2d..44705a6 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUCurrencyDisplayInfoProvider.java
@@ -13,6 +13,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.MissingResourceException;
+import java.util.Set;
 
 import android.icu.impl.CurrencyData.CurrencyDisplayInfo;
 import android.icu.impl.CurrencyData.CurrencyDisplayInfoProvider;
@@ -210,18 +211,19 @@
         public String getPluralName(String isoCode, String pluralKey ) {
             StandardPlural plural = StandardPlural.orNullFromString(pluralKey);
             String[] pluralsData = fetchPluralsData(isoCode);
+            Set<String> pluralKeys = fetchUnitPatterns().keySet();
 
             // See http://unicode.org/reports/tr35/#Currencies, especially the fallback rule.
             String result = null;
             if (plural != null) {
                 result = pluralsData[1 + plural.ordinal()];
             }
-            if (result == null && fallback) {
+            if (result == null && ( fallback || pluralKeys.contains(pluralKey) ) ) {
                 // First fall back to the "other" plural variant
                 // Note: If plural is already "other", this fallback is benign
                 result = pluralsData[1 + StandardPlural.OTHER.ordinal()];
             }
-            if (result == null && fallback) {
+            if (result == null && ( fallback || pluralKeys.contains(pluralKey) ) ) {
                 // If that fails, fall back to the display name
                 FormattingData formattingData = fetchFormattingData(isoCode);
                 result = formattingData.displayName;
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 8113915..3ed55c0 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUResourceBundle.java
@@ -274,7 +274,7 @@
                     }
                 }
             } catch (Throwable t) {
-                //System.err.println("Error in - " + new Integer(i).toString()
+                //System.err.println("Error in - " + i
                 // + " - " + t.toString());
                 // ignore the err - just skip that resource
             }
@@ -402,11 +402,11 @@
     }
 
     /**
-     * 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.
+     * Locates the resource specified by {@code path} in this resource bundle (performing any necessary fallback
+     * and following any aliases) and calls the specified {@code sink}'s {@code put()} method with that resource.
+     * Then walks the bundle's parent chain, calling {@code 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
+     * @param sink A {@code UResource.Sink} that gets called for each resource in the parent chain
      */
     public void getAllItemsWithFallback(String path, UResource.Sink sink)
             throws MissingResourceException {
@@ -435,16 +435,16 @@
     }
 
     /**
-     * Locates the resource specified by `path` in this resource bundle (performing any necessary fallback and
+     * Locates the resource specified by {@code 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
+     * following any aliases to get the individual resource values), and calls the specified {@code sink}'s {@code 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).
+     * @param sink A {@code 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 {
@@ -464,7 +464,43 @@
                         ReaderValue aliasedValue = new ReaderValue();
                         aliasedValue.reader = aliasedResourceImpl.wholeBundle.reader;
                         aliasedValue.res = aliasedResourceImpl.getResource();
-                        sink.put(key, aliasedValue, noFallback);
+
+                        if (aliasedValue.getType() != TABLE) {
+                            sink.put(key, aliasedValue, noFallback);
+                        } else {
+                            // if the resource we're aliasing over to is a table, the sink might iterate over its contents.
+                            // If it does, it'll get only the things defined in the actual alias target, not the things
+                            // the target inherits from its parent resources.  So we walk the parent chain for the *alias target*,
+                            // calling sink.put() for each of the parent tables we could be inheriting from.  This means
+                            // that sink.put() has to iterate over the children of multiple tables to get all of the inherited
+                            // resource values, but it already has to do that to handle normal vertical inheritance.
+                            int aliasedValueType = TABLE;
+                            String tablePath = aliasPath.substring("/LOCALE/".length());
+                            UResource.Key keyCopy = key.clone(); // sink.put() changes the key
+                            sink.put(keyCopy, aliasedValue, noFallback);
+                            while (aliasedValueType == TABLE && aliasedResource.getParent() != null) {
+                                ICUResourceBundle newAliasedResource = aliasedResource.getParent().findWithFallback(tablePath);
+                                if (newAliasedResource.key.equals(aliasedResource.key)) {
+                                    aliasedResource = newAliasedResource;
+                                } else {
+                                    // the findWithFallback() call above might follow an alias.  If it does, we'll get
+                                    // back the alias target at the wrong level (e.g., if we're in en_CA, we're calling
+                                    // findWithFallback() on en, and if it follows an alias, we get back the alias target
+                                    // in en, even if it also exists in en_CA).  So we check the keys to see if we followed
+                                    // an alias, and if we did, we re-fetch the alias target from our original resource bundle
+                                    tablePath = tablePath.substring(0, tablePath.lastIndexOf('/'));
+                                    tablePath += "/" + newAliasedResource.key;
+                                    aliasedResource = ICUResourceBundle.this.findWithFallback(tablePath);
+                                }
+                                aliasedResourceImpl = (ICUResourceBundleImpl) aliasedResource;
+                                aliasedValue = new ReaderValue();
+                                aliasedValue.reader = aliasedResourceImpl.wholeBundle.reader;
+                                aliasedValue.res = aliasedResourceImpl.getResource();
+                                aliasedValueType = aliasedValue.getType(); // sink.put() messes up the value
+                                keyCopy = key.clone(); // sink.put() messes up the key
+                                sink.put(keyCopy, aliasedValue, noFallback);
+                            }
+                        }
                     } else {
                         sink.put(key, value, noFallback);
                     }
@@ -1293,7 +1329,7 @@
         return result;
     }
 
-    private static String getParentLocaleID(String name, String origName, OpenType openType) {
+    public static String getParentLocaleID(String name, String origName, OpenType openType) {
         // early out if the locale ID has a variant code or ends with _
         if (name.endsWith("_") || !ULocale.getVariant(name).isEmpty()) {
             int lastUnderbarPos = name.lastIndexOf('_');
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 d6ebaa4..e34ad4c 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ICUService.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ICUService.java
@@ -597,6 +597,8 @@
                         Factory f = lIter.previous();
                         f.updateVisibleIDs(mutableMap);
                     }
+                    // Capture the return value in a local variable.
+                    // Avoids returning an idcache value changed by another thread (could be null after clearCaches()).
                     Map<String, Factory> result = Collections.unmodifiableMap(mutableMap);
                     this.idcache = result;
                     return result;
diff --git a/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java b/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java
index 6ed1cad..2ed8790 100644
--- a/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleFallbackData.java
@@ -20,19 +20,56 @@
 
     private static Map<String, String> buildDefaultScriptTable() {
         Map<String, String> t = new HashMap<>();
+        t.put("aaf", "Mlym");
+        t.put("aao", "Arab");
+        t.put("aat", "Grek");
         t.put("ab", "Cyrl");
-        t.put("abq", "Cyrl");
-        t.put("adp", "Tibt");
+        t.put("abh", "Arab");
+        t.put("abl", "Rjng");
+        t.put("abv", "Arab");
+        t.put("acm", "Arab");
+        t.put("acq", "Arab");
+        t.put("acw", "Arab");
+        t.put("acx", "Arab");
+        t.put("adf", "Arab");
+        t.put("adx", "Tibt");
         t.put("ady", "Cyrl");
         t.put("ae", "Avst");
         t.put("aeb", "Arab");
+        t.put("aec", "Arab");
+        t.put("aee", "Arab");
+        t.put("aeq", "Arab");
+        t.put("afb", "Arab");
+        t.put("agi", "Deva");
+        t.put("agj", "Ethi");
+        t.put("agx", "Cyrl");
+        t.put("ahg", "Ethi");
         t.put("aho", "Ahom");
-        t.put("ajt", "Arab");
+        t.put("ahr", "Deva");
+        t.put("aib", "Arab");
+        t.put("aij", "Hebr");
+        t.put("ain", "Kana");
+        t.put("aio", "Mymr");
+        t.put("aiq", "Arab");
         t.put("akk", "Xsux");
+        t.put("akv", "Cyrl");
+        t.put("alk", "Laoo");
+        t.put("all", "Mlym");
+        t.put("alr", "Cyrl");
         t.put("alt", "Cyrl");
+        t.put("alw", "Ethi");
         t.put("am", "Ethi");
+        t.put("ams", "Jpan");
+        t.put("amw", "Syrc");
+        t.put("ani", "Cyrl");
+        t.put("anp", "Deva");
+        t.put("anr", "Deva");
+        t.put("anu", "Ethi");
+        t.put("aot", "Beng");
         t.put("apc", "Arab");
         t.put("apd", "Arab");
+        t.put("aph", "Deva");
+        t.put("aqc", "Cyrl");
         t.put("ar", "Arab");
         t.put("arc", "Armi");
         t.put("arq", "Arab");
@@ -41,200 +78,525 @@
         t.put("arz", "Arab");
         t.put("as", "Beng");
         t.put("ase", "Sgnw");
+        t.put("ask", "Arab");
+        t.put("atn", "Arab");
+        t.put("atv", "Cyrl");
+        t.put("auj", "Arab");
+        t.put("auz", "Arab");
         t.put("av", "Cyrl");
+        t.put("avd", "Arab");
         t.put("avl", "Arab");
         t.put("awa", "Deva");
+        t.put("awn", "Ethi");
+        t.put("axm", "Armn");
+        t.put("ayh", "Arab");
+        t.put("ayl", "Arab");
+        t.put("ayn", "Arab");
+        t.put("ayp", "Arab");
         t.put("az_IQ", "Arab");
         t.put("az_IR", "Arab");
         t.put("az_RU", "Cyrl");
+        t.put("azb", "Arab");
         t.put("ba", "Cyrl");
         t.put("bal", "Arab");
         t.put("bap", "Deva");
         t.put("bax", "Bamu");
+        t.put("bbl", "Geor");
         t.put("bcq", "Ethi");
+        t.put("bdv", "Orya");
+        t.put("bdz", "Arab");
         t.put("be", "Cyrl");
+        t.put("bee", "Deva");
         t.put("bej", "Arab");
+        t.put("bfb", "Deva");
         t.put("bfq", "Taml");
         t.put("bft", "Arab");
+        t.put("bfu", "Tibt");
+        t.put("bfw", "Orya");
         t.put("bfy", "Deva");
+        t.put("bfz", "Deva");
         t.put("bg", "Cyrl");
         t.put("bgc", "Deva");
+        t.put("bgd", "Deva");
         t.put("bgn", "Arab");
+        t.put("bgp", "Arab");
+        t.put("bgq", "Deva");
+        t.put("bgw", "Deva");
         t.put("bgx", "Grek");
+        t.put("bha", "Deva");
         t.put("bhb", "Deva");
+        t.put("bhd", "Deva");
+        t.put("bhe", "Arab");
+        t.put("bhh", "Cyrl");
         t.put("bhi", "Deva");
+        t.put("bhj", "Deva");
+        t.put("bhm", "Arab");
+        t.put("bhn", "Syrc");
         t.put("bho", "Deva");
-        t.put("bji", "Ethi");
+        t.put("bht", "Takr");
+        t.put("bhu", "Deva");
+        t.put("biy", "Deva");
+        t.put("bjf", "Syrc");
         t.put("bjj", "Deva");
+        t.put("bjm", "Arab");
+        t.put("blk", "Mymr");
         t.put("blt", "Tavt");
+        t.put("bmj", "Deva");
         t.put("bn", "Beng");
+        t.put("bns", "Deva");
         t.put("bo", "Tibt");
+        t.put("bph", "Cyrl");
+        t.put("bpx", "Deva");
         t.put("bpy", "Beng");
         t.put("bqi", "Arab");
         t.put("bra", "Deva");
+        t.put("brb", "Khmr");
+        t.put("brd", "Deva");
         t.put("brh", "Arab");
+        t.put("brk", "Arab");
+        t.put("brv", "Laoo");
         t.put("brx", "Deva");
+        t.put("bsh", "Arab");
+        t.put("bsk", "Arab");
         t.put("bsq", "Bass");
         t.put("bst", "Ethi");
+        t.put("btd", "Batk");
+        t.put("btm", "Batk");
         t.put("btv", "Deva");
         t.put("bua", "Cyrl");
+        t.put("bwe", "Mymr");
+        t.put("bxm", "Cyrl");
+        t.put("bxu", "Mong");
+        t.put("byh", "Deva");
         t.put("byn", "Ethi");
+        t.put("byw", "Deva");
+        t.put("bzi", "Thai");
+        t.put("cbn", "Thai");
         t.put("ccp", "Cakm");
+        t.put("cde", "Telu");
+        t.put("cdh", "Deva");
+        t.put("cdi", "Gujr");
+        t.put("cdj", "Deva");
+        t.put("cdm", "Deva");
+        t.put("cdo", "Hans");
+        t.put("cdz", "Beng");
         t.put("ce", "Cyrl");
+        t.put("cgk", "Tibt");
+        t.put("chg", "Arab");
         t.put("chm", "Cyrl");
         t.put("chr", "Cher");
+        t.put("chx", "Deva");
+        t.put("cih", "Deva");
         t.put("cja", "Arab");
+        t.put("cji", "Cyrl");
         t.put("cjm", "Cham");
+        t.put("cjy", "Hans");
         t.put("ckb", "Arab");
+        t.put("ckt", "Cyrl");
+        t.put("clh", "Arab");
+        t.put("clw", "Cyrl");
         t.put("cmg", "Soyo");
+        t.put("cna", "Tibt");
+        t.put("cnp", "Hans");
+        t.put("cog", "Thai");
         t.put("cop", "Copt");
+        t.put("cpg", "Grek");
         t.put("cr", "Cans");
         t.put("crh", "Cyrl");
+        t.put("crj", "Cans");
         t.put("crk", "Cans");
         t.put("crl", "Cans");
+        t.put("crm", "Cans");
+        t.put("csh", "Mymr");
+        t.put("csp", "Hans");
         t.put("csw", "Cans");
         t.put("ctd", "Pauc");
+        t.put("ctg", "Beng");
+        t.put("ctn", "Deva");
+        t.put("ctt", "Taml");
         t.put("cu", "Cyrl");
+        t.put("cuu", "Lana");
         t.put("cv", "Cyrl");
+        t.put("czh", "Hans");
+        t.put("czk", "Hebr");
+        t.put("daq", "Deva");
         t.put("dar", "Cyrl");
         t.put("dcc", "Arab");
-        t.put("dgl", "Arab");
+        t.put("ddo", "Cyrl");
+        t.put("def", "Arab");
+        t.put("deh", "Arab");
+        t.put("der", "Beng");
+        t.put("dhi", "Deva");
+        t.put("dhn", "Gujr");
+        t.put("dho", "Deva");
+        t.put("dhw", "Deva");
+        t.put("dka", "Tibt");
+        t.put("dlg", "Cyrl");
         t.put("dmf", "Medf");
+        t.put("dmk", "Arab");
+        t.put("dml", "Arab");
+        t.put("dng", "Cyrl");
+        t.put("dnu", "Mymr");
+        t.put("dnv", "Mymr");
         t.put("doi", "Deva");
-        t.put("drh", "Mong");
+        t.put("dox", "Ethi");
+        t.put("dre", "Tibt");
+        t.put("drq", "Deva");
         t.put("drs", "Ethi");
+        t.put("dry", "Deva");
+        t.put("dso", "Orya");
         t.put("dty", "Deva");
+        t.put("dub", "Gujr");
+        t.put("duh", "Deva");
+        t.put("dus", "Deva");
         t.put("dv", "Thaa");
+        t.put("dwk", "Orya");
+        t.put("dwz", "Deva");
         t.put("dz", "Tibt");
+        t.put("dzl", "Tibt");
+        t.put("ecr", "Grek");
+        t.put("ecy", "Cprt");
         t.put("egy", "Egyp");
         t.put("eky", "Kali");
         t.put("el", "Grek");
+        t.put("emg", "Deva");
+        t.put("emu", "Deva");
+        t.put("enf", "Cyrl");
+        t.put("enh", "Cyrl");
+        t.put("era", "Taml");
         t.put("esg", "Gonm");
+        t.put("esh", "Arab");
         t.put("ett", "Ital");
+        t.put("eve", "Cyrl");
+        t.put("evn", "Cyrl");
         t.put("fa", "Arab");
+        t.put("fay", "Arab");
+        t.put("faz", "Arab");
         t.put("fia", "Arab");
+        t.put("fmu", "Deva");
         t.put("fub", "Arab");
         t.put("gan", "Hans");
+        t.put("gaq", "Orya");
+        t.put("gas", "Gujr");
+        t.put("gau", "Telu");
+        t.put("gbj", "Orya");
+        t.put("gbk", "Deva");
+        t.put("gbl", "Gujr");
         t.put("gbm", "Deva");
         t.put("gbz", "Arab");
+        t.put("gdb", "Orya");
+        t.put("gdo", "Cyrl");
+        t.put("gdx", "Deva");
         t.put("gez", "Ethi");
-        t.put("ggn", "Deva");
+        t.put("ggg", "Arab");
+        t.put("gha", "Arab");
+        t.put("ghe", "Deva");
+        t.put("ghr", "Arab");
+        t.put("ght", "Tibt");
+        t.put("gig", "Arab");
+        t.put("gin", "Cyrl");
         t.put("gjk", "Arab");
         t.put("gju", "Arab");
+        t.put("gld", "Cyrl");
+        t.put("glh", "Arab");
         t.put("glk", "Arab");
         t.put("gmv", "Ethi");
+        t.put("gmy", "Linb");
+        t.put("goe", "Tibt");
         t.put("gof", "Ethi");
+        t.put("gok", "Deva");
         t.put("gom", "Deva");
         t.put("gon", "Telu");
         t.put("got", "Goth");
+        t.put("gra", "Deva");
         t.put("grc", "Cprt");
         t.put("grt", "Beng");
+        t.put("gru", "Ethi");
         t.put("gu", "Gujr");
         t.put("gvr", "Deva");
         t.put("gwc", "Arab");
+        t.put("gwf", "Arab");
         t.put("gwt", "Arab");
+        t.put("gyo", "Deva");
+        t.put("gzi", "Arab");
         t.put("ha_CM", "Arab");
         t.put("ha_SD", "Arab");
+        t.put("hac", "Arab");
         t.put("hak", "Hans");
+        t.put("har", "Ethi");
         t.put("haz", "Arab");
+        t.put("hbo", "Hebr");
         t.put("hdy", "Ethi");
         t.put("he", "Hebr");
         t.put("hi", "Deva");
+        t.put("hii", "Takr");
+        t.put("hit", "Xsux");
+        t.put("hkh", "Arab");
+        t.put("hlb", "Deva");
         t.put("hlu", "Hluw");
         t.put("hmd", "Plrd");
+        t.put("hmj", "Bopo");
+        t.put("hmq", "Bopo");
         t.put("hnd", "Arab");
         t.put("hne", "Deva");
         t.put("hnj", "Hmnp");
+        t.put("hnj_AU", "Laoo");
+        t.put("hnj_CN", "Laoo");
+        t.put("hnj_FR", "Laoo");
+        t.put("hnj_GF", "Laoo");
+        t.put("hnj_LA", "Laoo");
+        t.put("hnj_MM", "Laoo");
+        t.put("hnj_SR", "Laoo");
+        t.put("hnj_TH", "Laoo");
+        t.put("hnj_VN", "Laoo");
         t.put("hno", "Arab");
         t.put("hoc", "Deva");
+        t.put("hoh", "Arab");
         t.put("hoj", "Deva");
+        t.put("how", "Hani");
+        t.put("hoy", "Deva");
+        t.put("hpo", "Mymr");
+        t.put("hrt", "Syrc");
+        t.put("hrz", "Arab");
         t.put("hsn", "Hans");
+        t.put("hss", "Arab");
+        t.put("htx", "Xsux");
+        t.put("hut", "Deva");
+        t.put("huy", "Hebr");
+        t.put("huz", "Cyrl");
         t.put("hy", "Armn");
+        t.put("hyw", "Armn");
         t.put("ii", "Yiii");
+        t.put("imy", "Lyci");
         t.put("inh", "Cyrl");
+        t.put("int", "Mymr");
+        t.put("ior", "Ethi");
+        t.put("iru", "Taml");
+        t.put("isk", "Arab");
+        t.put("itk", "Hebr");
+        t.put("itl", "Cyrl");
         t.put("iu", "Cans");
         t.put("iw", "Hebr");
         t.put("ja", "Jpan");
+        t.put("jad", "Arab");
+        t.put("jat", "Arab");
+        t.put("jbe", "Hebr");
+        t.put("jbn", "Arab");
+        t.put("jct", "Cyrl");
+        t.put("jda", "Tibt");
+        t.put("jdg", "Arab");
+        t.put("jdt", "Cyrl");
+        t.put("jee", "Deva");
+        t.put("jge", "Geor");
         t.put("ji", "Hebr");
+        t.put("jje", "Hang");
+        t.put("jkm", "Mymr");
         t.put("jml", "Deva");
+        t.put("jna", "Takr");
+        t.put("jnd", "Arab");
+        t.put("jnl", "Deva");
+        t.put("jns", "Deva");
+        t.put("jog", "Arab");
+        t.put("jpa", "Hebr");
+        t.put("jpr", "Hebr");
+        t.put("jrb", "Hebr");
+        t.put("jrb_MA", "Arab");
+        t.put("jul", "Deva");
+        t.put("jun", "Orya");
+        t.put("juy", "Orya");
+        t.put("jya", "Tibt");
+        t.put("jye", "Hebr");
         t.put("ka", "Geor");
         t.put("kaa", "Cyrl");
+        t.put("kap", "Cyrl");
         t.put("kaw", "Kawi");
         t.put("kbd", "Cyrl");
+        t.put("kbu", "Arab");
         t.put("kby", "Arab");
+        t.put("kca", "Cyrl");
+        t.put("kdq", "Beng");
         t.put("kdt", "Thai");
+        t.put("ket", "Cyrl");
+        t.put("kex", "Deva");
+        t.put("key", "Telu");
+        t.put("kfa", "Knda");
+        t.put("kfb", "Deva");
+        t.put("kfc", "Telu");
+        t.put("kfd", "Knda");
+        t.put("kfe", "Taml");
+        t.put("kfh", "Mlym");
+        t.put("kfi", "Taml");
+        t.put("kfk", "Deva");
+        t.put("kfm", "Arab");
+        t.put("kfp", "Deva");
+        t.put("kfq", "Deva");
         t.put("kfr", "Deva");
+        t.put("kfs", "Deva");
+        t.put("kfx", "Deva");
         t.put("kfy", "Deva");
+        t.put("kgj", "Deva");
+        t.put("kgy", "Deva");
         t.put("khb", "Talu");
+        t.put("khf", "Thai");
+        t.put("khg", "Tibt");
         t.put("khn", "Deva");
         t.put("kht", "Mymr");
+        t.put("khv", "Cyrl");
         t.put("khw", "Arab");
+        t.put("kif", "Deva");
+        t.put("kim", "Cyrl");
+        t.put("kip", "Deva");
         t.put("kjg", "Laoo");
+        t.put("kjh", "Cyrl");
+        t.put("kjl", "Deva");
+        t.put("kjo", "Deva");
+        t.put("kjp", "Mymr");
+        t.put("kjt", "Thai");
         t.put("kk", "Cyrl");
         t.put("kk_AF", "Arab");
         t.put("kk_CN", "Arab");
         t.put("kk_IR", "Arab");
         t.put("kk_MN", "Arab");
+        t.put("kkf", "Tibt");
+        t.put("kkh", "Lana");
+        t.put("kkt", "Deva");
+        t.put("kle", "Deva");
+        t.put("klj", "Arab");
+        t.put("klr", "Deva");
         t.put("km", "Khmr");
+        t.put("kmj", "Deva");
+        t.put("kmz", "Arab");
         t.put("kn", "Knda");
         t.put("ko", "Kore");
         t.put("koi", "Cyrl");
         t.put("kok", "Deva");
+        t.put("kpt", "Cyrl");
+        t.put("kpy", "Cyrl");
+        t.put("kqd", "Syrc");
         t.put("kqy", "Ethi");
+        t.put("kra", "Deva");
         t.put("krc", "Cyrl");
+        t.put("krk", "Cyrl");
+        t.put("krr", "Khmr");
         t.put("kru", "Deva");
+        t.put("krv", "Khmr");
         t.put("ks", "Arab");
+        t.put("ksu", "Mymr");
+        t.put("ksw", "Mymr");
+        t.put("ksz", "Deva");
         t.put("ktb", "Ethi");
+        t.put("ktl", "Arab");
+        t.put("ktp", "Plrd");
         t.put("ku_LB", "Arab");
+        t.put("kuf", "Laoo");
         t.put("kum", "Cyrl");
         t.put("kv", "Cyrl");
+        t.put("kva", "Cyrl");
+        t.put("kvq", "Mymr");
+        t.put("kvt", "Mymr");
         t.put("kvx", "Arab");
-        t.put("kxc", "Ethi");
-        t.put("kxl", "Deva");
+        t.put("kvy", "Kali");
+        t.put("kxf", "Mymr");
+        t.put("kxk", "Mymr");
         t.put("kxm", "Thai");
         t.put("kxp", "Arab");
         t.put("ky", "Cyrl");
         t.put("ky_CN", "Arab");
-        t.put("kzh", "Arab");
+        t.put("kyu", "Kali");
+        t.put("kyv", "Deva");
+        t.put("kyw", "Deva");
         t.put("lab", "Lina");
         t.put("lad", "Hebr");
+        t.put("lae", "Deva");
         t.put("lah", "Arab");
+        t.put("lbc", "Lisu");
         t.put("lbe", "Cyrl");
+        t.put("lbf", "Deva");
+        t.put("lbj", "Tibt");
+        t.put("lbm", "Deva");
+        t.put("lbo", "Laoo");
+        t.put("lbr", "Deva");
         t.put("lcp", "Thai");
         t.put("lep", "Lepc");
         t.put("lez", "Cyrl");
+        t.put("lhm", "Deva");
+        t.put("lhs", "Syrc");
         t.put("lif", "Deva");
         t.put("lis", "Lisu");
+        t.put("lkh", "Tibt");
         t.put("lki", "Arab");
+        t.put("lmh", "Deva");
         t.put("lmn", "Telu");
         t.put("lo", "Laoo");
+        t.put("loy", "Deva");
+        t.put("lpo", "Plrd");
         t.put("lrc", "Arab");
+        t.put("lrk", "Arab");
+        t.put("lrl", "Arab");
+        t.put("lsa", "Arab");
+        t.put("lsd", "Hebr");
+        t.put("lss", "Arab");
+        t.put("luk", "Tibt");
+        t.put("luu", "Deva");
+        t.put("luv", "Arab");
         t.put("luz", "Arab");
         t.put("lwl", "Thai");
+        t.put("lwm", "Thai");
+        t.put("lya", "Tibt");
         t.put("lzh", "Hans");
         t.put("mag", "Deva");
         t.put("mai", "Deva");
         t.put("man_GN", "Nkoo");
+        t.put("mby", "Arab");
         t.put("mde", "Arab");
         t.put("mdf", "Cyrl");
         t.put("mdx", "Ethi");
+        t.put("mdy", "Ethi");
         t.put("mfa", "Arab");
+        t.put("mfi", "Arab");
         t.put("mgp", "Deva");
+        t.put("mhj", "Arab");
+        t.put("mid", "Mand");
+        t.put("mjl", "Deva");
+        t.put("mjq", "Mlym");
+        t.put("mjr", "Mlym");
+        t.put("mjt", "Deva");
+        t.put("mju", "Telu");
+        t.put("mjv", "Mlym");
+        t.put("mjz", "Deva");
         t.put("mk", "Cyrl");
+        t.put("mkb", "Deva");
+        t.put("mke", "Deva");
         t.put("mki", "Arab");
+        t.put("mkm", "Thai");
         t.put("ml", "Mlym");
+        t.put("mlf", "Thai");
         t.put("mn", "Cyrl");
         t.put("mn_CN", "Mong");
         t.put("mni", "Beng");
+        t.put("mnj", "Arab");
+        t.put("mns", "Cyrl");
         t.put("mnw", "Mymr");
+        t.put("mpz", "Thai");
         t.put("mr", "Deva");
+        t.put("mra", "Thai");
         t.put("mrd", "Deva");
         t.put("mrj", "Cyrl");
         t.put("mro", "Mroo");
+        t.put("mrr", "Deva");
         t.put("ms_CC", "Arab");
+        t.put("mtm", "Cyrl");
         t.put("mtr", "Deva");
+        t.put("mud", "Cyrl");
+        t.put("muk", "Tibt");
+        t.put("mut", "Deva");
+        t.put("muv", "Taml");
+        t.put("muz", "Ethi");
+        t.put("mvf", "Mong");
         t.put("mvy", "Arab");
+        t.put("mvz", "Ethi");
         t.put("mwr", "Deva");
+        t.put("mwt", "Mymr");
         t.put("mww", "Hmnp");
         t.put("my", "Mymr");
         t.put("mym", "Ethi");
@@ -242,134 +604,407 @@
         t.put("myz", "Mand");
         t.put("mzn", "Arab");
         t.put("nan", "Hans");
+        t.put("nao", "Deva");
+        t.put("ncd", "Deva");
+        t.put("ncq", "Laoo");
+        t.put("ndf", "Cyrl");
         t.put("ne", "Deva");
+        t.put("neg", "Cyrl");
+        t.put("neh", "Tibt");
+        t.put("nei", "Xsux");
         t.put("new", "Deva");
+        t.put("ngt", "Laoo");
+        t.put("nio", "Cyrl");
+        t.put("nit", "Telu");
+        t.put("niv", "Cyrl");
+        t.put("nli", "Arab");
+        t.put("nlm", "Arab");
+        t.put("nlx", "Deva");
+        t.put("nmm", "Deva");
         t.put("nnp", "Wcho");
         t.put("nod", "Lana");
         t.put("noe", "Deva");
+        t.put("nog", "Cyrl");
+        t.put("noi", "Deva");
         t.put("non", "Runr");
+        t.put("nos", "Yiii");
+        t.put("npb", "Tibt");
         t.put("nqo", "Nkoo");
+        t.put("nsd", "Yiii");
+        t.put("nsf", "Yiii");
         t.put("nsk", "Cans");
         t.put("nst", "Tnsa");
+        t.put("nsv", "Yiii");
+        t.put("nty", "Yiii");
+        t.put("ntz", "Arab");
+        t.put("nwc", "Newa");
+        t.put("nwx", "Deva");
+        t.put("nyl", "Thai");
+        t.put("nyq", "Arab");
+        t.put("oaa", "Cyrl");
+        t.put("oac", "Cyrl");
+        t.put("oar", "Syrc");
+        t.put("oav", "Geor");
+        t.put("obm", "Phnx");
+        t.put("obr", "Mymr");
+        t.put("odk", "Arab");
+        t.put("oht", "Xsux");
         t.put("oj", "Cans");
         t.put("ojs", "Cans");
+        t.put("okm", "Hang");
+        t.put("oko", "Hani");
+        t.put("okz", "Khmr");
+        t.put("ola", "Deva");
+        t.put("ole", "Tibt");
+        t.put("omk", "Cyrl");
+        t.put("omp", "Mtei");
+        t.put("omr", "Modi");
+        t.put("oon", "Deva");
         t.put("or", "Orya");
+        t.put("ort", "Telu");
         t.put("oru", "Arab");
+        t.put("orv", "Cyrl");
         t.put("os", "Cyrl");
         t.put("osa", "Osge");
+        t.put("osc", "Ital");
+        t.put("osi", "Java");
         t.put("ota", "Arab");
+        t.put("otb", "Tibt");
         t.put("otk", "Orkh");
-        t.put("oui", "Ougr");
+        t.put("oty", "Gran");
         t.put("pa", "Guru");
         t.put("pa_PK", "Arab");
         t.put("pal", "Phli");
+        t.put("paq", "Cyrl");
+        t.put("pbt", "Arab");
+        t.put("pcb", "Khmr");
+        t.put("pce", "Mymr");
+        t.put("pcf", "Mlym");
+        t.put("pcg", "Mlym");
+        t.put("pch", "Deva");
+        t.put("pci", "Deva");
+        t.put("pcj", "Telu");
+        t.put("peg", "Orya");
         t.put("peo", "Xpeo");
+        t.put("pgd", "Khar");
+        t.put("pgg", "Deva");
+        t.put("pgl", "Ogam");
+        t.put("pgn", "Ital");
+        t.put("phd", "Deva");
+        t.put("phk", "Mymr");
         t.put("phl", "Arab");
         t.put("phn", "Phnx");
+        t.put("pho", "Laoo");
+        t.put("phr", "Arab");
+        t.put("pht", "Thai");
+        t.put("phv", "Arab");
+        t.put("phw", "Deva");
+        t.put("pi", "Sinh");
         t.put("pka", "Brah");
+        t.put("pkr", "Mlym");
+        t.put("plk", "Arab");
+        t.put("pll", "Mymr");
+        t.put("pmh", "Brah");
         t.put("pnt", "Grek");
-        t.put("ppa", "Deva");
         t.put("pra", "Khar");
+        t.put("prc", "Arab");
         t.put("prd", "Arab");
+        t.put("prt", "Thai");
+        t.put("prx", "Arab");
         t.put("ps", "Arab");
+        t.put("psh", "Arab");
+        t.put("psi", "Arab");
+        t.put("pst", "Arab");
+        t.put("pum", "Deva");
+        t.put("pwo", "Mymr");
+        t.put("pwr", "Deva");
+        t.put("pww", "Thai");
+        t.put("pyx", "Mymr");
+        t.put("qxq", "Arab");
+        t.put("raa", "Deva");
+        t.put("rab", "Deva");
+        t.put("raf", "Deva");
+        t.put("rah", "Beng");
         t.put("raj", "Deva");
+        t.put("rav", "Deva");
+        t.put("rbb", "Mymr");
+        t.put("rdb", "Arab");
+        t.put("rei", "Orya");
         t.put("rhg", "Rohg");
-        t.put("rif", "Tfng");
+        t.put("rji", "Deva");
         t.put("rjs", "Deva");
+        t.put("rka", "Khmr");
+        t.put("rki", "Mymr");
         t.put("rkt", "Beng");
+        t.put("rmi", "Armn");
         t.put("rmt", "Arab");
+        t.put("rmz", "Mymr");
+        t.put("rom_BG", "Cyrl");
+        t.put("rsk", "Cyrl");
+        t.put("rtw", "Deva");
         t.put("ru", "Cyrl");
         t.put("rue", "Cyrl");
+        t.put("rut", "Cyrl");
+        t.put("rwr", "Deva");
         t.put("ryu", "Kana");
         t.put("sa", "Deva");
         t.put("sah", "Cyrl");
+        t.put("sam", "Samr");
         t.put("sat", "Olck");
         t.put("saz", "Saur");
+        t.put("sbn", "Arab");
+        t.put("sbu", "Tibt");
         t.put("sck", "Deva");
         t.put("scl", "Arab");
+        t.put("scl_IN", "Arab");
+        t.put("scp", "Deva");
+        t.put("sct", "Laoo");
+        t.put("scu", "Takr");
+        t.put("scx", "Grek");
         t.put("sd", "Arab");
         t.put("sd_IN", "Deva");
+        t.put("sdb", "Arab");
+        t.put("sdf", "Arab");
+        t.put("sdg", "Arab");
         t.put("sdh", "Arab");
+        t.put("sds", "Arab");
+        t.put("sel", "Cyrl");
+        t.put("sfm", "Plrd");
         t.put("sga", "Ogam");
+        t.put("sgh", "Cyrl");
+        t.put("sgj", "Deva");
+        t.put("sgr", "Arab");
+        t.put("sgt", "Tibt");
         t.put("sgw", "Ethi");
+        t.put("sgy", "Arab");
+        t.put("shd", "Arab");
         t.put("shi", "Tfng");
+        t.put("shm", "Arab");
         t.put("shn", "Mymr");
         t.put("shu", "Arab");
+        t.put("shv", "Arab");
         t.put("si", "Sinh");
+        t.put("sia", "Cyrl");
+        t.put("sip", "Tibt");
+        t.put("siy", "Arab");
+        t.put("siz", "Arab");
+        t.put("sjd", "Cyrl");
+        t.put("sjp", "Deva");
+        t.put("sjt", "Cyrl");
+        t.put("skb", "Thai");
+        t.put("skj", "Deva");
         t.put("skr", "Arab");
+        t.put("smh", "Yiii");
         t.put("smp", "Samr");
+        t.put("smu", "Khmr");
+        t.put("smy", "Arab");
+        t.put("soa", "Tavt");
         t.put("sog", "Sogd");
+        t.put("soi", "Deva");
         t.put("sou", "Thai");
+        t.put("spt", "Tibt");
+        t.put("spv", "Orya");
+        t.put("sqo", "Arab");
+        t.put("sqq", "Laoo");
+        t.put("sqt", "Arab");
         t.put("sr", "Cyrl");
         t.put("srb", "Sora");
+        t.put("srh", "Arab");
         t.put("srx", "Deva");
+        t.put("srz", "Arab");
+        t.put("ssh", "Arab");
+        t.put("sss", "Laoo");
+        t.put("sts", "Arab");
+        t.put("stv", "Ethi");
+        t.put("sty", "Cyrl");
+        t.put("suz", "Deva");
+        t.put("sva", "Geor");
         t.put("swb", "Arab");
+        t.put("swi", "Hani");
         t.put("swv", "Deva");
+        t.put("syc", "Syrc");
         t.put("syl", "Beng");
+        t.put("syn", "Syrc");
         t.put("syr", "Syrc");
+        t.put("syw", "Deva");
         t.put("ta", "Taml");
+        t.put("tab", "Cyrl");
         t.put("taj", "Deva");
+        t.put("tbk", "Tagb");
+        t.put("tcn", "Tibt");
+        t.put("tco", "Mymr");
+        t.put("tcx", "Taml");
         t.put("tcy", "Knda");
+        t.put("tda", "Tfng");
+        t.put("tdb", "Deva");
         t.put("tdd", "Tale");
         t.put("tdg", "Deva");
         t.put("tdh", "Deva");
         t.put("te", "Telu");
+        t.put("tes", "Java");
         t.put("tg", "Cyrl");
         t.put("tg_PK", "Arab");
+        t.put("tge", "Deva");
+        t.put("tgf", "Tibt");
         t.put("th", "Thai");
+        t.put("the", "Deva");
+        t.put("thf", "Deva");
+        t.put("thi", "Tale");
         t.put("thl", "Deva");
+        t.put("thm", "Thai");
         t.put("thq", "Deva");
         t.put("thr", "Deva");
+        t.put("ths", "Deva");
         t.put("ti", "Ethi");
         t.put("tig", "Ethi");
+        t.put("tij", "Deva");
+        t.put("tin", "Cyrl");
+        t.put("tjl", "Mymr");
+        t.put("tjo", "Arab");
+        t.put("tkb", "Deva");
+        t.put("tks", "Arab");
         t.put("tkt", "Deva");
+        t.put("tmr", "Syrc");
+        t.put("tnv", "Cakm");
+        t.put("tov", "Arab");
+        t.put("tpu", "Khmr");
+        t.put("tra", "Arab");
+        t.put("trg", "Hebr");
+        t.put("trm", "Arab");
         t.put("trw", "Arab");
         t.put("tsd", "Grek");
-        t.put("tsf", "Deva");
         t.put("tsj", "Tibt");
         t.put("tt", "Cyrl");
+        t.put("tth", "Laoo");
+        t.put("tto", "Laoo");
         t.put("tts", "Thai");
+        t.put("tvn", "Mymr");
+        t.put("twm", "Deva");
         t.put("txg", "Tang");
         t.put("txo", "Toto");
+        t.put("tyr", "Tavt");
         t.put("tyv", "Cyrl");
+        t.put("ude", "Cyrl");
+        t.put("udg", "Mlym");
         t.put("udi", "Aghb");
         t.put("udm", "Cyrl");
         t.put("ug", "Arab");
         t.put("ug_KZ", "Cyrl");
         t.put("ug_MN", "Cyrl");
         t.put("uga", "Ugar");
+        t.put("ugh", "Cyrl");
+        t.put("ugo", "Thai");
         t.put("uk", "Cyrl");
+        t.put("uki", "Orya");
+        t.put("ulc", "Cyrl");
         t.put("unr", "Beng");
         t.put("unr_NP", "Deva");
         t.put("unx", "Beng");
         t.put("ur", "Arab");
+        t.put("urk", "Thai");
+        t.put("ush", "Arab");
+        t.put("uum", "Grek");
         t.put("uz_AF", "Arab");
         t.put("uz_CN", "Cyrl");
+        t.put("uzs", "Arab");
+        t.put("vaa", "Taml");
+        t.put("vaf", "Arab");
+        t.put("vah", "Deva");
         t.put("vai", "Vaii");
+        t.put("vas", "Deva");
+        t.put("vav", "Deva");
+        t.put("vay", "Deva");
+        t.put("vgr", "Arab");
+        t.put("vmd", "Knda");
+        t.put("vmh", "Arab");
         t.put("wal", "Ethi");
+        t.put("wbk", "Arab");
         t.put("wbq", "Telu");
         t.put("wbr", "Deva");
+        t.put("wlo", "Arab");
+        t.put("wme", "Deva");
+        t.put("wne", "Arab");
         t.put("wni", "Arab");
         t.put("wsg", "Gong");
+        t.put("wsv", "Arab");
         t.put("wtm", "Deva");
         t.put("wuu", "Hans");
+        t.put("xal", "Cyrl");
+        t.put("xan", "Ethi");
+        t.put("xas", "Cyrl");
         t.put("xco", "Chrs");
         t.put("xcr", "Cari");
+        t.put("xdq", "Cyrl");
+        t.put("xhe", "Arab");
+        t.put("xhm", "Khmr");
+        t.put("xis", "Orya");
+        t.put("xka", "Arab");
+        t.put("xkc", "Arab");
+        t.put("xkj", "Arab");
+        t.put("xkp", "Arab");
         t.put("xlc", "Lyci");
         t.put("xld", "Lydi");
+        t.put("xly", "Elym");
         t.put("xmf", "Geor");
         t.put("xmn", "Mani");
         t.put("xmr", "Merc");
         t.put("xna", "Narb");
         t.put("xnr", "Deva");
+        t.put("xpg", "Grek");
+        t.put("xpi", "Ogam");
+        t.put("xpm", "Cyrl");
         t.put("xpr", "Prti");
+        t.put("xrm", "Cyrl");
+        t.put("xrn", "Cyrl");
         t.put("xsa", "Sarb");
         t.put("xsr", "Deva");
+        t.put("xub", "Taml");
+        t.put("xuj", "Taml");
+        t.put("xve", "Ital");
+        t.put("xvi", "Arab");
+        t.put("xwo", "Cyrl");
+        t.put("xzh", "Marc");
+        t.put("yai", "Cyrl");
+        t.put("ybh", "Deva");
+        t.put("ybi", "Deva");
+        t.put("ydg", "Arab");
+        t.put("yea", "Mlym");
+        t.put("yej", "Grek");
+        t.put("yeu", "Telu");
+        t.put("ygp", "Plrd");
+        t.put("yhd", "Hebr");
         t.put("yi", "Hebr");
+        t.put("yig", "Yiii");
+        t.put("yih", "Hebr");
+        t.put("yiv", "Yiii");
+        t.put("ykg", "Cyrl");
+        t.put("yna", "Plrd");
+        t.put("ynk", "Cyrl");
+        t.put("yoi", "Jpan");
+        t.put("yoy", "Thai");
+        t.put("yrk", "Cyrl");
+        t.put("ysd", "Yiii");
+        t.put("ysn", "Yiii");
+        t.put("ysp", "Yiii");
+        t.put("ysr", "Cyrl");
+        t.put("ysy", "Plrd");
+        t.put("yud", "Hebr");
         t.put("yue", "Hant");
         t.put("yue_CN", "Hans");
+        t.put("yug", "Cyrl");
+        t.put("yux", "Cyrl");
+        t.put("ywq", "Plrd");
+        t.put("ywu", "Plrd");
+        t.put("zau", "Tibt");
+        t.put("zba", "Arab");
+        t.put("zch", "Hani");
         t.put("zdj", "Arab");
+        t.put("zeh", "Hani");
+        t.put("zen", "Tfng");
+        t.put("zgb", "Hani");
         t.put("zgh", "Tfng");
+        t.put("zgm", "Hani");
+        t.put("zgn", "Hani");
         t.put("zh", "Hans");
         t.put("zh_AU", "Hant");
         t.put("zh_BN", "Hant");
@@ -386,8 +1021,20 @@
         t.put("zh_TW", "Hant");
         t.put("zh_US", "Hant");
         t.put("zh_VN", "Hant");
+        t.put("zhd", "Hani");
         t.put("zhx", "Nshu");
+        t.put("zko", "Cyrl");
         t.put("zkt", "Kits");
+        t.put("zkz", "Cyrl");
+        t.put("zlj", "Hani");
+        t.put("zln", "Hani");
+        t.put("zlq", "Hani");
+        t.put("zqe", "Hani");
+        t.put("zrp", "Hebr");
+        t.put("zum", "Arab");
+        t.put("zyg", "Hani");
+        t.put("zyn", "Hani");
+        t.put("zzj", "Hani");
         return Collections.unmodifiableMap(t);
     }
 
@@ -442,6 +1089,7 @@
         t.put("en_GM", "en_001");
         t.put("en_GY", "en_001");
         t.put("en_HK", "en_001");
+        t.put("en_ID", "en_001");
         t.put("en_IE", "en_001");
         t.put("en_IL", "en_001");
         t.put("en_IM", "en_001");
@@ -514,6 +1162,7 @@
         t.put("es_EC", "es_419");
         t.put("es_GT", "es_419");
         t.put("es_HN", "es_419");
+        t.put("es_JP", "es_419");
         t.put("es_MX", "es_419");
         t.put("es_NI", "es_419");
         t.put("es_PA", "es_419");
@@ -533,6 +1182,9 @@
         t.put("kk_Arab", "root");
         t.put("ks_Deva", "root");
         t.put("ku_Arab", "root");
+        t.put("kxv_Deva", "root");
+        t.put("kxv_Orya", "root");
+        t.put("kxv_Telu", "root");
         t.put("ky_Arab", "root");
         t.put("ky_Latn", "root");
         t.put("ml_Arab", "root");
@@ -541,6 +1193,7 @@
         t.put("ms_Arab", "root");
         t.put("nb", "no");
         t.put("nn", "no");
+        t.put("no_NO", "no");
         t.put("pa_Arab", "root");
         t.put("pt_AO", "pt_PT");
         t.put("pt_CH", "pt_PT");
diff --git a/android_icu4j/src/main/java/android/icu/impl/LocaleIDParser.java b/android_icu4j/src/main/java/android/icu/impl/LocaleIDParser.java
index a9f3186..10e0665 100644
--- a/android_icu4j/src/main/java/android/icu/impl/LocaleIDParser.java
+++ b/android_icu4j/src/main/java/android/icu/impl/LocaleIDParser.java
@@ -367,6 +367,15 @@
         }
     }
 
+    // There are no strict limitation of the syntax of variant in the legacy
+    // locale format. If the locale is constructed from unicode_locale_id
+    // as defined in UTS35, then we know each unicode_variant_subtag
+    // could have max length of 8 ((alphanum{5,8} | digit alphanum{3})
+    // 179 would allow 20 unicode_variant_subtag with sep in the
+    // unicode_locale_id
+    // 8*20 + 1*(20-1) = 179
+    private static final int MAX_VARIANTS_LENGTH = 179;
+
     /**
      * Advance index past variant, and accumulate normalized variant in buffer.  This ignores
      * the codepage information from POSIX ids.  Index must be immediately after the country
@@ -434,10 +443,12 @@
                     c = UNDERSCORE;
                 }
                 append(c);
+                if (buffer.length() - oldBlen > MAX_VARIANTS_LENGTH) {
+                    throw new IllegalArgumentException("variants is too long");
+                }
             }
         }
         --index; // unget
-
         return oldBlen;
     }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/Norm2AllModes.java b/android_icu4j/src/main/java/android/icu/impl/Norm2AllModes.java
index 701f311..1b1c6f8 100644
--- a/android_icu4j/src/main/java/android/icu/impl/Norm2AllModes.java
+++ b/android_icu4j/src/main/java/android/icu/impl/Norm2AllModes.java
@@ -328,6 +328,9 @@
     public static Norm2AllModes getNFKC_CFInstance() {
         return getInstanceFromSingleton(NFKC_CFSingleton.INSTANCE);
     }
+    public static Norm2AllModes getNFKC_SCFInstance() {
+        return getInstanceFromSingleton(NFKC_SCFSingleton.INSTANCE);
+    }
     // For use in properties APIs.
     public static Normalizer2WithImpl getN2WithImpl(int index) {
         switch(index) {
@@ -404,4 +407,7 @@
     private static final class NFKC_CFSingleton {
         private static final Norm2AllModesSingleton INSTANCE=new Norm2AllModesSingleton("nfkc_cf");
     }
+    private static final class NFKC_SCFSingleton {
+        private static final Norm2AllModesSingleton INSTANCE=new Norm2AllModesSingleton("nfkc_scf");
+    }
 }
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 4673179..83abdf3 100644
--- a/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
+++ b/android_icu4j/src/main/java/android/icu/impl/OlsonTimeZone.java
@@ -477,7 +477,7 @@
     /**
      * Construct from a resource bundle
      * @param top the top-level zoneinfo resource bundle.  This is used
-     * to lookup the rule that `res' may refer to, if there is one.
+     * to lookup the rule that {@code res} may refer to, if there is one.
      * @param res the resource bundle of the zone to be constructed
      * @param id time zone ID
      */
diff --git a/android_icu4j/src/main/java/android/icu/impl/PropsVectors.java b/android_icu4j/src/main/java/android/icu/impl/PropsVectors.java
index 99f52fd..490d48a 100644
--- a/android_icu4j/src/main/java/android/icu/impl/PropsVectors.java
+++ b/android_icu4j/src/main/java/android/icu/impl/PropsVectors.java
@@ -380,7 +380,7 @@
         // sort the properties vectors to find unique vector values
         Integer[] indexArray = new Integer[rows];
         for (int i = 0; i < rows; ++i) {
-            indexArray[i] = Integer.valueOf(columns * i);
+            indexArray[i] = columns * i;
         }
 
         Arrays.sort(indexArray, new Comparator<Integer>() {
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 5b7c16a..b8b3ced 100644
--- a/android_icu4j/src/main/java/android/icu/impl/RuleCharacterIterator.java
+++ b/android_icu4j/src/main/java/android/icu/impl/RuleCharacterIterator.java
@@ -219,11 +219,11 @@
      * }
      * iterator.setPos(pos);
      *
-     * @param p a position object previously returned by getPos(),
+     * @param p a position object previously returned by {@code getPos()},
      * or null.  If not null, it will be updated and returned.  If
      * null, a new position object will be allocated and returned.
-     * @return a position object which may be passed to setPos(),
-     * either `p,' or if `p' == null, a newly-allocated object
+     * @return a position object which may be passed to setPos(), either
+     * {@code p}, or if {@code p} == null, a newly-allocated object
      */
     public Position getPos(Position p) {
         if (p == null) {
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 1c6875b..82d948b 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCaseProps.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCaseProps.java
@@ -264,34 +264,6 @@
      * - for k include the Kelvin sign
      */
     public final void addCaseClosure(int c, UnicodeSet set) {
-        /*
-         * Hardcode the case closure of i and its relatives and ignore the
-         * data file data for these characters.
-         * The Turkic dotless i and dotted I with their case mapping conditions
-         * and case folding option make the related characters behave specially.
-         * This code matches their closure behavior to their case folding behavior.
-         */
-
-        switch(c) {
-        case 0x49:
-            /* regular i and I are in one equivalence class */
-            set.add(0x69);
-            return;
-        case 0x69:
-            set.add(0x49);
-            return;
-        case 0x130:
-            /* dotted I is in a class with <0069 0307> (for canonical equivalence with <0049 0307>) */
-            set.add(iDot);
-            return;
-        case 0x131:
-            /* dotless i is in a class by itself */
-            return;
-        default:
-            /* otherwise use the data file data */
-            break;
-        }
-
         int props=trie.get(c);
         if(!propsHasException(props)) {
             if(getTypeFromProps(props)!=NONE) {
@@ -306,19 +278,41 @@
              * c has exceptions, so there may be multiple simple and/or
              * full case mappings. Add them all.
              */
-            int excOffset0, excOffset=getExceptionsOffset(props);
-            int closureOffset;
+            int excOffset=getExceptionsOffset(props);
             int excWord=exceptions.charAt(excOffset++);
-            int index, closureLength, fullLength, length;
+            int excOffset0=excOffset;
 
-            excOffset0=excOffset;
+            // Hardcode the case closure of i and its relatives and ignore the
+            // data file data for these characters.
+            // The Turkic dotless i and dotted I with their case mapping conditions
+            // and case folding option make the related characters behave specially.
+            // This code matches their closure behavior to their case folding behavior.
+            if ((excWord&EXC_CONDITIONAL_FOLD) != 0) {
+                // These characters have Turkic case foldings. Hardcode their closure.
+                if (c == 0x49) {
+                    // Regular i and I are in one equivalence class.
+                    set.add(0x69);
+                    return;
+                } else if (c == 0x130) {
+                    // Dotted I is in a class with <0069 0307>
+                    // (for canonical equivalence with <0049 0307>).
+                    set.add(iDot);
+                    return;
+                }
+            } else if (c == 0x69) {
+                set.add(0x49);
+                return;
+            } else if (c == 0x131) {
+                // Dotless i is in a class by itself.
+                return;
+            }
 
             /* add all simple case mappings */
-            for(index=EXC_LOWER; index<=EXC_TITLE; ++index) {
+            for(int index=EXC_LOWER; index<=EXC_TITLE; ++index) {
                 if(hasSlot(excWord, index)) {
                     excOffset=excOffset0;
-                    c=getSlotValue(excWord, index, excOffset);
-                    set.add(c);
+                    int mapping=getSlotValue(excWord, index, excOffset);
+                    set.add(mapping);
                 }
             }
             if(hasSlot(excWord, EXC_DELTA)) {
@@ -328,6 +322,7 @@
             }
 
             /* get the closure string pointer & length */
+            int closureOffset, closureLength;
             if(hasSlot(excWord, EXC_CLOSURE)) {
                 excOffset=excOffset0;
                 long value=getSlotValueAndOffset(excWord, EXC_CLOSURE, excOffset);
@@ -342,7 +337,7 @@
             if(hasSlot(excWord, EXC_FULL_MAPPINGS)) {
                 excOffset=excOffset0;
                 long value=getSlotValueAndOffset(excWord, EXC_FULL_MAPPINGS, excOffset);
-                fullLength=(int)value;
+                int fullLength=(int)value;
 
                 /* start of full case mapping strings */
                 excOffset=(int)(value>>32)+1;
@@ -354,7 +349,7 @@
                 fullLength>>=4;
 
                 /* add the full case folding string */
-                length=fullLength&0xf;
+                int length=fullLength&0xf;
                 if(length!=0) {
                     set.add(exceptions.substring(excOffset, excOffset+length));
                     excOffset+=length;
@@ -371,9 +366,104 @@
 
             /* add each code point in the closure string */
             int limit=closureOffset+closureLength;
-            for(index=closureOffset; index<limit; index+=UTF16.getCharCount(c)) {
-                c=exceptions.codePointAt(index);
-                set.add(c);
+            for(int index=closureOffset; index<limit; index+=UTF16.getCharCount(c)) {
+                int mapping=exceptions.codePointAt(index);
+                set.add(mapping);
+            }
+        }
+    }
+
+    public final void addSimpleCaseClosure(int c, UnicodeSet set) {
+        int props=trie.get(c);
+        if(!propsHasException(props)) {
+            if(getTypeFromProps(props)!=NONE) {
+                /* add the one simple case mapping, no matter what type it is */
+                int delta=getDelta(props);
+                if(delta!=0) {
+                    set.add(c+delta);
+                }
+            }
+        } else {
+            // c has exceptions. Add the mappings relevant for scf=Simple_Case_Folding.
+            int excOffset=getExceptionsOffset(props);
+            int excWord=exceptions.charAt(excOffset++);
+            int excOffset0=excOffset;
+
+            // Hardcode the case closure of i and its relatives and ignore the
+            // data file data for these characters, like in ucase_addCaseClosure().
+            if ((excWord&EXC_CONDITIONAL_FOLD) != 0) {
+                // These characters have Turkic case foldings. Hardcode their closure.
+                if (c == 0x49) {
+                    // Regular i and I are in one equivalence class.
+                    set.add(0x69);
+                    return;
+                } else if (c == 0x130) {
+                    // For scf=Simple_Case_Folding, dotted I is in a class by itself.
+                    return;
+                }
+            } else if (c == 0x69) {
+                set.add(0x49);
+                return;
+            } else if (c == 0x131) {
+                // Dotless i is in a class by itself.
+                return;
+            }
+
+            // Add all simple case mappings.
+            for(int index=EXC_LOWER; index<=EXC_TITLE; ++index) {
+                if(hasSlot(excWord, index)) {
+                    excOffset=excOffset0;
+                    int mapping=getSlotValue(excWord, index, excOffset);
+                    set.add(mapping);
+                }
+            }
+            if(hasSlot(excWord, EXC_DELTA)) {
+                excOffset=excOffset0;
+                int delta=getSlotValue(excWord, EXC_DELTA, excOffset);
+                int mapping = (excWord&EXC_DELTA_IS_NEGATIVE)==0 ? c+delta : c-delta;
+                set.add(mapping);
+            }
+
+            /* get the closure string pointer & length */
+            int closureOffset, closureLength;
+            if(hasSlot(excWord, EXC_CLOSURE)) {
+                excOffset=excOffset0;
+                long value=getSlotValueAndOffset(excWord, EXC_CLOSURE, excOffset);
+                closureLength=(int)value&CLOSURE_MAX_LENGTH; /* higher bits are reserved */
+                closureOffset=(int)(value>>32)+1; /* behind this slot, unless there are full case mappings */
+            } else {
+                closureLength=0;
+                closureOffset=0;
+            }
+
+            // Skip the full case mappings.
+            if(closureLength > 0 && hasSlot(excWord, EXC_FULL_MAPPINGS)) {
+                excOffset=excOffset0;
+                long value=getSlotValueAndOffset(excWord, EXC_FULL_MAPPINGS, excOffset);
+                int fullLength=(int)value;
+
+                /* start of full case mapping strings */
+                excOffset=(int)(value>>32)+1;
+
+                fullLength&=0xffff; /* bits 16 and higher are reserved */
+
+                // Skip all 4 full case mappings.
+                excOffset+=fullLength&FULL_LOWER;
+                fullLength>>=4;
+                excOffset+=fullLength&0xf;
+                fullLength>>=4;
+                excOffset+=fullLength&0xf;
+                fullLength>>=4;
+                excOffset+=fullLength;
+
+                closureOffset=excOffset; /* behind full case mappings */
+            }
+
+            // Add each code point in the closure string whose scf maps back to c.
+            int limit=closureOffset+closureLength;
+            for(int index=closureOffset; index<limit; index+=UTF16.getCharCount(c)) {
+                int mapping=exceptions.codePointAt(index);
+                set.add(mapping);
             }
         }
     }
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 822f399..3aba5b2 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UCharacterProperty.java
@@ -12,11 +12,14 @@
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.MissingResourceException;
 
 import android.icu.lang.UCharacter;
 import android.icu.lang.UCharacter.HangulSyllableType;
+import android.icu.lang.UCharacter.IdentifierStatus;
+import android.icu.lang.UCharacter.IdentifierType;
 import android.icu.lang.UCharacter.NumericType;
 import android.icu.lang.UCharacterCategory;
 import android.icu.lang.UProperty;
@@ -110,8 +113,10 @@
     public static final int SRC_INSC=13;
     public static final int SRC_VO=14;
     public static final int SRC_EMOJI=15;
+    public static final int SRC_IDSU=16;
+    public static final int SRC_ID_COMPAT_MATH=17;
     /** One more than the highest UPropertySource (SRC_) constant. */
-    public static final int SRC_COUNT=16;
+    public static final int SRC_COUNT=18;
 
     private static final class LayoutProps {
         private static final class IsAcceptable implements ICUBinary.Authenticate {
@@ -378,6 +383,54 @@
         }
     }
 
+    /** Ranges (start/limit pairs) of ID_Compat_Math_Continue (only), from UCD PropList.txt. */
+    private static final int[] ID_COMPAT_MATH_CONTINUE = {
+        0x00B2, 0x00B3 + 1,
+        0x00B9, 0x00B9 + 1,
+        0x2070, 0x2070 + 1,
+        0x2074, 0x207E + 1,
+        0x2080, 0x208E + 1
+    };
+
+    /** ID_Compat_Math_Start characters, from UCD PropList.txt. */
+    private static final int[] ID_COMPAT_MATH_START = {
+        0x2202,
+        0x2207,
+        0x221E,
+        0x1D6C1,
+        0x1D6DB,
+        0x1D6FB,
+        0x1D715,
+        0x1D735,
+        0x1D74F,
+        0x1D76F,
+        0x1D789,
+        0x1D7A9,
+        0x1D7C3
+    };
+
+    private class MathCompatBinaryProperty extends BinaryProperty {
+        int which;
+        MathCompatBinaryProperty(int which) {
+            super(SRC_ID_COMPAT_MATH);
+            this.which=which;
+        }
+        @Override
+        boolean contains(int c) {
+            if (which == UProperty.ID_COMPAT_MATH_CONTINUE) {
+                for (int i = 0; i < ID_COMPAT_MATH_CONTINUE.length; i += 2) {
+                    if (c < ID_COMPAT_MATH_CONTINUE[i]) { return false; }  // below range start
+                    if (c < ID_COMPAT_MATH_CONTINUE[i + 1]) { return true; }  // below range limit
+                }
+            }
+            if (c < ID_COMPAT_MATH_START[0]) { return false; }  // fastpath for common scripts
+            for (int startChar : ID_COMPAT_MATH_START) {
+                if (c == startChar) { return true; }
+            }
+            return false;
+        }
+    }
+
     BinaryProperty[] binProps={
         /*
          * Binary-property implementations must be in order of corresponding UProperty,
@@ -569,6 +622,15 @@
         new EmojiBinaryProperty(UProperty.RGI_EMOJI_TAG_SEQUENCE),
         new EmojiBinaryProperty(UProperty.RGI_EMOJI_ZWJ_SEQUENCE),
         new EmojiBinaryProperty(UProperty.RGI_EMOJI),
+        new BinaryProperty(SRC_IDSU) {  // IDS_UNARY_OPERATOR
+            // New in Unicode 15.1 for just two characters.
+            @Override
+            boolean contains(int c) {
+                return 0x2FFE<=c && c<=0x2FFF;
+            }
+        },
+        new MathCompatBinaryProperty(UProperty.ID_COMPAT_MATH_START),
+        new MathCompatBinaryProperty(UProperty.ID_COMPAT_MATH_CONTINUE),
     };
 
     public boolean hasBinaryProperty(int c, int which) {
@@ -806,6 +868,18 @@
                 return LayoutProps.INSTANCE.maxVoValue;
             }
         },
+        new IntProperty(SRC_PROPSVEC) {  // IDENTIFIER_STATUS
+            @Override
+            int getValue(int c) {
+                int value = getAdditional(c, 2) >>> ID_TYPE_SHIFT;
+                return value >= ID_TYPE_ALLOWED_MIN ?
+                        IdentifierStatus.ALLOWED.ordinal() : IdentifierStatus.RESTRICTED.ordinal();
+            }
+            @Override
+            int getMaxValue(int which) {
+                return IdentifierStatus.ALLOWED.ordinal();
+            }
+        },
     };
 
     public int getIntPropertyValue(int c, int which) {
@@ -879,6 +953,7 @@
         } else {
             switch(which) {
             case UProperty.SCRIPT_EXTENSIONS:
+            case UProperty.IDENTIFIER_TYPE:
                 return SRC_PROPSVEC;
             default:
                 return SRC_NONE; /* undefined */
@@ -1386,20 +1461,73 @@
     /*
      * Properties in vector word 2
      * Bits
-     * 31..26   unused since ICU 70 added uemoji.icu;
-     *          in ICU 57..69 stored emoji properties
+     * 31..26   ICU 75: Identifier_Type bit set
+     *          ICU 70..74: unused
+     *          ICU 57..69: emoji properties; moved to uemoji.icu in ICU 70
      * 25..20   Line Break
      * 19..15   Sentence Break
      * 14..10   Word Break
      *  9.. 5   Grapheme Cluster Break
      *  4.. 0   Decomposition Type
      */
-    //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
+
+    // https://www.unicode.org/reports/tr39/#Identifier_Status_and_Type
+    // The Identifier_Type maps each code point to a *set* of one or more values.
+    // Some can be combined with others, some can only occur alone.
+    // Exclusion & Limited_Use are combinable bits, but cannot occur together.
+    // We use this forbidden combination for enumerated values.
+    // We use 6 bits for all possible combinations.
+    // If more combinable values are added, then we need to use more bits.
+    //
+    // We do not store separate data for Identifier_Status:
+    // We can derive that from the encoded Identifier_Type via a simple range check.
+
+    // vate static final int ID_TYPE_MASK = 0xfc000000;
+    private static final int ID_TYPE_SHIFT = 26;
+
+    // A high bit for use in idTypeToEncoded[] but not used in the data
+    private static final int ID_TYPE_BIT = 0x80;
+
+    // Combinable bits
+    private static final int ID_TYPE_EXCLUSION = 0x20;
+    private static final int ID_TYPE_LIMITED_USE = 0x10;
+    private static final int ID_TYPE_UNCOMMON_USE = 8;
+    private static final int ID_TYPE_TECHNICAL = 4;
+    private static final int ID_TYPE_OBSOLETE = 2;
+    private static final int ID_TYPE_NOT_XID = 1;
+
+    // Exclusive values
+    private static final int ID_TYPE_NOT_CHARACTER = 0;
+
+    // Forbidden bit combination used for enumerating other exclusive values
+    private static final int ID_TYPE_FORBIDDEN = ID_TYPE_EXCLUSION | ID_TYPE_LIMITED_USE; // 0x30
+    private static final int ID_TYPE_DEPRECATED = ID_TYPE_FORBIDDEN; // 0x30
+    private static final int ID_TYPE_DEFAULT_IGNORABLE = ID_TYPE_FORBIDDEN + 1; // 0x31
+    private static final int ID_TYPE_NOT_NFKC = ID_TYPE_FORBIDDEN + 2; // 0x32
+
+    private static final int ID_TYPE_ALLOWED_MIN = ID_TYPE_FORBIDDEN + 0xc; // 0x3c
+    private static final int ID_TYPE_INCLUSION = ID_TYPE_FORBIDDEN + 0xe; // 0x3e
+    private static final int ID_TYPE_RECOMMENDED = ID_TYPE_FORBIDDEN + 0xf; // 0x3f
+
+    /**
+     * Maps UIdentifierType to encoded bits.
+     * When UPROPS_ID_TYPE_BIT is set, then use "&" to test whether the value bit is set.
+     * When UPROPS_ID_TYPE_BIT is not set, then compare ("==") the array value with the data value.
+     */
+    private static final int[] idTypeToEncoded = {
+        ID_TYPE_NOT_CHARACTER,
+        ID_TYPE_DEPRECATED,
+        ID_TYPE_DEFAULT_IGNORABLE,
+        ID_TYPE_NOT_NFKC,
+        ID_TYPE_BIT | ID_TYPE_NOT_XID,
+        ID_TYPE_BIT | ID_TYPE_EXCLUSION,
+        ID_TYPE_BIT | ID_TYPE_OBSOLETE,
+        ID_TYPE_BIT | ID_TYPE_TECHNICAL,
+        ID_TYPE_BIT | ID_TYPE_UNCOMMON_USE,
+        ID_TYPE_BIT | ID_TYPE_LIMITED_USE,
+        ID_TYPE_INCLUSION,
+        ID_TYPE_RECOMMENDED
+    };
 
     private static final int LB_MASK          = 0x03f00000;
     private static final int LB_SHIFT         = 20;
@@ -1506,7 +1634,7 @@
     private static final class IsAcceptable implements ICUBinary.Authenticate {
         @Override
         public boolean isDataVersionAcceptable(byte version[]) {
-            return version[0] == 7;
+            return version[0] == 8;
         }
     }
     private static final int DATA_FORMAT = 0x5550726F;  // "UPro"
@@ -1665,6 +1793,73 @@
         return LayoutProps.INSTANCE.addPropertyStarts(src, set);
     }
 
+    static void mathCompat_addPropertyStarts(UnicodeSet set) {
+        // range limits
+        for (int c : ID_COMPAT_MATH_CONTINUE) {
+            set.add(c);
+        }
+        // single characters
+        for (int c : ID_COMPAT_MATH_START) {
+            set.add(c);
+            set.add(c + 1);
+        }
+    }
+
+    public boolean hasIDType(int c, int typeIndex) {
+        if (typeIndex < 0 || typeIndex >= idTypeToEncoded.length) {
+            return false;
+        }
+        int encodedType = idTypeToEncoded[typeIndex];
+        int value = getAdditional(c, 2) >>> ID_TYPE_SHIFT;
+        if ((encodedType & ID_TYPE_BIT) != 0) {
+            return value < ID_TYPE_FORBIDDEN && (value & encodedType) != 0;
+        } else {
+            return value == encodedType;
+        }
+    }
+
+    public boolean hasIDType(int c, IdentifierType type) {
+        return hasIDType(c, type.ordinal());
+    }
+
+    private static void maybeAddType(int value, int bit, IdentifierType t,
+            EnumSet<IdentifierType> types) {
+        if ((value & bit) != 0) {
+            types.add(t);
+        }
+    }
+
+    public int getIDTypes(int c, EnumSet<IdentifierType> types) {
+        types.clear();
+        int value = getAdditional(c, 2) >>> ID_TYPE_SHIFT;;
+        if ((value & ID_TYPE_FORBIDDEN) == ID_TYPE_FORBIDDEN || value == ID_TYPE_NOT_CHARACTER) {
+            // single value
+            IdentifierType t;
+            switch (value) {
+                case ID_TYPE_NOT_CHARACTER: t = IdentifierType.NOT_CHARACTER; break;
+                case ID_TYPE_DEPRECATED: t = IdentifierType.DEPRECATED; break;
+                case ID_TYPE_DEFAULT_IGNORABLE: t = IdentifierType.DEFAULT_IGNORABLE; break;
+                case ID_TYPE_NOT_NFKC: t = IdentifierType.NOT_NFKC; break;
+                case ID_TYPE_INCLUSION: t = IdentifierType.INCLUSION; break;
+                case ID_TYPE_RECOMMENDED: t = IdentifierType.RECOMMENDED; break;
+                default:
+                    throw new IllegalStateException(
+                            String.format("unknown IdentifierType data value 0x%02x", value));
+            }
+            types.add(t);
+            return 1;
+        } else {
+            // one or more combinable bits
+            maybeAddType(value, ID_TYPE_NOT_XID, IdentifierType.NOT_XID, types);
+            maybeAddType(value, ID_TYPE_EXCLUSION, IdentifierType.EXCLUSION, types);
+            maybeAddType(value, ID_TYPE_OBSOLETE, IdentifierType.OBSOLETE, types);
+            maybeAddType(value, ID_TYPE_TECHNICAL, IdentifierType.TECHNICAL, types);
+            maybeAddType(value, ID_TYPE_UNCOMMON_USE, IdentifierType.UNCOMMON_USE, types);
+            maybeAddType(value, ID_TYPE_LIMITED_USE, IdentifierType.LIMITED_USE, types);
+            return types.size();
+        }
+    }
+
     // This static initializer block must be placed after
     // other static member initialization
     static {
diff --git a/android_icu4j/src/main/java/android/icu/impl/UTS46.java b/android_icu4j/src/main/java/android/icu/impl/UTS46.java
index 327e5ee..e8f68c4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/UTS46.java
+++ b/android_icu4j/src/main/java/android/icu/impl/UTS46.java
@@ -301,14 +301,6 @@
         }
         return length;
     }
-    // Some non-ASCII characters are equivalent to sequences with
-    // non-LDH ASCII characters. To find them:
-    // grep disallowed_STD3_valid IdnaMappingTable.txt (or uts46.txt)
-    private static boolean
-    isNonASCIIDisallowedSTD3Valid(int c) {
-        return c==0x2260 || c==0x226E || c==0x226F;
-    }
-
 
     // Replace the label in dest with the label string, if the label was modified.
     // If label==dest then the label was modified in-place and labelLength
@@ -422,10 +414,7 @@
                 }
             } else {
                 oredChars|=c;
-                if(disallowNonLDHDot && isNonASCIIDisallowedSTD3Valid(c)) {
-                    addLabelError(info, Error.DISALLOWED);
-                    labelString.setCharAt(i, '\ufffd');
-                } else if(c==0xfffd) {
+                if(c==0xfffd) {
                     addLabelError(info, Error.DISALLOWED);
                 }
             }
diff --git a/android_icu4j/src/main/java/android/icu/impl/ZoneMeta.java b/android_icu4j/src/main/java/android/icu/impl/ZoneMeta.java
index ae5c271..c96c991 100644
--- a/android_icu4j/src/main/java/android/icu/impl/ZoneMeta.java
+++ b/android_icu4j/src/main/java/android/icu/impl/ZoneMeta.java
@@ -14,14 +14,12 @@
 package android.icu.impl;
 
 import java.lang.ref.SoftReference;
-import java.text.ParsePosition;
 import java.util.Collections;
 import java.util.Locale;
 import java.util.MissingResourceException;
 import java.util.Set;
 import java.util.TreeSet;
 
-import android.icu.text.NumberFormat;
 import android.icu.util.Output;
 import android.icu.util.SimpleTimeZone;
 import android.icu.util.TimeZone;
@@ -70,7 +68,7 @@
             systemZones = REF_SYSTEM_ZONES.get();
         }
         if (systemZones == null) {
-            Set<String> systemIDs = new TreeSet<String>();
+            Set<String> systemIDs = new TreeSet<>();
             String[] allIDs = getZoneIDs();
             for (String id : allIDs) {
                 // exclude Etc/Unknown
@@ -80,7 +78,7 @@
                 systemIDs.add(id);
             }
             systemZones = Collections.unmodifiableSet(systemIDs);
-            REF_SYSTEM_ZONES = new SoftReference<Set<String>>(systemZones);
+            REF_SYSTEM_ZONES = new SoftReference<>(systemZones);
         }
         return systemZones;
     }
@@ -97,7 +95,7 @@
             canonicalSystemZones = REF_CANONICAL_SYSTEM_ZONES.get();
         }
         if (canonicalSystemZones == null) {
-            Set<String> canonicalSystemIDs = new TreeSet<String>();
+            Set<String> canonicalSystemIDs = new TreeSet<>();
             String[] allIDs = getZoneIDs();
             for (String id : allIDs) {
                 // exclude Etc/Unknown
@@ -110,7 +108,7 @@
                 }
             }
             canonicalSystemZones = Collections.unmodifiableSet(canonicalSystemIDs);
-            REF_CANONICAL_SYSTEM_ZONES = new SoftReference<Set<String>>(canonicalSystemZones);
+            REF_CANONICAL_SYSTEM_ZONES = new SoftReference<>(canonicalSystemZones);
         }
         return canonicalSystemZones;
     }
@@ -129,7 +127,7 @@
             canonicalSystemLocationZones = REF_CANONICAL_SYSTEM_LOCATION_ZONES.get();
         }
         if (canonicalSystemLocationZones == null) {
-            Set<String> canonicalSystemLocationIDs = new TreeSet<String>();
+            Set<String> canonicalSystemLocationIDs = new TreeSet<>();
             String[] allIDs = getZoneIDs();
             for (String id : allIDs) {
                 // exclude Etc/Unknown
@@ -145,7 +143,7 @@
                 }
             }
             canonicalSystemLocationZones = Collections.unmodifiableSet(canonicalSystemLocationIDs);
-            REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<Set<String>>(canonicalSystemLocationZones);
+            REF_CANONICAL_SYSTEM_LOCATION_ZONES = new SoftReference<>(canonicalSystemLocationZones);
         }
         return canonicalSystemLocationZones;
     }
@@ -183,7 +181,7 @@
         }
 
         // Filter by region/rawOffset
-        Set<String> result = new TreeSet<String>();
+        Set<String> result = new TreeSet<>();
         for (String id : baseSet) {
             if (region != null) {
                 String r = getRegion(id);
@@ -340,9 +338,9 @@
         return zoneIdx;
     }
 
-    private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<String, String>();
-    private static ICUCache<String, String> REGION_CACHE = new SimpleCache<String, String>();
-    private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<String, Boolean>();
+    private static ICUCache<String, String> CANONICAL_ID_CACHE = new SimpleCache<>();
+    private static ICUCache<String, String> REGION_CACHE = new SimpleCache<>();
+    private static ICUCache<String, Boolean> SINGLE_COUNTRY_CACHE = new SimpleCache<>();
 
     public static String getCanonicalCLDRID(TimeZone tz) {
         if (tz instanceof OlsonTimeZone) {
@@ -354,7 +352,7 @@
     /**
      * Return the canonical id for this tzid defined by CLDR, which might be
      * the id itself. If the given tzid is not known, return null.
-     * 
+     *
      * Note: This internal API supports all known system IDs and "Etc/Unknown" (which is
      * NOT a system ID).
      */
@@ -421,6 +419,33 @@
     }
 
     /**
+     * Returns primary IANA zone ID for the input zone ID. When input zone ID
+     * is not known, this method returns null.
+     *
+     * @param tzid  An input zone ID.
+     * @return  A primary IANA zone ID equivalent to the input zone ID.
+     */
+    public static String getIanaID(String tzid) {
+        // First, get CLDR canonical ID
+        String canonicalID = getCanonicalCLDRID(tzid);
+        if (canonicalID == null) {
+            return null;
+        }
+        // Find IANA mapping if any.
+        UResourceBundle keyTypeData = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
+                "keyTypeData", ICUResourceBundle.ICU_DATA_CLASS_LOADER);
+        UResourceBundle ianaMap = keyTypeData.get("ianaMap");
+        UResourceBundle ianaTzMap = ianaMap.get("timezone");
+        try {
+            return ianaTzMap.getString(canonicalID.replace('/', ':'));
+        } catch (MissingResourceException e) {
+            // No IANA zone ID mapping. In this case, ianaId set by getCanonicalCLDRID()
+            // is also a primary IANA id.
+            return canonicalID;
+        }
+    }
+
+    /**
      * Return the region code for this tzid.
      * If tzid is not a system zone ID, this method returns null.
      */
@@ -477,7 +502,7 @@
         if (singleZone == null) {
             Set<String> ids = TimeZone.getAvailableIDs(SystemTimeZoneType.CANONICAL_LOCATION, country, null);
             assert(ids.size() >= 1);
-            singleZone = Boolean.valueOf(ids.size() <= 1);
+            singleZone = ids.size() <= 1;
             SINGLE_COUNTRY_CACHE.put(tzid, singleZone);
         }
 
@@ -622,8 +647,7 @@
             // fields[1] - hour / 5-bit
             // fields[2] - min  / 6-bit
             // fields[3] - sec  / 6-bit
-            Integer key = Integer.valueOf(
-                    fields[0] * (fields[1] | fields[2] << 5 | fields[3] << 11));
+            Integer key = fields[0] * (fields[1] | fields[2] << 5 | fields[3] << 11);
             return CUSTOM_ZONE_CACHE.getInstance(key, fields);
         }
         return null;
@@ -655,66 +679,25 @@
      * @return Returns true when the given custom id is valid.
      */
     static boolean parseCustomID(String id, int[] fields) {
-        NumberFormat numberFormat = null;
-
         if (id != null && id.length() > kGMT_ID.length() &&
-                id.toUpperCase(Locale.ENGLISH).startsWith(kGMT_ID)) {
-            ParsePosition pos = new ParsePosition(kGMT_ID.length());
+                id.substring(0, 3).equalsIgnoreCase(kGMT_ID)) {
             int sign = 1;
             int hour = 0;
             int min = 0;
             int sec = 0;
 
-            if (id.charAt(pos.getIndex()) == 0x002D /*'-'*/) {
+            int[] pos = new int[1];
+            pos[0] = kGMT_ID.length();
+            if (id.charAt(pos[0]) == 0x002D /*'-'*/) {
                 sign = -1;
-            } else if (id.charAt(pos.getIndex()) != 0x002B /*'+'*/) {
+            } else if (id.charAt(pos[0]) != 0x002B /*'+'*/) {
                 return false;
             }
-            pos.setIndex(pos.getIndex() + 1);
-
-            numberFormat = NumberFormat.getInstance();
-            numberFormat.setParseIntegerOnly(true);
-
-            // Look for either hh:mm, hhmm, or hh
-            int start = pos.getIndex();
-
-            Number n = numberFormat.parse(id, pos);
-            if (pos.getIndex() == start) {
-                return false;
-            }
-            hour = n.intValue();
-
-            if (pos.getIndex() < id.length()){
-                if (pos.getIndex() - start > 2
-                        || id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
-                    return false;
-                }
-                // hh:mm
-                pos.setIndex(pos.getIndex() + 1);
-                int oldPos = pos.getIndex();
-                n = numberFormat.parse(id, pos);
-                if ((pos.getIndex() - oldPos) != 2) {
-                    // must be 2 digits
-                    return false;
-                }
-                min = n.intValue();
-                if (pos.getIndex() < id.length()) {
-                    if (id.charAt(pos.getIndex()) != 0x003A /*':'*/) {
-                        return false;
-                    }
-                    // [:ss]
-                    pos.setIndex(pos.getIndex() + 1);
-                    oldPos = pos.getIndex();
-                    n = numberFormat.parse(id, pos);
-                    if (pos.getIndex() != id.length()
-                            || (pos.getIndex() - oldPos) != 2) {
-                        return false;
-                    }
-                    sec = n.intValue();
-                }
-            } else {
-                // Supported formats are below -
-                //
+            pos[0]++;
+            int start = pos[0];
+            hour = Utility.parseNumber(id, pos, 10);
+            if (pos[0] == id.length()) {
+                // Handle the following cases
                 // HHmmss
                 // Hmmss
                 // HHmm
@@ -722,27 +705,57 @@
                 // HH
                 // H
 
-                int length = pos.getIndex() - start;
-                if (length <= 0 || 6 < length) {
-                    // invalid length
-                    return false;
-                }
+                // Get all digits
+                // Should be 1 to 6 digits.
+                int length = pos[0] - start;
                 switch (length) {
-                    case 1:
-                    case 2:
+                    case 1: // H
+                    case 2: // HH
                         // already set to hour
                         break;
-                    case 3:
-                    case 4:
+                    case 3: // Hmm
+                    case 4: // HHmm
                         min = hour % 100;
                         hour /= 100;
                         break;
-                    case 5:
-                    case 6:
+                    case 5: // Hmmss
+                    case 6: // HHmmss
                         sec = hour % 100;
                         min = (hour/100) % 100;
                         hour /= 10000;
                         break;
+                    default:
+                        // invalid range
+                        return false;
+                }
+            } else {
+                // Handle the following cases
+                // HH:mm:ss
+                // H:mm:ss
+                // HH:mm
+                // H:mm
+                if (pos[0] - start < 1 || pos[0] - start > 2 || id.charAt(pos[0]) != 0x003A /*':'*/) {
+                    return false;
+                }
+                pos[0]++; // skip : after H
+                if (id.length() == pos[0]) {
+                    return false;
+                }
+                start = pos[0];
+                min = Utility.parseNumber(id, pos, 10);
+                if (pos[0] - start != 2) {
+                    return false;
+                }
+                if (id.length() > pos[0]) {
+                    if (id.charAt(pos[0]) != 0x003A /*':'*/) {
+                        return false;
+                    }
+                    pos[0]++; // skip : after mm
+                    start = pos[0];
+                    sec = Utility.parseNumber(id, pos, 10);
+                    if (pos[0] - start != 2 || id.length() > pos[0]) {
+                        return false;
+                    }
                 }
             }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java
index 6b0bfbc..4aecc01 100644
--- a/android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/BurmeseBreakEngine.java
@@ -202,7 +202,7 @@
 
             // Did we find a word on this iteration? If so, push it on the break stack
             if (wordLength > 0) {
-                foundBreaks.push(Integer.valueOf(current + wordLength));
+                foundBreaks.push(current + wordLength);
             }
         }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java
index f746b98..6bbaa9f 100644
--- a/android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/KhmerBreakEngine.java
@@ -210,7 +210,7 @@
 
             // Did we find a word on this iteration? If so, push it on the break stack
             if (wordLength > 0) {
-                foundBreaks.push(Integer.valueOf(current + wordLength));
+                foundBreaks.push(current + wordLength);
             }
         }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java
index 9db413d..22b4343 100644
--- a/android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/LaoBreakEngine.java
@@ -210,7 +210,7 @@
 
             // Did we find a word on this iteration? If so, push it on the break stack
             if (wordLength > 0) {
-                foundBreaks.push(Integer.valueOf(current + wordLength));
+                foundBreaks.push(current + wordLength);
             }
         }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java
index 957c8d3..c4fb1d3 100644
--- a/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/MlBreakEngine.java
@@ -9,17 +9,17 @@
 import static android.icu.impl.CharacterIteration.next32;
 import static android.icu.impl.CharacterIteration.previous32;
 
+import java.text.CharacterIterator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
 import android.icu.impl.ICUData;
 import android.icu.text.UnicodeSet;
 import android.icu.util.UResourceBundle;
 import android.icu.util.UResourceBundleIterator;
 
-import java.text.CharacterIterator;
-import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.HashMap;
-
 enum ModelIndex {
     kUWStart(0), kBWStart(6), kTWStart(9);
     private final int value;
diff --git a/android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java b/android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java
index 9d663bd..4f32d46 100644
--- a/android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java
+++ b/android_icu4j/src/main/java/android/icu/impl/breakiter/ThaiBreakEngine.java
@@ -254,7 +254,7 @@
 
             // Did we find a word on this iteration? If so, push it on the break stack
             if (wordLength > 0) {
-                foundBreaks.push(Integer.valueOf(current + wordLength));
+                foundBreaks.push(current + wordLength);
             }
         }
 
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 219d37e..e576392 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
@@ -25,6 +25,7 @@
 import android.icu.text.Normalizer2;
 import android.icu.text.UnicodeSet;
 import android.icu.text.UnicodeSetIterator;
+import android.icu.util.ICUInputTooLongException;
 import android.icu.util.ULocale;
 
 /**
@@ -866,10 +867,21 @@
         return ce32;
     }
 
+    // ICU-22517
+    // This constant defines a limit for the addOnlyClosure to return
+    // error, to avoid taking a long time for canonical closure expansion.
+    // Please let us know if you have a reasonable use case that needed
+    // for a practical Collation rule that needs to increase this limit.
+    // This value is needed for compiling a rule with eight Hangul syllables such as
+    // "&a=bì«Šì«Šì«Šì«Šì«Šì«Šì«Šì«Š" without error, which should be more than realistic
+    // usage.
+    static private int kClosureLoopLimit = 6560;
+
     private int addOnlyClosure(CharSequence nfdPrefix, CharSequence nfdString,
                 long[] newCEs, int newCEsLength, int ce32) {
         // Map from canonically equivalent input to the CEs. (But not from the all-NFD input.)
         // TODO: make CanonicalIterator work with CharSequence, or maybe change arguments here to String
+        int loop = 0;
         if(nfdPrefix.length() == 0) {
             CanonicalIterator stringIter = new CanonicalIterator(nfdString.toString());
             String prefix = "";
@@ -877,6 +889,9 @@
                 String str = stringIter.next();
                 if(str == null) { break; }
                 if(ignoreString(str) || str.contentEquals(nfdString)) { continue; }
+                if (loop++ > kClosureLoopLimit) {
+                    throw new ICUInputTooLongException("Too many closure");
+                }
                 ce32 = addIfDifferent(prefix, str, newCEs, newCEsLength, ce32);
             }
         } else {
@@ -891,6 +906,9 @@
                     String str = stringIter.next();
                     if(str == null) { break; }
                     if(ignoreString(str) || (samePrefix && str.contentEquals(nfdString))) { continue; }
+                    if (loop++ > kClosureLoopLimit) {
+                        throw new ICUInputTooLongException("Too many closure");
+                    }
                     ce32 = addIfDifferent(prefix, str, newCEs, newCEsLength, ce32);
                 }
                 stringIter.reset();
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java b/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java
index ac47e51..c774af4 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/LSR.java
@@ -3,8 +3,14 @@
 // License & terms of use: http://www.unicode.org/copyright.html
 package android.icu.impl.locale;
 
+import java.util.HashMap;
+import java.util.List;
 import java.util.Objects;
 
+import android.icu.lang.UCharacter;
+import android.icu.lang.UProperty;
+import android.icu.lang.UScript;
+
 /**
  * @hide Only a subset of ICU is exposed in Android
  */
@@ -93,4 +99,146 @@
     public int hashCode() {
         return Objects.hash(language, script, region, flags);
     }
+
+    // This method is added only to support encodeToIntForResource()
+    // It only support [a-z]{2,3} and will not work for other cases.
+    private int encodeLanguageToInt() {
+        assert language.length() >= 2;
+        assert language.length() <= 3;
+        assert language.charAt(0) >= 'a';
+        assert language.charAt(0) <= 'z';
+        assert language.charAt(1) >= 'a';
+        assert language.charAt(1) <= 'z';
+        assert language.length() == 2 || language.charAt(2) >= 'a';
+        assert language.length() == 2 || language.charAt(2) <= 'z';
+        return language.charAt(0) - 'a' + 1 +
+               27 * (language.charAt(1) - 'a' + 1) +
+               ((language.length() == 2) ? 0 : 27 * 27 * (language.charAt(2) - 'a' + 1));
+    }
+    // This method is added only to support encodeToIntForResource()
+    // It only support [A-Z][a-z]{3} which defined in UScript and does not work for other cases.
+    private int encodeScriptToInt() {
+        int ret = UScript.getCodeFromName(script);
+        assert ret != UScript.INVALID_CODE;
+        return ret;
+    }
+    // This method is added only to support encodeToIntForResource()
+    // It only support [A-Z]{2} and the code in m49 but does not work for other cases.
+    private int encodeRegionToInt(List<String> m49) {
+        assert region.length() >= 2;
+        assert region.length() <= 3;
+        if (region.length() == 3) {
+            int index = m49.indexOf(region);
+            assert index >= 0;
+            if (index < 0) {
+                throw new IllegalStateException(
+                    "Please add '" + region + "' to M49 in LocaleDistanceMapper.java");
+            }
+            return index;
+        }
+        assert region.charAt(0) >= 'A';
+        assert region.charAt(0) <= 'Z';
+        assert region.charAt(1) >= 'A';
+        assert region.charAt(1) <= 'Z';
+        // 'AA' => 1+27*1  = 28
+        // ...
+        // 'AZ' => 1+27*26 = 703
+        // 'BA' => 2+27*1  = 29
+        // ...
+        // 'IN' => 9+27*14 = 387
+        // 'ZZ' => 26+27*26 = 728
+        return (region.charAt(0) - 'A' + 1) + 27 * (region.charAt(1) - 'A' + 1);
+    }
+    // This is designed to only support encoding some LSR into resources but not for other cases.
+    public int encodeToIntForResource(List<String> m49) {
+        return (encodeLanguageToInt() + (27*27*27) * encodeRegionToInt(m49)) |
+            (encodeScriptToInt() << 24);
+    }
+
+    // BEGIN Android patch: Save ~1MB zygote heap. http://b/331291118
+    // ~7k LSR instances and ~21k strings are created from this path.
+    private static class CachedDecoder {
+        private static final String[] DECODED_ZERO =
+                new String[] {/*lang=*/ "", /*script=*/ "", /*region=*/ ""};
+        private static final String[] DECODED_ONE =
+                new String[] {/*lang=*/ "skip", /*script=*/ "script", /*region=*/ ""};
+
+        private final HashMap<Integer, String> langsCache;
+        private final HashMap<Integer, String> scriptsCache;
+        private final HashMap<Integer, String> regionsCache;
+
+        private final String[] m49;
+
+        CachedDecoder(String[] m49) {
+            int estLangCacheCapacity = 556;  // ~= LocaleIDs._languages.length
+            langsCache = new HashMap<>(estLangCacheCapacity);
+            scriptsCache = new HashMap<>(UCharacter.getIntPropertyMaxValue(UProperty.SCRIPT));
+            int estRegionCacheCapacity = 253;  // ~= LocaleIDs._countries.length
+            regionsCache = new HashMap<>(estRegionCacheCapacity);
+            this.m49 = m49;
+        }
+
+        /**
+         * @return a String[3] object where the first element is a language code, the second element
+         *   is a script code, and the third element is a region code.
+         */
+        String[] decode(int encoded) {
+            if (encoded == 0) {
+                return DECODED_ZERO;
+            }
+            if (encoded == 1) {
+                return DECODED_ONE;
+            }
+
+            int encodedLang = encoded & 0x00ffffff;
+            encodedLang %= 27*27*27;
+            String lang = langsCache.computeIfAbsent(encodedLang, CachedDecoder::toLanguage);
+
+            int encodedScript = (encoded >> 24) & 0x000000ff;
+            String script = scriptsCache.computeIfAbsent(encodedScript, UScript::getShortName);
+
+            int encodedRegion = encoded & 0x00ffffff;
+            encodedRegion /= 27 * 27 * 27;
+            encodedRegion %= 27 * 27;
+
+            String region;
+            if (encodedRegion < 27) {
+                region = m49[encodedRegion];
+            } else {
+                region = regionsCache.computeIfAbsent(encodedRegion, CachedDecoder::toRegion);
+            }
+
+            return new String[] {lang, script, region};
+        }
+
+        private static String toLanguage(int encoded) {
+            StringBuilder res = new StringBuilder(3);
+            res.append((char)('a' + ((encoded % 27) - 1)));
+            res.append((char)('a' + (((encoded / 27 ) % 27) - 1)));
+            if (encoded / (27 * 27) != 0) {
+                res.append((char)('a' + ((encoded / (27 * 27)) - 1)));
+            }
+            return res.toString();
+        }
+
+        private static String toRegion(int encoded) {
+            StringBuilder res = new StringBuilder(3);
+            res.append((char)('A' + ((encoded % 27) - 1)));
+            res.append((char)('A' + (((encoded / 27) % 27) - 1)));
+            return res.toString();
+        }
+    }
+
+    public static LSR[] decodeInts(int[] nums, String[] m49) {
+        LSR[] lsrs = new LSR[nums.length];
+
+        CachedDecoder decoder = new CachedDecoder(m49);
+        for (int i = 0; i < nums.length; ++i) {
+            int encoded = nums[i];
+            String[] lsrStrings = decoder.decode(encoded);
+            lsrs[i] = new LSR(lsrStrings[0], lsrStrings[1], lsrStrings[2], LSR.IMPLICIT_LSR);
+        }
+        return lsrs;
+    }
+    // END Android patch: Save ~1MB zygote heap. http://b/331291118
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LikelySubtags.java b/android_icu4j/src/main/java/android/icu/impl/locale/LikelySubtags.java
new file mode 100644
index 0000000..6c4b699
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/LikelySubtags.java
@@ -0,0 +1,595 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+package android.icu.impl.locale;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.TreeMap;
+
+import android.icu.impl.ICUData;
+import android.icu.impl.ICUResourceBundle;
+import android.icu.impl.UResource;
+import android.icu.util.BytesTrie;
+import android.icu.util.Region;
+import android.icu.util.ULocale;
+
+/**
+ * @hide Only a subset of ICU is exposed in Android
+ */
+public final class LikelySubtags {
+    private static final String PSEUDO_ACCENTS_PREFIX = "'";  // -XA, -PSACCENT
+    private static final String PSEUDO_BIDI_PREFIX = "+";  // -XB, -PSBIDI
+    private static final String PSEUDO_CRACKED_PREFIX = ",";  // -XC, -PSCRACK
+
+    public static final int SKIP_SCRIPT = 1;
+
+    private static final boolean DEBUG_OUTPUT = LSR.DEBUG_OUTPUT;
+
+    // VisibleForTesting
+    /**
+     * @hide Only a subset of ICU is exposed in Android
+     */
+    public static final class Data {
+        public final Map<String, String> languageAliases;
+        public final Map<String, String> regionAliases;
+        public final byte[] trie;
+        public final LSR[] lsrs;
+
+        public Data(Map<String, String> languageAliases, Map<String, String> regionAliases,
+                byte[] trie, LSR[] lsrs) {
+            this.languageAliases = languageAliases;
+            this.regionAliases = regionAliases;
+            this.trie = trie;
+            this.lsrs = lsrs;
+        }
+
+        private static UResource.Value getValue(UResource.Table table,
+                String key, UResource.Value value) {
+            if (!table.findValue(key, value)) {
+                throw new MissingResourceException(
+                        "langInfo.res missing data", "", "likely/" + key);
+            }
+            return value;
+        }
+
+        // VisibleForTesting
+        public static Data load() throws MissingResourceException {
+            ICUResourceBundle langInfo = ICUResourceBundle.getBundleInstance(
+                    ICUData.ICU_BASE_NAME, "langInfo",
+                    ICUResourceBundle.ICU_DATA_CLASS_LOADER, ICUResourceBundle.OpenType.DIRECT);
+            UResource.Value value = langInfo.getValueWithFallback("likely");
+            UResource.Table likelyTable = value.getTable();
+
+            Map<String, String> languageAliases;
+            if (likelyTable.findValue("languageAliases", value)) {
+                String[] pairs = value.getStringArray();
+                languageAliases = new HashMap<>(pairs.length / 2);
+                for (int i = 0; i < pairs.length; i += 2) {
+                    languageAliases.put(pairs[i], pairs[i + 1]);
+                }
+            } else {
+                languageAliases = Collections.emptyMap();
+            }
+
+            Map<String, String> regionAliases;
+            if (likelyTable.findValue("regionAliases", value)) {
+                String[] pairs = value.getStringArray();
+                regionAliases = new HashMap<>(pairs.length / 2);
+                for (int i = 0; i < pairs.length; i += 2) {
+                    regionAliases.put(pairs[i], pairs[i + 1]);
+                }
+            } else {
+                regionAliases = Collections.emptyMap();
+            }
+
+            ByteBuffer buffer = getValue(likelyTable, "trie", value).getBinary();
+            byte[] trie = new byte[buffer.remaining()];
+            buffer.get(trie);
+
+            String[] m49 = getValue(likelyTable, "m49", value).getStringArray();
+            LSR[] lsrs = LSR.decodeInts(getValue(likelyTable, "lsrnum", value).getIntVector(), m49);
+            return new Data(languageAliases, regionAliases, trie, lsrs);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) { return true; }
+            if (other == null || !getClass().equals(other.getClass())) { return false; }
+            Data od = (Data)other;
+            return
+                    languageAliases.equals(od.languageAliases) &&
+                    regionAliases.equals(od.regionAliases) &&
+                    Arrays.equals(trie, od.trie) &&
+                    Arrays.equals(lsrs, od.lsrs);
+        }
+
+        @Override
+        public int hashCode() {  // unused; silence ErrorProne
+            return 1;
+        }
+    }
+
+    // VisibleForTesting
+    public static final LikelySubtags INSTANCE = new LikelySubtags(Data.load());
+
+    private final Map<String, String> languageAliases;
+    private final Map<String, String> regionAliases;
+
+    // The trie maps each lang+script+region (encoded in ASCII) to an index into lsrs.
+    // There is also a trie value for each intermediate lang and lang+script.
+    // '*' is used instead of "und", "Zzzz"/"" and "ZZ"/"".
+    private final BytesTrie trie;
+    private final long trieUndState;
+    private final long trieUndZzzzState;
+    private final int defaultLsrIndex;
+    private final long[] trieFirstLetterStates = new long[26];
+    private final LSR[] lsrs;
+
+    private LikelySubtags(LikelySubtags.Data data) {
+        languageAliases = data.languageAliases;
+        regionAliases = data.regionAliases;
+        trie = new BytesTrie(data.trie, 0);
+        lsrs = data.lsrs;
+
+        // Cache the result of looking up language="und" encoded as "*", and "und-Zzzz" ("**").
+        BytesTrie.Result result = trie.next('*');
+        assert result.hasNext();
+        trieUndState = trie.getState64();
+        result = trie.next('*');
+        assert result.hasNext();
+        trieUndZzzzState = trie.getState64();
+        result = trie.next('*');
+        assert result.hasValue();
+        defaultLsrIndex = trie.getValue();
+        trie.reset();
+
+        for (char c = 'a'; c <= 'z'; ++c) {
+            result = trie.next(c);
+            if (result == BytesTrie.Result.NO_VALUE) {
+                trieFirstLetterStates[c - 'a'] = trie.getState64();
+            }
+            trie.reset();
+        }
+
+        if (DEBUG_OUTPUT) {
+            System.out.println("*** likely subtags");
+            for (Map.Entry<String, LSR> mapping : getTable().entrySet()) {
+                System.out.println(mapping);
+            }
+        }
+    }
+
+    /**
+     * Implementation of LocaleMatcher.canonicalize(ULocale).
+     */
+    public ULocale canonicalize(ULocale locale) {
+        String lang = locale.getLanguage();
+        String lang2 = languageAliases.get(lang);
+        String region = locale.getCountry();
+        String region2 = regionAliases.get(region);
+        if (lang2 != null || region2 != null) {
+            return new ULocale(
+                lang2 == null ? lang : lang2,
+                locale.getScript(),
+                region2 == null ? region : region2);
+        }
+        return locale;
+    }
+
+    private static String getCanonical(Map<String, String> aliases, String alias) {
+        String canonical = aliases.get(alias);
+        return canonical == null ? alias : canonical;
+    }
+
+    // VisibleForTesting
+    public LSR makeMaximizedLsrFrom(ULocale locale, boolean returnInputIfUnmatch) {
+        String name = locale.getName();  // Faster than .toLanguageTag().
+        if (name.startsWith("@x=")) {
+            String tag = locale.toLanguageTag();
+            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);
+        }
+        LSR max = makeMaximizedLsr(locale.getLanguage(), locale.getScript(), locale.getCountry(),
+                locale.getVariant(), returnInputIfUnmatch);
+        if (max.language.isEmpty() && max.script.isEmpty() && max.region.isEmpty()) {
+            return new LSR(locale.getLanguage(), locale.getScript(), locale.getCountry(), LSR.EXPLICIT_LSR);
+        }
+        return max;
+    }
+
+    public LSR makeMaximizedLsrFrom(Locale locale) {
+        String tag = locale.toLanguageTag();
+        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(),
+                locale.getVariant(), false);
+    }
+
+    private LSR makeMaximizedLsr(String language, String script, String region, String variant, boolean returnInputIfUnmatch) {
+        // Handle pseudolocales like en-XA, ar-XB, fr-PSCRACK.
+        // They should match only themselves,
+        // not other locales with what looks like the same language and script subtags.
+        if (!returnInputIfUnmatch) {
+            if (region.length() == 2 && region.charAt(0) == 'X') {
+                switch (region.charAt(1)) {
+                case 'A':
+                    return new LSR(PSEUDO_ACCENTS_PREFIX + language,
+                            PSEUDO_ACCENTS_PREFIX + script, region, LSR.EXPLICIT_LSR);
+                case 'B':
+                    return new LSR(PSEUDO_BIDI_PREFIX + language,
+                            PSEUDO_BIDI_PREFIX + script, region, LSR.EXPLICIT_LSR);
+                case 'C':
+                    return new LSR(PSEUDO_CRACKED_PREFIX + language,
+                            PSEUDO_CRACKED_PREFIX + script, region, LSR.EXPLICIT_LSR);
+                default:  // normal locale
+                    break;
+                }
+            }
+
+            if (variant.startsWith("PS")) {
+                int lsrFlags = region.isEmpty() ?
+                            LSR.EXPLICIT_LANGUAGE | LSR.EXPLICIT_SCRIPT : LSR.EXPLICIT_LSR;
+                switch (variant) {
+                case "PSACCENT":
+                    return new LSR(PSEUDO_ACCENTS_PREFIX + language,
+                            PSEUDO_ACCENTS_PREFIX + script,
+                            region.isEmpty() ? "XA" : region, lsrFlags);
+                case "PSBIDI":
+                    return new LSR(PSEUDO_BIDI_PREFIX + language,
+                            PSEUDO_BIDI_PREFIX + script,
+                            region.isEmpty() ? "XB" : region, lsrFlags);
+                case "PSCRACK":
+                    return new LSR(PSEUDO_CRACKED_PREFIX + language,
+                            PSEUDO_CRACKED_PREFIX + script,
+                            region.isEmpty() ? "XC" : region, lsrFlags);
+                default:  // normal locale
+                    break;
+                }
+            }
+        }
+
+        language = getCanonical(languageAliases, language);
+        // (We have no script mappings.)
+        region = getCanonical(regionAliases, region);
+        return maximize(language, script, region, returnInputIfUnmatch);
+    }
+
+    /**
+     * Helper method to find out a region is a macroregion
+     */
+    private boolean isMacroregion(String region) {
+        Region.RegionType type = Region.getInstance(region).getType();
+        return type == Region.RegionType.WORLD ||
+            type == Region.RegionType.CONTINENT ||
+            type == Region.RegionType.SUBCONTINENT ;
+    }
+
+    /**
+     * Raw access to addLikelySubtags. Input must be in canonical format, eg "en", not "eng" or "EN".
+     */
+    private LSR maximize(String language, String script, String region, boolean returnInputIfUnmatch) {
+        if (language.equals("und")) {
+            language = "";
+        }
+        if (script.equals("Zzzz")) {
+            script = "";
+        }
+        if (region.equals("ZZ")) {
+            region = "";
+        }
+        if (!script.isEmpty() && !region.isEmpty() && !language.isEmpty()) {
+            return new LSR(language, script, region, LSR.EXPLICIT_LSR);  // already maximized
+        }
+
+        boolean retainLanguage = false;
+        boolean retainScript = false;
+        boolean retainRegion = false;
+        BytesTrie iter = new BytesTrie(trie);
+        long state;
+        int value;
+        // Small optimization: Array lookup for first language letter.
+        int c0;
+        if (language.length() >= 2 && 0 <= (c0 = language.charAt(0) - 'a') && c0 <= 25 &&
+                (state = trieFirstLetterStates[c0]) != 0) {
+            value = trieNext(iter.resetToState64(state), language, 1);
+        } else {
+            value = trieNext(iter, language, 0);
+        }
+        boolean matchLanguage = (value >= 0);
+        boolean matchScript = false;
+        if (value >= 0) {
+            retainLanguage = ! language.isEmpty();
+            state = iter.getState64();
+        } else {
+            retainLanguage = true;
+            iter.resetToState64(trieUndState);  // "und" ("*")
+            state = 0;
+        }
+
+        if (value >= 0 && !script.isEmpty()) {
+            matchScript = true;
+        }
+        if (value > 0) {
+            // Intermediate or final value from just language.
+            if (value == SKIP_SCRIPT) {
+                value = 0;
+            }
+            retainScript = ! script.isEmpty();
+        } else {
+            value = trieNext(iter, script, 0);
+            if (value >= 0) {
+                retainScript = ! script.isEmpty();
+                state = iter.getState64();
+            } else {
+                retainScript = true;
+                if (state == 0) {
+                    iter.resetToState64(trieUndZzzzState);  // "und-Zzzz" ("**")
+                } else {
+                    iter.resetToState64(state);
+                    value = trieNext(iter, "", 0);
+                    assert value >= 0;
+                    state = iter.getState64();
+                }
+            }
+        }
+
+        boolean matchRegion = false;
+        if (value > 0) {
+            // Final value from just language or language+script.
+            retainRegion = ! region.isEmpty();
+        } else {
+            value = trieNext(iter, region, 0);
+            if (value >= 0) {
+                if (!region.isEmpty() && !isMacroregion(region)) {
+                    retainRegion = true;
+                    matchRegion = true;
+                }
+            } else {
+                retainRegion = true;
+                if (state == 0) {
+                    value = defaultLsrIndex;
+                } else {
+                    iter.resetToState64(state);
+                    value = trieNext(iter, "", 0);
+                    assert value > 0;
+                }
+            }
+        }
+        LSR result = lsrs[value];
+
+        if (returnInputIfUnmatch &&
+            (!(matchLanguage || matchScript || (matchRegion && language.isEmpty())))) {
+            return new LSR("", "", "", LSR.EXPLICIT_LSR);  // no matching.
+        }
+        if (language.isEmpty()) {
+            language = "und";
+        }
+
+        if (! (retainLanguage || retainScript || retainRegion)) {
+            assert result.flags == LSR.IMPLICIT_LSR;
+            return result;
+        }
+        if (!retainLanguage) {
+            language = result.language;
+        }
+        if (!retainScript) {
+            script = result.script;
+        }
+        if (!retainRegion) {
+            region = result.region;
+        }
+        int retainMask = (retainLanguage ? 4 : 0) + (retainScript ? 2 : 0) + (retainRegion ? 1 : 0);
+        // retainOldMask flags = LSR explicit-subtag flags
+        return new LSR(language, script, region, retainMask);
+    }
+
+    /**
+     * Tests whether lsr is "more likely" than other.
+     * For example, fr-Latn-FR is more likely than fr-Latn-CH because
+     * FR is the default region for fr-Latn.
+     *
+     * <p>The likelyInfo caches lookup information between calls.
+     * The return value is an updated likelyInfo value,
+     * with bit 0 set if lsr is "more likely".
+     * The initial value of likelyInfo must be negative.
+     */
+    int compareLikely(LSR lsr, LSR other, int likelyInfo) {
+        // If likelyInfo >= 0:
+        // likelyInfo bit 1 is set if the previous comparison with lsr
+        // was for equal language and script.
+        // Otherwise the scripts differed.
+        if (!lsr.language.equals(other.language)) {
+            return 0xfffffffc;  // negative, lsr not better than other
+        }
+        if (!lsr.script.equals(other.script)) {
+            int index;
+            if (likelyInfo >= 0 && (likelyInfo & 2) == 0) {
+                index = likelyInfo >> 2;
+            } else {
+                index = getLikelyIndex(lsr.language, "");
+                likelyInfo = index << 2;
+            }
+            LSR likely = lsrs[index];
+            if (lsr.script.equals(likely.script)) {
+                return likelyInfo | 1;
+            } else {
+                return likelyInfo & ~1;
+            }
+        }
+        if (!lsr.region.equals(other.region)) {
+            int index;
+            if (likelyInfo >= 0 && (likelyInfo & 2) != 0) {
+                index = likelyInfo >> 2;
+            } else {
+                index = getLikelyIndex(lsr.language, lsr.region);
+                likelyInfo = (index << 2) | 2;
+            }
+            LSR likely = lsrs[index];
+            if (lsr.region.equals(likely.region)) {
+                return likelyInfo | 1;
+            } else {
+                return likelyInfo & ~1;
+            }
+        }
+        return likelyInfo & ~1;  // lsr not better than other
+    }
+
+    // Subset of maximize().
+    private int getLikelyIndex(String language, String script) {
+        if (language.equals("und")) {
+            language = "";
+        }
+        if (script.equals("Zzzz")) {
+            script = "";
+        }
+
+        BytesTrie iter = new BytesTrie(trie);
+        long state;
+        int value;
+        // Small optimization: Array lookup for first language letter.
+        int c0;
+        if (language.length() >= 2 && 0 <= (c0 = language.charAt(0) - 'a') && c0 <= 25 &&
+                (state = trieFirstLetterStates[c0]) != 0) {
+            value = trieNext(iter.resetToState64(state), language, 1);
+        } else {
+            value = trieNext(iter, language, 0);
+        }
+        if (value >= 0) {
+            state = iter.getState64();
+        } else {
+            iter.resetToState64(trieUndState);  // "und" ("*")
+            state = 0;
+        }
+
+        if (value > 0) {
+            // Intermediate or final value from just language.
+            if (value == SKIP_SCRIPT) {
+                value = 0;
+            }
+        } else {
+            value = trieNext(iter, script, 0);
+            if (value >= 0) {
+                state = iter.getState64();
+            } else {
+                if (state == 0) {
+                    iter.resetToState64(trieUndZzzzState);  // "und-Zzzz" ("**")
+                } else {
+                    iter.resetToState64(state);
+                    value = trieNext(iter, "", 0);
+                    assert value >= 0;
+                    state = iter.getState64();
+                }
+            }
+        }
+
+        if (value > 0) {
+            // Final value from just language or language+script.
+        } else {
+            value = trieNext(iter, "", 0);
+            assert value > 0;
+        }
+        return value;
+    }
+
+    private static final int trieNext(BytesTrie iter, String s, int i) {
+        BytesTrie.Result result;
+        if (s.isEmpty()) {
+            result = iter.next('*');
+        } else {
+            int end = s.length() - 1;
+            for (;; ++i) {
+                int c = s.charAt(i);
+                if (i < end) {
+                    if (!iter.next(c).hasNext()) {
+                        return -1;
+                    }
+                } else {
+                    // last character of this subtag
+                    result = iter.next(c | 0x80);
+                    break;
+                }
+            }
+        }
+        switch (result) {
+        case NO_MATCH: return -1;
+        case NO_VALUE: return 0;
+        case INTERMEDIATE_VALUE:
+            assert iter.getValue() == SKIP_SCRIPT;
+            return SKIP_SCRIPT;
+        case FINAL_VALUE: return iter.getValue();
+        default: return -1;
+        }
+    }
+
+    public LSR minimizeSubtags(String languageIn, String scriptIn, String regionIn,
+            ULocale.Minimize fieldToFavor) {
+        LSR max = maximize(languageIn, scriptIn, regionIn, true);
+        if (max.language.isEmpty() && max.region.isEmpty() && max.script.isEmpty()) {
+            // Cannot match, return as is
+            return new LSR(languageIn, scriptIn, regionIn, LSR.EXPLICIT_LSR);
+        }
+        LSR test = maximize(max.language, "", "", true);
+        if (test.isEquivalentTo(max)) {
+            return new LSR(max.language, "", "", LSR.DONT_CARE_FLAGS);
+        }
+        if (ULocale.Minimize.FAVOR_REGION == fieldToFavor) {
+            test = maximize(max.language, "", max.region, true);
+            if (test.isEquivalentTo(max)) {
+                return new LSR(max.language, "", max.region, LSR.DONT_CARE_FLAGS);
+            }
+            test = maximize(max.language, max.script, "", true);
+            if (test.isEquivalentTo(max)) {
+                return new LSR(max.language, max.script, "", LSR.DONT_CARE_FLAGS);
+            }
+        } else {
+            test = maximize(max.language, max.script, "", true);
+            if (test.isEquivalentTo(max)) {
+                return new LSR(max.language, max.script, "", LSR.DONT_CARE_FLAGS);
+            }
+            test = maximize(max.language, "", max.region, true);
+            if (test.isEquivalentTo(max)) {
+                return new LSR(max.language, "", max.region, LSR.DONT_CARE_FLAGS);
+            }
+        }
+        return new LSR(max.language, max.script, max.region, LSR.DONT_CARE_FLAGS);
+    }
+
+    private Map<String, LSR> getTable() {
+        Map<String, LSR> map = new TreeMap<>();
+        StringBuilder sb = new StringBuilder();
+        for (BytesTrie.Entry entry : trie) {
+            sb.setLength(0);
+            int length = entry.bytesLength();
+            for (int i = 0; i < length;) {
+                byte b = entry.byteAt(i++);
+                if (b == '*') {
+                    sb.append("*-");
+                } else if (b >= 0) {
+                    sb.append((char) b);
+                } else {  // end of subtag
+                    sb.append((char) (b & 0x7f)).append('-');
+                }
+            }
+            assert sb.length() > 0 && sb.charAt(sb.length() - 1) == '-';
+            sb.setLength(sb.length() - 1);
+            map.put(sb.toString(), lsrs[entry.value]);
+        }
+        return map;
+    }
+
+    @Override
+    public String toString() {
+        return getTable().toString();
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java
index de6fe96..4416108 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleDistance.java
@@ -156,14 +156,12 @@
             String[] partitions = getValue(matchTable, "partitions", value).getStringArray();
 
             Set<LSR> paradigmLSRs;
-            if (matchTable.findValue("paradigms", value)) {
-                String[] paradigms = value.getStringArray();
+            if (matchTable.findValue("paradigmnum", value)) {
+                String[] m49 = getValue(langInfo.getValueWithFallback("likely").getTable(),
+                    "m49", value).getStringArray();
+                LSR[] paradigms = LSR.decodeInts(getValue(matchTable, "paradigmnum", value).getIntVector(), m49);
                 // LinkedHashSet for stable order; otherwise a unit test is flaky.
-                paradigmLSRs = new LinkedHashSet<>(paradigms.length / 3);
-                for (int i = 0; i < paradigms.length; i += 3) {
-                    paradigmLSRs.add(new LSR(paradigms[i], paradigms[i + 1], paradigms[i + 2],
-                            LSR.DONT_CARE_FLAGS));
-                }
+                paradigmLSRs = new LinkedHashSet<LSR>(Arrays.asList(paradigms));
             } else {
                 paradigmLSRs = Collections.emptySet();
             }
@@ -232,8 +230,8 @@
     // VisibleForTesting
     public int testOnlyDistance(ULocale desired, ULocale supported,
             int threshold, FavorSubtag favorSubtag) {
-        LSR supportedLSR = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(supported);
-        LSR desiredLSR = XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(desired);
+        LSR supportedLSR = LikelySubtags.INSTANCE.makeMaximizedLsrFrom(supported, false);
+        LSR desiredLSR = LikelySubtags.INSTANCE.makeMaximizedLsrFrom(desired, false);
         int indexAndDistance = getBestIndexAndDistance(desiredLSR, new LSR[] { supportedLSR }, 1,
                 shiftDistance(threshold), favorSubtag, LocaleMatcher.Direction.WITH_ONE_WAY);
         return getDistanceFloor(indexAndDistance);
@@ -257,7 +255,7 @@
         long desLangState = desLangDistance >= 0 && supportedLSRsLength > 1 ? iter.getState64() : 0;
         // Index of the supported LSR with the lowest distance.
         int bestIndex = -1;
-        // Cached lookup info from XLikelySubtags.compareLikely().
+        // Cached lookup info from LikelySubtags.compareLikely().
         int bestLikelyInfo = -1;
         for (int slIndex = 0; slIndex < supportedLSRsLength; ++slIndex) {
             LSR supported = supportedLSRs[slIndex];
@@ -376,7 +374,7 @@
                     if (direction != LocaleMatcher.Direction.ONLY_TWO_WAY ||
                             // Is there also a match when we swap desired/supported?
                             isMatch(supported, desired, shiftedThreshold, favorSubtag)) {
-                        bestLikelyInfo = XLikelySubtags.INSTANCE.compareLikely(
+                        bestLikelyInfo = LikelySubtags.INSTANCE.compareLikely(
                                 supported, supportedLSRs[bestIndex], bestLikelyInfo);
                         if ((bestLikelyInfo & 1) != 0) {
                             // This supported locale matches as well as the previous best match,
diff --git a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleExtensions.java b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleExtensions.java
index 0e1ed89..94242c0 100644
--- a/android_icu4j/src/main/java/android/icu/impl/locale/LocaleExtensions.java
+++ b/android_icu4j/src/main/java/android/icu/impl/locale/LocaleExtensions.java
@@ -44,12 +44,12 @@
         CALENDAR_JAPANESE = new LocaleExtensions();
         CALENDAR_JAPANESE._id = "u-ca-japanese";
         CALENDAR_JAPANESE._map = new TreeMap<Character, Extension>();
-        CALENDAR_JAPANESE._map.put(Character.valueOf(UnicodeLocaleExtension.SINGLETON), UnicodeLocaleExtension.CA_JAPANESE);
+        CALENDAR_JAPANESE._map.put(UnicodeLocaleExtension.SINGLETON, UnicodeLocaleExtension.CA_JAPANESE);
 
         NUMBER_THAI = new LocaleExtensions();
         NUMBER_THAI._id = "u-nu-thai";
         NUMBER_THAI._map = new TreeMap<Character, Extension>();
-        NUMBER_THAI._map.put(Character.valueOf(UnicodeLocaleExtension.SINGLETON), UnicodeLocaleExtension.NU_THAI);
+        NUMBER_THAI._map.put(UnicodeLocaleExtension.SINGLETON, UnicodeLocaleExtension.NU_THAI);
     }
 
     private LocaleExtensions() {
@@ -71,7 +71,7 @@
         }
 
         // Build extension map
-        _map = new TreeMap<Character, Extension>();
+        _map = new TreeMap<>();
         if (hasExtension) {
             for (Entry<CaseInsensitiveChar, String> ext : extensions.entrySet()) {
                 char key = AsciiUtil.toLower(ext.getKey().value());
@@ -86,7 +86,7 @@
                 }
 
                 Extension e = new Extension(key, AsciiUtil.toLowerString(value));
-                _map.put(Character.valueOf(key), e);
+                _map.put(key, e);
             }
         }
 
@@ -111,7 +111,7 @@
             }
 
             UnicodeLocaleExtension ule = new UnicodeLocaleExtension(uaset, ukmap);
-            _map.put(Character.valueOf(UnicodeLocaleExtension.SINGLETON), ule);
+            _map.put(UnicodeLocaleExtension.SINGLETON, ule);
         }
 
         if (_map.size() == 0) {
@@ -128,11 +128,11 @@
     }
 
     public Extension getExtension(Character key) {
-        return _map.get(Character.valueOf(AsciiUtil.toLower(key.charValue())));
+        return _map.get(AsciiUtil.toLower(key.charValue()));
     }
 
     public String getExtensionValue(Character key) {
-        Extension ext = _map.get(Character.valueOf(AsciiUtil.toLower(key.charValue())));
+        Extension ext = _map.get(AsciiUtil.toLower(key.charValue()));
         if (ext == null) {
             return null;
         }
@@ -140,7 +140,7 @@
     }
 
     public Set<String> getUnicodeLocaleAttributes() {
-        Extension ext = _map.get(Character.valueOf(UnicodeLocaleExtension.SINGLETON));
+        Extension ext = _map.get(UnicodeLocaleExtension.SINGLETON);
         if (ext == null) {
             return Collections.emptySet();
         }
@@ -149,7 +149,7 @@
     }
 
     public Set<String> getUnicodeLocaleKeys() {
-        Extension ext = _map.get(Character.valueOf(UnicodeLocaleExtension.SINGLETON));
+        Extension ext = _map.get(UnicodeLocaleExtension.SINGLETON);
         if (ext == null) {
             return Collections.emptySet();
         }
@@ -158,7 +158,7 @@
     }
 
     public String getUnicodeLocaleType(String unicodeLocaleKey) {
-        Extension ext = _map.get(Character.valueOf(UnicodeLocaleExtension.SINGLETON));
+        Extension ext = _map.get(UnicodeLocaleExtension.SINGLETON);
         if (ext == null) {
             return null;
         }
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
deleted file mode 100644
index f8aea60..0000000
--- a/android_icu4j/src/main/java/android/icu/impl/locale/XLikelySubtags.java
+++ /dev/null
@@ -1,590 +0,0 @@
-/* GENERATED SOURCE. DO NOT MODIFY. */
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
-package android.icu.impl.locale;
-
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.MissingResourceException;
-import java.util.TreeMap;
-
-import android.icu.impl.ICUData;
-import android.icu.impl.ICUResourceBundle;
-import android.icu.impl.UResource;
-import android.icu.util.BytesTrie;
-import android.icu.util.ULocale;
-
-/**
- * @hide Only a subset of ICU is exposed in Android
- */
-public final class XLikelySubtags {
-    private static final String PSEUDO_ACCENTS_PREFIX = "'";  // -XA, -PSACCENT
-    private static final String PSEUDO_BIDI_PREFIX = "+";  // -XB, -PSBIDI
-    private static final String PSEUDO_CRACKED_PREFIX = ",";  // -XC, -PSCRACK
-
-    public static final int SKIP_SCRIPT = 1;
-
-    private static final boolean DEBUG_OUTPUT = LSR.DEBUG_OUTPUT;
-
-    // VisibleForTesting
-    /**
-     * @hide Only a subset of ICU is exposed in Android
-     */
-    public static final class Data {
-        public final Map<String, String> languageAliases;
-        public final Map<String, String> regionAliases;
-        public final byte[] trie;
-        public final LSR[] lsrs;
-
-        public Data(Map<String, String> languageAliases, Map<String, String> regionAliases,
-                byte[] trie, LSR[] lsrs) {
-            this.languageAliases = languageAliases;
-            this.regionAliases = regionAliases;
-            this.trie = trie;
-            this.lsrs = lsrs;
-        }
-
-        private static UResource.Value getValue(UResource.Table table,
-                String key, UResource.Value value) {
-            if (!table.findValue(key, value)) {
-                throw new MissingResourceException(
-                        "langInfo.res missing data", "", "likely/" + key);
-            }
-            return value;
-        }
-
-        // VisibleForTesting
-        public static Data load() throws MissingResourceException {
-            ICUResourceBundle langInfo = ICUResourceBundle.getBundleInstance(
-                    ICUData.ICU_BASE_NAME, "langInfo",
-                    ICUResourceBundle.ICU_DATA_CLASS_LOADER, ICUResourceBundle.OpenType.DIRECT);
-            UResource.Value value = langInfo.getValueWithFallback("likely");
-            UResource.Table likelyTable = value.getTable();
-
-            Map<String, String> languageAliases;
-            if (likelyTable.findValue("languageAliases", value)) {
-                String[] pairs = value.getStringArray();
-                languageAliases = new HashMap<>(pairs.length / 2);
-                for (int i = 0; i < pairs.length; i += 2) {
-                    languageAliases.put(pairs[i], pairs[i + 1]);
-                }
-            } else {
-                languageAliases = Collections.emptyMap();
-            }
-
-            Map<String, String> regionAliases;
-            if (likelyTable.findValue("regionAliases", value)) {
-                String[] pairs = value.getStringArray();
-                regionAliases = new HashMap<>(pairs.length / 2);
-                for (int i = 0; i < pairs.length; i += 2) {
-                    regionAliases.put(pairs[i], pairs[i + 1]);
-                }
-            } else {
-                regionAliases = Collections.emptyMap();
-            }
-
-            ByteBuffer buffer = getValue(likelyTable, "trie", value).getBinary();
-            byte[] trie = new byte[buffer.remaining()];
-            buffer.get(trie);
-
-            String[] lsrSubtags = getValue(likelyTable, "lsrs", value).getStringArray();
-            LSR[] lsrs = new LSR[lsrSubtags.length / 3];
-            for (int i = 0, j = 0; i < lsrSubtags.length; i += 3, ++j) {
-                lsrs[j] = new LSR(lsrSubtags[i], lsrSubtags[i + 1], lsrSubtags[i + 2],
-                        LSR.IMPLICIT_LSR);
-            }
-
-            return new Data(languageAliases, regionAliases, trie, lsrs);
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (this == other) { return true; }
-            if (other == null || !getClass().equals(other.getClass())) { return false; }
-            Data od = (Data)other;
-            return
-                    languageAliases.equals(od.languageAliases) &&
-                    regionAliases.equals(od.regionAliases) &&
-                    Arrays.equals(trie, od.trie) &&
-                    Arrays.equals(lsrs, od.lsrs);
-        }
-
-        @Override
-        public int hashCode() {  // unused; silence ErrorProne
-            return 1;
-        }
-    }
-
-    // VisibleForTesting
-    public static final XLikelySubtags INSTANCE = new XLikelySubtags(Data.load());
-
-    private final Map<String, String> languageAliases;
-    private final Map<String, String> regionAliases;
-
-    // The trie maps each lang+script+region (encoded in ASCII) to an index into lsrs.
-    // There is also a trie value for each intermediate lang and lang+script.
-    // '*' is used instead of "und", "Zzzz"/"" and "ZZ"/"".
-    private final BytesTrie trie;
-    private final long trieUndState;
-    private final long trieUndZzzzState;
-    private final int defaultLsrIndex;
-    private final long[] trieFirstLetterStates = new long[26];
-    private final LSR[] lsrs;
-
-    private XLikelySubtags(XLikelySubtags.Data data) {
-        languageAliases = data.languageAliases;
-        regionAliases = data.regionAliases;
-        trie = new BytesTrie(data.trie, 0);
-        lsrs = data.lsrs;
-
-        // Cache the result of looking up language="und" encoded as "*", and "und-Zzzz" ("**").
-        BytesTrie.Result result = trie.next('*');
-        assert result.hasNext();
-        trieUndState = trie.getState64();
-        result = trie.next('*');
-        assert result.hasNext();
-        trieUndZzzzState = trie.getState64();
-        result = trie.next('*');
-        assert result.hasValue();
-        defaultLsrIndex = trie.getValue();
-        trie.reset();
-
-        for (char c = 'a'; c <= 'z'; ++c) {
-            result = trie.next(c);
-            if (result == BytesTrie.Result.NO_VALUE) {
-                trieFirstLetterStates[c - 'a'] = trie.getState64();
-            }
-            trie.reset();
-        }
-
-        if (DEBUG_OUTPUT) {
-            System.out.println("*** likely subtags");
-            for (Map.Entry<String, LSR> mapping : getTable().entrySet()) {
-                System.out.println(mapping);
-            }
-        }
-    }
-
-    /**
-     * Implementation of LocaleMatcher.canonicalize(ULocale).
-     */
-    public ULocale canonicalize(ULocale locale) {
-        String lang = locale.getLanguage();
-        String lang2 = languageAliases.get(lang);
-        String region = locale.getCountry();
-        String region2 = regionAliases.get(region);
-        if (lang2 != null || region2 != null) {
-            return new ULocale(
-                lang2 == null ? lang : lang2,
-                locale.getScript(),
-                region2 == null ? region : region2);
-        }
-        return locale;
-    }
-
-    private static String getCanonical(Map<String, String> aliases, String alias) {
-        String canonical = aliases.get(alias);
-        return canonical == null ? alias : canonical;
-    }
-
-    // VisibleForTesting
-    public LSR makeMaximizedLsrFrom(ULocale locale) {
-        String name = locale.getName();  // Faster than .toLanguageTag().
-        if (name.startsWith("@x=")) {
-            String tag = locale.toLanguageTag();
-            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(),
-                locale.getVariant());
-    }
-
-    public LSR makeMaximizedLsrFrom(Locale locale) {
-        String tag = locale.toLanguageTag();
-        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(),
-                locale.getVariant());
-    }
-
-    private LSR makeMaximizedLsr(String language, String script, String region, String variant) {
-        // Handle pseudolocales like en-XA, ar-XB, fr-PSCRACK.
-        // They should match only themselves,
-        // not other locales with what looks like the same language and script subtags.
-        if (region.length() == 2 && region.charAt(0) == 'X') {
-            switch (region.charAt(1)) {
-            case 'A':
-                return new LSR(PSEUDO_ACCENTS_PREFIX + language,
-                        PSEUDO_ACCENTS_PREFIX + script, region, LSR.EXPLICIT_LSR);
-            case 'B':
-                return new LSR(PSEUDO_BIDI_PREFIX + language,
-                        PSEUDO_BIDI_PREFIX + script, region, LSR.EXPLICIT_LSR);
-            case 'C':
-                return new LSR(PSEUDO_CRACKED_PREFIX + language,
-                        PSEUDO_CRACKED_PREFIX + script, region, LSR.EXPLICIT_LSR);
-            default:  // normal locale
-                break;
-            }
-        }
-
-        if (variant.startsWith("PS")) {
-            int lsrFlags = region.isEmpty() ?
-                    LSR.EXPLICIT_LANGUAGE | LSR.EXPLICIT_SCRIPT : LSR.EXPLICIT_LSR;
-            switch (variant) {
-            case "PSACCENT":
-                return new LSR(PSEUDO_ACCENTS_PREFIX + language,
-                        PSEUDO_ACCENTS_PREFIX + script,
-                        region.isEmpty() ? "XA" : region, lsrFlags);
-            case "PSBIDI":
-                return new LSR(PSEUDO_BIDI_PREFIX + language,
-                        PSEUDO_BIDI_PREFIX + script,
-                        region.isEmpty() ? "XB" : region, lsrFlags);
-            case "PSCRACK":
-                return new LSR(PSEUDO_CRACKED_PREFIX + language,
-                        PSEUDO_CRACKED_PREFIX + script,
-                        region.isEmpty() ? "XC" : region, lsrFlags);
-            default:  // normal locale
-                break;
-            }
-        }
-
-        language = getCanonical(languageAliases, language);
-        // (We have no script mappings.)
-        region = getCanonical(regionAliases, region);
-        return maximize(language, script, region);
-    }
-
-    /**
-     * Raw access to addLikelySubtags. Input must be in canonical format, eg "en", not "eng" or "EN".
-     */
-    private LSR maximize(String language, String script, String region) {
-        if (language.equals("und")) {
-            language = "";
-        }
-        if (script.equals("Zzzz")) {
-            script = "";
-        }
-        if (region.equals("ZZ")) {
-            region = "";
-        }
-        if (!script.isEmpty() && !region.isEmpty() && !language.isEmpty()) {
-            return new LSR(language, script, region, LSR.EXPLICIT_LSR);  // already maximized
-        }
-
-        int retainOldMask = 0;
-        BytesTrie iter = new BytesTrie(trie);
-        long state;
-        int value;
-        // Small optimization: Array lookup for first language letter.
-        int c0;
-        if (language.length() >= 2 && 0 <= (c0 = language.charAt(0) - 'a') && c0 <= 25 &&
-                (state = trieFirstLetterStates[c0]) != 0) {
-            value = trieNext(iter.resetToState64(state), language, 1);
-        } else {
-            value = trieNext(iter, language, 0);
-        }
-        if (value >= 0) {
-            if (!language.isEmpty()) {
-                retainOldMask |= 4;
-            }
-            state = iter.getState64();
-        } else {
-            retainOldMask |= 4;
-            iter.resetToState64(trieUndState);  // "und" ("*")
-            state = 0;
-        }
-
-        if (value > 0) {
-            // Intermediate or final value from just language.
-            if (value == SKIP_SCRIPT) {
-                value = 0;
-            }
-            if (!script.isEmpty()) {
-                retainOldMask |= 2;
-            }
-        } else {
-            value = trieNext(iter, script, 0);
-            if (value >= 0) {
-                if (!script.isEmpty()) {
-                    retainOldMask |= 2;
-                }
-                state = iter.getState64();
-            } else {
-                retainOldMask |= 2;
-                if (state == 0) {
-                    iter.resetToState64(trieUndZzzzState);  // "und-Zzzz" ("**")
-                } else {
-                    iter.resetToState64(state);
-                    value = trieNext(iter, "", 0);
-                    assert value >= 0;
-                    state = iter.getState64();
-                }
-            }
-        }
-
-        if (value > 0) {
-            // Final value from just language or language+script.
-            if (!region.isEmpty()) {
-                retainOldMask |= 1;
-            }
-        } else {
-            value = trieNext(iter, region, 0);
-            if (value >= 0) {
-                if (!region.isEmpty()) {
-                    retainOldMask |= 1;
-                }
-            } else {
-                retainOldMask |= 1;
-                if (state == 0) {
-                    value = defaultLsrIndex;
-                } else {
-                    iter.resetToState64(state);
-                    value = trieNext(iter, "", 0);
-                    assert value > 0;
-                }
-            }
-        }
-        LSR result = lsrs[value];
-
-        if (language.isEmpty()) {
-            language = "und";
-        }
-
-        if (retainOldMask == 0) {
-            assert result.flags == LSR.IMPLICIT_LSR;
-            return result;
-        }
-        if ((retainOldMask & 4) == 0) {
-            language = result.language;
-        }
-        if ((retainOldMask & 2) == 0) {
-            script = result.script;
-        }
-        if ((retainOldMask & 1) == 0) {
-            region = result.region;
-        }
-        // retainOldMask flags = LSR explicit-subtag flags
-        return new LSR(language, script, region, retainOldMask);
-    }
-
-    /**
-     * Tests whether lsr is "more likely" than other.
-     * For example, fr-Latn-FR is more likely than fr-Latn-CH because
-     * FR is the default region for fr-Latn.
-     *
-     * <p>The likelyInfo caches lookup information between calls.
-     * The return value is an updated likelyInfo value,
-     * with bit 0 set if lsr is "more likely".
-     * The initial value of likelyInfo must be negative.
-     */
-    int compareLikely(LSR lsr, LSR other, int likelyInfo) {
-        // If likelyInfo >= 0:
-        // likelyInfo bit 1 is set if the previous comparison with lsr
-        // was for equal language and script.
-        // Otherwise the scripts differed.
-        if (!lsr.language.equals(other.language)) {
-            return 0xfffffffc;  // negative, lsr not better than other
-        }
-        if (!lsr.script.equals(other.script)) {
-            int index;
-            if (likelyInfo >= 0 && (likelyInfo & 2) == 0) {
-                index = likelyInfo >> 2;
-            } else {
-                index = getLikelyIndex(lsr.language, "");
-                likelyInfo = index << 2;
-            }
-            LSR likely = lsrs[index];
-            if (lsr.script.equals(likely.script)) {
-                return likelyInfo | 1;
-            } else {
-                return likelyInfo & ~1;
-            }
-        }
-        if (!lsr.region.equals(other.region)) {
-            int index;
-            if (likelyInfo >= 0 && (likelyInfo & 2) != 0) {
-                index = likelyInfo >> 2;
-            } else {
-                index = getLikelyIndex(lsr.language, lsr.region);
-                likelyInfo = (index << 2) | 2;
-            }
-            LSR likely = lsrs[index];
-            if (lsr.region.equals(likely.region)) {
-                return likelyInfo | 1;
-            } else {
-                return likelyInfo & ~1;
-            }
-        }
-        return likelyInfo & ~1;  // lsr not better than other
-    }
-
-    // Subset of maximize().
-    private int getLikelyIndex(String language, String script) {
-        if (language.equals("und")) {
-            language = "";
-        }
-        if (script.equals("Zzzz")) {
-            script = "";
-        }
-
-        BytesTrie iter = new BytesTrie(trie);
-        long state;
-        int value;
-        // Small optimization: Array lookup for first language letter.
-        int c0;
-        if (language.length() >= 2 && 0 <= (c0 = language.charAt(0) - 'a') && c0 <= 25 &&
-                (state = trieFirstLetterStates[c0]) != 0) {
-            value = trieNext(iter.resetToState64(state), language, 1);
-        } else {
-            value = trieNext(iter, language, 0);
-        }
-        if (value >= 0) {
-            state = iter.getState64();
-        } else {
-            iter.resetToState64(trieUndState);  // "und" ("*")
-            state = 0;
-        }
-
-        if (value > 0) {
-            // Intermediate or final value from just language.
-            if (value == SKIP_SCRIPT) {
-                value = 0;
-            }
-        } else {
-            value = trieNext(iter, script, 0);
-            if (value >= 0) {
-                state = iter.getState64();
-            } else {
-                if (state == 0) {
-                    iter.resetToState64(trieUndZzzzState);  // "und-Zzzz" ("**")
-                } else {
-                    iter.resetToState64(state);
-                    value = trieNext(iter, "", 0);
-                    assert value >= 0;
-                    state = iter.getState64();
-                }
-            }
-        }
-
-        if (value > 0) {
-            // Final value from just language or language+script.
-        } else {
-            value = trieNext(iter, "", 0);
-            assert value > 0;
-        }
-        return value;
-    }
-
-    private static final int trieNext(BytesTrie iter, String s, int i) {
-        BytesTrie.Result result;
-        if (s.isEmpty()) {
-            result = iter.next('*');
-        } else {
-            int end = s.length() - 1;
-            for (;; ++i) {
-                int c = s.charAt(i);
-                if (i < end) {
-                    if (!iter.next(c).hasNext()) {
-                        return -1;
-                    }
-                } else {
-                    // last character of this subtag
-                    result = iter.next(c | 0x80);
-                    break;
-                }
-            }
-        }
-        switch (result) {
-        case NO_MATCH: return -1;
-        case NO_VALUE: return 0;
-        case INTERMEDIATE_VALUE:
-            assert iter.getValue() == SKIP_SCRIPT;
-            return SKIP_SCRIPT;
-        case FINAL_VALUE: return iter.getValue();
-        default: return -1;
-        }
-    }
-
-    LSR minimizeSubtags(String languageIn, String scriptIn, String regionIn,
-            ULocale.Minimize fieldToFavor) {
-        LSR result = maximize(languageIn, scriptIn, regionIn);
-
-        // We could try just a series of checks, like:
-        // LSR result2 = addLikelySubtags(languageIn, "", "");
-        // if result.equals(result2) return result2;
-        // However, we can optimize 2 of the cases:
-        //   (languageIn, "", "")
-        //   (languageIn, "", regionIn)
-
-        // value00 = lookup(result.language, "", "")
-        BytesTrie iter = new BytesTrie(trie);
-        int value = trieNext(iter, result.language, 0);
-        assert value >= 0;
-        if (value == 0) {
-            value = trieNext(iter, "", 0);
-            assert value >= 0;
-            if (value == 0) {
-                value = trieNext(iter, "", 0);
-            }
-        }
-        assert value > 0;
-        LSR value00 = lsrs[value];
-        boolean favorRegionOk = false;
-        if (result.script.equals(value00.script)) { //script is default
-            if (result.region.equals(value00.region)) {
-                return new LSR(result.language, "", "", LSR.DONT_CARE_FLAGS);
-            } else if (fieldToFavor == ULocale.Minimize.FAVOR_REGION) {
-                return new LSR(result.language, "", result.region, LSR.DONT_CARE_FLAGS);
-            } else {
-                favorRegionOk = true;
-            }
-        }
-
-        // The last case is not as easy to optimize.
-        // Maybe do later, but for now use the straightforward code.
-        LSR result2 = maximize(languageIn, scriptIn, "");
-        if (result2.equals(result)) {
-            return new LSR(result.language, result.script, "", LSR.DONT_CARE_FLAGS);
-        } else if (favorRegionOk) {
-            return new LSR(result.language, "", result.region, LSR.DONT_CARE_FLAGS);
-        }
-        return result;
-    }
-
-    private Map<String, LSR> getTable() {
-        Map<String, LSR> map = new TreeMap<>();
-        StringBuilder sb = new StringBuilder();
-        for (BytesTrie.Entry entry : trie) {
-            sb.setLength(0);
-            int length = entry.bytesLength();
-            for (int i = 0; i < length;) {
-                byte b = entry.byteAt(i++);
-                if (b == '*') {
-                    sb.append("*-");
-                } else if (b >= 0) {
-                    sb.append((char) b);
-                } else {  // end of subtag
-                    sb.append((char) (b & 0x7f)).append('-');
-                }
-            }
-            assert sb.length() > 0 && sb.charAt(sb.length() - 1) == '-';
-            sb.setLength(sb.length() - 1);
-            map.put(sb.toString(), lsrs[entry.value]);
-        }
-        return map;
-    }
-
-    @Override
-    public String toString() {
-        return getTable().toString();
-    }
-}
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/ConstantAffixModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/ConstantAffixModifier.java
index e3c970c..349b326 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/ConstantAffixModifier.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/ConstantAffixModifier.java
@@ -91,7 +91,7 @@
     }
 
     @Override
-    public boolean semanticallyEquivalent(Modifier other) {
+    public boolean strictEquals(Modifier other) {
         if (!(other instanceof ConstantAffixModifier)) {
             return false;
         }
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java
index 19b21d3..a9b780b 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/ConstantMultiFieldModifier.java
@@ -98,14 +98,11 @@
     }
 
     @Override
-    public boolean semanticallyEquivalent(Modifier other) {
+    public boolean strictEquals(Modifier other) {
         if (!(other instanceof ConstantMultiFieldModifier)) {
             return false;
         }
         ConstantMultiFieldModifier _other = (ConstantMultiFieldModifier) other;
-        if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) {
-            return true;
-        }
         return Arrays.equals(prefixChars, _other.prefixChars) && Arrays.equals(prefixFields, _other.prefixFields)
                 && Arrays.equals(suffixChars, _other.suffixChars) && Arrays.equals(suffixFields, _other.suffixFields)
                 && overwrite == _other.overwrite && strong == _other.strong;
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 6e24f96..b75ae8c 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
@@ -720,7 +720,7 @@
 
         StringBuilder sb = new StringBuilder();
         toScientificString(sb);
-        return Double.valueOf(sb.toString());
+        return Double.parseDouble(sb.toString());
     }
 
     @Override
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 932a814..b706bcb 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
@@ -82,7 +82,7 @@
         ICUResourceBundle resource = (ICUResourceBundle) UResourceBundle
                 .getBundleInstance(ICUData.ICU_BASE_NAME, locale);
         String result = resource.getStringWithFallback("NumberElements/minimumGroupingDigits");
-        return Short.valueOf(result);
+        return Short.parseShort(result);
     }
 
     /**
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 39bb3e9..8ba56ba 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
@@ -1301,8 +1301,8 @@
     }
 
     /**
-     * Sets modifiers to a combination of `leadFormats` (one per plural form)
-     * and `trailFormat` appended to each.
+     * Sets modifiers to a combination of {@code leadFormats} (one per plural form)
+     * and {@code 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
@@ -1342,9 +1342,9 @@
     }
 
     /**
-     * Produces a plural-appropriate Modifier for a unit: `quantity` is taken as
+     * Produces a plural-appropriate Modifier for a unit: {@code quantity} is taken as
      * the final smallest unit, while the larger unit values must be provided
-     * via `micros.mixedMeasures`.
+     * via {@code micros.mixedMeasures}.
      *
      * Does not call parent.processQuantity, so cannot get a MicroProps instance
      * that way. Instead, the instance is passed in as a parameter.
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 be75b29..4fd22d6 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
@@ -82,8 +82,8 @@
 
     /**
      * As MicroProps is the "base instance", this implementation of
-     * MircoPropsGenerator.processQuantity() just ensures that the output
-     * `micros` is correctly initialized.
+     * {@code MircoPropsGenerator.processQuantity()} just ensures that the output
+     * {@code micros} is correctly initialized.
      * <p>
      * For the "safe" invocation of this function, micros must not be *this,
      * such that a copy of the base instance is made. For the "unsafe" path,
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 1d21c87..90b46d6 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
@@ -105,9 +105,9 @@
     }
 
     /**
-     * Produces a plural-appropriate Modifier for a mixed unit: `quantity` is
+     * Produces a plural-appropriate Modifier for a mixed unit: {@code quantity} is
      * taken as the final smallest unit, while the larger unit values must be
-     * provided by `micros.mixedMeasures`, micros being the MicroProps instance
+     * provided by {@code micros.mixedMeasures}, micros being the MicroProps instance
      * returned by the parent.
      *
      * This function must not be called if this instance has no parent: call
@@ -123,9 +123,9 @@
     }
 
     /**
-     * Produces a plural-appropriate Modifier for a mixed unit: `quantity` is
+     * Produces a plural-appropriate Modifier for a mixed unit: {@code quantity} is
      * taken as the final smallest unit, while the larger unit values must be
-     * provided via `micros.mixedMeasures`.
+     * provided via {@code micros.mixedMeasures}.
      *
      * Does not call parent.processQuantity, so cannot get a MicroProps instance
      * that way. Instead, the instance is passed in as a parameter.
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java b/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java
index db6c27d..08a4156 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/Modifier.java
@@ -26,6 +26,7 @@
         POS;
 
         static final int COUNT = Signum.values().length;
+        public static final Signum[] VALUES = Signum.values();
     };
 
     /**
@@ -87,8 +88,45 @@
     public Parameters getParameters();
 
     /**
+     * Returns whether this Modifier equals another Modifier.
+     */
+    public boolean strictEquals(Modifier other);
+
+    /**
      * Returns whether this Modifier is *semantically equivalent* to the other Modifier;
      * in many cases, this is the same as equal, but parameters should be ignored.
      */
-    public boolean semanticallyEquivalent(Modifier other);
+    default boolean semanticallyEquivalent(Modifier other) {
+        Parameters paramsThis = this.getParameters();
+        Parameters paramsOther = other.getParameters();
+        if (paramsThis == null && paramsOther == null) {
+            return this.strictEquals(other);
+        } else if (paramsThis == null || paramsOther == null) {
+            return false;
+        } else if (paramsThis.obj == null && paramsOther.obj == null) {
+            return this.strictEquals(other);
+        } else if (paramsThis.obj == null || paramsOther.obj == null) {
+            return false;
+        }
+        for (Signum signum : Signum.VALUES) {
+            for (StandardPlural plural : StandardPlural.VALUES) {
+                Modifier mod1 = paramsThis.obj.getModifier(signum, plural);
+                Modifier mod2 = paramsOther.obj.getModifier(signum, plural);
+                if (mod1 == mod2) {
+                    // Equal pointers
+                    continue;
+                } else if (mod1 == null || mod2 == null) {
+                    // One pointer is null but not the other
+                    return false;
+                } else if (!mod1.strictEquals(mod2)) {
+                    // The modifiers are NOT equivalent
+                    return false;
+                } else {
+                    // The modifiers are equivalent
+                    continue;
+                }
+            }
+        }
+        return true;
+    }
 }
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 e84af1e..b4bce00 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
@@ -352,7 +352,7 @@
     }
 
     @Override
-    public boolean semanticallyEquivalent(Modifier other) {
+    public boolean strictEquals(Modifier other) {
         // This method is not currently used. (unsafe path not used in range formatting)
         assert false;
         return false;
diff --git a/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java b/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java
index 8a34133..34ad9f8 100644
--- a/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java
+++ b/android_icu4j/src/main/java/android/icu/impl/number/SimpleModifier.java
@@ -73,14 +73,11 @@
     }
 
     @Override
-    public boolean semanticallyEquivalent(Modifier other) {
+    public boolean strictEquals(Modifier other) {
         if (!(other instanceof SimpleModifier)) {
             return false;
         }
         SimpleModifier _other = (SimpleModifier) other;
-        if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) {
-            return true;
-        }
         return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong;
     }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java b/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java
index 6d0918c..3d0e6bd 100644
--- a/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/personname/FieldModifierImpl.java
@@ -4,7 +4,6 @@
 package android.icu.impl.personname;
 
 import java.util.Locale;
-import java.util.StringTokenizer;
 
 import android.icu.lang.UCharacter;
 import android.icu.text.BreakIterator;
@@ -31,9 +30,18 @@
             case INITIAL_CAP:
                 return new InitialCapModifier(formatterImpl.getLocale());
             case INITIAL:
-                return new InitialModifier(formatterImpl.getInitialPattern(), formatterImpl.getInitialSequencePattern());
+                return new InitialModifier(formatterImpl.getLocale(), formatterImpl.getInitialPattern(), formatterImpl.getInitialSequencePattern());
+            case RETAIN:
+                // "retain" is handled by InitialMofidier and PersonNamePattern.NameFieldImpl
+                return NOOP_MODIFIER;
             case MONOGRAM:
                 return MONOGRAM_MODIFIER;
+            case GENITIVE:
+                // no built-in implementation for deriving genitive from nominative; PersonName object must supply
+                return NOOP_MODIFIER;
+            case VOCATIVE:
+                // no built-in implementation for deriving vocative from nominative; PersonName object must supply
+                return NOOP_MODIFIER;
             default:
                 throw new IllegalArgumentException("Invalid modifier ID " + modifierID);
         }
@@ -104,25 +112,47 @@
      * (In English, these patterns put periods after each initial and connect them with spaces.)
      * This is default behavior of the "initial" modifier.
      */
-    private static class InitialModifier extends FieldModifierImpl {
+    static class InitialModifier extends FieldModifierImpl {
+        private final Locale locale;
         private final SimpleFormatter initialFormatter;
         private final SimpleFormatter initialSequenceFormatter;
+        private boolean retainPunctuation;
 
-        public InitialModifier(String initialPattern, String initialSequencePattern) {
+        public InitialModifier(Locale locale, String initialPattern, String initialSequencePattern) {
+            this.locale = locale;
             this.initialFormatter = SimpleFormatter.compile(initialPattern);
             this.initialSequenceFormatter = SimpleFormatter.compile(initialSequencePattern);
+            this.retainPunctuation = false;
+        }
+
+        public void setRetainPunctuation(boolean retain) {
+            this.retainPunctuation = retain;
         }
 
         @Override
         public String modifyField(String fieldValue) {
+            String separator = "";
             String result = null;
-            StringTokenizer tok = new StringTokenizer(fieldValue, " ");
-            while (tok.hasMoreTokens()) {
-                String curInitial = getFirstGrapheme(tok.nextToken());
-                if (result == null) {
-                    result = initialFormatter.format(curInitial);
+            BreakIterator bi = BreakIterator.getWordInstance(locale);
+            bi.setText(fieldValue);
+            int wordStart = bi.first();
+            for (int wordEnd = bi.next(); wordEnd != BreakIterator.DONE; wordStart = wordEnd, wordEnd = bi.next()) {
+                String word = fieldValue.substring(wordStart, wordEnd);
+                if (Character.isLetter(word.charAt(0))) {
+                    String curInitial = getFirstGrapheme(word);
+                    if (result == null) {
+                        result = initialFormatter.format(curInitial);
+                    } else if (retainPunctuation) {
+                        result = result + separator + initialFormatter.format(curInitial);
+                        separator = "";
+                    } else {
+                        result = initialSequenceFormatter.format(result, initialFormatter.format(curInitial));
+                    }
+                } else if (Character.isWhitespace(word.charAt(0))) {
+                    // coalesce a sequence of whitespace characters down to a single space
+                    separator = separator + word.charAt(0);
                 } else {
-                    result = initialSequenceFormatter.format(result, initialFormatter.format(curInitial));
+                    separator = separator + word;
                 }
             }
             return result;
diff --git a/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java
index 98473da..a17a3d1 100644
--- a/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java
+++ b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNameFormatterImpl.java
@@ -34,43 +34,39 @@
     private final String initialSequencePattern;
     private final boolean capitalizeSurname;
     private final String foreignSpaceReplacement;
-    private final boolean formatterLocaleUsesSpaces;
+    private final String nativeSpaceReplacement;
     private final PersonNameFormatter.Length length;
     private final PersonNameFormatter.Usage usage;
     private final PersonNameFormatter.Formality formality;
-    private final Set<PersonNameFormatter.Options> options;
+    private final PersonNameFormatter.DisplayOrder displayOrder;
 
     public PersonNameFormatterImpl(Locale locale,
                                    PersonNameFormatter.Length length,
                                    PersonNameFormatter.Usage usage,
                                    PersonNameFormatter.Formality formality,
-                                   Set<PersonNameFormatter.Options> options) {
-        // null for `options` is the same as the empty set
-        if (options == null) {
-            options = new HashSet<>();
-        }
-
+                                   PersonNameFormatter.DisplayOrder displayOrder,
+                                   boolean surnameAllCaps) {
         // save off our creation parameters (these are only used if we have to create a second formatter)
         this.length = length;
         this.usage = usage;
         this.formality = formality;
-        this.options = options;
+        this.displayOrder = displayOrder;
+        this.capitalizeSurname = surnameAllCaps;
 
         // load simple property values from the resource bundle (or the options set)
         ICUResourceBundle rb = (ICUResourceBundle)UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
         this.locale = locale;
         this.initialPattern = rb.getStringWithFallback("personNames/initialPattern/initial");
         this.initialSequencePattern = rb.getStringWithFallback("personNames/initialPattern/initialSequence");
-        this.capitalizeSurname = options.contains(PersonNameFormatter.Options.SURNAME_ALLCAPS);
         this.foreignSpaceReplacement = rb.getStringWithFallback("personNames/foreignSpaceReplacement");
-        this.formatterLocaleUsesSpaces = !LOCALES_THAT_DONT_USE_SPACES.contains(locale.getLanguage());
+        this.nativeSpaceReplacement = rb.getStringWithFallback("personNames/nativeSpaceReplacement");
 
         // asjust for combinations of parameters that don't make sense in practice
         if (usage == PersonNameFormatter.Usage.MONOGRAM) {
             // we don't support SORTING in conjunction with MONOGRAM; if the caller passes in SORTING, remove it from
             // the options list
-            options.remove(PersonNameFormatter.Options.SORTING);
-        } else if (options.contains(PersonNameFormatter.Options.SORTING)) {
+            displayOrder = PersonNameFormatter.DisplayOrder.DEFAULT;
+        } else if (displayOrder == PersonNameFormatter.DisplayOrder.SORTING) {
             // we only support SORTING in conjunction with REFERRING; if the caller passes in ADDRESSING, treat it
             // the same as REFERRING
             usage = PersonNameFormatter.Usage.REFERRING;
@@ -80,9 +76,12 @@
         // different for different names), load patterns for both given-first and surname-first names.  (If the user has
         // specified SORTING, we don't need to do this-- we just load the "sorting" patterns and ignore the name's order.)
         final String RESOURCE_PATH_PREFIX = "personNames/namePattern/";
-        String resourceNameBody = length.toString().toLowerCase() + "-" + usage.toString().toLowerCase() + "-"
-                + formality.toString().toLowerCase();
-        if (!options.contains(PersonNameFormatter.Options.SORTING)) {
+        String lengthStr = (length != PersonNameFormatter.Length.DEFAULT) ? length.toString().toLowerCase()
+                : rb.getStringWithFallback("personNames/parameterDefault/length");
+        String formalityStr = (formality != PersonNameFormatter.Formality.DEFAULT) ? formality.toString().toLowerCase()
+                : rb.getStringWithFallback("personNames/parameterDefault/formality");
+        String resourceNameBody = lengthStr + "-" + usage.toString().toLowerCase() + "-" + formalityStr;
+        if (displayOrder != PersonNameFormatter.DisplayOrder.SORTING) {
             ICUResourceBundle gnFirstResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "givenFirst-" + resourceNameBody);
             ICUResourceBundle snFirstResource = rb.getWithFallback(RESOURCE_PATH_PREFIX + "surnameFirst-" + resourceNameBody);
 
@@ -106,54 +105,80 @@
     /**
      * THIS IS A DUMMY CONSTRUCTOR JUST FOR THE USE OF THE UNIT TESTS TO CHECK SOME OF THE INTERNAL IMPLEMENTATION!
      */
-    public PersonNameFormatterImpl(Locale locale, String[] patterns) {
+    public PersonNameFormatterImpl(Locale locale, String[] gnFirstPatterns, String[] snFirstPatterns, String[] gnFirstLocales, String[] snFirstLocales) {
         // first, set dummy values for the other fields
-        snFirstPatterns = null;
-        gnFirstLocales = null;
-        snFirstLocales = null;
         length = PersonNameFormatter.Length.MEDIUM;
         usage = PersonNameFormatter.Usage.REFERRING;
         formality = PersonNameFormatter.Formality.FORMAL;
-        options = Collections.emptySet();
+        displayOrder = PersonNameFormatter.DisplayOrder.DEFAULT;
         initialPattern = "{0}.";
         initialSequencePattern = "{0} {1}";
         capitalizeSurname = false;
         foreignSpaceReplacement = " ";
-        formatterLocaleUsesSpaces = true;
+        nativeSpaceReplacement = " ";
 
-        // then, set values for the fields we actually care about
+        // then, set values for the fields we actually care about (all but gnFirstPatterns are optional)
         this.locale = locale;
-        gnFirstPatterns = PersonNamePattern.makePatterns(patterns, this);
+        this.gnFirstPatterns = PersonNamePattern.makePatterns(gnFirstPatterns, this);
+        this.snFirstPatterns = (snFirstPatterns != null) ? PersonNamePattern.makePatterns(snFirstPatterns, this) : null;
+        if (gnFirstLocales != null) {
+            this.gnFirstLocales = new HashSet<>();
+            Collections.addAll(this.gnFirstLocales, gnFirstLocales);
+        } else {
+            this.gnFirstLocales = null;
+        }
+        if (snFirstLocales != null) {
+            this.snFirstLocales = new HashSet<>();
+            Collections.addAll(this.snFirstLocales, snFirstLocales);
+        } else {
+            this.snFirstLocales = null;
+        }
+    }
 
+    @Override
+    public String toString() {
+        return "PersonNameFormatter: " + displayOrder + "-" + length + "-" + usage + "-" + formality + ", " + locale;
     }
 
     public String formatToString(PersonName name) {
         // TODO: Should probably return a FormattedPersonName object
 
-        // if the formatter is for a language that doesn't use spaces between words and the name is from a language
-        // that does, create a formatter for the NAME'S locale and use THAT to format the name
         Locale nameLocale = getNameLocale(name);
-        boolean nameLocaleUsesSpaces = !LOCALES_THAT_DONT_USE_SPACES.contains(nameLocale.getLanguage());
-        if (!formatterLocaleUsesSpaces && nameLocaleUsesSpaces) {
-            PersonNameFormatterImpl nativeFormatter = new PersonNameFormatterImpl(nameLocale, this.length,
-                    this.usage, this.formality, this.options);
-            String result = nativeFormatter.formatToString(name);
+        String nameScript = getNameScript(name);
 
-            // BUT, if the name is actually written in the formatter locale's script, replace any spaces in the name
-            // with the foreignSpaceReplacement character
-            if (!foreignSpaceReplacement.equals(" ") && scriptMatchesLocale(result, this.locale)) {
-                result = result.replace(" ", this.foreignSpaceReplacement);
+        if (!nameScriptMatchesLocale(nameScript, this.locale)) {
+            Locale newFormattingLocale;
+            if (formattingLocaleExists(nameLocale)) {
+                newFormattingLocale = nameLocale;
+            } else {
+                newFormattingLocale = newLocaleWithScript(null, nameScript, nameLocale.getCountry());
             }
-            return result;
+            PersonNameFormatterImpl nameLocaleFormatter = new PersonNameFormatterImpl(newFormattingLocale, this.length,
+                    this.usage, this.formality, this.displayOrder, this.capitalizeSurname);
+            return nameLocaleFormatter.formatToString(name);
         }
 
-        // if we get down to here, we're just doing normal formatting-- if we have both given-first and surname-first
-        // rules, choose which one to use based on the name's locale and preferred field order
+        String result = null;
+
+        // choose the GN-first or SN-first pattern based on the name itself and use that to format it
         if (snFirstPatterns == null || nameIsGnFirst(name)) {
-            return getBestPattern(gnFirstPatterns, name).format(name);
+            result = getBestPattern(gnFirstPatterns, name).format(name);
         } else {
-            return getBestPattern(snFirstPatterns, name).format(name);
+            result = getBestPattern(snFirstPatterns, name).format(name);
         }
+
+        // if either of the space-replacement characters is something other than a space,
+        // check to see if the name locale's language matches the formatter locale's language.
+        // If they match, replace all spaces with the native space-replacement character,
+        // and if they don't, replace all spaces with the foreign space-replacement character
+        if (!nativeSpaceReplacement.equals(" ") || !foreignSpaceReplacement.equals(" ")) {
+            if (localesMatch(nameLocale, this.locale)) {
+                result = result.replace(" ", nativeSpaceReplacement);
+            } else {
+                result = result.replace(" ", foreignSpaceReplacement);
+            }
+        }
+        return result;
     }
 
     public Locale getLocale() {
@@ -166,7 +191,8 @@
 
     public PersonNameFormatter.Formality getFormality() { return formality; }
 
-    public Set<PersonNameFormatter.Options> getOptions() { return options; }
+    public PersonNameFormatter.DisplayOrder getDisplayOrder() { return displayOrder; }
+    public boolean getSurnameAllCaps() { return capitalizeSurname; }
 
     public String getInitialPattern() {
         return initialPattern;
@@ -180,7 +206,7 @@
         return capitalizeSurname;
     }
 
-    private final Set<String> LOCALES_THAT_DONT_USE_SPACES = new HashSet<>(Arrays.asList("ja", "zh", "th", "yue", "km", "lo"));
+    static final Set<String> NON_DEFAULT_SCRIPTS = new HashSet<>(Arrays.asList("Hani", "Hira", "Kana"));
 
     /**
      * Returns the value of the resource, as a string array.
@@ -205,36 +231,72 @@
      * @return If true, use given-first order to format the name; if false, use surname-first order.
      */
     private boolean nameIsGnFirst(PersonName name) {
-        // the name can declare its order-- check that first (it overrides any locale-based calculation)
-        Set<PersonName.FieldModifier> modifiers = new HashSet<>();
-        String preferredOrder = name.getFieldValue(PersonName.NameField.PREFERRED_ORDER, modifiers);
-        if (preferredOrder != null) {
-            if (preferredOrder.equals("givenFirst")) {
-                return true;
-            } else if (preferredOrder.equals("surnameFirst")) {
-                return false;
-            } else {
-                throw new IllegalArgumentException("Illegal preferredOrder value " + preferredOrder);
-            }
+        // if the formatter has its display order set to one of the "force" values, that overrides
+        // all this logic and the name's preferred-order property
+        if (this.displayOrder == PersonNameFormatter.DisplayOrder.FORCE_GIVEN_FIRST) {
+            return true;
+        } else if (this.displayOrder == PersonNameFormatter.DisplayOrder.FORCE_SURNAME_FIRST) {
+            return false;
         }
 
-        String localeStr = getNameLocale(name).toString();
+        // the name can declare its order-- check that first (it overrides any locale-based calculation)
+        if (name.getPreferredOrder() == PersonName.PreferredOrder.GIVEN_FIRST) {
+            return true;
+        } else if (name.getPreferredOrder() == PersonName.PreferredOrder.SURNAME_FIRST) {
+            return false;
+        }
+
+        // Otherwise, search the gnFirstLocales and snFirstLocales for the locale's name.
+        // For our purposes, the "locale's name" is the locale the name itself gives us (if it
+        // has one), or the locale we guess for the name (if it doesn't).
+        Locale nameLocale = name.getNameLocale();
+        if (nameLocale == null) {
+            nameLocale = getNameLocale(name);
+        }
+
+        // this is a hack to deal with certain script codes that are valid, but not the default, for their locales--
+        // to make the parent-chain lookup work right, we need to replace any of those script codes (in the name's locale)
+        // with the appropriate default script for whatever language and region we have
+        ULocale nameULocale = ULocale.forLocale(nameLocale);
+        if (NON_DEFAULT_SCRIPTS.contains(nameULocale.getScript())) {
+            ULocale.Builder builder = new ULocale.Builder();
+            builder.setLocale(nameULocale);
+            builder.setScript(null);
+            nameULocale = ULocale.addLikelySubtags(builder.build());
+        }
+
+        // now search for the locale in the gnFirstLocales and snFirstLocales lists...
+        String localeStr = nameULocale.getName();
+        String origLocaleStr = localeStr;
+        String languageCode = nameULocale.getLanguage();
+
         do {
+            // first check if the locale is in one of those lists
             if (gnFirstLocales.contains(localeStr)) {
                 return true;
             } else if (snFirstLocales.contains(localeStr)) {
                 return false;
             }
 
-            int lastUnderbarPos = localeStr.lastIndexOf("_");
-            if (lastUnderbarPos >= 0) {
-                localeStr = localeStr.substring(0, lastUnderbarPos);
-            } else {
-                localeStr = "root";
+            // if not, try again with "und" in place of the language code (this lets us use "und_CN" to match
+            // all locales with a region code of "CN" and makes sure the last thing we try is always "und", which
+            // is required to be in gnFirstLocales or snFirstLocales)
+            String undStr = localeStr.replaceAll("^" + languageCode, "und");
+            if (gnFirstLocales.contains(undStr)) {
+                return true;
+            } else if (snFirstLocales.contains(undStr)) {
+                return false;
             }
-        } while (!localeStr.equals("root"));
 
-        // should never get here-- "root" should always be in one of the locales
+            // if we haven't found the locale ID yet, look up its parent locale ID and try again-- if getParentLocaleID()
+            // returns null (i.e., we have a locale ID, such as "zh_Hant", that inherits directly from "root"), try again
+            // with just the locale ID's language code (this fixes it so that "zh_Hant" matches "zh", even though "zh" isn't,
+            // strictly speaking, its parent locale)
+            String parentLocaleStr = ICUResourceBundle.getParentLocaleID(localeStr, origLocaleStr, ICUResourceBundle.OpenType.LOCALE_DEFAULT_ROOT);
+            localeStr = (parentLocaleStr != null) ? parentLocaleStr : languageCode;
+        } while (localeStr != null);
+
+        // should never get here ("und" should always be in gnFirstLocales or snFirstLocales), but if we do...
         return true;
     }
 
@@ -267,6 +329,67 @@
     }
 
     /**
+     * Internal function to figure out the name's script by examining its characters.
+     * @param name The name for which we need the script
+     * @return The four-letter script code for the name.
+     */
+    private String getNameScript(PersonName name) {
+        // Rather than exhaustively checking all the fields in the name, we just check the given-name
+        // and surname fields, giving preference to the script of the surname if they're different
+        // (we concatenate them into one string for simplicity).  The "name script" is the script
+        // of the first character we find whose script isn't "common".  If that script is one
+        // of the scripts used by the specified locale, we have a match.
+        String givenName = name.getFieldValue(PersonName.NameField.SURNAME, Collections.emptySet());
+        String surname = name.getFieldValue(PersonName.NameField.GIVEN, Collections.emptySet());
+        String nameText = ((surname != null) ? surname : "") + ((givenName != null) ? givenName : "");
+        int stringScript = UScript.UNKNOWN;
+        for (int i = 0; stringScript == UScript.UNKNOWN && i < nameText.length(); i++) {
+            int c = nameText.codePointAt(i);
+            int charScript = UScript.getScript(c);
+            if (charScript != UScript.COMMON && charScript != UScript.INHERITED && charScript != UScript.UNKNOWN) {
+                stringScript = charScript;
+            }
+        }
+        return UScript.getShortName(stringScript);
+    }
+
+    private Locale newLocaleWithScript(Locale oldLocale, String scriptCode, String regionCode) {
+        Locale workingLocale;
+        String localeScript;
+
+        // if we got the "unknown" script code, don't do anything with it-- just return the original locale
+        if (scriptCode.equals("Zzzz")) {
+            return oldLocale;
+        }
+
+        Locale.Builder builder = new Locale.Builder();
+        if (oldLocale != null) {
+            workingLocale = oldLocale;
+            builder.setLocale(oldLocale);
+            localeScript = ULocale.addLikelySubtags(ULocale.forLocale(oldLocale)).getScript();
+        } else {
+            ULocale tmpLocale = ULocale.addLikelySubtags(new ULocale("und_" + scriptCode));
+            builder.setLanguage(tmpLocale.getLanguage());
+            workingLocale = ULocale.addLikelySubtags(new ULocale(tmpLocale.getLanguage())).toLocale();
+            localeScript = workingLocale.getScript();
+
+            if (regionCode != null) {
+                builder.setRegion(regionCode);
+            }
+        }
+
+        // if the detected character script matches one of the default scripts for the name's locale,
+        // use the name locale's default script code in the locale ID we return (this converts a detected
+        // script of "Hani" to "Hans" for "zh", "Hant" for "zh_Hant", and "Jpan" for "ja")
+        if (!scriptCode.equals(localeScript) && nameScriptMatchesLocale(scriptCode, workingLocale)) {
+            scriptCode = localeScript;
+        }
+
+        builder.setScript(scriptCode);
+        return builder.build();
+    }
+
+    /**
      * Internal function to figure out the name's locale when the name doesn't specify it.
      * (Note that this code assumes that if the locale is specified, it includes a language
      * code.)
@@ -274,57 +397,69 @@
      * @return The name's (real or guessed) locale.
      */
     private Locale getNameLocale(PersonName name) {
-        // if the name specifies its locale, we can just return it
-        Locale nameLocale = name.getNameLocale();
-        if (nameLocale == null) {
-            // if not, we look at the characters in the name.  If their script matches the default script for the formatter's
-            // locale, we use the formatter's locale as the name's locale
-            int formatterScript = UScript.getCodeFromName(ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript());
-            String givenName = name.getFieldValue(PersonName.NameField.GIVEN, new HashSet<PersonName.FieldModifier>());
-            int nameScript = UScript.INVALID_CODE;
-            for (int i = 0; nameScript == UScript.INVALID_CODE && i < givenName.length(); i++) {
-                // the script of the name is the script of the first character in the name whose script isn't
-                // COMMON or INHERITED
-                int script = UScript.getScript(givenName.charAt(i));
-                if (script != UScript.COMMON && script != UScript.INHERITED) {
-                    nameScript = script;
-                }
-            }
-            if (formatterScript == nameScript) {
-                nameLocale = this.locale;
-            } else {
-                // if the name's script is different from the formatter's script, we use addLikelySubtags() to find the
-                // default language for the name's script and use THAT as the name's locale
-                nameLocale = new Locale(ULocale.addLikelySubtags(new ULocale("und_" + UScript.getShortName(nameScript))).getLanguage());
-            }
-            // TODO: This algorithm has a few deficiencies: First, it assumes the script of the string is the script of the first
-            // character in the string that's not COMMON or INHERITED.  This won't work well for some languages, such as Japanese,
-            // that use multiple scripts.  Doing better would require adding a new getScript(String) method on UScript, which
-            // might be something we want.  Second, we only look at the given-name field.  This field should always be populated,
-            // but if it isn't, we're stuck.  Looking at all the fields requires API on PersonName that we don't need anywhere
-            // else.
-        }
-        return nameLocale;
+        return newLocaleWithScript(name.getNameLocale(), getNameScript(name), null);
     }
 
     /**
-     * Returns true if the script of `s` is one of the default scripts for `locale`.
-     * This function only checks the script of the first character whose script isn't "common,"
-     * so it probably won't work right on mixed-script strings.
+     * Returns true if the characters in the name match one of the scripts for the specified locale.
      */
-    private boolean scriptMatchesLocale(String s, Locale locale) {
-        int[] localeScripts = UScript.getCode(locale);
-        int stringScript = UScript.COMMON;
-        for (int i = 0; stringScript == UScript.COMMON && i < s.length(); i++) {
-            char c = s.charAt(i);
-            stringScript = UScript.getScript(c);
+    private boolean nameScriptMatchesLocale(String nameScriptID, Locale formatterLocale) {
+        // if the script code is the "unknown" script, pretend it matches everything
+        if (nameScriptID.equals("Zzzz")) {
+            return true;
         }
 
+        int[] localeScripts = UScript.getCode(formatterLocale);
+        int nameScript = UScript.getCodeFromName(nameScriptID);
+
         for (int localeScript : localeScripts) {
-            if (localeScript == stringScript) {
+            if (localeScript == nameScript || (localeScript == UScript.SIMPLIFIED_HAN && nameScript == UScript.HAN) || (localeScript == UScript.TRADITIONAL_HAN && nameScript == UScript.HAN)) {
                 return true;
             }
         }
         return false;
     }
+
+    /**
+     * Returns true if there's actual name formatting data for the specified locale (i.e., when
+     * we fetch the resource data, we don't fall back to root).
+     */
+    private boolean formattingLocaleExists(Locale formattingLocale) {
+        // NOTE: What we really want to test for here is whether we're falling back to root for either the resource bundle itself
+        // or for the personNames/nameOrderLocales/givenFirst and personNames/nameOrderLocales/surnameFirst resources.
+        // The problem is that getBundleInstance() doesn't return root when it can't find what it's looking for; it returns
+        // ULocale.getDefault().  We could theoretically get around this by passing OpenType.LOCALE_ROOT, but this
+        // bypasses the parent-locale table, so fallback across script can happen (ja_Latn falls back to ja instead of root).
+        // So I'm checking to see if the language code got changed and using that as a surrogate for falling back to root.
+        String formattingLanguage = formattingLocale.getLanguage();
+        ICUResourceBundle mainRB = ICUResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ULocale.forLocale(formattingLocale), ICUResourceBundle.OpenType.LOCALE_DEFAULT_ROOT);
+        if (!mainRB.getULocale().getLanguage().equals(formattingLanguage)) {
+            return false;
+        }
+
+        ICUResourceBundle gnFirstResource = mainRB.getWithFallback("personNames/nameOrderLocales/givenFirst");
+        ICUResourceBundle snFirstResource = mainRB.getWithFallback("personNames/nameOrderLocales/surnameFirst");
+
+        return gnFirstResource.getULocale().getLanguage().equals(formattingLanguage) || snFirstResource.getULocale().getLanguage().equals(formattingLanguage);
+    }
+
+    /**
+     * Returns true if the two locales should be considered equivalent for space-replacement purposes.
+     */
+    private boolean localesMatch(Locale nameLocale, Locale formatterLocale) {
+        String nameLanguage = nameLocale.getLanguage();
+        String formatterLanguage = formatterLocale.getLanguage();
+
+        if (nameLanguage.equals(formatterLanguage)) {
+            return true;
+        }
+
+        // HACK to make Japanese and Chinese names use the native format and native space replacement
+        // (do we want to do something more general here?)
+        if ((nameLanguage.equals("ja") || nameLanguage.equals("zh")) && (formatterLanguage.equals("ja") || formatterLanguage.equals("zh"))) {
+            return true;
+        }
+
+        return false;
+    }
 }
diff --git a/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java
index d612454..f24e2f2 100644
--- a/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java
+++ b/android_icu4j/src/main/java/android/icu/impl/personname/PersonNamePattern.java
@@ -4,9 +4,11 @@
 package android.icu.impl.personname;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -28,6 +30,11 @@
         return result;
     }
 
+    @Override
+    public String toString() {
+        return patternText;
+    }
+
     private PersonNamePattern(String patternText, PersonNameFormatterImpl formatterImpl) {
         this.patternText = patternText;
 
@@ -89,6 +96,11 @@
         StringBuilder textBefore = new StringBuilder();
         StringBuilder textAfter = new StringBuilder();
 
+        // if the name doesn't have a surname field and the pattern doesn't have a given-name field,
+        // we actually format a modified version of the name object where the contents of the
+        // given-name field has been copied into the surname field
+        name = hackNameForEmptyFields(name);
+
         // the logic below attempts to implement the following algorithm:
         // - If one or more fields at the beginning of the name are empty, also skip all literal text
         //   from the beginning of the name up to the first populated field.
@@ -149,7 +161,7 @@
     public int numEmptyFields(PersonName name) {
         int result = 0;
         for (Element element : patternElements) {
-            result += element.isPopulated(name) ? 0 : 1;
+            result += (!element.isLiteral() && !element.isPopulated(name)) ? 1 : 0;
         }
         return result;
     }
@@ -162,6 +174,11 @@
      * @param s2 The literal text after the omitted field.
      */
     private String coalesce(StringBuilder s1, StringBuilder s2) {
+        // if the contents of s2 occur at the end of s1, we just use s1
+        if (endsWith(s1, s2)) {
+            s2.setLength(0);
+        }
+
         // get the range of non-whitespace characters at the beginning of s1
         int p1 = 0;
         while (p1 < s1.length() && !Character.isWhitespace(s1.charAt(p1))) {
@@ -193,6 +210,45 @@
     }
 
     /**
+     * Returns true if s1 ends with s2.
+     */
+    private boolean endsWith(StringBuilder s1, StringBuilder s2) {
+        int p1 = s1.length() - 1;
+        int p2 = s2.length() - 1;
+
+        while (p1 >= 0 && p2 >= 0 && s1.charAt(p1) == s2.charAt(p2)) {
+            --p1;
+            --p2;
+        }
+        return p2 < 0;
+    }
+
+    private PersonName hackNameForEmptyFields(PersonName originalName) {
+        // this is a hack to deal with mononyms (name objects that don't have both a given name and a surname)--
+        // if the name object has a given-name field but not a surname field and the pattern either doesn't
+        // have a given-name field or only has "{given-initial}", we return a PersonName object that will
+        // return the value of the given-name field when asked for the value of the surname field and that
+        // will return null when asked for the value of the given-name field (all other field values and
+        // properties of the underlying object are returned unchanged)
+        PersonName result = originalName;
+        if (originalName.getFieldValue(PersonName.NameField.SURNAME, Collections.emptySet()) == null) {
+            boolean patternHasNonInitialGivenName = false;
+            for (PersonNamePattern.Element element : patternElements) {
+                if (!element.isLiteral()
+                        && ((NameFieldImpl)element).fieldID == PersonName.NameField.GIVEN
+                        && !((NameFieldImpl)element).modifiers.containsKey(PersonName.FieldModifier.INITIAL)) {
+                    patternHasNonInitialGivenName = true;
+                    break;
+                }
+            }
+            if (!patternHasNonInitialGivenName) {
+                return new GivenToSurnamePersonName(originalName);
+            }
+        }
+        return result;
+    }
+
+    /**
      * A single element in a NamePattern.  This is either a name field or a range of literal text.
      */
     private interface Element {
@@ -211,6 +267,11 @@
             this.text = text;
         }
 
+        @Override
+        public String toString() {
+            return text;
+        }
+
         public boolean isLiteral() {
             return true;
         }
@@ -249,6 +310,26 @@
             for (PersonName.FieldModifier modifierID : modifierIDs) {
                 this.modifiers.put(modifierID, FieldModifierImpl.forName(modifierID, formatterImpl));
             }
+
+            if (this.modifiers.containsKey(PersonName.FieldModifier.RETAIN)
+                    && this.modifiers.containsKey(PersonName.FieldModifier.INITIAL)) {
+                FieldModifierImpl.InitialModifier initialModifier
+                        = (FieldModifierImpl.InitialModifier) this.modifiers.get(PersonName.FieldModifier.INITIAL);
+                initialModifier.setRetainPunctuation(true);
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            sb.append(fieldID);
+            for (PersonName.FieldModifier modifier : modifiers.keySet()) {
+                sb.append("-");
+                sb.append(modifier.toString());
+            }
+            sb.append("}");
+            return sb.toString();
         }
 
         public boolean isLiteral() {
@@ -267,10 +348,48 @@
         }
 
         public boolean isPopulated(PersonName name) {
-            // just check whether the unmodified field contains a value
-            Set<PersonName.FieldModifier> modifierIDs = new HashSet<>();
-            String fieldValue = name.getFieldValue(fieldID, modifierIDs);
-            return fieldValue != null && !fieldValue.isEmpty();
+            String result = this.format(name);
+            return result != null && ! result.isEmpty();
+        }
+    }
+
+    /**
+     * Internal class used when formatting a mononym (a PersonName object that only has
+     * a given-name field).  If the name doesn't have a surname field and the pattern
+     * doesn't have a given-name field (or only has one that produces an initial), we
+     * use this class to behave as though the value supplied in the given-name field
+     * had instead been supplied in the surname field.
+     */
+    private static class GivenToSurnamePersonName implements PersonName {
+        private PersonName underlyingPersonName;
+
+        public GivenToSurnamePersonName(PersonName underlyingPersonName) {
+            this.underlyingPersonName = underlyingPersonName;
+        }
+
+        @Override
+        public String toString() {
+            return "Inverted version of " + underlyingPersonName.toString();
+        }
+        @Override
+        public Locale getNameLocale() {
+            return underlyingPersonName.getNameLocale();
+        }
+
+        @Override
+        public PreferredOrder getPreferredOrder() {
+            return underlyingPersonName.getPreferredOrder();
+        }
+
+        @Override
+        public String getFieldValue(NameField identifier, Set<FieldModifier> modifiers) {
+            if (identifier == NameField.SURNAME) {
+                return underlyingPersonName.getFieldValue(NameField.GIVEN, modifiers);
+            } else if (identifier == NameField.GIVEN) {
+                return null;
+            } else {
+                return underlyingPersonName.getFieldValue(identifier, modifiers);
+            }
         }
     }
 }
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 4e614ac..6778d77 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
@@ -16,7 +16,8 @@
 import android.icu.util.Measure;
 
 /**
- * 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 {@code meter}
+ * to {@code foot+inch}.
  * <p>
  * 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
@@ -137,11 +138,12 @@
     }
 
     /**
-     * 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 {@code quantity} of the {@code inputUnit}, expressed in terms of the biggest
+     * unit in the MeasureUnit {@code outputUnit}, is greater than or equal to {@code 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 {@code meter} and the target unit is {@code foot+inch}. Therefore,
+     * this function will convert the {@code quantity} from {@code meter} to {@code foot}, then, it will
+     * compare the value in {@code foot} with the {@code limit}.
      */
     public boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
         assert !units_.isEmpty();
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 4e6c60f..bc179c6 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
@@ -12,6 +12,7 @@
 
 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;
@@ -36,7 +37,7 @@
     }
 
     /**
-     * Extracts the factor from a `SingleUnitImpl` to its Basic Unit.
+     * Extracts the factor from a {@code SingleUnitImpl} to its Basic Unit.
      *
      * @param singleUnit
      * @return
@@ -81,6 +82,15 @@
 
     }
 
+    // Map the MeasureUnitImpl for a simple unit to its corresponding SimpleUnitID,
+    // then get the specialMappingName for that SimpleUnitID (which may be null if
+    // the simple unit converts to base using factor + offset instelad of a special mapping).
+    protected String getSpecialMappingName(MeasureUnitImpl simpleUnit) {
+        if (!checkSimpleUnit(simpleUnit)) return null;
+        String simpleIdentifier = simpleUnit.getSingleUnits().get(0).getSimpleUnitID();
+        return this.mapToConversionRate.get(simpleIdentifier).getSpecialMappingName();
+    }
+
     public MeasureUnitImpl extractCompoundBaseUnit(MeasureUnitImpl measureUnit) {
         ArrayList<SingleUnitImpl> baseUnits = this.extractBaseUnits(measureUnit);
 
@@ -106,7 +116,7 @@
 
     /**
      * @param singleUnit An instance of SingleUnitImpl.
-     * @return The base units in the `SingleUnitImpl` with applying the dimensionality only and not the SI prefix.
+     * @return The base units in the {@code SingleUnitImpl} with applying the dimensionality only and not the SI prefix.
      * <p>
      * NOTE:
      * This method is helpful when checking the convertibility because no need to check convertibility.
@@ -124,10 +134,17 @@
     }
 
     /**
-     * Checks if the `MeasureUnitImpl` is simple or not.
+     * @return The measurement systems for the specified unit.
+     */
+    public String extractSystems(SingleUnitImpl singleUnit) {
+        return mapToConversionRate.get(singleUnit.getSimpleUnitID()).getSystems();
+    }
+
+    /**
+     * Checks if the {@code MeasureUnitImpl} is simple or not.
      *
      * @param measureUnitImpl
-     * @return true if the `MeasureUnitImpl` is simple, false otherwise.
+     * @return true if the {@code MeasureUnitImpl} is simple, false otherwise.
      */
     private boolean checkSimpleUnit(MeasureUnitImpl measureUnitImpl) {
         if (measureUnitImpl.getComplexity() != MeasureUnit.Complexity.SINGLE) return false;
@@ -162,6 +179,8 @@
                 String target = null;
                 String factor = null;
                 String offset = "0";
+                String special = null;
+                String systems = null;
                 for (int j = 0; simpleUnitConversionInfo.getKeyAndValue(j, key, value); j++) {
                     assert (value.getType() == UResourceBundle.STRING);
 
@@ -174,16 +193,20 @@
                         factor = valueString;
                     } else if ("offset".equals(keyString)) {
                         offset = valueString;
+                    } else if ("special".equals(keyString)) {
+                        special = valueString; // the name of a special mapping used instead of factor + optional offset.
+                    } else if ("systems".equals(keyString)) {
+                        systems = value.toString(); // still want the spaces here
                     } else {
-                        assert false : "The key must be target, factor or offset";
+                        assert false : "The key must be target, factor, offset, special, or systems";
                     }
                 }
 
                 // HERE a single conversion rate data should be loaded
                 assert (target != null);
-                assert (factor != null);
+                assert (factor != null || special != null);
 
-                mapToConversionRate.put(simpleUnit, new ConversionRateInfo(simpleUnit, target, factor, offset));
+                mapToConversionRate.put(simpleUnit, new ConversionRateInfo(simpleUnit, target, factor, offset, special, systems));
             }
 
 
@@ -204,12 +227,16 @@
         private final String target;
         private final String conversionRate;
         private final BigDecimal offset;
+        private final String specialMappingName; // the name of a special mapping used instead of factor + optional offset.
+        private final String systems;
 
-        public ConversionRateInfo(String simpleUnit, String target, String conversionRate, String offset) {
+        public ConversionRateInfo(String simpleUnit, String target, String conversionRate, String offset, String special, String systems) {
             this.simpleUnit = simpleUnit;
             this.target = target;
             this.conversionRate = conversionRate;
             this.offset = forNumberWithDivision(offset);
+            this.specialMappingName = special;
+            this.systems = systems;
         }
 
         private static BigDecimal forNumberWithDivision(String numberWithDivision) {
@@ -244,7 +271,24 @@
          * @return The conversion rate from this unit to the base unit.
          */
         public String getConversionRate() {
+            if (conversionRate==null) {
+                throw new IllegalIcuArgumentException("trying to use a null conversion rate (for special?)");
+            }
             return conversionRate;
         }
+
+        /**
+         * @return The name of the special conversion system for this unit (used instead of factor + optional offset).
+         */
+        public String getSpecialMappingName() {
+            return specialMappingName;
+        }
+
+        /**
+         * @return The measurement systems this unit belongs to.
+         */
+        public String getSystems() {
+            return systems;
+        }
     }
 }
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 1277b1b..975665b 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
@@ -799,6 +799,20 @@
 
         @Override
         public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) {
+            String special1 = this.conversionRates.getSpecialMappingName(o1);
+            String special2 = this.conversionRates.getSpecialMappingName(o2);
+            if (special1 != null || special2 != null) {
+                if (special1 == null) {
+                    // non-specials come first
+                    return -1;
+                }
+                if (special2 == null) {
+                    // non-specials come first
+                    return 1;
+                }
+                // both are specials, compare lexicographically
+                return special1.compareTo(special2);
+            }
             BigDecimal factor1 = this.conversionRates.getFactorToBase(o1).getConversionRate();
             BigDecimal factor2 = this.conversionRates.getFactorToBase(o2).getConversionRate();
 
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 3fc1b4e..02b19c7 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
@@ -30,7 +30,7 @@
      */
     private String simpleUnitID = "";
     /**
-     * Determine the power of the `SingleUnit`. For example, for "square-meter", the dimensionality will be `2`.
+     * Determine the power of the {@code SingleUnit}. For example, for "square-meter", the dimensionality will be {@code 2}.
      * <p>
      * NOTE:
      * Default dimensionality is 1.
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 fd15712..2c8a42d 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
@@ -7,7 +7,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Locale;
+import java.util.List;
 import java.util.Map;
 
 import android.icu.impl.ICUData;
@@ -96,36 +96,39 @@
             }
         }
 
-        String region = locale.getCountry();
+        String region = ULocale.getRegionForSupplementalData(locale, true);
 
         // Check the locale system tag, e.g `ms=metric`.
         String localeSystem = locale.getKeywordValue("measure");
-        boolean isLocaleSystem = false;
-        if (measurementSystem.containsKey(localeSystem)) {
-            isLocaleSystem = true;
-            region = measurementSystem.get(localeSystem);
-        }
-
-        // Check the region tag, e.g. `rg=uszzz`.
-        if (!isLocaleSystem) {
-            String localeRegion = locale.getKeywordValue("rg");
-            if (localeRegion != null && localeRegion.length() >= 3) {
-                if (localeRegion.equals("default")) {
-                    region = localeRegion;
-                } else if (Character.isDigit(localeRegion.charAt(0))) {
-                    region = localeRegion.substring(0, 3); // e.g. 001
-                } else {
-                    // Capitalize the first two character of the region, e.g. ukzzzz or usca
-                    region = localeRegion.substring(0, 2).toUpperCase(Locale.ROOT);
-                }
-            }
-        }
+        boolean isLocaleSystem = measurementSystem.containsKey(localeSystem);
 
         String[] subUsages = getAllUsages(usage);
         UnitPreference[] result = null;
         for (String subUsage :
                 subUsages) {
             result = getUnitPreferences(category, subUsage, region);
+
+            if (result != null && isLocaleSystem) {
+                ConversionRates rates = new ConversionRates();
+                boolean unitsMatchSystem = true;
+                for (UnitPreference unitPref : result) {
+                    MeasureUnitImpl measureUnit = MeasureUnitImpl.forIdentifier(unitPref.getUnit());
+                    List<SingleUnitImpl> singleUnits = new ArrayList<>(measureUnit.getSingleUnits());
+                    for (SingleUnitImpl singleUnit : singleUnits) {
+                        String systems = rates.extractSystems(singleUnit);
+                        if (!systems.contains("metric_adjacent")) {
+                            if (!systems.contains(localeSystem)) {
+                                unitsMatchSystem = false;
+                            }
+                        }
+                    }
+                }
+                if (!unitsMatchSystem) {
+                    String newRegion = measurementSystem.get(localeSystem);
+                    result = getUnitPreferences(category, subUsage, newRegion);
+                }
+            }
+
             if (result != null) break;
         }
 
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
index 8aea577..d18e4e7 100644
--- a/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
+++ b/android_icu4j/src/main/java/android/icu/impl/units/UnitsConverter.java
@@ -7,12 +7,14 @@
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.regex.Pattern;
 
 import android.icu.impl.IllegalIcuArgumentException;
 import android.icu.util.MeasureUnit;
 
+// TODO ICU-22683: Consider splitting handling of special mappings into separate (possibly internal) class
 /**
  * @hide Only a subset of ICU is exposed in Android
  */
@@ -20,6 +22,8 @@
     private BigDecimal conversionRate;
     private boolean reciprocal;
     private BigDecimal offset;
+    private String specialSource;
+    private String specialTarget;
 
     /**
      * Constructor of <code>UnitsConverter</code>.
@@ -46,6 +50,7 @@
      * NOTE:
      * - source and target must be under the same category
      * - e.g. meter to mile --> both of them are length units.
+     * This converts from source to base to target (one of those may be a no-op).
      *
      * @param source          represents the source unit.
      * @param target          represents the target unit.
@@ -57,21 +62,38 @@
             throw new IllegalIcuArgumentException("input units must be convertible or reciprocal");
         }
 
-        Factor sourceToBase = conversionRates.getFactorToBase(source);
-        Factor targetToBase = conversionRates.getFactorToBase(target);
+        this.specialSource = conversionRates.getSpecialMappingName(source);
+        this.specialTarget = conversionRates.getSpecialMappingName(target);
 
-        if (convertibility == Convertibility.CONVERTIBLE) {
-            this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate();
+        if (this.specialSource == null && this.specialTarget == null) {
+            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;
         } else {
-            assert convertibility == Convertibility.RECIPROCAL;
-            this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate();
+            this.reciprocal = false;
+            this.offset = BigDecimal.ZERO;
+            if (this.specialSource == null) {
+                // conversionRate is for source to base only
+                this.conversionRate = conversionRates.getFactorToBase(source).getConversionRate();
+            } else if (this.specialTarget == null) {
+                // conversionRate is for base to target only
+                this.conversionRate = conversionRates.getFactorToBase(target).getConversionRate();
+            } else {
+                this.conversionRate = BigDecimal.ONE;
+            }
         }
-        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) {
@@ -114,8 +136,34 @@
         return true;
     }
 
+    // Convert inputValue (source) to base then to target
     public BigDecimal convert(BigDecimal inputValue) {
-        BigDecimal result = inputValue.multiply(this.conversionRate).add(offset);
+        BigDecimal result = inputValue;
+        if (this.specialSource != null || this.specialTarget != null) {
+            BigDecimal base = inputValue;
+            // convert input (=source) to base
+            if (this.specialSource != null) {
+                // We  have a special mapping from source to base (not using factor, offset).
+                // Currently the only supported mapping is a scale-based mapping for beaufort.
+                base = (this.specialSource.equals("beaufort"))?
+                    scaleToBase(inputValue, minMetersPerSecForBeaufort): inputValue;
+            } else {
+                // Standard mapping (using factor, offset) from source to base.
+                base = inputValue.multiply(this.conversionRate);
+            }
+            // convert base to result (=target)
+            if (this.specialTarget != null) {
+                // We  have a special mapping from base to target (not using factor, offset).
+                // Currently the only supported mapping is a scale-based mapping for beaufort.
+                result = (this.specialTarget.equals("beaufort"))?
+                    baseToScale(base, minMetersPerSecForBeaufort): base;
+            } else {
+                // Standard mapping (using factor, offset) from base to target.
+                result = base.divide(this.conversionRate, DECIMAL128);
+            }
+            return result;
+        }
+        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;
@@ -128,8 +176,33 @@
         return result;
     }
 
+    // Convert inputValue (target) to base then to source
     public BigDecimal convertInverse(BigDecimal inputValue) {
         BigDecimal result = inputValue;
+        if (this.specialSource != null || this.specialTarget != null) {
+            BigDecimal base = inputValue;
+            // convert input (=target) to base
+            if (this.specialTarget != null) {
+                // We  have a special mapping from target to base (not using factor, offset).
+                // Currently the only supported mapping is a scale-based mapping for beaufort.
+                base = (this.specialTarget.equals("beaufort"))?
+                    scaleToBase(inputValue, minMetersPerSecForBeaufort): inputValue;
+            } else {
+                // Standard mapping (using factor, offset) from target to base.
+                base = inputValue.multiply(this.conversionRate);
+            }
+            // convert base to result (=source)
+            if (this.specialSource != null) {
+                // We  have a special mapping from base to source (not using factor, offset).
+                // Currently the only supported mapping is a scale-based mapping for beaufort.
+                result = (this.specialSource.equals("beaufort"))?
+                    baseToScale(base, minMetersPerSecForBeaufort): base;
+            } else {
+                // Standard mapping (using factor, offset) from base to source.
+                result = base.divide(this.conversionRate, DECIMAL128);
+            }
+            return result;
+        }
         if (this.reciprocal) {
             // We should see no offsets for reciprocal conversions - they don't make sense:
             assert offset == BigDecimal.ZERO;
@@ -143,6 +216,64 @@
         return result;
     }
 
+    // TODO per CLDR-17421 and ICU-22683: consider getting the data below from CLDR
+    private static final BigDecimal[] minMetersPerSecForBeaufort = {
+        // Minimum m/s (base) values for each Bft value, plus an extra artificial value;
+        // when converting from Bft to m/s, the middle of the range will be used
+        // (Values from table in Wikipedia, except for artificial value).
+        // Since this is 0 based, max Beaufort value is thus array dimension minus 2.
+        BigDecimal.valueOf(0.0), // 0 Bft
+        BigDecimal.valueOf(0.3), // 1
+        BigDecimal.valueOf(1.6), // 2
+        BigDecimal.valueOf(3.4), // 3
+        BigDecimal.valueOf(5.5), // 4
+        BigDecimal.valueOf(8.0), // 5
+        BigDecimal.valueOf(10.8), // 6
+        BigDecimal.valueOf(13.9), // 7
+        BigDecimal.valueOf(17.2), // 8
+        BigDecimal.valueOf(20.8), // 9
+        BigDecimal.valueOf(24.5), // 10
+        BigDecimal.valueOf(28.5), // 11
+        BigDecimal.valueOf(32.7), // 12
+        BigDecimal.valueOf(36.9), // 13
+        BigDecimal.valueOf(41.4), // 14
+        BigDecimal.valueOf(46.1), // 15
+        BigDecimal.valueOf(51.1), // 16
+        BigDecimal.valueOf(55.8), // 17
+        BigDecimal.valueOf(61.4), // artificial end of range 17 to give reasonable midpoint
+    };
+
+    // Convert from what should be discrete scale values for a particular unit like beaufort
+    // to a corresponding value in the base unit (which can have any decimal value, like meters/sec).
+    // First we round the scale value to the nearest integer (in case it is specified with a fractional value),
+    // then we map that to a value in middle of the range of corresponding base values.
+    // This can handle different scales, specified by minBaseForScaleValues[].
+    private BigDecimal scaleToBase(BigDecimal scaleValue, BigDecimal[] minBaseForScaleValues) {
+        BigDecimal pointFive = BigDecimal.valueOf(0.5);
+        BigDecimal scaleAdjust = scaleValue.abs().add(pointFive); // adjust up for later truncation
+        BigDecimal scaleAdjustCapped = scaleAdjust.min(BigDecimal.valueOf(minBaseForScaleValues.length - 2));
+        int scaleIndex = scaleAdjustCapped.intValue();
+        // Return midpont of range (the final range uses an articial end to produce reasonable midpoint)
+        return minBaseForScaleValues[scaleIndex].add(minBaseForScaleValues[scaleIndex + 1]).multiply(pointFive);
+    }
+
+    // Convert from a value in the base unit (which can have any decimal value, like meters/sec) to a corresponding
+    // discrete value in a scale (like beaufort), where each scale value represents a range of base values.
+    // We binary-search the ranges to find the one that contains the specified base value, and return its index.
+    // This can handle different scales, specified by minBaseForScaleValues[].
+    private BigDecimal baseToScale(BigDecimal baseValue, BigDecimal[] minBaseForScaleValues) {
+        int scaleIndex = Arrays.binarySearch(minBaseForScaleValues, baseValue.abs());
+        if (scaleIndex < 0) {
+            // since our first array entry is 0, this value will always be -2 or less
+            scaleIndex = -scaleIndex - 2;
+        }
+        int scaleMax = minBaseForScaleValues.length - 2;
+        if (scaleIndex > scaleMax) {
+            scaleIndex = scaleMax;
+        }
+        return BigDecimal.valueOf(scaleIndex);
+    }
+
     /**
      * @hide Only a subset of ICU is exposed in Android
      */
@@ -203,6 +334,14 @@
         private int exponentSecPerJulianYear = 0;
         /** Exponent for the speed of light meters per second" conversion rate constant */
         private int exponentSpeedOfLightMetersPerSecond = 0;
+        /** Exponent for https://en.wikipedia.org/wiki/Japanese_units_of_measurement */
+        private int exponentShoToM3 = 0;
+        /** Exponent for https://en.wikipedia.org/wiki/Japanese_units_of_measurement */
+        private int exponentTsuboToM2 = 0;
+        /** Exponent for https://en.wikipedia.org/wiki/Japanese_units_of_measurement */
+        private int exponentShakuToM = 0;
+        /** Exponent for Atomic Mass Unit */
+        private int exponentAMU = 0;
 
         /**
          * Creates Empty Factor
@@ -259,12 +398,16 @@
             result.exponentMetersPerAU = this.exponentMetersPerAU;
             result.exponentSecPerJulianYear = this.exponentSecPerJulianYear;
             result.exponentSpeedOfLightMetersPerSecond = this.exponentSpeedOfLightMetersPerSecond;
+            result.exponentShoToM3 = this.exponentShoToM3;
+            result.exponentTsuboToM2 = this.exponentTsuboToM2;
+            result.exponentShakuToM = this.exponentShakuToM;
+            result.exponentAMU = this.exponentAMU;
 
             return result;
         }
 
         /**
-         * Returns a single `BigDecimal` that represent the conversion rate after substituting all the constants.
+         * Returns a single {@code BigDecimal} that represent the conversion rate after substituting all the constants.
          *
          * In ICU4C, see Factor::substituteConstants().
          */
@@ -289,6 +432,10 @@
             resultCollector.multiply(new BigDecimal("149597870700"), this.exponentMetersPerAU);
             resultCollector.multiply(new BigDecimal("31557600"), this.exponentSecPerJulianYear);
             resultCollector.multiply(new BigDecimal("299792458"), this.exponentSpeedOfLightMetersPerSecond);
+            resultCollector.multiply(new BigDecimal("0.001803906836964688204"), this.exponentShoToM3);   // 2401/(1331*1000)
+            resultCollector.multiply(new BigDecimal("3.305785123966942"), this.exponentTsuboToM2);    // 400/121
+            resultCollector.multiply(new BigDecimal("0.033057851239669"), this.exponentShakuToM);     // 4/121
+            resultCollector.multiply(new BigDecimal("1.66053878283E-27"), this.exponentAMU);
 
             return resultCollector.factorNum.divide(resultCollector.factorDen, DECIMAL128);
         }
@@ -349,6 +496,10 @@
             result.exponentSecPerJulianYear = this.exponentSecPerJulianYear * power;
             result.exponentSpeedOfLightMetersPerSecond =
                 this.exponentSpeedOfLightMetersPerSecond * power;
+            result.exponentShoToM3 = this.exponentShoToM3 * power;
+            result.exponentTsuboToM2 = this.exponentTsuboToM2 * power;
+            result.exponentShakuToM = this.exponentShakuToM * power;
+            result.exponentAMU = this.exponentAMU * power;
 
             return result;
         }
@@ -371,6 +522,10 @@
             result.exponentSecPerJulianYear = this.exponentSecPerJulianYear - other.exponentSecPerJulianYear;
             result.exponentSpeedOfLightMetersPerSecond =
                 this.exponentSpeedOfLightMetersPerSecond - other.exponentSpeedOfLightMetersPerSecond;
+            result.exponentShoToM3 = this.exponentShoToM3 - other.exponentShoToM3;
+            result.exponentTsuboToM2 = this.exponentTsuboToM2 - other.exponentTsuboToM2;
+            result.exponentShakuToM = this.exponentShakuToM - other.exponentShakuToM;
+            result.exponentAMU = this.exponentAMU - other.exponentAMU;
 
             return result;
         }
@@ -393,12 +548,16 @@
             result.exponentSecPerJulianYear = this.exponentSecPerJulianYear + other.exponentSecPerJulianYear;
             result.exponentSpeedOfLightMetersPerSecond =
                 this.exponentSpeedOfLightMetersPerSecond + other.exponentSpeedOfLightMetersPerSecond;
+            result.exponentShoToM3 = this.exponentShoToM3 + other.exponentShoToM3;
+            result.exponentTsuboToM2 = this.exponentTsuboToM2 + other.exponentTsuboToM2;
+            result.exponentShakuToM = this.exponentShakuToM  + other.exponentShakuToM;
+            result.exponentAMU = this.exponentAMU  + other.exponentAMU;
 
             return result;
         }
 
         /**
-         * Adds Entity with power or not. For example, `12 ^ 3` or `12`.
+         * Adds Entity with power or not. For example, {@code 12 ^ 3} or {@code 12}.
          *
          * @param poweredEntity
          */
@@ -440,11 +599,19 @@
                 this.exponentMetersPerAU += power;
             } else if ("PI".equals(entity)) {
                 this.exponentPi += power;
-             } else if ("sec_per_julian_year".equals(entity)) {
+            } else if ("sec_per_julian_year".equals(entity)) {
                 this.exponentSecPerJulianYear += power;
             } else if ("speed_of_light_meters_per_second".equals(entity)) {
                 this.exponentSpeedOfLightMetersPerSecond += power;
-           } else {
+            } else if ("sho_to_m3".equals(entity)) {
+                this.exponentShoToM3 += power;
+            } else if ("tsubo_to_m2".equals(entity)) {
+                this.exponentTsuboToM2 += power;
+            } else if ("shaku_to_m".equals(entity)) {
+                this.exponentShakuToM += power;
+            } else if ("AMU".equals(entity)) {
+                this.exponentAMU += power;
+            } else {
                 BigDecimal decimalEntity = new BigDecimal(entity).pow(power, DECIMAL128);
                 this.factorNum = this.factorNum.multiply(decimalEntity);
             }
diff --git a/android_icu4j/src/main/java/android/icu/impl/units/UnitsData.java b/android_icu4j/src/main/java/android/icu/impl/units/UnitsData.java
index a57bc55..3a416c6 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
@@ -230,10 +230,10 @@
     }
 
     /**
-     * A Resource Sink that collects information from `unitQuantities` in the
-     * `units` resource to provide key->value lookups from base unit to
+     * A Resource Sink that collects information from {@code unitQuantities} in the
+     * {@code units} resource to provide key->value lookups from base unit to
      * category, as well as preserving ordering information for these
-     * categories. See `units.txt`.
+     * categories. See {@code units.txt}.
      *
      * For example: "kilogram" -> "mass", "meter-per-second" -> "speed".
      *
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 383642e..8d349bb 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
@@ -14,32 +14,32 @@
 import android.icu.util.ULocale;
 
 /**
- * `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to
- * one of the complex units based on the limits.
+ * {@code UnitsRouter} responsible for converting from a single unit (such as {@code meter} or
+ * {@code meter-per-second}) to one of the complex units based on the limits.
  * For example:
- * if the input is `meter` and the output as following
- * {`foot+inch`, limit: 3.0}
- * {`inch`     , limit: no value (-inf)}
- * Thus means if the input in `meter` is greater than or equal to `3.0 feet`, the output will be in
- * `foot+inch`, otherwise, the output will be in `inch`.
+ * if the input is {@code meter} and the output as following
+ * {{@code foot+inch}, limit: 3.0}
+ * {{@code inch}     , limit: no value (-inf)}
+ * Thus means if the input in {@code meter} is greater than or equal to {@code 3.0 feet},
+ * the output will be in {@code foot+inch}, otherwise, the output will be in {@code inch}.
  * <p>
  * NOTE:
  * the output units and their limits MUST BE in order, for example, if the output units, from the
  * previous example, are the following:
- * {`inch`     , limit: no value (-inf)}
- * {`foot+inch`, limit: 3.0}
- * IN THIS CASE THE OUTPUT WILL BE ALWAYS IN `inch`.
+ * {{@code inch}     , limit: no value (-inf)}
+ * {{@code foot+inch}, limit: 3.0}
+ * IN THIS CASE THE OUTPUT WILL BE ALWAYS IN {@code inch}.
  * <p>
  * NOTE:
- * the output units  and their limits will be extracted from the units preferences database by knowing
+ * the output units and their limits will be extracted from the units preferences database by knowing
  * the followings:
  * - input unit
  * - locale
  * - usage
  * <p>
  * DESIGN:
- * `UnitRouter` uses internally `ComplexUnitConverter` in order to convert the input units to the
- * desired complex units and to check the limit too.
+ * {@code UnitRouter} uses internally {@code ComplexUnitConverter} in order to convert the input
+ * units to the desired complex units and to check the limit too.
  * @hide Only a subset of ICU is exposed in Android
  */
 public class UnitsRouter {
@@ -148,12 +148,12 @@
 
     /**
      * Contains the complex unit converter and the limit which representing the smallest value that the
-     * converter should accept. For example, if the converter is converting to `foot+inch` and the limit
-     * equals 3.0, thus means the converter should not convert to a value less than `3.0 feet`.
+     * converter should accept. For example, if the converter is converting to {@code foot+inch} and the
+     * limit equals 3.0, thus means the converter should not convert to a value less than {@code 3.0 feet}.
      * <p>
      * NOTE:
-     * if the limit doest not has a value `i.e. (std::numeric_limits<double>::lowest())`, this mean there
-     * is no limit for the converter.
+     * if the limit doest not has a value (i.e. {@code std::numeric_limits<double>::lowest()}),
+     * this mean there is no limit for the converter.
      * @hide Only a subset of ICU is exposed in Android
      */
     public static class ConverterPreference {
diff --git a/android_icu4j/src/main/java/android/icu/lang/CharSequences.java b/android_icu4j/src/main/java/android/icu/lang/CharSequences.java
index 6ec0048..dee800f 100644
--- a/android_icu4j/src/main/java/android/icu/lang/CharSequences.java
+++ b/android_icu4j/src/main/java/android/icu/lang/CharSequences.java
@@ -125,7 +125,7 @@
      * Same results as turning the code point into a string (with the [ugly] new StringBuilder().appendCodePoint(codepoint).toString())
      * and comparing, but much faster (no object creation). 
      * Actually, there is one difference; a null compares as less.
-     * Note that this (=String) order is UTF-16 order -- *not* code point order.
+     * Note that this (=String) order is UTF-16 order -- <i>not</i> code point order.
      * 
      * @deprecated This API is ICU internal only.
      * @hide draft / provisional / internal are hidden on Android
@@ -169,7 +169,7 @@
      * Utility to compare a string to a code point.
      * Same results as turning the code point into a string and comparing, but much faster (no object creation). 
      * Actually, there is one difference; a null compares as less.
-     * Note that this (=String) order is UTF-16 order -- *not* code point order.
+     * Note that this (=String) order is UTF-16 order -- <i>not</i> code point order.
      * 
      * @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/lang/UCharacter.java b/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
index 8dc245a..b25205f 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UCharacter.java
@@ -11,6 +11,7 @@
 package android.icu.lang;
 
 import java.lang.ref.SoftReference;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Locale;
@@ -1132,7 +1133,7 @@
          * @hide unsupported on Android
          */
         @Deprecated
-        public static final int COUNT = 328;
+        public static final int COUNT = 329;
 
         // blocks objects ---------------------------------------------------
 
@@ -2519,9 +2520,7 @@
         {
             super(name);
             m_id_ = id;
-            // Android-changed: Avoid leaking flagged UnicodeBlock until ICU 74 data is integrated.
-            // if (id >= 0) {
-            if (id >= 0 && id < BLOCKS_.length) {
+            if (id >= 0) {
                 BLOCKS_[id] = this;
             }
         }
@@ -3314,7 +3313,7 @@
          * @hide unsupported on Android
          */
         @Deprecated
-        public static final int COUNT = 43;
+        public static final int COUNT = 48;
     }
 
     /**
@@ -3547,6 +3546,56 @@
         public static final int UPRIGHT = 3;
     }
 
+    /**
+     * Identifier Status constants.
+     * See https://www.unicode.org/reports/tr39/#Identifier_Status_and_Type.
+     *
+     * @see UProperty#IDENTIFIER_STATUS
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum IdentifierStatus {
+        /** @hide draft / provisional / internal are hidden on Android*/
+        RESTRICTED,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        ALLOWED,
+    }
+
+    /**
+     * Identifier Type constants.
+     * See https://www.unicode.org/reports/tr39/#Identifier_Status_and_Type.
+     *
+     * @see UProperty#IDENTIFIER_TYPE
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public enum IdentifierType {
+        /** @hide draft / provisional / internal are hidden on Android*/
+        NOT_CHARACTER,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        DEPRECATED,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        DEFAULT_IGNORABLE,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        NOT_NFKC,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        NOT_XID,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        EXCLUSION,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        OBSOLETE,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        TECHNICAL,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        UNCOMMON_USE,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        LIMITED_USE,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        INCLUSION,
+        /** @hide draft / provisional / internal are hidden on Android*/
+        RECOMMENDED,
+    }
+
     // public data members -----------------------------------------------
 
     /**
@@ -3981,79 +4030,79 @@
     }
 
     /**
-     * Determines if the specified code point may be any part of a Unicode
-     * identifier other than the starting character.
-     * A code point may be part of a Unicode identifier if and only if it is
-     * one of the following:
-     * <ul>
-     * <li> Lu Uppercase letter
-     * <li> Ll Lowercase letter
-     * <li> Lt Titlecase letter
-     * <li> Lm Modifier letter
-     * <li> Lo Other letter
-     * <li> Nl Letter number
-     * <li> Pc Connecting punctuation character
-     * <li> Nd decimal number
-     * <li> Mc Spacing combining mark
-     * <li> Mn Non-spacing mark
-     * <li> Cf formatting code
-     * </ul>
-     * Up-to-date Unicode implementation of
-     * java.lang.Character.isUnicodeIdentifierPart().<br>
-     * 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
-     *         identifier suffix after the first character
+     * Determines if the specified character is permissible as a
+     * non-initial character of an identifier
+     * according to UAX #31 Unicode Identifier and Pattern Syntax.
+     *
+     * <p>Same as Unicode ID_Continue ({@link UProperty#ID_CONTINUE}).
+     *
+     * <p>Note that this differs from {@link java.lang.Character#isUnicodeIdentifierPart(char)}
+     * which implements a different identifier profile.
+     *
+     * @param ch the code point to be tested
+     * @return true if the code point may occur as a non-initial character of an identifier
      */
     public static boolean isUnicodeIdentifierPart(int ch)
     {
-        // if props == 0, it will just fall through and return false
-        // cat == format
-        return ((1 << getType(ch))
-                & ((1 << UCharacterCategory.UPPERCASE_LETTER)
-                        | (1 << UCharacterCategory.LOWERCASE_LETTER)
-                        | (1 << UCharacterCategory.TITLECASE_LETTER)
-                        | (1 << UCharacterCategory.MODIFIER_LETTER)
-                        | (1 << UCharacterCategory.OTHER_LETTER)
-                        | (1 << UCharacterCategory.LETTER_NUMBER)
-                        | (1 << UCharacterCategory.CONNECTOR_PUNCTUATION)
-                        | (1 << UCharacterCategory.DECIMAL_DIGIT_NUMBER)
-                        | (1 << UCharacterCategory.COMBINING_SPACING_MARK)
-                        | (1 << UCharacterCategory.NON_SPACING_MARK))) != 0
-                        || isIdentifierIgnorable(ch);
+        return hasBinaryProperty(ch, UProperty.ID_CONTINUE);  // single code point
     }
 
     /**
-     * Determines if the specified code point is permissible as the first
-     * character in a Unicode identifier.
-     * A code point may start a Unicode identifier if it is of type either
-     * <ul>
-     * <li> Lu Uppercase letter
-     * <li> Ll Lowercase letter
-     * <li> Lt Titlecase letter
-     * <li> Lm Modifier letter
-     * <li> Lo Other letter
-     * <li> Nl Letter number
-     * </ul>
-     * Up-to-date Unicode implementation of
-     * java.lang.Character.isUnicodeIdentifierStart().<br>
-     * 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
+     * Determines if the specified character is permissible as the first character in an identifier
+     * according to UAX #31 Unicode Identifier and Pattern Syntax.
+     *
+     * <p>Same as Unicode ID_Start ({@link UProperty#ID_START}).
+     *
+     * <p>Note that this differs from {@link java.lang.Character#isUnicodeIdentifierStart(char)}
+     * which implements a different identifier profile.
+     *
+     * @param ch the code point to be tested
+     * @return true if the code point may start an identifier
      */
     public static boolean isUnicodeIdentifierStart(int ch)
     {
-        /*int cat = getType(ch);*/
-        // if props == 0, it will just fall through and return false
-        return ((1 << getType(ch))
-                & ((1 << UCharacterCategory.UPPERCASE_LETTER)
-                        | (1 << UCharacterCategory.LOWERCASE_LETTER)
-                        | (1 << UCharacterCategory.TITLECASE_LETTER)
-                        | (1 << UCharacterCategory.MODIFIER_LETTER)
-                        | (1 << UCharacterCategory.OTHER_LETTER)
-                        | (1 << UCharacterCategory.LETTER_NUMBER))) != 0;
+        return hasBinaryProperty(ch, UProperty.ID_START);  // single code point
+    }
+
+    /**
+     * Does the set of Identifier_Type values code point c contain the given type?
+     *
+     * <p>Used for UTS #39 General Security Profile for Identifiers
+     * (https://www.unicode.org/reports/tr39/#General_Security_Profile).
+     *
+     * <p>Each code point maps to a <i>set</i> of UIdentifierType values.
+     *
+     * @param c code point
+     * @param type Identifier_Type to check
+     * @return true if type is in Identifier_Type(c)
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final boolean hasIdentifierType(int c, IdentifierType type) {
+        return UCharacterProperty.INSTANCE.hasIDType(c, type);
+    }
+
+    /**
+     * Writes code point c's Identifier_Type as a set of IdentifierType values and
+     * returns the number of types.
+     * The set is cleared before c's types are added.
+     *
+     * <p>Used for UTS #39 General Security Profile for Identifiers
+     * (https://www.unicode.org/reports/tr39/#General_Security_Profile).
+     *
+     * <p>Each code point maps to a <i>set</i> of IdentifierType values.
+     * There is always at least one type.
+     * Only some of the types can be combined with others,
+     * and usually only a small number of types occur together.
+     * Future versions might add additional types.
+     * See UTS #39 and its data files for details.
+     *
+     * @param c code point
+     * @param types output set
+     * @return number of values in c's Identifier_Type
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final int getIdentifierTypes(int c, EnumSet<IdentifierType> types) {
+        return UCharacterProperty.INSTANCE.getIDTypes(c, types);
     }
 
     /**
@@ -4404,9 +4453,10 @@
     /**
      * <strong>[icu]</strong> Returns the most current Unicode name of the argument code point, or
      * null if the character is unassigned or outside the range
-     * UCharacter.MIN_VALUE and UCharacter.MAX_VALUE or does not have a name.
+     * {@code UCharacter.MIN_VALUE} and {@code UCharacter.MAX_VALUE} or does not
+     * have a name.
      * <br>
-     * Note calling any methods related to code point names, e.g. get*Name*()
+     * Note calling any methods related to code point names, e.g. {@code getName()}
      * 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
@@ -4462,7 +4512,7 @@
      * <li> Extended name in the form of
      *      "&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*()
+     * Note calling any methods related to code point names, e.g. {@code getName()}
      * 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
@@ -4476,7 +4526,7 @@
      * Returns null if the character is unassigned or outside the range
      * 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*()
+     * Note calling any methods related to code point names, e.g. {@code getName()}
      * 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
@@ -4506,7 +4556,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*()
+     * Note calling any methods related to code point names, e.g. {@code getName()}
      * 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
@@ -4545,7 +4595,7 @@
      * <li> Extended name in the form of
      *      "&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*()
+     * Note calling any methods related to code point names, e.g. {@code getName()}
      * 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
@@ -4559,7 +4609,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*()
+     * Note calling any methods related to code point names, e.g. {@code getName()}
      * 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
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 f0057c3..0011881 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UProperty.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UProperty.java
@@ -280,7 +280,7 @@
     /**
      * <p>Binary property Case_Sensitive.
      * <p>Either the source of a case
-     * mapping or _in_ the target of a case mapping. Not the same as
+     * mapping or <i>in</i> the target of a case mapping. Not the same as
      * the general category Cased_Letter.
      */
     public static final int CASE_SENSITIVE = 34;
@@ -529,12 +529,36 @@
     public static final int RGI_EMOJI=71;
 
     /**
+     * Binary property IDS_Unary_Operator.
+     * For programmatic determination of Ideographic Description Sequences.
+     *
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final int IDS_UNARY_OPERATOR = 72;
+
+    /**
+     * Binary property ID_Compat_Math_Start.
+     * <p>Used in mathematical identifier profile in UAX #31.
+     *
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final int ID_COMPAT_MATH_START = 73;
+
+    /**
+     * Binary property ID_Compat_Math_Continue.
+     * <p>Used in mathematical identifier profile in UAX #31.
+     *
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final int ID_COMPAT_MATH_CONTINUE = 74;
+
+    /**
      * One more than the last constant for binary Unicode properties.
      * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int BINARY_LIMIT = 72;
+    public static final int BINARY_LIMIT = 75;
 
     /**
      * Enumerated property Bidi_Class.
@@ -714,12 +738,20 @@
     public static final int VERTICAL_ORIENTATION = 0x1018;
 
     /**
+     * Enumerated property Identifier_Status.
+     * Used for UTS #39 General Security Profile for Identifiers
+     * (https://www.unicode.org/reports/tr39/#General_Security_Profile).
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final int IDENTIFIER_STATUS = 0x1019;
+
+    /**
      * One more than the last constant for enumerated/integer Unicode properties.
      * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int INT_LIMIT = 0x1019;
+    public static final int INT_LIMIT = 0x101A;
 
     /**
      * Bitmask property General_Category_Mask.
@@ -880,12 +912,27 @@
      */
     public static final int OTHER_PROPERTY_START=SCRIPT_EXTENSIONS;
     /**
+     * Miscellaneous property Identifier_Type.
+     * Used for UTS #39 General Security Profile for Identifiers
+     * (https://www.unicode.org/reports/tr39/#General_Security_Profile).
+     *
+     * <p>Corresponds to {@link UCharacter#hasIdentifierType(int, UCharacter.IdentifierType)} and
+     * {@link UCharacter#getIdentifierTypes(int, java.util.EnumSet)}.
+     *
+     * <p>Each code point maps to a <i>set</i> of IdentifierType values.
+     *
+     * @see UCharacter#hasIdentifierType(int, UCharacter.IdentifierType)
+     * @see UCharacter#getIdentifierTypes(int, java.util.EnumSet)
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    public static final int IDENTIFIER_TYPE = 0x7001;
+    /**
      * One more than the last constant for Unicode properties with unusual value types.
      * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int OTHER_PROPERTY_LIMIT=0x7001;
+    public static final int OTHER_PROPERTY_LIMIT = 0x7002;
 
     /**
      * Selector constants for UCharacter.getPropertyName() and
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 1a98ba2..f4972e5 100644
--- a/android_icu4j/src/main/java/android/icu/lang/UScript.java
+++ b/android_icu4j/src/main/java/android/icu/lang/UScript.java
@@ -880,6 +880,9 @@
     /***/
     public static final int NAG_MUNDARI = 199; /* Nagm */
 
+    /** @hide unsupported on Android*/
+    public static final int ARABIC_NASTALIQ = 200; /* Aran */
+
     /**
      * One more than the highest normal UScript code.
      * The highest value is available via UCharacter.getIntPropertyMaxValue(UProperty.SCRIPT).
@@ -888,7 +891,7 @@
      * @hide unsupported on Android
      */
     @Deprecated
-    public static final int CODE_LIMIT   = 200;
+    public static final int CODE_LIMIT   = 201;
 
     private static int[] getCodesFromLocale(ULocale locale) {
         // Multi-script languages, equivalent to the LocaleScript data
@@ -964,7 +967,20 @@
      */
     public static final int[] getCode(String nameOrAbbrOrLocale) {
         boolean triedCode = false;
-        if (nameOrAbbrOrLocale.indexOf('_') < 0 && nameOrAbbrOrLocale.indexOf('-') < 0) {
+        int lastSepPos = nameOrAbbrOrLocale.indexOf('_');
+        if (lastSepPos < 0) {
+            lastSepPos = nameOrAbbrOrLocale.indexOf('-');
+        }
+        // Favor interpretation of nameOrAbbrOrLocale as a script alias if either
+        // 1. nameOrAbbrOrLocale does not contain -/_. Handles Han, Mro, Nko, etc.
+        // 2. The last instance of -/_ is at offset 3, and the portion after that is
+        //    longer than 4 characters (i.e. not a script or region code). This handles
+        //    Old_Hungarian, Old_Italic, etc. ("old" is a valid language code)
+        // 3. The last instance of -/_ is at offset 7, and the portion after that is
+        //    3 characters. This handles New_Tai_Lue ("new" is a valid language code).
+        if ( lastSepPos < 0
+                || (lastSepPos == 3 && nameOrAbbrOrLocale.length() > 8)
+                || (lastSepPos == 7 && nameOrAbbrOrLocale.length() == 11) ) {
             int propNum = UCharacter.getPropertyValueEnumNoThrow(UProperty.SCRIPT, nameOrAbbrOrLocale);
             if (propNum != UProperty.UNDEFINED) {
                 return new int[] {propNum};
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 9909e66..099fbdd 100644
--- a/android_icu4j/src/main/java/android/icu/math/BigDecimal.java
+++ b/android_icu4j/src/main/java/android/icu/math/BigDecimal.java
@@ -2814,7 +2814,7 @@
         // Reminder: a zero double returns '0.0', so we cannot fastpath to
         // use the constant ZERO. This might be important enough to justify
         // a factory approach, a cache, or a few private constants, later.
-        return new android.icu.math.BigDecimal((new java.lang.Double(dub)).toString());
+        return new android.icu.math.BigDecimal(Double.toString(dub));
     }
 
     /**
diff --git a/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java
index e01f72c..6b969b9 100644
--- a/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java
+++ b/android_icu4j/src/main/java/android/icu/message2/DateTimeFormatterFactory.java
@@ -1,32 +1,52 @@
 /* GENERATED SOURCE. DO NOT MODIFY. */
 // © 2022 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
+// License & terms of use: https://www.unicode.org/copyright.html
 
 package android.icu.message2;
 
+import java.util.Calendar;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Objects;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
-import android.icu.impl.locale.AsciiUtil;
 import android.icu.text.DateFormat;
-import android.icu.text.SimpleDateFormat;
 
 /**
  * Creates a {@link Formatter} doing formatting of date / time, similar to
  * <code>{exp, date}</code> and <code>{exp, time}</code> in {@link android.icu.text.MessageFormat}.
  */
 class DateTimeFormatterFactory implements FormatterFactory {
+    private final String kind;
+
+    // "datetime", "date", "time"
+    DateTimeFormatterFactory(String kind) {
+        switch (kind) {
+            case "date":
+                break;
+            case "time":
+                break;
+            case "datetime":
+                break;
+            default:
+                kind = "datetime";
+        }
+        this.kind = kind;
+    }
 
     private static int stringToStyle(String option) {
-        switch (AsciiUtil.toUpperString(option)) {
-            case "FULL": return DateFormat.FULL;
-            case "LONG": return DateFormat.LONG;
-            case "MEDIUM": return DateFormat.MEDIUM;
-            case "SHORT": return DateFormat.SHORT;
-            case "": // intentional fall-through
-            case "DEFAULT": return DateFormat.DEFAULT;
-            default: throw new IllegalArgumentException("Invalid datetime style: " + option);
+        switch (option) {
+            case "full":
+                return DateFormat.FULL;
+            case "long":
+                return DateFormat.LONG;
+            case "medium":
+                return DateFormat.MEDIUM;
+            case "short":
+                return DateFormat.SHORT;
+            default:
+                throw new IllegalArgumentException("Invalid datetime style: " + option);
         }
     }
 
@@ -38,49 +58,279 @@
      */
     @Override
     public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
-        DateFormat df;
+        int dateStyle = DateFormat.NONE;
+        int timeStyle = DateFormat.NONE;
+        switch (kind) {
+            case "date":
+                dateStyle = getDateTimeStyle(fixedOptions, "style");
+                break;
+            case "time":
+                timeStyle = getDateTimeStyle(fixedOptions, "style");
+                break;
+            case "datetime": // $FALL-THROUGH$
+            default:
+                dateStyle = getDateTimeStyle(fixedOptions, "dateStyle");
+                timeStyle = getDateTimeStyle(fixedOptions, "timeStyle");
+                break;
+        }
 
         // TODO: how to handle conflicts. What if we have both skeleton and style, or pattern?
-        Object opt = fixedOptions.get("skeleton");
-        if (opt != null) {
-            String skeleton = Objects.toString(opt);
-            df = DateFormat.getInstanceForSkeleton(skeleton, locale);
-            return new DateTimeFormatter(df);
-        }
-
-        opt = fixedOptions.get("pattern");
-        if (opt != null) {
-            String pattern = Objects.toString(opt);
-            SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
-            return new DateTimeFormatter(sf);
-        }
-
-        int dateStyle = DateFormat.NONE;
-        opt = fixedOptions.get("datestyle");
-        if (opt != null) {
-            dateStyle = stringToStyle(Objects.toString(opt, ""));
-        }
-
-        int timeStyle = DateFormat.NONE;
-        opt = fixedOptions.get("timestyle");
-        if (opt != null) {
-            timeStyle = stringToStyle(Objects.toString(opt, ""));
-        }
-
         if (dateStyle == DateFormat.NONE && timeStyle == DateFormat.NONE) {
-            // Match the MessageFormat behavior
-            dateStyle = DateFormat.SHORT;
-            timeStyle = DateFormat.SHORT;
-        }
-        df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
+            String skeleton = "";
+            switch (kind) {
+                case "date":
+                    skeleton = getDateFieldOptions(fixedOptions);
+                    break;
+                case "time":
+                    skeleton = getTimeFieldOptions(fixedOptions);
+                    break;
+                case "datetime": // $FALL-THROUGH$
+                default:
+                    skeleton = getDateFieldOptions(fixedOptions);
+                    skeleton += getTimeFieldOptions(fixedOptions);
+                    break;
+            }
 
-        return new DateTimeFormatter(df);
+            if (skeleton.isEmpty()) {
+                // Custom option, icu namespace
+                skeleton = OptUtils.getString(fixedOptions, "icu:skeleton", "");
+            }
+            if (!skeleton.isEmpty()) {
+                DateFormat df = DateFormat.getInstanceForSkeleton(skeleton, locale);
+                return new DateTimeFormatter(locale, df);
+            }
+
+            // No skeletons, custom or otherwise, match fallback to short / short as per spec.
+            switch (kind) {
+                case "date":
+                    dateStyle = DateFormat.SHORT;
+                    timeStyle = DateFormat.NONE;
+                    break;
+                case "time":
+                    dateStyle = DateFormat.NONE;
+                    timeStyle = DateFormat.SHORT;
+                    break;
+                case "datetime": // $FALL-THROUGH$
+                default:
+                    dateStyle = DateFormat.SHORT;
+                    timeStyle = DateFormat.SHORT;
+            }
+        }
+
+        DateFormat df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
+        return new DateTimeFormatter(locale, df);
+    }
+
+    private static int getDateTimeStyle(Map<String, Object> options, String key) {
+        String opt = OptUtils.getString(options, key);
+        if (opt != null) {
+            return stringToStyle(opt);
+        }
+        return DateFormat.NONE;
+    }
+
+    private static String getDateFieldOptions(Map<String, Object> options) {
+        StringBuilder skeleton = new StringBuilder();
+        String opt;
+
+        // In all the switches below we just ignore invalid options.
+        // Would be nice to report (log?), but ICU does not have a clear policy on how to do that.
+        // But we don't want to throw, that is too drastic.
+
+        opt = OptUtils.getString(options, "weekday", "");
+        switch (opt) {
+            case "long":
+                skeleton.append("EEEE");
+                break;
+            case "short":
+                skeleton.append("E");
+                break;
+            case "narrow":
+                skeleton.append("EEEEEE");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "era", "");
+        switch (opt) {
+            case "long":
+                skeleton.append("GGGG");
+                break;
+            case "short":
+                skeleton.append("G");
+                break;
+            case "narrow":
+                skeleton.append("GGGGG");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "year", "");
+        switch (opt) {
+            case "numeric":
+                skeleton.append("y");
+                break;
+            case "2-digit":
+                skeleton.append("yy");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "month", "");
+        switch (opt) {
+            case "numeric":
+                skeleton.append("M");
+                break;
+            case "2-digit":
+                skeleton.append("MM");
+                break;
+            case "long":
+                skeleton.append("MMMM");
+                break;
+            case "short":
+                skeleton.append("MMM");
+                break;
+            case "narrow":
+                skeleton.append("MMMMM");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "day", "");
+        switch (opt) {
+            case "numeric":
+                skeleton.append("d");
+                break;
+            case "2-digit":
+                skeleton.append("dd");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+        return skeleton.toString();
+    }
+
+    private static String getTimeFieldOptions(Map<String, Object> options) {
+        StringBuilder skeleton = new StringBuilder();
+        String opt;
+
+        // In all the switches below we just ignore invalid options.
+        // Would be nice to report (log?), but ICU does not have a clear policy on how to do that.
+        // But we don't want to throw, that is too drastic.
+
+        int showHour = 0;
+        opt = OptUtils.getString(options, "hour", "");
+        switch (opt) {
+            case "numeric":
+                showHour = 1;
+                break;
+            case "2-digit":
+                showHour = 2;
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+        if (showHour > 0) {
+            String hourCycle = "";
+            opt = OptUtils.getString(options, "hourCycle", "");
+            switch (opt) {
+                case "h11":
+                    hourCycle = "K";
+                    break;
+                case "h12":
+                    hourCycle = "h";
+                    break;
+                case "h23":
+                    hourCycle = "H";
+                    break;
+                case "h24":
+                    hourCycle = "k";
+                    break;
+                default:
+                    hourCycle = "j"; // default for the locale
+            }
+            skeleton.append(hourCycle);
+            if (showHour == 2) {
+                skeleton.append(hourCycle);
+            }
+        }
+
+        opt = OptUtils.getString(options, "minute", "");
+        switch (opt) {
+            case "numeric":
+                skeleton.append("m");
+                break;
+            case "2-digit":
+                skeleton.append("mm");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "second", "");
+        switch (opt) {
+            case "numeric":
+                skeleton.append("s");
+                break;
+            case "2-digit":
+                skeleton.append("ss");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "fractionalSecondDigits", "");
+        switch (opt) {
+            case "1":
+                skeleton.append("S");
+                break;
+            case "2":
+                skeleton.append("SS");
+                break;
+            case "3":
+                skeleton.append("SSS");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        opt = OptUtils.getString(options, "timeZoneName", "");
+        switch (opt) {
+            case "long":
+                skeleton.append("z");
+                break;
+            case "short":
+                skeleton.append("zzzz");
+                break;
+            case "shortOffset":
+                skeleton.append("O");
+                break;
+            case "longOffset":
+                skeleton.append("OOOO");
+                break;
+            case "shortGeneric":
+                skeleton.append("v");
+                break;
+            case "longGeneric":
+                skeleton.append("vvvv");
+                break;
+            default:
+                // invalid value, we just ignore it.
+        }
+
+        return skeleton.toString();
     }
 
     private static class DateTimeFormatter implements Formatter {
         private final DateFormat icuFormatter;
+        private final Locale locale;
 
-        private DateTimeFormatter(DateFormat df) {
+        private DateTimeFormatter(Locale locale, DateFormat df) {
+            this.locale = locale;
             this.icuFormatter = df;
         }
 
@@ -91,7 +341,24 @@
         public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
             // TODO: use a special type to indicate function without input argument.
             if (toFormat == null) {
-                throw new IllegalArgumentException("The date to format can't be null");
+                return null;
+            }
+            if (toFormat instanceof CharSequence) {
+                toFormat = parseIso8601(toFormat.toString());
+                // We were unable to parse the input as iso date
+                if (toFormat instanceof CharSequence) {
+                    return new FormattedPlaceholder(
+                            toFormat, new PlainStringFormattedValue("{|" + toFormat + "|}"));
+                }
+            }
+            if (toFormat instanceof Calendar) {
+                TimeZone tz = ((Calendar) toFormat).getTimeZone();
+                long milis = ((Calendar) toFormat).getTimeInMillis();
+                android.icu.util.TimeZone icuTz = android.icu.util.TimeZone.getTimeZone(tz.getID());
+                android.icu.util.Calendar calendar =
+                        android.icu.util.Calendar.getInstance(icuTz, locale);
+                calendar.setTimeInMillis(milis);
+                toFormat = calendar;
             }
             String result = icuFormatter.format(toFormat);
             return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
@@ -102,7 +369,68 @@
          */
         @Override
         public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
-            return format(toFormat, variableOptions).toString();
+            FormattedPlaceholder result = format(toFormat, variableOptions);
+            return result != null ? result.toString() : null;
         }
     }
+
+    private final static Pattern ISO_PATTERN = Pattern.compile(
+            "^(([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])){1}(T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]{1,3})?(Z|[+-]((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?$");
+
+    private static Integer safeParse(String str) {
+        if (str == null || str.isEmpty())
+            return null;
+        return Integer.parseInt(str);
+    }
+
+    private static Object parseIso8601(String text) {
+        Matcher m = ISO_PATTERN.matcher(text);
+        if (m.find() && m.groupCount() == 12 && !m.group().isEmpty()) {
+            Integer year = safeParse(m.group(2));
+            Integer month = safeParse(m.group(3));
+            Integer day = safeParse(m.group(4));
+            Integer hour = safeParse(m.group(6));
+            Integer minute = safeParse(m.group(7));
+            Integer second = safeParse(m.group(8));
+            Integer millisecond = 0;
+            if (m.group(9) != null) {
+                String z = (m.group(9) + "000").substring(1, 4);
+                millisecond = safeParse(z);
+            } else {
+                millisecond = 0;
+            }
+            String tzPart = m.group(10);
+
+            if (hour == null) {
+                hour = 0;
+                minute = 0;
+                second = 0;
+            }
+
+            android.icu.util.GregorianCalendar gc = new android.icu.util.GregorianCalendar(
+                    year, month - 1, day, hour, minute, second);
+            gc.set(android.icu.util.Calendar.MILLISECOND, millisecond);
+
+            if (tzPart != null) {
+                if (tzPart.equals("Z")) {
+                    gc.setTimeZone(android.icu.util.TimeZone.GMT_ZONE);
+                } else {
+                    int sign = tzPart.startsWith("-") ? -1 : 1;
+                    String[] tzParts = tzPart.substring(1).split(":");
+                    if (tzParts.length == 2) {
+                        Integer tzHour = safeParse(tzParts[0]);
+                        Integer tzMin = safeParse(tzParts[1]);
+                        if (tzHour != null && tzMin != null) {
+                            int offset = sign * (tzHour * 60 + tzMin) * 60 * 1000;
+                            gc.setTimeZone(new android.icu.util.SimpleTimeZone(offset, "offset"));
+                        }
+                    }
+                }
+            }
+
+            return gc;
+        }
+        return text;
+    }
+
 }
diff --git a/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java b/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java
index 960723a..d9b4197 100644
--- a/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java
+++ b/android_icu4j/src/main/java/android/icu/message2/FormattedMessage.java
@@ -1,6 +1,6 @@
 /* GENERATED SOURCE. DO NOT MODIFY. */
 // © 2022 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
+// License & terms of use: https://www.unicode.org/copyright.html
 
 package android.icu.message2;
 
@@ -118,5 +118,4 @@
     public AttributedCharacterIterator toCharacterIterator() {
         throw new RuntimeException("Not yet implemented.");
     }
-
 }
diff --git a/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java b/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java
index 16bd52d..aa139db 100644
--- a/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java
+++ b/android_icu4j/src/main/java/android/icu/message2/FormattedPlaceholder.java
@@ -1,6 +1,6 @@
 /* GENERATED SOURCE. DO NOT MODIFY. */
 // © 2022 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
+// License & terms of use: https://www.unicode.org/copyright.html
 
 package android.icu.message2;
 
diff --git a/android_icu4j/src/main/java/android/icu/message2/Formatter.java b/android_icu4j/src/main/java/android/icu/message2/Formatter.java
index 0eeac56..2572d3f 100644
--- a/android_icu4j/src/main/java/android/icu/message2/Formatter.java
+++ b/android_icu4j/src/main/java/android/icu/message2/Formatter.java
@@ -1,6 +1,6 @@
 /* GENERATED SOURCE. DO NOT MODIFY. */
 // © 2022 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
+// License & terms of use: https://www.unicode.org/copyright.html
 
 package android.icu.message2;
 
diff --git a/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java
index 6b89c31..0d1832d 100644
--- a/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java
+++ b/android_icu4j/src/main/java/android/icu/message2/FormatterFactory.java
@@ -1,6 +1,6 @@
 /* GENERATED SOURCE. DO NOT MODIFY. */
 // © 2022 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
+// License & terms of use: https://www.unicode.org/copyright.html
 
 package android.icu.message2;
 
diff --git a/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java b/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java
index 6798b98..7057ae4 100644
--- a/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java
+++ b/android_icu4j/src/main/java/android/icu/message2/IdentityFormatterFactory.java
@@ -1,6 +1,6 @@
 /* GENERATED SOURCE. DO NOT MODIFY. */
 // © 2022 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html
+// License & terms of use: https://www.unicode.org/copyright.html
 
 package android.icu.message2;
 
@@ -26,7 +26,8 @@
          */
         @Override
         public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
-            return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(Objects.toString(toFormat)));
+            return new FormattedPlaceholder(
+                    toFormat, new PlainStringFormattedValue(Objects.toString(toFormat)));
         }
 
         /**
diff --git a/android_icu4j/src/main/java/android/icu/message2/InputSource.java b/android_icu4j/src/main/java/android/icu/message2/InputSource.java
new file mode 100644
index 0000000..7122486
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/InputSource.java
@@ -0,0 +1,85 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+class InputSource {
+    final String buffer;
+
+    private int cursor;
+    private int lastReadCursor = -1;
+    private int lastReadCount = 0;
+
+    InputSource(String input) {
+        if (input == null) {
+            throw new IllegalArgumentException("Input string should not be null");
+        }
+        this.buffer = input;
+        this.cursor = 0;
+    }
+
+    boolean atEnd() {
+        return cursor >= buffer.length();
+    }
+
+    int peekChar() {
+        if (atEnd()) {
+            return -1;
+        }
+        return buffer.charAt(cursor);
+    }
+
+    int readCodePoint() {
+        // TODO: remove this?
+        // START Detect possible infinite loop
+        if (lastReadCursor != cursor) {
+            lastReadCursor = cursor;
+            lastReadCount = 1;
+        } else {
+            lastReadCount++;
+            if (lastReadCount >= 10) {
+                throw new RuntimeException("Stuck in a loop!");
+            }
+        }
+        // END Detect possible infinite loop
+
+        if (atEnd()) {
+            return -1;
+        }
+
+        char c = buffer.charAt(cursor++);
+        if (Character.isHighSurrogate(c)) {
+            if (!atEnd()) {
+                char c2 = buffer.charAt(cursor++);
+                if (Character.isLowSurrogate(c2)) {
+                    return Character.toCodePoint(c, c2);
+                } else { // invalid, high surrogate followed by non-surrogate
+                    cursor--;
+                    return c;
+                }
+            }
+        }
+        return c;
+    }
+
+    // Backup a number of characters.
+    void backup(int amount) {
+        // TODO: validate
+        cursor -= amount;
+    }
+
+    int getPosition() {
+        return cursor;
+    }
+
+    void skip(int amount) {
+        // TODO: validate
+        cursor += amount;
+    }
+
+    void gotoPosition(int position) {
+        // TODO: validate
+        cursor = position;
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFDataModel.java b/android_icu4j/src/main/java/android/icu/message2/MFDataModel.java
new file mode 100644
index 0000000..4436f57
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFDataModel.java
@@ -0,0 +1,523 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This maps closely to the official specification.
+ * Since it is not final, we will not add javadoc everywhere.
+ *
+ * <p>See <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md">the
+ * latest description</a>.</p>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+@SuppressWarnings("javadoc")
+public class MFDataModel {
+
+    private MFDataModel() {
+        // Prevent instantiation
+    }
+
+    // Messages
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface Message {
+        // Provides a common type for PatternMessage and SelectMessage.
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class PatternMessage implements Message {
+        public final List<Declaration> declarations;
+        public final Pattern pattern;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public PatternMessage(List<Declaration> declarations, Pattern pattern) {
+            this.declarations = declarations;
+            this.pattern = pattern;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class SelectMessage implements Message {
+        public final List<Declaration> declarations;
+        public final List<Expression> selectors;
+        public final List<Variant> variants;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public SelectMessage(
+                List<Declaration> declarations,
+                List<Expression> selectors,
+                List<Variant> variants) {
+            this.declarations = declarations;
+            this.selectors = selectors;
+            this.variants = variants;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface Declaration {
+        // Provides a common type for InputDeclaration, LocalDeclaration, and UnsupportedStatement.
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class InputDeclaration implements Declaration {
+        public final String name;
+        public final VariableExpression value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public InputDeclaration(String name, VariableExpression value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class LocalDeclaration implements Declaration {
+        public final String name;
+        public final Expression value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public LocalDeclaration(String name, Expression value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class UnsupportedStatement implements Declaration {
+        public final String keyword;
+        public final String body;
+        public final List<Expression> expressions;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public UnsupportedStatement(String keyword, String body, List<Expression> expressions) {
+            this.keyword = keyword;
+            this.body = body;
+            this.expressions = expressions;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface LiteralOrCatchallKey {
+        // Provides a common type for the selection keys: Variant, Literal, or CatchallKey.
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Variant implements LiteralOrCatchallKey {
+        public final List<LiteralOrCatchallKey> keys;
+        public final Pattern value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Variant(List<LiteralOrCatchallKey> keys, Pattern value) {
+            this.keys = keys;
+            this.value = value;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class CatchallKey implements LiteralOrCatchallKey {
+        // String value; // Always '*' in MF2
+    }
+
+    // Patterns
+
+    // type Pattern = Array<string | Expression | Markup>;
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Pattern {
+        public final List<PatternPart> parts;
+
+        Pattern() {
+            this.parts = new ArrayList<>();
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface PatternPart {
+        // Provides a common type for StringPart and Expression.
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class StringPart implements PatternPart {
+        public final String value;
+
+        StringPart(String value) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface Expression extends PatternPart {
+        // Provides a common type for all kind of expressions:
+        // LiteralExpression, VariableExpression, FunctionExpression, UnsupportedExpression, Markup
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class LiteralExpression implements Expression {
+        public final Literal arg;
+        public final Annotation annotation;
+        public final List<Attribute> attributes;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public LiteralExpression(Literal arg, Annotation annotation, List<Attribute> attributes) {
+            this.arg = arg;
+            this.annotation = annotation;
+            this.attributes = attributes;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class VariableExpression implements Expression {
+        public final VariableRef arg;
+        public final Annotation annotation;
+        public final List<Attribute> attributes;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public VariableExpression(
+                VariableRef arg, Annotation annotation, List<Attribute> attributes) {
+            this.arg = arg;
+            this.annotation = annotation;
+            this.attributes = attributes;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface Annotation {
+        // Provides a common type for FunctionAnnotation, UnsupportedAnnotation
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class FunctionExpression implements Expression {
+        public final FunctionAnnotation annotation;
+        public final List<Attribute> attributes;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public FunctionExpression(FunctionAnnotation annotation, List<Attribute> attributes) {
+            this.annotation = annotation;
+            this.attributes = attributes;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class UnsupportedExpression implements Expression {
+        public final UnsupportedAnnotation annotation;
+        public final List<Attribute> attributes;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public UnsupportedExpression(UnsupportedAnnotation annotation, List<Attribute> attributes) {
+            this.annotation = annotation;
+            this.attributes = attributes;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Attribute {
+        public final String name;
+        public final LiteralOrVariableRef value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Attribute(String name, LiteralOrVariableRef value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
+
+    // Expressions
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public interface LiteralOrVariableRef {
+        // Provides a common type for Literal and VariableRef,
+        // to represent things like `foo` / `|foo|` / `1234` (literals)
+        // and `$foo` (VariableRef), as argument for placeholders or value in options.
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Literal implements LiteralOrVariableRef, LiteralOrCatchallKey {
+        public final String value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Literal(String value) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class VariableRef implements LiteralOrVariableRef {
+        public final String name;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public VariableRef(String name) {
+            this.name = name;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class FunctionAnnotation implements Annotation {
+        public final String name;
+        public final Map<String, Option> options;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public FunctionAnnotation(String name, Map<String, Option> options) {
+            this.name = name;
+            this.options = options;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Option {
+        public final String name;
+        public final LiteralOrVariableRef value;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Option(String name, LiteralOrVariableRef value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class UnsupportedAnnotation implements Annotation {
+        public final String source;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public UnsupportedAnnotation(String source) {
+            this.source = source;
+        }
+    }
+
+    // Markup
+
+    /**
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Markup implements Expression {
+        enum Kind {
+            OPEN,
+            CLOSE,
+            STANDALONE
+        }
+
+        public final Kind kind;
+        public final String name;
+        public final Map<String, Option> options;
+        public final List<Attribute> attributes;
+
+        /**
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Markup(
+                Kind kind, String name, Map<String, Option> options, List<Attribute> attributes) {
+            this.kind = kind;
+            this.name = name;
+            this.options = options;
+            this.attributes = attributes;
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFDataModelFormatter.java b/android_icu4j/src/main/java/android/icu/message2/MFDataModelFormatter.java
new file mode 100644
index 0000000..347c4cc
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFDataModelFormatter.java
@@ -0,0 +1,602 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import android.icu.message2.MFDataModel.Annotation;
+import android.icu.message2.MFDataModel.CatchallKey;
+import android.icu.message2.MFDataModel.Declaration;
+import android.icu.message2.MFDataModel.Expression;
+import android.icu.message2.MFDataModel.FunctionAnnotation;
+import android.icu.message2.MFDataModel.FunctionExpression;
+import android.icu.message2.MFDataModel.InputDeclaration;
+import android.icu.message2.MFDataModel.Literal;
+import android.icu.message2.MFDataModel.LiteralExpression;
+import android.icu.message2.MFDataModel.LiteralOrCatchallKey;
+import android.icu.message2.MFDataModel.LiteralOrVariableRef;
+import android.icu.message2.MFDataModel.LocalDeclaration;
+import android.icu.message2.MFDataModel.Option;
+import android.icu.message2.MFDataModel.Pattern;
+import android.icu.message2.MFDataModel.SelectMessage;
+import android.icu.message2.MFDataModel.StringPart;
+import android.icu.message2.MFDataModel.UnsupportedAnnotation;
+import android.icu.message2.MFDataModel.UnsupportedExpression;
+import android.icu.message2.MFDataModel.VariableRef;
+import android.icu.message2.MFDataModel.Variant;
+import android.icu.util.Calendar;
+import android.icu.util.CurrencyAmount;
+
+/**
+ * Takes an {@link MFDataModel} and formats it to a {@link String}
+ * (and later on we will also implement formatting to a {@code FormattedMessage}).
+ */
+// TODO: move this in the MessageFormatter?
+class MFDataModelFormatter {
+    private final Locale locale;
+    private final MFDataModel.Message dm;
+
+    private final MFFunctionRegistry standardFunctions;
+    private final MFFunctionRegistry customFunctions;
+    private static final MFFunctionRegistry EMPTY_REGISTY = MFFunctionRegistry.builder().build();
+
+    MFDataModelFormatter(
+            MFDataModel.Message dm, Locale locale, MFFunctionRegistry customFunctionRegistry) {
+        this.locale = locale;
+        this.dm = dm;
+        this.customFunctions =
+                customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;
+
+        standardFunctions =
+                MFFunctionRegistry.builder()
+                        // Date/time formatting
+                        .setFormatter("datetime", new DateTimeFormatterFactory("datetime"))
+                        .setFormatter("date", new DateTimeFormatterFactory("date"))
+                        .setFormatter("time", new DateTimeFormatterFactory("time"))
+                        .setDefaultFormatterNameForType(Date.class, "datetime")
+                        .setDefaultFormatterNameForType(Calendar.class, "datetime")
+                        .setDefaultFormatterNameForType(java.util.Calendar.class, "datetime")
+
+                        // Number formatting
+                        .setFormatter("number", new NumberFormatterFactory("number"))
+                        .setFormatter("integer", new NumberFormatterFactory("integer"))
+                        .setDefaultFormatterNameForType(Integer.class, "number")
+                        .setDefaultFormatterNameForType(Double.class, "number")
+                        .setDefaultFormatterNameForType(Number.class, "number")
+                        .setDefaultFormatterNameForType(CurrencyAmount.class, "number")
+
+                        // Format that returns "to string"
+                        .setFormatter("string", new IdentityFormatterFactory())
+                        .setDefaultFormatterNameForType(String.class, "string")
+                        .setDefaultFormatterNameForType(CharSequence.class, "string")
+
+                        // Register the standard selectors
+                        .setSelector("number", new NumberFormatterFactory("number"))
+                        .setSelector("integer", new NumberFormatterFactory("integer"))
+                        .setSelector("string", new TextSelectorFactory())
+                        .setSelector("icu:gender", new TextSelectorFactory())
+                        .build();
+    }
+
+    String format(Map<String, Object> arguments) {
+        MFDataModel.Pattern patternToRender = null;
+        if (arguments == null) {
+            arguments = new HashMap<>();
+        }
+
+        Map<String, Object> variables;
+        if (dm instanceof MFDataModel.PatternMessage) {
+            MFDataModel.PatternMessage pm = (MFDataModel.PatternMessage) dm;
+            variables = resolveDeclarations(pm.declarations, arguments);
+            patternToRender = pm.pattern;
+        } else if (dm instanceof MFDataModel.SelectMessage) {
+            MFDataModel.SelectMessage sm = (MFDataModel.SelectMessage) dm;
+            variables = resolveDeclarations(sm.declarations, arguments);
+            patternToRender = findBestMatchingPattern(sm, variables, arguments);
+        } else {
+            formattingError("");
+            return "ERROR!";
+        }
+
+        if (patternToRender == null) {
+            return "ERROR!";
+        }
+
+        StringBuilder result = new StringBuilder();
+        for (MFDataModel.PatternPart part : patternToRender.parts) {
+            if (part instanceof MFDataModel.StringPart) {
+                MFDataModel.StringPart strPart = (StringPart) part;
+                result.append(strPart.value);
+            } else if (part instanceof MFDataModel.Expression) {
+                FormattedPlaceholder formattedExpression =
+                        formatExpression((Expression) part, variables, arguments);
+                result.append(formattedExpression.getFormattedValue().toString());
+            } else if (part instanceof MFDataModel.Markup) {
+                // Ignore
+            } else if (part instanceof MFDataModel.UnsupportedExpression) {
+                // Ignore
+            } else {
+                formattingError("Unknown part type: " + part);
+            }
+        }
+        return result.toString();
+    }
+
+    private Pattern findBestMatchingPattern(
+            SelectMessage sm, Map<String, Object> variables, Map<String, Object> arguments) {
+        Pattern patternToRender = null;
+
+        // ====================================
+        // spec: ### Resolve Selectors
+        // ====================================
+
+        // Collect all the selector functions in an array, to reuse
+        List<Expression> selectors = sm.selectors;
+        // spec: Let `res` be a new empty list of resolved values that support selection.
+        List<ResolvedSelector> res = new ArrayList<>(selectors.size());
+        // spec: For each _selector_ `sel`, in source order,
+        for (Expression sel : selectors) {
+            // spec: Let `rv` be the resolved value of `sel`.
+            FormattedPlaceholder fph = formatExpression(sel, variables, arguments);
+            String functionName = null;
+            Object argument = null;
+            Map<String, Object> options = new HashMap<>();
+            if (fph.getInput() instanceof ResolvedExpression) {
+                ResolvedExpression re = (ResolvedExpression) fph.getInput();
+                argument = re.argument;
+                functionName = re.functionName;
+                options.putAll(re.options);
+            } else if (fph.getInput() instanceof MFDataModel.VariableExpression) {
+                MFDataModel.VariableExpression ve = (MFDataModel.VariableExpression) fph.getInput();
+                argument = resolveLiteralOrVariable(ve.arg, variables, arguments);
+                if (ve.annotation instanceof FunctionAnnotation) {
+                    functionName = ((FunctionAnnotation) ve.annotation).name;
+                }
+            } else if (fph.getInput() instanceof LiteralExpression) {
+                LiteralExpression le = (LiteralExpression) fph.getInput();
+                argument = le.arg;
+                if (le.annotation instanceof FunctionAnnotation) {
+                    functionName = ((FunctionAnnotation) le.annotation).name;
+                }
+            }
+            SelectorFactory funcFactory = standardFunctions.getSelector(functionName);
+            if (funcFactory == null) {
+                funcFactory = customFunctions.getSelector(functionName);
+            }
+            // spec: If selection is supported for `rv`:
+            if (funcFactory != null) {
+                Selector selectorFunction = funcFactory.createSelector(locale, options);
+                ResolvedSelector rs = new ResolvedSelector(argument, options, selectorFunction);
+                // spec: Append `rv` as the last element of the list `res`.
+                res.add(rs);
+            } else {
+                throw new IllegalArgumentException("Unknown selector type: " + functionName);
+            }
+        }
+
+        // This should not be possible, we added one function for each selector,
+        // or we have thrown an exception.
+        // But just in case someone removes the throw above?
+        if (res.size() != selectors.size()) {
+            throw new IllegalArgumentException(
+                    "Something went wrong, not enough selector functions, "
+                            + res.size() + " vs. " + selectors.size());
+        }
+
+        // ====================================
+        // spec: ### Resolve Preferences
+        // ====================================
+
+        // spec: Let `pref` be a new empty list of lists of strings.
+        List<List<String>> pref = new ArrayList<>();
+        // spec: For each index `i` in `res`:
+        for (int i = 0; i < res.size(); i++) {
+            // spec: Let `keys` be a new empty list of strings.
+            List<String> keys = new ArrayList<>();
+            // spec: For each _variant_ `var` of the message:
+            for (Variant var : sm.variants) {
+                // spec: Let `key` be the `var` key at position `i`.
+                LiteralOrCatchallKey key = var.keys.get(i);
+                // spec: If `key` is not the catch-all key `'*'`:
+                if (key instanceof CatchallKey) {
+                    keys.add("*");
+                } else if (key instanceof Literal) {
+                    // spec: Assert that `key` is a _literal_.
+                    // spec: Let `ks` be the resolved value of `key`.
+                    String ks = ((Literal) key).value;
+                    // spec: Append `ks` as the last element of the list `keys`.
+                    keys.add(ks);
+                } else {
+                    formattingError("Literal expected, but got " + key);
+                }
+            }
+            // spec: Let `rv` be the resolved value at index `i` of `res`.
+            ResolvedSelector rv = res.get(i);
+            // spec: Let `matches` be the result of calling the method MatchSelectorKeys(`rv`, `keys`)
+            List<String> matches = matchSelectorKeys(rv, keys);
+            // spec: Append `matches` as the last element of the list `pref`.
+            pref.add(matches);
+        }
+
+        // ====================================
+        // spec: ### Filter Variants
+        // ====================================
+
+        // spec: Let `vars` be a new empty list of _variants_.
+        List<Variant> vars = new ArrayList<>();
+        // spec: For each _variant_ `var` of the message:
+        for (Variant var : sm.variants) {
+            // spec: For each index `i` in `pref`:
+            int found = 0;
+            for (int i = 0; i < pref.size(); i++) {
+                // spec: Let `key` be the `var` key at position `i`.
+                LiteralOrCatchallKey key = var.keys.get(i);
+                // spec: If `key` is the catch-all key `'*'`:
+                if (key instanceof CatchallKey) {
+                    // spec: Continue the inner loop on `pref`.
+                    found++;
+                    continue;
+                }
+                // spec: Assert that `key` is a _literal_.
+                if (!(key instanceof Literal)) {
+                    formattingError("Literal expected");
+                }
+                // spec: Let `ks` be the resolved value of `key`.
+                String ks = ((Literal) key).value;
+                // spec: Let `matches` be the list of strings at index `i` of `pref`.
+                List<String> matches = pref.get(i);
+                // spec: If `matches` includes `ks`:
+                if (matches.contains(ks)) {
+                    // spec: Continue the inner loop on `pref`.
+                    found++;
+                    continue;
+                } else {
+                    // spec: Else:
+                    // spec: Continue the outer loop on message _variants_.
+                    break;
+                }
+            }
+            if (found == pref.size()) {
+                // spec: Append `var` as the last element of the list `vars`.
+                vars.add(var);
+            }
+        }
+
+        // ====================================
+        // spec: ### Sort Variants
+        // ====================================
+        // spec: Let `sortable` be a new empty list of (integer, _variant_) tuples.
+        List<IntVarTuple> sortable = new ArrayList<>();
+        // spec: For each _variant_ `var` of `vars`:
+        for (Variant var : vars) {
+            // spec: Let `tuple` be a new tuple (-1, `var`).
+            IntVarTuple tuple = new IntVarTuple(-1, var);
+            // spec: Append `tuple` as the last element of the list `sortable`.
+            sortable.add(tuple);
+        }
+        // spec: Let `len` be the integer count of items in `pref`.
+        int len = pref.size();
+        // spec: Let `i` be `len` - 1.
+        int i = len - 1;
+        // spec: While `i` >= 0:
+        while (i >= 0) {
+            // spec: Let `matches` be the list of strings at index `i` of `pref`.
+            List<String> matches = pref.get(i);
+            // spec: Let `minpref` be the integer count of items in `matches`.
+            int minpref = matches.size();
+            // spec: For each tuple `tuple` of `sortable`:
+            for (IntVarTuple tuple : sortable) {
+                // spec: Let `matchpref` be an integer with the value `minpref`.
+                int matchpref = minpref;
+                // spec: Let `key` be the `tuple` _variant_ key at position `i`.
+                LiteralOrCatchallKey key = tuple.variant.keys.get(i);
+                // spec: If `key` is not the catch-all key `'*'`:
+                if (!(key instanceof CatchallKey)) {
+                    // spec: Assert that `key` is a _literal_.
+                    if (!(key instanceof Literal)) {
+                        formattingError("Literal expected");
+                    }
+                    // spec: Let `ks` be the resolved value of `key`.
+                    String ks = ((Literal) key).value;
+                    // spec: Let `matchpref` be the integer position of `ks` in `matches`.
+                    matchpref = matches.indexOf(ks);
+                }
+                // spec: Set the `tuple` integer value as `matchpref`.
+                tuple.integer = matchpref;
+            }
+            // spec: Set `sortable` to be the result of calling the method `SortVariants(sortable)`.
+            sortable.sort(MFDataModelFormatter::sortVariants);
+            // spec: Set `i` to be `i` - 1.
+            i--;
+        }
+        // spec: Let `var` be the _variant_ element of the first element of `sortable`.
+        IntVarTuple var = sortable.get(0);
+        // spec: Select the _pattern_ of `var`.
+        patternToRender = var.variant.value;
+
+        // And should do that only once, when building the data model.
+        if (patternToRender == null) {
+            // If there was a case with all entries in the keys `*` this should not happen
+            throw new IllegalArgumentException(
+                    "The selection went wrong, cannot select any option.");
+        }
+
+        return patternToRender;
+    }
+
+    /* spec:
+     * `SortVariants` is a method whose single argument is
+     * a list of (integer, _variant_) tuples.
+     * It returns a list of (integer, _variant_) tuples.
+     * Any implementation of `SortVariants` is acceptable
+     * as long as it satisfies the following requirements:
+     *
+     * 1. Let `sortable` be an arbitrary list of (integer, _variant_) tuples.
+     * 1. Let `sorted` be `SortVariants(sortable)`.
+     * 1. `sorted` is the result of sorting `sortable` using the following comparator:
+     *    1. `(i1, v1)` <= `(i2, v2)` if and only if `i1 <= i2`.
+     * 1. The sort is stable (pairs of tuples from `sortable` that are equal
+     *    in their first element have the same relative order in `sorted`).
+     */
+    private static int sortVariants(IntVarTuple o1, IntVarTuple o2) {
+        int result = Integer.compare(o1.integer, o2.integer);
+        if (result != 0) {
+            return result;
+        }
+        List<LiteralOrCatchallKey> v1 = o1.variant.keys;
+        List<LiteralOrCatchallKey> v2 = o1.variant.keys;
+        if (v1.size() != v2.size()) {
+            formattingError("The number of keys is not equal.");
+        }
+        for (int i = 0; i < v1.size(); i++) {
+            LiteralOrCatchallKey k1 = v1.get(i);
+            LiteralOrCatchallKey k2 = v2.get(i);
+            String s1 = k1 instanceof Literal ? ((Literal) k1).value : "*";
+            String s2 = k2 instanceof Literal ? ((Literal) k2).value : "*";
+            int cmp = s1.compareTo(s2);
+            if (cmp != 0) {
+                return cmp;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * spec:
+     * The method MatchSelectorKeys is determined by the implementation.
+     * It takes as arguments a resolved _selector_ value `rv` and a list of string keys `keys`,
+     * and returns a list of string keys in preferential order.
+     * The returned list MUST contain only unique elements of the input list `keys`.
+     * The returned list MAY be empty.
+     * The most-preferred key is first,
+     * with each successive key appearing in order by decreasing preference.
+     */
+    @SuppressWarnings("static-method")
+    private List<String> matchSelectorKeys(ResolvedSelector rv, List<String> keys) {
+        return rv.selectorFunction.matches(rv.argument, keys, rv.options);
+    }
+
+    private static class ResolvedSelector {
+        final Object argument;
+        final Map<String, Object> options;
+        final Selector selectorFunction;
+
+        public ResolvedSelector(
+                Object argument, Map<String, Object> options, Selector selectorFunction) {
+            this.argument = argument;
+            this.options = new HashMap<>(options);
+            this.selectorFunction = selectorFunction;
+        }
+    }
+
+    private static void formattingError(String message) {
+        throw new IllegalArgumentException(message);
+    }
+
+    private FormatterFactory getFormattingFunctionFactoryByName(
+            Object toFormat, String functionName) {
+        // Get a function name from the type of the object to format
+        if (functionName == null || functionName.isEmpty()) {
+            if (toFormat == null) {
+                // The object to format is null, and no function provided.
+                return null;
+            }
+            Class<?> clazz = toFormat.getClass();
+            functionName = standardFunctions.getDefaultFormatterNameForType(clazz);
+            if (functionName == null) {
+                functionName = customFunctions.getDefaultFormatterNameForType(clazz);
+            }
+            if (functionName == null) {
+                throw new IllegalArgumentException(
+                        "Object to format without a function, and unknown type: "
+                                + toFormat.getClass().getName());
+            }
+        }
+
+        FormatterFactory func = standardFunctions.getFormatter(functionName);
+        if (func == null) {
+            func = customFunctions.getFormatter(functionName);
+        }
+        return func;
+    }
+
+    private static Object resolveLiteralOrVariable(
+            LiteralOrVariableRef value,
+            Map<String, Object> localVars,
+            Map<String, Object> arguments) {
+        if (value instanceof Literal) {
+            String val = ((Literal) value).value;
+            Number nr = OptUtils.asNumber(val);
+            if (nr != null) {
+                return nr;
+            }
+            return val;
+        } else if (value instanceof VariableRef) {
+            String varName = ((VariableRef) value).name;
+            Object val = localVars.get(varName);
+            if (val == null) {
+                val = localVars.get(varName);
+            }
+            if (val == null) {
+                val = arguments.get(varName);
+            }
+            return val;
+        }
+        return value;
+    }
+
+    private static Map<String, Object> convertOptions(
+            Map<String, Option> options,
+            Map<String, Object> localVars,
+            Map<String, Object> arguments) {
+        Map<String, Object> result = new HashMap<>();
+        for (Option option : options.values()) {
+            result.put(option.name, resolveLiteralOrVariable(option.value, localVars, arguments));
+        }
+        return result;
+    }
+
+    /**
+     * Formats an expression.
+     *
+     * @param expression the expression to format
+     * @param variables local variables, created from declarations (`.input` and `.local`)
+     * @param arguments the arguments passed at runtime to be formatted (`mf.format(arguments)`)
+     */
+    private FormattedPlaceholder formatExpression(
+            Expression expression, Map<String, Object> variables, Map<String, Object> arguments) {
+
+        Annotation annotation = null; // function name
+        String functionName = null;
+        Object toFormat = null;
+        Map<String, Object> options = new HashMap<>();
+        String fallbackString = "{\uFFFD}";
+
+        if (expression instanceof MFDataModel.VariableExpression) {
+            MFDataModel.VariableExpression varPart = (MFDataModel.VariableExpression) expression;
+            fallbackString = "{$" + varPart.arg.name + "}";
+            annotation = varPart.annotation; // function name & options
+            Object resolved = resolveLiteralOrVariable(varPart.arg, variables, arguments);
+            if (resolved instanceof FormattedPlaceholder) {
+                Object input = ((FormattedPlaceholder) resolved).getInput();
+                if (input instanceof ResolvedExpression) {
+                    ResolvedExpression re = (ResolvedExpression) input;
+                    toFormat = re.argument;
+                    functionName = re.functionName;
+                    options.putAll(re.options);
+                } else {
+                    toFormat = input;
+                }
+            } else {
+                toFormat = resolved;
+            }
+        } else if (expression
+                instanceof MFDataModel.FunctionExpression) { // Function without arguments
+            MFDataModel.FunctionExpression fe = (FunctionExpression) expression;
+            fallbackString = "{:" + fe.annotation.name + "}";
+            annotation = fe.annotation;
+        } else if (expression instanceof MFDataModel.LiteralExpression) {
+            MFDataModel.LiteralExpression le = (LiteralExpression) expression;
+            annotation = le.annotation;
+            fallbackString = "{|" + le.arg.value + "|}";
+            toFormat = resolveLiteralOrVariable(le.arg, variables, arguments);
+        } else if (expression instanceof MFDataModel.Markup) {
+            // No output on markup, for now (we only format to string)
+            return new FormattedPlaceholder(expression, new PlainStringFormattedValue(""));
+        } else {
+            UnsupportedExpression ue = (UnsupportedExpression) expression;
+            char sigil = ue.annotation.source.charAt(0);
+            return new FormattedPlaceholder(
+                    expression, new PlainStringFormattedValue("{" + sigil + "}"));
+        }
+
+        if (annotation instanceof FunctionAnnotation) {
+            FunctionAnnotation fa = (FunctionAnnotation) annotation;
+            if (functionName != null && !functionName.equals(fa.name)) {
+                formattingError(
+                        "invalid function overrides, '" + functionName + "' <> '" + fa.name + "'");
+            }
+            functionName = fa.name;
+            Map<String, Object> newOptions = convertOptions(fa.options, variables, arguments);
+            options.putAll(newOptions);
+        } else if (annotation instanceof UnsupportedAnnotation) {
+            // We don't know how to format unsupported annotations
+            return new FormattedPlaceholder(expression, new PlainStringFormattedValue(fallbackString));
+        }
+
+        FormatterFactory funcFactory = getFormattingFunctionFactoryByName(toFormat, functionName);
+        if (funcFactory == null) {
+            return new FormattedPlaceholder(expression, new PlainStringFormattedValue(fallbackString));
+        }
+        Formatter ff = funcFactory.createFormatter(locale, options);
+        String res = ff.formatToString(toFormat, arguments);
+        if (res == null) {
+            res = fallbackString;
+        }
+
+        ResolvedExpression resExpression = new ResolvedExpression(toFormat, functionName, options);
+        return new FormattedPlaceholder(resExpression, new PlainStringFormattedValue(res));
+    }
+
+    static class ResolvedExpression implements Expression {
+        final Object argument;
+        final String functionName;
+        final Map<String, Object> options;
+
+        public ResolvedExpression(
+                Object argument, String functionName, Map<String, Object> options) {
+            this.argument = argument;
+            this.functionName = functionName;
+            this.options = options;
+        }
+    }
+
+    private Map<String, Object> resolveDeclarations(
+            List<MFDataModel.Declaration> declarations, Map<String, Object> arguments) {
+        Map<String, Object> variables = new HashMap<>();
+        String name;
+        Expression value;
+        if (declarations != null) {
+            for (Declaration declaration : declarations) {
+                if (declaration instanceof InputDeclaration) {
+                    name = ((InputDeclaration) declaration).name;
+                    value = ((InputDeclaration) declaration).value;
+                } else if (declaration instanceof LocalDeclaration) {
+                    name = ((LocalDeclaration) declaration).name;
+                    value = ((LocalDeclaration) declaration).value;
+                } else {
+                    continue;
+                }
+                try {
+                    // There it no need to succeed in solving everything.
+                    // For example there is no problem is `$b` is not defined below:
+                    // .local $a = {$b :number}
+                    // {{ Hello {$user}! }}
+                    FormattedPlaceholder fmt = formatExpression(value, variables, arguments);
+                    // If it works, all good
+                    variables.put(name, fmt);
+                } catch (Exception e) {
+                    // It's OK to ignore the failure in this context, see comment above.
+                }
+            }
+        }
+        return variables;
+    }
+
+    private static class IntVarTuple {
+        int integer;
+        final Variant variant;
+
+        public IntVarTuple(int integer, Variant variant) {
+            this.integer = integer;
+            this.variant = variant;
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFDataModelValidator.java b/android_icu4j/src/main/java/android/icu/message2/MFDataModelValidator.java
new file mode 100644
index 0000000..aecd281
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFDataModelValidator.java
@@ -0,0 +1,205 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import android.icu.message2.MFDataModel.Annotation;
+import android.icu.message2.MFDataModel.CatchallKey;
+import android.icu.message2.MFDataModel.Declaration;
+import android.icu.message2.MFDataModel.Expression;
+import android.icu.message2.MFDataModel.FunctionAnnotation;
+import android.icu.message2.MFDataModel.FunctionExpression;
+import android.icu.message2.MFDataModel.InputDeclaration;
+import android.icu.message2.MFDataModel.Literal;
+import android.icu.message2.MFDataModel.LiteralExpression;
+import android.icu.message2.MFDataModel.LiteralOrCatchallKey;
+import android.icu.message2.MFDataModel.LiteralOrVariableRef;
+import android.icu.message2.MFDataModel.LocalDeclaration;
+import android.icu.message2.MFDataModel.Option;
+import android.icu.message2.MFDataModel.PatternMessage;
+import android.icu.message2.MFDataModel.SelectMessage;
+import android.icu.message2.MFDataModel.VariableExpression;
+import android.icu.message2.MFDataModel.VariableRef;
+import android.icu.message2.MFDataModel.Variant;
+
+// I can merge all this in the MFDataModel class and make it private
+class MFDataModelValidator {
+    private final MFDataModel.Message message;
+    private final Set<String> declaredVars = new HashSet<>();
+
+    MFDataModelValidator(MFDataModel.Message message) {
+        this.message = message;
+    }
+
+    boolean validate() throws MFParseException {
+        if (message instanceof PatternMessage) {
+            validateDeclarations(((PatternMessage) message).declarations);
+        } else if (message instanceof SelectMessage) {
+            SelectMessage sm = (SelectMessage) message;
+            validateDeclarations(sm.declarations);
+            validateSelectors(sm.selectors);
+            int selectorCount = sm.selectors.size();
+            validateVariants(sm.variants, selectorCount);
+        }
+        return true;
+    }
+
+    private boolean validateVariants(List<Variant> variants, int selectorCount)
+            throws MFParseException {
+        if (variants == null || variants.isEmpty()) {
+            error("Selection messages must have at least one variant");
+        }
+
+        // Look for an entry with all keys = '*'
+        boolean hasUltimateFallback = false;
+        Set<String> fakeKeys = new HashSet<>();
+        for (Variant variant : variants) {
+            if (variant.keys == null || variant.keys.isEmpty()) {
+                error("Selection variants must have at least one key");
+            }
+            if (variant.keys.size() != selectorCount) {
+                error("Selection variants must have the same number of variants as the selectors.");
+            }
+            int catchAllCount = 0;
+            StringJoiner fakeKey = new StringJoiner("<<::>>");
+            for (LiteralOrCatchallKey key : variant.keys) {
+                if (key instanceof CatchallKey) {
+                    catchAllCount++;
+                    fakeKey.add("*");
+                } else if (key instanceof Literal) {
+                    fakeKey.add(((Literal) key).value);
+                }
+            }
+            if (fakeKeys.contains(fakeKey.toString())) {
+                error("Dumplicate combination of keys");
+            } else {
+                fakeKeys.add(fakeKey.toString());
+            }
+            if (catchAllCount == selectorCount) {
+                hasUltimateFallback = true;
+            }
+        }
+        if (!hasUltimateFallback) {
+            error("There must be one variant with all the keys being '*'");
+        }
+        return true;
+    }
+
+    private boolean validateSelectors(List<Expression> selectors) throws MFParseException {
+        if (selectors == null || selectors.isEmpty()) {
+            error("Selection messages must have selectors");
+        }
+        return true;
+    }
+
+    /*
+     * .input {$foo :number} .input {$foo} => ERROR
+     * .input {$foo :number} .local $foo={$bar} => ERROR, local foo overrides an input
+     * .local $foo={...} .local $foo={...} => ERROR, foo declared twice
+     * .local $a={$foo} .local $b={$foo} => NOT AN ERROR (foo is used, not declared)
+     * .local $a={:f opt=$foo} .local $foo={$foo} => ERROR, foo declared after beeing used in opt
+     */
+    private boolean validateDeclarations(List<Declaration> declarations) throws MFParseException {
+        if (declarations == null || declarations.isEmpty()) {
+            return true;
+        }
+        for (Declaration declaration : declarations) {
+            if (declaration instanceof LocalDeclaration) {
+                LocalDeclaration ld = (LocalDeclaration) declaration;
+                validateExpression(ld.value, false);
+                addVariableDeclaration(ld.name);
+            } else if (declaration instanceof InputDeclaration) {
+                InputDeclaration id = (InputDeclaration) declaration;
+                validateExpression(id.value, true);
+            }
+        }
+        return true;
+    }
+
+    /*
+     * One might also consider checking if the same variable is used with more than one type:
+     *   .local $a = {$foo :number}
+     *   .local $b = {$foo :string}
+     *   .local $c = {$foo :datetime}
+     *
+     * But this is not necesarily an error.
+     * If $foo is a number, then it might be formatter as a number, or as date (epoch time),
+     * or something else.
+     *
+     * So it is not safe to complain. Especially with custom functions:
+     *   # get the first name from a `Person` object
+     *   .local $b = {$person :getField fieldName=firstName}
+     *   # get formats a `Person` object
+     *   .local $b = {$person :person}
+     */
+    private void validateExpression(Expression expression, boolean fromInput)
+            throws MFParseException {
+        String argName = null;
+        Annotation annotation = null;
+        if (expression instanceof Literal) {
+            // ...{foo}... or ...{|foo|}... or ...{123}...
+            // does not declare anything
+        } else if (expression instanceof LiteralExpression) {
+            LiteralExpression le = (LiteralExpression) expression;
+            argName = le.arg.value;
+            annotation = le.annotation;
+        } else if (expression instanceof VariableExpression) {
+            VariableExpression ve = (VariableExpression) expression;
+            // ...{$foo :bar opt1=|str| opt2=$x opt3=$y}...
+            // .input {$foo :number} => declares `foo`, if already declared is an error
+            // .local $a={$foo} => declares `a`, but only used `foo`, does not declare it
+            argName = ve.arg.name;
+            annotation = ve.annotation;
+        } else if (expression instanceof FunctionExpression) {
+            // ...{$foo :bar opt1=|str| opt2=$x opt3=$y}...
+            FunctionExpression fe = (FunctionExpression) expression;
+            annotation = fe.annotation;
+        }
+
+        if (annotation instanceof FunctionAnnotation) {
+            FunctionAnnotation fa = (FunctionAnnotation) annotation;
+            if (fa.options != null) {
+                for (Option opt : fa.options.values()) {
+                    LiteralOrVariableRef val = opt.value;
+                    if (val instanceof VariableRef) {
+                        // We had something like {:f option=$val}, it means we's seen `val`
+                        // It is not a declaration, so not an error.
+                        addVariableDeclaration(((VariableRef) val).name);
+                    }
+                }
+            }
+        }
+
+        // We chech the argument name after options to prevent errors like this:
+        // .local $foo = {$a :b option=$foo}
+        if (argName != null) {
+            // if we come from `.input {$foo :function}` then `varName` is null
+            // and `argName` is `foo`
+            if (fromInput) {
+                addVariableDeclaration(argName);
+            } else {
+                // Remember that we've seen it, to complain if there is a declaration later
+                declaredVars.add(argName);
+            }
+        }
+    }
+
+    private boolean addVariableDeclaration(String varName) throws MFParseException {
+        if (declaredVars.contains(varName)) {
+            error("Variable '" + varName + "' already declared");
+            return false;
+        }
+        declaredVars.add(varName);
+        return true;
+    }
+
+    private void error(String text) throws MFParseException {
+        throw new MFParseException(text, -1);
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFFunctionRegistry.java b/android_icu4j/src/main/java/android/icu/message2/MFFunctionRegistry.java
new file mode 100644
index 0000000..4269f09
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFFunctionRegistry.java
@@ -0,0 +1,349 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class is used to register mappings between various function
+ * names and the factories that can create those functions.
+ *
+ * <p>For example to add formatting for a {@code Person} object one would need to:</p>
+ * <ul>
+ *   <li>write a function (class, lambda, etc.) that does the formatting proper
+ *     (implementing {@link Formatter})</li>
+ *   <li>write a factory that creates such a function
+ *     (implementing {@link FormatterFactory})</li>
+ *  <li>add a mapping from the function name as used in the syntax
+ *    (for example {@code "person"}) to the factory</li>
+ *  <li>optionally add a mapping from the class to format ({@code ...Person.class}) to
+ *     the formatter name ({@code "person"}), so that one can use a placeholder in the message
+ *     without specifying a function (for example {@code "... {$me} ..."} instead of
+ *     {@code "... {$me :person} ..."}, if the class of {@code $me} is an {@code instanceof Person}).
+ *     </li>
+ * </ul>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class MFFunctionRegistry {
+    private final Map<String, FormatterFactory> formattersMap;
+    private final Map<String, SelectorFactory> selectorsMap;
+    private final Map<Class<?>, String> classToFormatter;
+
+    private MFFunctionRegistry(Builder builder) {
+        this.formattersMap = new HashMap<>(builder.formattersMap);
+        this.selectorsMap = new HashMap<>(builder.selectorsMap);
+        this.classToFormatter = new HashMap<>(builder.classToFormatter);
+    }
+
+    /**
+     * Creates a builder.
+     *
+     * @return the Builder.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Returns the formatter factory used to create the formatter for function
+     * named {@code name}.
+     *
+     * <p>Note: function name here means the name used to refer to the function in the
+     * MessageFormat 2 syntax, for example {@code "... {$exp :datetime} ..."}<br>
+     * The function name here is {@code "datetime"}, and does not have to correspond to the
+     * name of the methods / classes used to implement the functionality.</p>
+     *
+     * <p>For example one might write a {@code PersonFormatterFactory} returning a {@code PersonFormatter},
+     * and map that to the MessageFormat function named {@code "person"}.<br>
+     * The only name visible to the users of MessageFormat syntax will be {@code "person"}.</p>
+     *
+     * @param formatterName the function name.
+     * @return the factory creating formatters for {@code name}. Returns {@code null} if none is registered.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public FormatterFactory getFormatter(String formatterName) {
+        return formattersMap.get(formatterName);
+    }
+
+    /**
+     * Get all know names that have a mappings from name to {@link FormatterFactory}.
+     *
+     * @return a set of all the known formatter names.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Set<String> getFormatterNames() {
+        return formattersMap.keySet();
+    }
+
+    /**
+     * Returns the name of the formatter used to format an object of type {@code clazz}.
+     *
+     * @param clazz the class of the object to format.
+     * @return the name of the formatter class, if registered. Returns {@code null} otherwise.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public String getDefaultFormatterNameForType(Class<?> clazz) {
+        // Search for the class "as is", to save time.
+        // If we don't find it then we iterate the registered classes and check
+        // if the class is an instanceof the ones registered.
+        // For example a BuddhistCalendar when we only registered Calendar
+        String result = classToFormatter.get(clazz);
+        if (result != null) {
+            return result;
+        }
+        // We didn't find the class registered explicitly "as is"
+        for (Map.Entry<Class<?>, String> e : classToFormatter.entrySet()) {
+            if (e.getKey().isAssignableFrom(clazz)) {
+                return e.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get all know classes that have a mappings from class to function name.
+     *
+     * @return a set of all the known classes that have mapping to function names.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Set<Class<?>> getDefaultFormatterTypes() {
+        return classToFormatter.keySet();
+    }
+
+    /**
+     * Returns the selector factory used to create the selector for function
+     * named {@code name}.
+     *
+     * <p>Note: the same comments about naming as the ones on {@code getFormatter} apply.</p>
+     *
+     * @param selectorName the selector name.
+     * @return the factory creating selectors for {@code name}. Returns {@code null} if none is registered.
+     * @see #getFormatter(String)
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public SelectorFactory getSelector(String selectorName) {
+        return selectorsMap.get(selectorName);
+    }
+
+    /**
+     * Get all know names that have a mappings from name to {@link SelectorFactory}.
+     *
+     * @return a set of all the known selector names.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public Set<String> getSelectorNames() {
+        return selectorsMap.keySet();
+    }
+
+    /**
+     * A {@code Builder} used to build instances of {@link MFFunctionRegistry}.
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide Only a subset of ICU is exposed in Android
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static class Builder {
+        private final Map<String, FormatterFactory> formattersMap = new HashMap<>();
+        private final Map<String, SelectorFactory> selectorsMap = new HashMap<>();
+        private final Map<Class<?>, String> classToFormatter = new HashMap<>();
+
+        // Prevent direct creation
+        private Builder() {}
+
+        /**
+         * Adds all the mapping from another registry to this one.
+         *
+         * @param functionRegistry the registry to copy from.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder addAll(MFFunctionRegistry functionRegistry) {
+            formattersMap.putAll(functionRegistry.formattersMap);
+            selectorsMap.putAll(functionRegistry.selectorsMap);
+            classToFormatter.putAll(functionRegistry.classToFormatter);
+            return this;
+        }
+
+        /**
+         * Adds a mapping from a formatter name to a {@link FormatterFactory}.
+         *
+         * @param formatterName the function name (as used in the MessageFormat 2 syntax).
+         * @param formatterFactory the factory that handles the name.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setFormatter(String formatterName, FormatterFactory formatterFactory) {
+            formattersMap.put(formatterName, formatterFactory);
+            return this;
+        }
+
+        /**
+         * Remove the formatter associated with the name.
+         *
+         * @param formatterName the name of the formatter to remove.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder removeFormatter(String formatterName) {
+            formattersMap.remove(formatterName);
+            return this;
+        }
+
+        /**
+         * Remove all the formatter mappings.
+         *
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder clearFormatters() {
+            formattersMap.clear();
+            return this;
+        }
+
+        /**
+         * Adds a mapping from a type to format to a {@link FormatterFactory} formatter name.
+         *
+         * @param clazz the class of the type to format.
+         * @param formatterName the formatter name (as used in the MessageFormat 2 syntax).
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setDefaultFormatterNameForType(Class<?> clazz, String formatterName) {
+            classToFormatter.put(clazz, formatterName);
+            return this;
+        }
+
+        /**
+         * Remove the function name associated with the class.
+         *
+         * @param clazz the class to remove the mapping for.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder removeDefaultFormatterNameForType(Class<?> clazz) {
+            classToFormatter.remove(clazz);
+            return this;
+        }
+
+        /**
+         * Remove all the class to formatter-names mappings.
+         *
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder clearDefaultFormatterNames() {
+            classToFormatter.clear();
+            return this;
+        }
+
+        /**
+         * Adds a mapping from a selector name to a {@link SelectorFactory}.
+         *
+         * @param selectorName the function name (as used in the MessageFormat 2 syntax).
+         * @param selectorFactory the factory that handles the name.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder setSelector(String selectorName, SelectorFactory selectorFactory) {
+            selectorsMap.put(selectorName, selectorFactory);
+            return this;
+        }
+
+        /**
+         * Remove the selector associated with the name.
+         *
+         * @param selectorName the name of the selector to remove.
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder removeSelector(String selectorName) {
+            selectorsMap.remove(selectorName);
+            return this;
+        }
+
+        /**
+         * Remove all the selector mappings.
+         *
+         * @return the builder, for fluent use.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public Builder clearSelectors() {
+            selectorsMap.clear();
+            return this;
+        }
+
+        /**
+         * Builds an instance of {@link MFFunctionRegistry}.
+         *
+         * @return the function registry created.
+         *
+         * @deprecated This API is for technology preview only.
+         * @hide draft / provisional / internal are hidden on Android
+         */
+        @Deprecated
+        public MFFunctionRegistry build() {
+            return new MFFunctionRegistry(this);
+        }
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFParseException.java b/android_icu4j/src/main/java/android/icu/message2/MFParseException.java
new file mode 100644
index 0000000..bac04c3
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFParseException.java
@@ -0,0 +1,34 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2024 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.text.ParseException;
+
+/**
+ * Used to report parsing errors in {@link MessageFormatter}.
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class MFParseException extends ParseException {
+    private static final long serialVersionUID = -7634219305388292407L;
+
+    /**
+     * Constructs a MFParseException with the specified message and offset.
+     *
+     * @param message the message
+     * @param errorOffset the position where the error is found while parsing.
+     */
+    public MFParseException(String message, int errorOffset) {
+        super(message, errorOffset);
+    }
+
+    @Override
+    public String getMessage() {
+        return super.getMessage();
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFParser.java b/android_icu4j/src/main/java/android/icu/message2/MFParser.java
new file mode 100644
index 0000000..efa7b77
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFParser.java
@@ -0,0 +1,789 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class parses a {@code MessageFormat 2} syntax into a data model {@link MFDataModel.Message}.
+ *
+ * <p>It is used by {@link MessageFormatter}, but it might be handy for various tools.</p>
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class MFParser {
+    private static final int EOF = -1;
+    private final InputSource input;
+
+    MFParser(String text) {
+        this.input = new InputSource(text);
+    }
+
+    /**
+     * Parses a {@code MessageFormat 2} syntax into a {@link MFDataModel.Message}.
+     *
+     * <p>It is used by {@link MessageFormatter}, but it might be handy for various tools.</p>
+     * @param input the text to parse
+     * @return the parsed {@code MFDataModel.Message}
+     * @throws MFParseException if errors are detected
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static MFDataModel.Message parse(String input) throws MFParseException {
+        return new MFParser(input).parseImpl();
+    }
+
+    // Parser proper
+    private MFDataModel.Message parseImpl() throws MFParseException {
+        MFDataModel.Message result;
+        int cp = input.peekChar();
+        if (cp == '.') { // declarations or .match
+            result = getComplexMessage();
+        } else if (cp == '{') { // `{` or `{{`
+            cp = input.readCodePoint();
+            cp = input.peekChar();
+            if (cp == '{') { // `{{`, complex body without declarations
+                input.backup(1); // let complexBody deal with the wrapping {{ and }}
+                MFDataModel.Pattern pattern = getQuotedPattern();
+                result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
+            } else { // placeholder
+                input.backup(1); // We want the '{' present, to detect the part as placeholder.
+                MFDataModel.Pattern pattern = getPattern();
+                result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
+            }
+        } else {
+            MFDataModel.Pattern pattern = getPattern();
+            result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern);
+        }
+        skipOptionalWhitespaces();
+        checkCondition(input.atEnd(), "Content detected after the end of the message.");
+        new MFDataModelValidator(result).validate();
+        return result;
+    }
+
+    // abnf: simple-message = [simple-start pattern]
+    // abnf: simple-start = simple-start-char / text-escape / placeholder
+    // abnf: pattern = *(text-char / text-escape / placeholder)
+    private MFDataModel.Pattern getPattern() throws MFParseException {
+        MFDataModel.Pattern pattern = new MFDataModel.Pattern();
+        while (true) {
+            MFDataModel.PatternPart part = getPatternPart();
+            if (part == null) {
+                break;
+            }
+            pattern.parts.add(part);
+        }
+        // checkCondition(!pattern.parts.isEmpty(), "Empty pattern");
+        return pattern;
+    }
+
+    private MFDataModel.PatternPart getPatternPart() throws MFParseException {
+        int cp = input.peekChar();
+        switch (cp) {
+            case EOF:
+                return null;
+            case '}': // This is the end, otherwise it would be escaped
+                return null;
+            case '{':
+                MFDataModel.Expression ph = getPlaceholder();
+                return ph;
+            default:
+                String plainText = getText();
+                MFDataModel.StringPart sp = new MFDataModel.StringPart(plainText);
+                return sp;
+        }
+    }
+
+    private String getText() {
+        StringBuilder result = new StringBuilder();
+        while (true) {
+            int cp = input.readCodePoint();
+            switch (cp) {
+                case EOF:
+                    return result.toString();
+                case '\\':
+                    cp = input.readCodePoint();
+                    if (cp == '\\' || cp == '{' || cp == '|' | cp == '}') {
+                        result.appendCodePoint(cp);
+                    } else { // TODO: Error, treat invalid escape?
+                        result.appendCodePoint('\\');
+                        result.appendCodePoint(cp);
+                    }
+                    break;
+                case '.':
+                case '@':
+                case '|':
+                    result.appendCodePoint(cp);
+                    break;
+                default:
+                    if (StringUtils.isContentChar(cp) || StringUtils.isWhitespace(cp)) {
+                        result.appendCodePoint(cp);
+                    } else {
+                        input.backup(1);
+                        return result.toString();
+                    }
+            }
+        }
+    }
+
+    // abnf: placeholder = expression / markup
+    // abnf: expression = literal-expression
+    // abnf: / variable-expression
+    // abnf: / annotation-expression
+    // abnf: literal-expression = "{" [s] literal [s annotation] *(s attribute) [s] "}"
+    // abnf: variable-expression = "{" [s] variable [s annotation] *(s attribute) [s] "}"
+    // abnf: annotation-expression = "{" [s] annotation *(s attribute) [s] "}"
+    // abnf: markup = "{" [s] "#" identifier *(s option) *(s attribute) [s] ["/"] "}" ; open and standalone
+    // abnf: / "{" [s] "/" identifier *(s option) *(s attribute) [s] "}" ; close
+    private MFDataModel.Expression getPlaceholder() throws MFParseException {
+        int cp = input.peekChar();
+        if (cp != '{') {
+            return null;
+        }
+        input.readCodePoint(); // consume the '{'
+        skipOptionalWhitespaces();
+        cp = input.peekChar();
+
+        MFDataModel.Expression result;
+        if (cp == '#' || cp == '/') {
+            result = getMarkup();
+        } else if (cp == '$') {
+            result = getVariableExpression();
+        } else if (StringUtils.isFunctionSigil(cp)
+                || StringUtils.isPrivateAnnotationSigil(cp)
+                || StringUtils.isReservedAnnotationSigil(cp)) {
+            result = getAnnotationExpression();
+        } else {
+            result = getLiteralExpression();
+        }
+
+        skipOptionalWhitespaces();
+        cp = input.readCodePoint(); // consume the '}'
+        checkCondition(cp == '}', "Unclosed placeholder");
+
+        return result;
+    }
+
+    private MFDataModel.Annotation getAnnotation() throws MFParseException {
+        int position = input.getPosition();
+        skipOptionalWhitespaces();
+
+        int cp = input.peekChar();
+        switch (cp) {
+            case '}':
+                return null;
+            case ':': // annotation, function
+                // abnf: function = ":" identifier *(s option)
+                input.readCodePoint(); // Consume the sigil
+                String identifier = getIdentifier();
+                checkCondition(identifier != null, "Annotation / function name missing");
+                Map<String, MFDataModel.Option> options = getOptions();
+                return new MFDataModel.FunctionAnnotation(identifier, options);
+            default: // reserved && private
+                if (StringUtils.isReservedAnnotationSigil(cp)
+                        || StringUtils.isPrivateAnnotationSigil(cp)) {
+                    cp = input.readCodePoint();
+                    // The sigil is part of the body.
+                    // Safe to cast to char, the code point is in BMP
+                    identifier = (char) cp + getIdentifier();
+                    String body = getReservedBody();
+                    return new MFDataModel.UnsupportedAnnotation(identifier + body);
+                }
+        }
+        input.gotoPosition(position);
+        return null;
+    }
+
+    private MFDataModel.Annotation getMarkupAnnotation() throws MFParseException {
+        skipOptionalWhitespaces();
+
+        int cp = input.peekChar();
+        switch (cp) {
+            case '}':
+                return null;
+            case '#':
+            case '/':
+                // abnf: markup = "{" [s] "#" identifier *(s option) *(s attribute) [s] ["/"] "}"  ; open and standalone
+                // abnf:        / "{" [s] "/" identifier *(s option) *(s attribute) [s] "}"  ; close
+                input.readCodePoint(); // Consume the sigil
+                String identifier = getIdentifier();
+                checkCondition(identifier != null, "Annotation / function name missing");
+                Map<String, MFDataModel.Option> options = getOptions();
+                return new MFDataModel.FunctionAnnotation(identifier, options);
+            default:
+                // reserved, private, function, something else,
+                return null;
+        }
+    }
+
+    // abnf: literal-expression = "{" [s] literal [s annotation] *(s attribute) [s] "}"
+    private MFDataModel.Expression getLiteralExpression() throws MFParseException {
+        MFDataModel.Literal literal = getLiteral();
+        checkCondition(literal != null, "Literal expression expected.");
+
+        MFDataModel.Annotation annotation = null;
+        int wsCount = skipWhitespaces();
+        if (wsCount > 0) { // we might have an annotation
+            annotation = getAnnotation();
+            if (annotation == null) {
+                // We had some spaces, but no annotation.
+                // So we put (some) back for the possible attributes.
+                input.backup(1);
+            }
+        }
+
+        List<MFDataModel.Attribute> attributes = getAttributes();
+
+        // Literal without a function, for example {|hello|} or {123}
+        return new MFDataModel.LiteralExpression(literal, annotation, attributes);
+    }
+
+    // abnf: variable-expression = "{" [s] variable [s annotation] *(s attribute) [s] "}"
+    private MFDataModel.VariableExpression getVariableExpression() throws MFParseException {
+        MFDataModel.VariableRef variableRef = getVariableRef();
+        MFDataModel.Annotation annotation = getAnnotation();
+        List<MFDataModel.Attribute> attributes = getAttributes();
+        // Variable without a function, for example {$foo}
+        return new MFDataModel.VariableExpression(variableRef, annotation, attributes);
+    }
+
+    // abnf: annotation-expression = "{" [s] annotation *(s attribute) [s] "}"
+    private MFDataModel.Expression getAnnotationExpression() throws MFParseException {
+        MFDataModel.Annotation annotation = getAnnotation();
+        List<MFDataModel.Attribute> attributes = getAttributes();
+
+        if (annotation instanceof MFDataModel.FunctionAnnotation) {
+            return new MFDataModel.FunctionExpression(
+                    (MFDataModel.FunctionAnnotation) annotation, attributes);
+        } else if (annotation instanceof MFDataModel.UnsupportedAnnotation) {
+            return new MFDataModel.UnsupportedExpression(
+                    (MFDataModel.UnsupportedAnnotation) annotation, attributes);
+        } else {
+            error("Unexpected annotation : " + annotation);
+        }
+        return null;
+    }
+
+    // abnf: markup = "{" [s] "#" identifier *(s option) *(s attribute) [s] ["/"] "}" ; open and standalone
+    // abnf: / "{" [s] "/" identifier *(s option) *(s attribute) [s] "}" ; close
+    private MFDataModel.Markup getMarkup() throws MFParseException {
+        int cp = input.peekChar(); // consume the '{'
+        checkCondition(cp == '#' || cp == '/', "Should not happen. Expecting a markup.");
+
+        MFDataModel.Markup.Kind kind =
+                cp == '/' ? MFDataModel.Markup.Kind.CLOSE : MFDataModel.Markup.Kind.OPEN;
+
+        MFDataModel.Annotation annotation = getMarkupAnnotation();
+        List<MFDataModel.Attribute> attributes = getAttributes();
+
+        cp = input.peekChar();
+        if (cp == '/') {
+            kind = MFDataModel.Markup.Kind.STANDALONE;
+            input.readCodePoint();
+        }
+
+        if (annotation instanceof MFDataModel.FunctionAnnotation) {
+            MFDataModel.FunctionAnnotation fa = (MFDataModel.FunctionAnnotation) annotation;
+            return new MFDataModel.Markup(kind, fa.name, fa.options, attributes);
+        }
+
+        return null;
+    }
+
+    private List<MFDataModel.Attribute> getAttributes() throws MFParseException {
+        List<MFDataModel.Attribute> result = new ArrayList<>();
+        while (true) {
+            MFDataModel.Attribute attribute = getAttribute();
+            if (attribute == null) {
+                break;
+            }
+            result.add(attribute);
+        }
+        return result;
+    }
+
+    // abnf: attribute = "@" identifier [[s] "=" [s] (literal / variable)]
+    private MFDataModel.Attribute getAttribute() throws MFParseException {
+        int position = input.getPosition();
+        if (skipWhitespaces() == 0) {
+            input.gotoPosition(position);
+            return null;
+        }
+        int cp = input.peekChar();
+        if (cp == '@') {
+            input.readCodePoint(); // consume the '@'
+            String id = getIdentifier();
+            int wsCount = skipWhitespaces();
+            cp = input.peekChar();
+            MFDataModel.LiteralOrVariableRef literalOrVariable = null;
+            if (cp == '=') {
+                input.readCodePoint();
+                skipOptionalWhitespaces();
+                literalOrVariable = getLiteralOrVariableRef();
+                checkCondition(literalOrVariable != null, "Attributes must have a value after `=`");
+            } else {
+                // was not equal, attribute without a value, put the "spaces" back.
+                input.backup(wsCount);
+            }
+            return new MFDataModel.Attribute(id, literalOrVariable);
+        } else {
+            input.gotoPosition(position);
+        }
+        return null;
+    }
+
+    // abnf: reserved-body = *([s] 1*(reserved-char / reserved-escape / quoted))
+    // abnf: reserved-escape = backslash ( backslash / "{" / "|" / "}" )
+    private String getReservedBody() throws MFParseException {
+        int spaceCount = skipWhitespaces();
+        StringBuilder result = new StringBuilder();
+        while (true) {
+            int cp = input.readCodePoint();
+            if (StringUtils.isReservedChar(cp)) {
+                result.appendCodePoint(cp);
+            } else if (cp == '\\') {
+                cp = input.readCodePoint();
+                checkCondition(
+                        cp == '{' || cp == '|' || cp == '}',
+                        "Invalid escape sequence. Only \\{, \\| and \\} are valid here.");
+                result.append(cp);
+            } else if (cp == '|') {
+                input.backup(1);
+                MFDataModel.Literal quoted = getQuotedLiteral();
+                result.append(quoted.value);
+            } else if (cp == EOF) {
+                return result.toString();
+            } else {
+                if (result.length() == 0) {
+                    input.backup(spaceCount + 1);
+                    return "";
+                } else {
+                    input.backup(1);
+                    return result.toString();
+                }
+            }
+        }
+    }
+
+    // abnf: identifier = [namespace ":"] name
+    // abnf: namespace = name
+    // abnf: name = name-start *name-char
+    private String getIdentifier() throws MFParseException {
+        String namespace = getName();
+        if (namespace == null) {
+            return null;
+        }
+        int cp = input.readCodePoint();
+        if (cp == ':') { // the previous name was namespace
+            String name = getName();
+            checkCondition(name != null, "Expected name after namespace '" + namespace + "'");
+            return namespace + ":" + name;
+        } else {
+            input.backup(1);
+        }
+        return namespace;
+    }
+
+    // abnf helper: *(s option)
+    private Map<String, MFDataModel.Option> getOptions() throws MFParseException {
+        Map<String, MFDataModel.Option> options = new LinkedHashMap<>();
+        while (true) {
+            MFDataModel.Option option = getOption();
+            if (option == null) {
+                break;
+            }
+            if (options.containsKey(option.name)) {
+                error("Duplicated option '" + option.name + "'");
+            }
+            options.put(option.name, option);
+        }
+        return options;
+    }
+
+    // abnf: option = identifier [s] "=" [s] (literal / variable)
+    private MFDataModel.Option getOption() throws MFParseException {
+        int position = input.getPosition();
+        skipOptionalWhitespaces();
+        String identifier = getIdentifier();
+        if (identifier == null) {
+            input.gotoPosition(position);
+            return null;
+        }
+        skipOptionalWhitespaces();
+        int cp = input.readCodePoint();
+        checkCondition(cp == '=', "Expected '='");
+        // skipOptionalWhitespaces();
+        MFDataModel.LiteralOrVariableRef litOrVar = getLiteralOrVariableRef();
+        if (litOrVar == null) {
+            error("Options must have a value. An empty string should be quoted.");
+        }
+        return new MFDataModel.Option(identifier, litOrVar);
+    }
+
+    private MFDataModel.LiteralOrVariableRef getLiteralOrVariableRef() throws MFParseException {
+        int cp = input.peekChar();
+        if (cp == '$') {
+            return getVariableRef();
+        }
+        return getLiteral();
+    }
+
+    // abnf: literal = quoted / unquoted
+    private MFDataModel.Literal getLiteral() throws MFParseException {
+        int cp = input.readCodePoint();
+        switch (cp) {
+            case '|': // quoted
+                // abnf: quoted = "|" *(quoted-char / quoted-escape) "|"
+                input.backup(1);
+                MFDataModel.Literal ql = getQuotedLiteral();
+                return ql;
+            default: // unquoted
+                input.backup(1);
+                MFDataModel.Literal unql = getUnQuotedLiteral();
+                return unql;
+        }
+    }
+
+    private MFDataModel.VariableRef getVariableRef() throws MFParseException {
+        int cp = input.readCodePoint();
+        if (cp != '$') {
+            checkCondition(cp == '$', "We can't get here");
+        }
+
+        // abnf: variable = "$" name
+        String name = getName();
+        checkCondition(name != null, "Invalid variable reference following $");
+        return new MFDataModel.VariableRef(name);
+    }
+
+    private MFDataModel.Literal getQuotedLiteral() throws MFParseException {
+        StringBuilder result = new StringBuilder();
+        int cp = input.readCodePoint();
+        checkCondition(cp == '|', "expected starting '|'");
+        while (true) {
+            cp = input.readCodePoint();
+            if (cp == EOF) {
+                break;
+            } else if (StringUtils.isQuotedChar(cp)) {
+                result.appendCodePoint(cp);
+            } else if (cp == '\\') {
+                cp = input.readCodePoint();
+                checkCondition(cp == '|', "Invalid escape sequence, only \"\\|\" is valid here");
+                result.appendCodePoint('|');
+            } else {
+                break;
+            }
+        }
+
+        checkCondition(cp == '|', "expected ending '|'");
+
+        return new MFDataModel.Literal(result.toString());
+    }
+
+    private MFDataModel.Literal getUnQuotedLiteral() throws MFParseException {
+        String name = getName();
+        if (name != null) {
+            return new MFDataModel.Literal(name);
+        }
+        return getNumberLiteral();
+    }
+
+    // abnf: ; number-literal matches JSON number (https://www.rfc-editor.org/rfc/rfc8259#section-6)
+    // abnf: number-literal = ["-"] (%x30 / (%x31-39 *DIGIT)) ["." 1*DIGIT] [%i"e" ["-" / "+"] 1*DIGIT]
+    private static final Pattern RE_NUMBER_LITERAL =
+            Pattern.compile("^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+\\-]?[0-9]+)?");
+
+    private MFDataModel.Literal getNumberLiteral() {
+        String numberString = peekWithRegExp(RE_NUMBER_LITERAL);
+        if (numberString != null) {
+            return new MFDataModel.Literal(numberString);
+        }
+        return null;
+    }
+
+    private void skipMandatoryWhitespaces() throws MFParseException {
+        int count = skipWhitespaces();
+        checkCondition(count > 0, "Space expected");
+    }
+
+    private void skipOptionalWhitespaces() {
+        skipWhitespaces();
+    }
+
+    private int skipWhitespaces() {
+        int skipCount = 0;
+        while (true) {
+            int cp = input.readCodePoint();
+            if (cp == EOF) {
+                return skipCount;
+            }
+            if (!StringUtils.isWhitespace(cp)) {
+                input.backup(1);
+                return skipCount;
+            }
+            skipCount++;
+        }
+    }
+
+    private MFDataModel.Message getComplexMessage() throws MFParseException {
+        List<MFDataModel.Declaration> declarations = new ArrayList<>();
+        boolean foundMatch = false;
+        while (true) {
+            MFDataModel.Declaration declaration = getDeclaration();
+            if (declaration == null) {
+                break;
+            }
+            if (declaration instanceof MatchDeclaration) {
+                foundMatch = true;
+                break;
+            }
+            declarations.add(declaration);
+        }
+        if (foundMatch) {
+            return getMatch(declarations);
+        } else { // Expect {{...}} or end of message
+            skipOptionalWhitespaces();
+            int cp = input.peekChar();
+            if (cp == EOF) {
+                // Only declarations, no pattern
+                return new MFDataModel.PatternMessage(declarations, null);
+            } else {
+                MFDataModel.Pattern pattern = getQuotedPattern();
+                return new MFDataModel.PatternMessage(declarations, pattern);
+            }
+        }
+    }
+
+    // abnf: matcher = match-statement 1*([s] variant)
+    // abnf: match-statement = match 1*([s] selector)
+    // abnf: selector = expression
+    // abnf: variant = key *(s key) [s] quoted-pattern
+    // abnf: key = literal / "*"
+    // abnf: match = %s".match"
+    private MFDataModel.SelectMessage getMatch(List<MFDataModel.Declaration> declarations)
+            throws MFParseException {
+        // ".match" was already consumed by the caller
+        // Look for selectors
+        List<MFDataModel.Expression> expressions = new ArrayList<>();
+        while (true) {
+            skipMandatoryWhitespaces();
+            MFDataModel.Expression expression = getPlaceholder();
+            if (expression == null) {
+                break;
+            }
+            checkCondition(
+                    !(expression instanceof MFDataModel.Markup), "Cannot do selection on markup");
+            expressions.add(expression);
+        }
+
+        checkCondition(!expressions.isEmpty(), "There should be at least one selector expression.");
+
+        // At this point we need to look for variants, which are key - value
+        List<MFDataModel.Variant> variants = new ArrayList<>();
+        while (true) {
+            MFDataModel.Variant variant = getVariant();
+            if (variant == null) {
+                break;
+            }
+            variants.add(variant);
+        }
+        return new MFDataModel.SelectMessage(declarations, expressions, variants);
+    }
+
+    // abnf: variant = key *(s key) [s] quoted-pattern
+    // abnf: key = literal / "*"
+    private MFDataModel.Variant getVariant() throws MFParseException {
+        List<MFDataModel.LiteralOrCatchallKey> keys = new ArrayList<>();
+        // abnf variant = key *(s key) [s] quoted-pattern
+        while (true) {
+            // Space is required between keys
+            MFDataModel.LiteralOrCatchallKey key = getKey(!keys.isEmpty());
+            if (key == null) {
+                break;
+            }
+            keys.add(key);
+        }
+        skipOptionalWhitespaces();
+        if (input.atEnd()) {
+            checkCondition(
+                    keys.isEmpty(), "After selector keys it is mandatory to have a pattern.");
+            return null;
+        }
+        MFDataModel.Pattern pattern = getQuotedPattern();
+        return new MFDataModel.Variant(keys, pattern);
+    }
+
+    private MFDataModel.LiteralOrCatchallKey getKey(boolean requireSpaces) throws MFParseException {
+        if (requireSpaces) {
+            skipMandatoryWhitespaces();
+        } else {
+            skipOptionalWhitespaces();
+        }
+        int cp = input.peekChar();
+        if (cp == '*') {
+            input.readCodePoint(); // consume the '*'
+            return new MFDataModel.CatchallKey();
+        }
+        if (cp == EOF) {
+            return null;
+        }
+        return getLiteral();
+    }
+
+    private static class MatchDeclaration implements MFDataModel.Declaration {
+        // Provides a common type that extends MFDataModel.Declaration but for match.
+        // There is no such thing in the data model.
+    }
+
+    // abnf: input-declaration = input [s] variable-expression
+    // abnf: local-declaration = local s variable [s] "=" [s] expression
+    // abnf: reserved-statement = reserved-keyword [s reserved-body] 1*([s] expression)
+    // abnf: reserved-keyword = "." name
+    private MFDataModel.Declaration getDeclaration() throws MFParseException {
+        int position = input.getPosition();
+        skipOptionalWhitespaces();
+        int cp = input.readCodePoint();
+        if (cp != '.') {
+            input.gotoPosition(position);
+            return null;
+        }
+        String declName = getName();
+        checkCondition(declName != null, "Expected a declaration after the '.'");
+
+        MFDataModel.Expression expression;
+        switch (declName) {
+            case "input":
+                skipMandatoryWhitespaces();
+                expression = getPlaceholder();
+                String inputVarName = null;
+                if (expression instanceof MFDataModel.VariableExpression) {
+                    inputVarName = ((MFDataModel.VariableExpression) expression).arg.name;
+                }
+                if (expression instanceof MFDataModel.VariableExpression) {
+                    return new MFDataModel.InputDeclaration(
+                            inputVarName, (MFDataModel.VariableExpression) expression);
+                }
+                break;
+            case "local":
+                // abnf: local-declaration = local s variable [s] "=" [s] expression
+                skipMandatoryWhitespaces();
+                MFDataModel.LiteralOrVariableRef varName = getVariableRef();
+                skipOptionalWhitespaces();
+                cp = input.readCodePoint();
+                checkCondition(cp == '=', declName);
+                skipOptionalWhitespaces();
+                expression = getPlaceholder();
+                if (varName instanceof MFDataModel.VariableRef) {
+                    return new MFDataModel.LocalDeclaration(
+                            ((MFDataModel.VariableRef) varName).name, expression);
+                }
+                break;
+            case "match":
+                return new MatchDeclaration();
+            default: // abnf: reserved-statement = reserved-keyword [s reserved-body] 1*([s] expression)
+                skipOptionalWhitespaces();
+                String body = getReservedBody();
+                List<MFDataModel.Expression> expressions = new ArrayList<>();
+                while (true) {
+                    skipOptionalWhitespaces();
+                    expression = getPlaceholder();
+                    // This also covers != null
+                    if (expression instanceof MFDataModel.VariableExpression) {
+                        expressions.add(expression);
+                    } else {
+                        break;
+                    }
+                }
+                return new MFDataModel.UnsupportedStatement(declName, body, expressions);
+        }
+        return null;
+    }
+
+    // quoted-pattern = "{{" pattern "}}"
+    private MFDataModel.Pattern getQuotedPattern() throws MFParseException { // {{ ... }}
+        int cp = input.readCodePoint();
+        checkCondition(cp == '{', "Expected { for a complex body");
+        cp = input.readCodePoint();
+        checkCondition(cp == '{', "Expected second { for a complex body");
+        MFDataModel.Pattern pattern = getPattern();
+        cp = input.readCodePoint();
+        checkCondition(cp == '}', "Expected } to end a complex body");
+        cp = input.readCodePoint();
+        checkCondition(cp == '}', "Expected second } to end a complex body");
+        return pattern;
+    }
+
+    private String getName() throws MFParseException {
+        StringBuilder result = new StringBuilder();
+        int cp = input.readCodePoint();
+        checkCondition(cp != EOF, "Expected name or namespace.");
+        if (!StringUtils.isNameStart(cp)) {
+            input.backup(1);
+            return null;
+        }
+        result.appendCodePoint(cp);
+        while (true) {
+            cp = input.readCodePoint();
+            if (StringUtils.isNameChar(cp)) {
+                result.appendCodePoint(cp);
+            } else if (cp == EOF) {
+                break;
+            } else {
+                input.backup(1);
+                break;
+            }
+        }
+        return result.toString();
+    }
+
+    private void checkCondition(boolean condition, String message) throws MFParseException {
+        if (!condition) {
+            error(message);
+        }
+    }
+
+    private void error(String message) throws MFParseException {
+        StringBuilder finalMsg = new StringBuilder();
+        if (input == null) {
+            finalMsg.append("Parse error: ");
+            finalMsg.append(message);
+        } else {
+            int position = input.getPosition();
+            finalMsg.append("Parse error [" + input.getPosition() + "]: ");
+            finalMsg.append(message);
+            finalMsg.append("\n");
+            if (position != EOF) {
+                finalMsg.append(input.buffer.substring(0, position));
+                finalMsg.append("^^^");
+                finalMsg.append(input.buffer.substring(position));
+            } else {
+                finalMsg.append(input.buffer);
+                finalMsg.append("^^^");
+            }
+        }
+        throw new MFParseException(finalMsg.toString(), input.getPosition());
+    }
+
+    private String peekWithRegExp(Pattern pattern) {
+        StringView sv = new StringView(input.buffer, input.getPosition());
+        Matcher m = pattern.matcher(sv);
+        boolean found = m.find();
+        if (found) {
+            input.skip(m.group().length());
+            return m.group();
+        }
+        return null;
+    }
+}
diff --git a/android_icu4j/src/main/java/android/icu/message2/MFSerializer.java b/android_icu4j/src/main/java/android/icu/message2/MFSerializer.java
new file mode 100644
index 0000000..e207999
--- /dev/null
+++ b/android_icu4j/src/main/java/android/icu/message2/MFSerializer.java
@@ -0,0 +1,382 @@
+/* GENERATED SOURCE. DO NOT MODIFY. */
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+package android.icu.message2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+
+import android.icu.message2.MFDataModel.Annotation;
+import android.icu.message2.MFDataModel.Attribute;
+import android.icu.message2.MFDataModel.CatchallKey;
+import android.icu.message2.MFDataModel.Declaration;
+import android.icu.message2.MFDataModel.Expression;
+import android.icu.message2.MFDataModel.FunctionAnnotation;
+import android.icu.message2.MFDataModel.FunctionExpression;
+import android.icu.message2.MFDataModel.InputDeclaration;
+import android.icu.message2.MFDataModel.Literal;
+import android.icu.message2.MFDataModel.LiteralExpression;
+import android.icu.message2.MFDataModel.LiteralOrCatchallKey;
+import android.icu.message2.MFDataModel.LiteralOrVariableRef;
+import android.icu.message2.MFDataModel.LocalDeclaration;
+import android.icu.message2.MFDataModel.Markup;
+import android.icu.message2.MFDataModel.Option;
+import android.icu.message2.MFDataModel.Pattern;
+import android.icu.message2.MFDataModel.PatternMessage;
+import android.icu.message2.MFDataModel.PatternPart;
+import android.icu.message2.MFDataModel.SelectMessage;
+import android.icu.message2.MFDataModel.StringPart;
+import android.icu.message2.MFDataModel.UnsupportedAnnotation;
+import android.icu.message2.MFDataModel.UnsupportedExpression;
+import android.icu.message2.MFDataModel.UnsupportedStatement;
+import android.icu.message2.MFDataModel.VariableExpression;
+import android.icu.message2.MFDataModel.VariableRef;
+import android.icu.message2.MFDataModel.Variant;
+
+/**
+ * This class serializes a MessageFormat 2 data model {@link MFDataModel.Message} to a string,
+ * with the proper MessageFormat 2 syntax.
+ *
+ * @deprecated This API is for technology preview only.
+ * @hide Only a subset of ICU is exposed in Android
+ * @hide draft / provisional / internal are hidden on Android
+ */
+@Deprecated
+public class MFSerializer {
+    private boolean shouldDoubleQuotePattern = false;
+    private boolean needSpace = false;
+    private final StringBuilder result = new StringBuilder();
+
+    /**
+     * Method converting the {@link MFDataModel.Message} to a string in MessageFormat 2 syntax.
+     *
+     * <p>The result is not necessarily identical with the original string parsed to generate
+     * the data model. But is is functionally equivalent.</p>
+     *
+     * @param message the data model message to serialize
+     * @return the serialized message, in MessageFormat 2 syntax
+     *
+     * @deprecated This API is for technology preview only.
+     * @hide draft / provisional / internal are hidden on Android
+     */
+    @Deprecated
+    public static String dataModelToString(MFDataModel.Message message) {
+        return new MFSerializer().messageToString(message);
+    }
+
+    private String messageToString(MFDataModel.Message message) {
+        if (message instanceof PatternMessage) {
+            patternMessageToString((PatternMessage) message);
+        } else if (message instanceof SelectMessage) {
+            selectMessageToString((SelectMessage) message);
+        } else {
+            errorType("Message", message);
+        }
+        return result.toString();
+    }
+
+    private void selectMessageToString(SelectMessage message) {
+        declarationsToString(message.declarations);
+        shouldDoubleQuotePattern = true;
+        addSpaceIfNeeded();
+        result.append(".match");
+        for (Expression selector : message.selectors) {
+            result.append(' ');
+            expressionToString(selector);
+        }
+        for (Variant variant : message.variants) {
+            variantToString(variant);
+        }
+    }
+
+    private void patternMessageToString(PatternMessage message) {
+        declarationsToString(message.declarations);
+        patternToString(message.pattern);
+    }
+
+    private void patternToString(Pattern pattern) {
+        addSpaceIfNeeded();
+        if (shouldDoubleQuotePattern) {
+            result.append("{{");
+        }
+        for (PatternPart part : pattern.parts) {
+            if (part instanceof StringPart) {
+                stringPartToString((StringPart) part);
+            } else {
+                expressionToString((Expression) part);
+            }
+        }
+        if (shouldDoubleQuotePattern) {
+            result.append("}}");
+        }
+    }
+
+    private void expressionToString(Expression expression) {
+        if (expression == null) {
+            return;
+        }
+        if (expression instanceof LiteralExpression) {
+            literalExpressionToString((LiteralExpression) expression);
+        } else if (expression instanceof VariableExpression) {
+            variableExpressionToString((VariableExpression) expression);
+        } else if (expression instanceof FunctionExpression) {
+            functionExpressionToString((FunctionExpression) expression);
+        } else if (expression instanceof Markup) {
+            markupToString((Markup) expression);
+        } else if (expression instanceof UnsupportedExpression) {
+            unsupportedExpressionToString((UnsupportedExpression) expression);
+        } else {
+            errorType("Expression", expression);
+        }
+    }
+
+    private void unsupportedExpressionToString(UnsupportedExpression ue) {
+        result.append('{');
+        annotationToString(ue.annotation);
+        attributesToString(ue.attributes);
+        result.append('}');
+    }
+
+    private void markupToString(Markup markup) {
+        result.append('{');
+        if (markup.kind == Markup.Kind.CLOSE) {
+            result.append('/');
+        } else {
+            result.append('#');
+        }
+        result.append(markup.name);
+        optionsToString(markup.options);
+        attributesToString(markup.attributes);
+        if (markup.kind == Markup.Kind.STANDALONE) {
+            result.append('/');
+        }
+        result.append('}');
+    }
+
+    private void optionsToString(Map<String, Option> options) {
+        for (Option option : options.values()) {
+            result.append(' ');
+            result.append(option.name);
+            result.append('=');
+            literalOrVariableRefToString(option.value);
+        }
+    }
+
+    private void functionExpressionToString(FunctionExpression fe) {
+        result.append('{');
+        annotationToString(fe.annotation);
+        attributesToString(fe.attributes);
+        result.append('}');
+    }
+
+    private void attributesToString(List<Attribute> attributes) {
+        if (attributes == null) {
+            return;
+        }
+        for (Attribute attribute : attributes) {
+            result.append(" @");
+            result.append(attribute.name);
+            // Attributes can be with without a value (for now?)
+            if (attribute.value != null) {
+                result.append('=');
+                literalOrVariableRefToString(attribute.value);
+            }
+        }
+    }
+
+    private void annotationToString(Annotation annotation) {
+        if (annotation == null) {
+            return;
+        }
+        if (annotation instanceof FunctionAnnotation) {
+            addSpaceIfNeeded();
+            result.append(":");
+            result.append(((FunctionAnnotation) annotation).name);
+            optionsToString(((FunctionAnnotation) annotation).options);
+        } else if (annotation instanceof UnsupportedAnnotation) {
+            addSpaceIfNeeded();
+            String value = ((UnsupportedAnnotation) annotation).source;
+            for (int i = 0; i < value.length(); i++) {
+                char c = value.charAt(i);
+                if (c == '\\' || c == '{' || c == '}') {
+                    result.append('\\');
+                }
+                result.append(c);
+            }
+        } else {
+            errorType("Annotation", annotation);
+        }
+    }
+
+    private void variableExpressionToString(VariableExpression ve) {
+        if (ve == null) {
+            return;
+        }
+        result.append('{');
+        literalOrVariableRefToString(ve.arg);
+        needSpace = true;
+        annotationToString(ve.annotation);
+        attributesToString(ve.attributes);
+        result.append('}');
+        needSpace = false;
+    }
+
+    private void literalOrVariableRefToString(LiteralOrVariableRef literalOrVarRef) {
+        if (literalOrVarRef instanceof Literal) {
+            literalToString((Literal) literalOrVarRef);
+        } else if (literalOrVarRef instanceof VariableRef) {
+            result.append("$" + ((VariableRef) literalOrVarRef).name);
+        } else {
+            errorType("LiteralOrVariableRef", literalOrVarRef);
+        }
+    }
+
+    // abnf: number-literal = ["-"] (%x30 / (%x31-39 *DIGIT)) ["." 1*DIGIT]
+    // [%i"e" ["-" / "+"] 1*DIGIT]
+    // Not identical to the one in the parser. This one has a $ at the end, to
+    // match the whole string
+    // TBD if it can be refactored to reuse.
+    private static final java.util.regex.Pattern RE_NUMBER_LITERAL =
+            java.util.regex.Pattern.compile("^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+\\-]?[0-9]+)?$");
+
+    private void literalToString(Literal literal) {
+        String value = literal.value;
+        Matcher matcher = RE_NUMBER_LITERAL.matcher(value);
+        if (matcher.find()) { // It is a number, output as is
+            result.append(value);
+        } else {
+            StringBuilder literalBuffer = new StringBuilder();
+            boolean wasName = true;
+            for (int i = 0; i < value.length(); i++) {
+                char c = value.charAt(i);
+                if (c == '\\' || c == '|') {
+                    literalBuffer.append('\\');
+                }
+                literalBuffer.append(c);
+                if (i == 0 && !StringUtils.isNameStart(c)) {
+                    wasName = false;
+                } else if (!StringUtils.isNameChar(c)) {
+                    wasName = false;
+                }
+            }
+            if (wasName && literalBuffer.length() != 0) {
+                result.append(literalBuffer);
+            } else {
+                result.append('|');
+                result.append(literalBuffer);
+                result.append('|');
+            }
+        }
+    }
+
+    private void literalExpressionToString(LiteralExpression le) {
+        result.append('{');
+        literalOrVariableRefToString(le.arg);
+        needSpace = true;
+        annotationToString(le.annotation);
+        attributesToString(le.attributes);
+        result.append('}');
+    }
+
+    private void stringPartToString(StringPart part) {
+        if (part.value.startsWith(".")) {
+            if (!shouldDoubleQuotePattern) {
+                shouldDoubleQuotePattern = true;
+                result.append("{{");
+            }
+        }
+        for (int i = 0; i < part.value.length(); i++) {
+            char c = part.value.charAt(i);
+            if (c == '\\' || c == '{' || c == '}') {
+                result.append('\\');
+            }
+            result.append(c);
+        }
+    }
+
+    private void declarationsToString(List<Declaration> declarations) {
+        if (declarations == null || declarations.isEmpty()) {
+            return;
+        }
+        shouldDoubleQuotePattern = true;
+        for (Declaration declaration : declarations) {
+            if (declaration instanceof LocalDeclaration) {
+                localDeclarationToString((LocalDeclaration) declaration);
+            } else if (declaration instanceof InputDeclaration) {
+                inputDeclarationToString((InputDeclaration) declaration);
+            } else if (declaration instanceof UnsupportedStatement) {
+                unsupportedStatementToString((UnsupportedStatement) declaration);
+            } else {
+                errorTy