1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.net; 18 19 import android.os.Environment; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 import android.os.StrictMode; 23 import android.util.Log; 24 25 import java.io.File; 26 import java.io.IOException; 27 import java.io.UnsupportedEncodingException; 28 import java.net.URLEncoder; 29 import java.nio.charset.StandardCharsets; 30 import java.util.AbstractList; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.LinkedHashSet; 34 import java.util.List; 35 import java.util.Locale; 36 import java.util.Objects; 37 import java.util.RandomAccess; 38 import java.util.Set; 39 40 import libcore.net.UriCodec; 41 42 /** 43 * Immutable URI reference. A URI reference includes a URI and a fragment, the 44 * component of the URI following a '#'. Builds and parses URI references 45 * which conform to 46 * <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC 2396</a>. 47 * 48 * <p>In the interest of performance, this class performs little to no 49 * validation. Behavior is undefined for invalid input. This class is very 50 * forgiving--in the face of invalid input, it will return garbage 51 * rather than throw an exception unless otherwise specified. 52 */ 53 public abstract class Uri implements Parcelable, Comparable<Uri> { 54 55 /* 56 57 This class aims to do as little up front work as possible. To accomplish 58 that, we vary the implementation depending on what the user passes in. 59 For example, we have one implementation if the user passes in a 60 URI string (StringUri) and another if the user passes in the 61 individual components (OpaqueUri). 62 63 *Concurrency notes*: Like any truly immutable object, this class is safe 64 for concurrent use. This class uses a caching pattern in some places where 65 it doesn't use volatile or synchronized. This is safe to do with ints 66 because getting or setting an int is atomic. It's safe to do with a String 67 because the internal fields are final and the memory model guarantees other 68 threads won't see a partially initialized instance. We are not guaranteed 69 that some threads will immediately see changes from other threads on 70 certain platforms, but we don't mind if those threads reconstruct the 71 cached result. As a result, we get thread safe caching with no concurrency 72 overhead, which means the most common case, access from a single thread, 73 is as fast as possible. 74 75 From the Java Language spec.: 76 77 "17.5 Final Field Semantics 78 79 ... when the object is seen by another thread, that thread will always 80 see the correctly constructed version of that object's final fields. 81 It will also see versions of any object or array referenced by 82 those final fields that are at least as up-to-date as the final fields 83 are." 84 85 In that same vein, all non-transient fields within Uri 86 implementations should be final and immutable so as to ensure true 87 immutability for clients even when they don't use proper concurrency 88 control. 89 90 For reference, from RFC 2396: 91 92 "4.3. Parsing a URI Reference 93 94 A URI reference is typically parsed according to the four main 95 components and fragment identifier in order to determine what 96 components are present and whether the reference is relative or 97 absolute. The individual components are then parsed for their 98 subparts and, if not opaque, to verify their validity. 99 100 Although the BNF defines what is allowed in each component, it is 101 ambiguous in terms of differentiating between an authority component 102 and a path component that begins with two slash characters. The 103 greedy algorithm is used for disambiguation: the left-most matching 104 rule soaks up as much of the URI reference string as it is capable of 105 matching. In other words, the authority component wins." 106 107 The "four main components" of a hierarchical URI consist of 108 <scheme>://<authority><path>?<query> 109 110 */ 111 112 /** Log tag. */ 113 private static final String LOG = Uri.class.getSimpleName(); 114 115 /** 116 * NOTE: EMPTY accesses this field during its own initialization, so this 117 * field *must* be initialized first, or else EMPTY will see a null value! 118 * 119 * Placeholder for strings which haven't been cached. This enables us 120 * to cache null. We intentionally create a new String instance so we can 121 * compare its identity and there is no chance we will confuse it with 122 * user data. 123 */ 124 @SuppressWarnings("RedundantStringConstructorCall") 125 private static final String NOT_CACHED = new String("NOT CACHED"); 126 127 /** 128 * The empty URI, equivalent to "". 129 */ 130 public static final Uri EMPTY = new HierarchicalUri(null, Part.NULL, 131 PathPart.EMPTY, Part.NULL, Part.NULL); 132 133 /** 134 * Prevents external subclassing. 135 */ Uri()136 private Uri() {} 137 138 /** 139 * Returns true if this URI is hierarchical like "http://google.com". 140 * Absolute URIs are hierarchical if the scheme-specific part starts with 141 * a '/'. Relative URIs are always hierarchical. 142 */ isHierarchical()143 public abstract boolean isHierarchical(); 144 145 /** 146 * Returns true if this URI is opaque like "mailto:nobody@google.com". The 147 * scheme-specific part of an opaque URI cannot start with a '/'. 148 */ isOpaque()149 public boolean isOpaque() { 150 return !isHierarchical(); 151 } 152 153 /** 154 * Returns true if this URI is relative, i.e. if it doesn't contain an 155 * explicit scheme. 156 * 157 * @return true if this URI is relative, false if it's absolute 158 */ isRelative()159 public abstract boolean isRelative(); 160 161 /** 162 * Returns true if this URI is absolute, i.e. if it contains an 163 * explicit scheme. 164 * 165 * @return true if this URI is absolute, false if it's relative 166 */ isAbsolute()167 public boolean isAbsolute() { 168 return !isRelative(); 169 } 170 171 /** 172 * Gets the scheme of this URI. Example: "http" 173 * 174 * @return the scheme or null if this is a relative URI 175 */ getScheme()176 public abstract String getScheme(); 177 178 /** 179 * Gets the scheme-specific part of this URI, i.e. everything between 180 * the scheme separator ':' and the fragment separator '#'. If this is a 181 * relative URI, this method returns the entire URI. Decodes escaped octets. 182 * 183 * <p>Example: "//www.google.com/search?q=android" 184 * 185 * @return the decoded scheme-specific-part 186 */ getSchemeSpecificPart()187 public abstract String getSchemeSpecificPart(); 188 189 /** 190 * Gets the scheme-specific part of this URI, i.e. everything between 191 * the scheme separator ':' and the fragment separator '#'. If this is a 192 * relative URI, this method returns the entire URI. Leaves escaped octets 193 * intact. 194 * 195 * <p>Example: "//www.google.com/search?q=android" 196 * 197 * @return the decoded scheme-specific-part 198 */ getEncodedSchemeSpecificPart()199 public abstract String getEncodedSchemeSpecificPart(); 200 201 /** 202 * Gets the decoded authority part of this URI. For 203 * server addresses, the authority is structured as follows: 204 * {@code [ userinfo '@' ] host [ ':' port ]} 205 * 206 * <p>Examples: "google.com", "bob@google.com:80" 207 * 208 * @return the authority for this URI or null if not present 209 */ getAuthority()210 public abstract String getAuthority(); 211 212 /** 213 * Gets the encoded authority part of this URI. For 214 * server addresses, the authority is structured as follows: 215 * {@code [ userinfo '@' ] host [ ':' port ]} 216 * 217 * <p>Examples: "google.com", "bob@google.com:80" 218 * 219 * @return the authority for this URI or null if not present 220 */ getEncodedAuthority()221 public abstract String getEncodedAuthority(); 222 223 /** 224 * Gets the decoded user information from the authority. 225 * For example, if the authority is "nobody@google.com", this method will 226 * return "nobody". 227 * 228 * @return the user info for this URI or null if not present 229 */ getUserInfo()230 public abstract String getUserInfo(); 231 232 /** 233 * Gets the encoded user information from the authority. 234 * For example, if the authority is "nobody@google.com", this method will 235 * return "nobody". 236 * 237 * @return the user info for this URI or null if not present 238 */ getEncodedUserInfo()239 public abstract String getEncodedUserInfo(); 240 241 /** 242 * Gets the encoded host from the authority for this URI. For example, 243 * if the authority is "bob@google.com", this method will return 244 * "google.com". 245 * 246 * @return the host for this URI or null if not present 247 */ getHost()248 public abstract String getHost(); 249 250 /** 251 * Gets the port from the authority for this URI. For example, 252 * if the authority is "google.com:80", this method will return 80. 253 * 254 * @return the port for this URI or -1 if invalid or not present 255 */ getPort()256 public abstract int getPort(); 257 258 /** 259 * Gets the decoded path. 260 * 261 * @return the decoded path, or null if this is not a hierarchical URI 262 * (like "mailto:nobody@google.com") or the URI is invalid 263 */ getPath()264 public abstract String getPath(); 265 266 /** 267 * Gets the encoded path. 268 * 269 * @return the encoded path, or null if this is not a hierarchical URI 270 * (like "mailto:nobody@google.com") or the URI is invalid 271 */ getEncodedPath()272 public abstract String getEncodedPath(); 273 274 /** 275 * Gets the decoded query component from this URI. The query comes after 276 * the query separator ('?') and before the fragment separator ('#'). This 277 * method would return "q=android" for 278 * "http://www.google.com/search?q=android". 279 * 280 * @return the decoded query or null if there isn't one 281 */ getQuery()282 public abstract String getQuery(); 283 284 /** 285 * Gets the encoded query component from this URI. The query comes after 286 * the query separator ('?') and before the fragment separator ('#'). This 287 * method would return "q=android" for 288 * "http://www.google.com/search?q=android". 289 * 290 * @return the encoded query or null if there isn't one 291 */ getEncodedQuery()292 public abstract String getEncodedQuery(); 293 294 /** 295 * Gets the decoded fragment part of this URI, everything after the '#'. 296 * 297 * @return the decoded fragment or null if there isn't one 298 */ getFragment()299 public abstract String getFragment(); 300 301 /** 302 * Gets the encoded fragment part of this URI, everything after the '#'. 303 * 304 * @return the encoded fragment or null if there isn't one 305 */ getEncodedFragment()306 public abstract String getEncodedFragment(); 307 308 /** 309 * Gets the decoded path segments. 310 * 311 * @return decoded path segments, each without a leading or trailing '/' 312 */ getPathSegments()313 public abstract List<String> getPathSegments(); 314 315 /** 316 * Gets the decoded last segment in the path. 317 * 318 * @return the decoded last segment or null if the path is empty 319 */ getLastPathSegment()320 public abstract String getLastPathSegment(); 321 322 /** 323 * Compares this Uri to another object for equality. Returns true if the 324 * encoded string representations of this Uri and the given Uri are 325 * equal. Case counts. Paths are not normalized. If one Uri specifies a 326 * default port explicitly and the other leaves it implicit, they will not 327 * be considered equal. 328 */ equals(Object o)329 public boolean equals(Object o) { 330 if (!(o instanceof Uri)) { 331 return false; 332 } 333 334 Uri other = (Uri) o; 335 336 return toString().equals(other.toString()); 337 } 338 339 /** 340 * Hashes the encoded string represention of this Uri consistently with 341 * {@link #equals(Object)}. 342 */ hashCode()343 public int hashCode() { 344 return toString().hashCode(); 345 } 346 347 /** 348 * Compares the string representation of this Uri with that of 349 * another. 350 */ compareTo(Uri other)351 public int compareTo(Uri other) { 352 return toString().compareTo(other.toString()); 353 } 354 355 /** 356 * Returns the encoded string representation of this URI. 357 * Example: "http://google.com/" 358 */ toString()359 public abstract String toString(); 360 361 /** 362 * Return a string representation of the URI that is safe to print 363 * to logs and other places where PII should be avoided. 364 * @hide 365 */ toSafeString()366 public String toSafeString() { 367 String scheme = getScheme(); 368 String ssp = getSchemeSpecificPart(); 369 if (scheme != null) { 370 if (scheme.equalsIgnoreCase("tel") || scheme.equalsIgnoreCase("sip") 371 || scheme.equalsIgnoreCase("sms") || scheme.equalsIgnoreCase("smsto") 372 || scheme.equalsIgnoreCase("mailto")) { 373 StringBuilder builder = new StringBuilder(64); 374 builder.append(scheme); 375 builder.append(':'); 376 if (ssp != null) { 377 for (int i=0; i<ssp.length(); i++) { 378 char c = ssp.charAt(i); 379 if (c == '-' || c == '@' || c == '.') { 380 builder.append(c); 381 } else { 382 builder.append('x'); 383 } 384 } 385 } 386 return builder.toString(); 387 } 388 } 389 // Not a sensitive scheme, but let's still be conservative about 390 // the data we include -- only the ssp, not the query params or 391 // fragment, because those can often have sensitive info. 392 StringBuilder builder = new StringBuilder(64); 393 if (scheme != null) { 394 builder.append(scheme); 395 builder.append(':'); 396 } 397 if (ssp != null) { 398 builder.append(ssp); 399 } 400 return builder.toString(); 401 } 402 403 /** 404 * Constructs a new builder, copying the attributes from this Uri. 405 */ buildUpon()406 public abstract Builder buildUpon(); 407 408 /** Index of a component which was not found. */ 409 private final static int NOT_FOUND = -1; 410 411 /** Placeholder value for an index which hasn't been calculated yet. */ 412 private final static int NOT_CALCULATED = -2; 413 414 /** 415 * Error message presented when a user tries to treat an opaque URI as 416 * hierarchical. 417 */ 418 private static final String NOT_HIERARCHICAL 419 = "This isn't a hierarchical URI."; 420 421 /** Default encoding. */ 422 private static final String DEFAULT_ENCODING = "UTF-8"; 423 424 /** 425 * Creates a Uri which parses the given encoded URI string. 426 * 427 * @param uriString an RFC 2396-compliant, encoded URI 428 * @throws NullPointerException if uriString is null 429 * @return Uri for this given uri string 430 */ parse(String uriString)431 public static Uri parse(String uriString) { 432 return new StringUri(uriString); 433 } 434 435 /** 436 * Creates a Uri from a file. The URI has the form 437 * "file://<absolute path>". Encodes path characters with the exception of 438 * '/'. 439 * 440 * <p>Example: "file:///tmp/android.txt" 441 * 442 * @throws NullPointerException if file is null 443 * @return a Uri for the given file 444 */ fromFile(File file)445 public static Uri fromFile(File file) { 446 if (file == null) { 447 throw new NullPointerException("file"); 448 } 449 450 PathPart path = PathPart.fromDecoded(file.getAbsolutePath()); 451 return new HierarchicalUri( 452 "file", Part.EMPTY, path, Part.NULL, Part.NULL); 453 } 454 455 /** 456 * An implementation which wraps a String URI. This URI can be opaque or 457 * hierarchical, but we extend AbstractHierarchicalUri in case we need 458 * the hierarchical functionality. 459 */ 460 private static class StringUri extends AbstractHierarchicalUri { 461 462 /** Used in parcelling. */ 463 static final int TYPE_ID = 1; 464 465 /** URI string representation. */ 466 private final String uriString; 467 StringUri(String uriString)468 private StringUri(String uriString) { 469 if (uriString == null) { 470 throw new NullPointerException("uriString"); 471 } 472 473 this.uriString = uriString; 474 } 475 readFrom(Parcel parcel)476 static Uri readFrom(Parcel parcel) { 477 return new StringUri(parcel.readString()); 478 } 479 describeContents()480 public int describeContents() { 481 return 0; 482 } 483 writeToParcel(Parcel parcel, int flags)484 public void writeToParcel(Parcel parcel, int flags) { 485 parcel.writeInt(TYPE_ID); 486 parcel.writeString(uriString); 487 } 488 489 /** Cached scheme separator index. */ 490 private volatile int cachedSsi = NOT_CALCULATED; 491 492 /** Finds the first ':'. Returns -1 if none found. */ findSchemeSeparator()493 private int findSchemeSeparator() { 494 return cachedSsi == NOT_CALCULATED 495 ? cachedSsi = uriString.indexOf(':') 496 : cachedSsi; 497 } 498 499 /** Cached fragment separator index. */ 500 private volatile int cachedFsi = NOT_CALCULATED; 501 502 /** Finds the first '#'. Returns -1 if none found. */ findFragmentSeparator()503 private int findFragmentSeparator() { 504 return cachedFsi == NOT_CALCULATED 505 ? cachedFsi = uriString.indexOf('#', findSchemeSeparator()) 506 : cachedFsi; 507 } 508 isHierarchical()509 public boolean isHierarchical() { 510 int ssi = findSchemeSeparator(); 511 512 if (ssi == NOT_FOUND) { 513 // All relative URIs are hierarchical. 514 return true; 515 } 516 517 if (uriString.length() == ssi + 1) { 518 // No ssp. 519 return false; 520 } 521 522 // If the ssp starts with a '/', this is hierarchical. 523 return uriString.charAt(ssi + 1) == '/'; 524 } 525 isRelative()526 public boolean isRelative() { 527 // Note: We return true if the index is 0 528 return findSchemeSeparator() == NOT_FOUND; 529 } 530 531 private volatile String scheme = NOT_CACHED; 532 getScheme()533 public String getScheme() { 534 @SuppressWarnings("StringEquality") 535 boolean cached = (scheme != NOT_CACHED); 536 return cached ? scheme : (scheme = parseScheme()); 537 } 538 parseScheme()539 private String parseScheme() { 540 int ssi = findSchemeSeparator(); 541 return ssi == NOT_FOUND ? null : uriString.substring(0, ssi); 542 } 543 544 private Part ssp; 545 getSsp()546 private Part getSsp() { 547 return ssp == null ? ssp = Part.fromEncoded(parseSsp()) : ssp; 548 } 549 getEncodedSchemeSpecificPart()550 public String getEncodedSchemeSpecificPart() { 551 return getSsp().getEncoded(); 552 } 553 getSchemeSpecificPart()554 public String getSchemeSpecificPart() { 555 return getSsp().getDecoded(); 556 } 557 parseSsp()558 private String parseSsp() { 559 int ssi = findSchemeSeparator(); 560 int fsi = findFragmentSeparator(); 561 562 // Return everything between ssi and fsi. 563 return fsi == NOT_FOUND 564 ? uriString.substring(ssi + 1) 565 : uriString.substring(ssi + 1, fsi); 566 } 567 568 private Part authority; 569 getAuthorityPart()570 private Part getAuthorityPart() { 571 if (authority == null) { 572 String encodedAuthority 573 = parseAuthority(this.uriString, findSchemeSeparator()); 574 return authority = Part.fromEncoded(encodedAuthority); 575 } 576 577 return authority; 578 } 579 getEncodedAuthority()580 public String getEncodedAuthority() { 581 return getAuthorityPart().getEncoded(); 582 } 583 getAuthority()584 public String getAuthority() { 585 return getAuthorityPart().getDecoded(); 586 } 587 588 private PathPart path; 589 getPathPart()590 private PathPart getPathPart() { 591 return path == null 592 ? path = PathPart.fromEncoded(parsePath()) 593 : path; 594 } 595 getPath()596 public String getPath() { 597 return getPathPart().getDecoded(); 598 } 599 getEncodedPath()600 public String getEncodedPath() { 601 return getPathPart().getEncoded(); 602 } 603 getPathSegments()604 public List<String> getPathSegments() { 605 return getPathPart().getPathSegments(); 606 } 607 parsePath()608 private String parsePath() { 609 String uriString = this.uriString; 610 int ssi = findSchemeSeparator(); 611 612 // If the URI is absolute. 613 if (ssi > -1) { 614 // Is there anything after the ':'? 615 boolean schemeOnly = ssi + 1 == uriString.length(); 616 if (schemeOnly) { 617 // Opaque URI. 618 return null; 619 } 620 621 // A '/' after the ':' means this is hierarchical. 622 if (uriString.charAt(ssi + 1) != '/') { 623 // Opaque URI. 624 return null; 625 } 626 } else { 627 // All relative URIs are hierarchical. 628 } 629 630 return parsePath(uriString, ssi); 631 } 632 633 private Part query; 634 getQueryPart()635 private Part getQueryPart() { 636 return query == null 637 ? query = Part.fromEncoded(parseQuery()) : query; 638 } 639 getEncodedQuery()640 public String getEncodedQuery() { 641 return getQueryPart().getEncoded(); 642 } 643 parseQuery()644 private String parseQuery() { 645 // It doesn't make sense to cache this index. We only ever 646 // calculate it once. 647 int qsi = uriString.indexOf('?', findSchemeSeparator()); 648 if (qsi == NOT_FOUND) { 649 return null; 650 } 651 652 int fsi = findFragmentSeparator(); 653 654 if (fsi == NOT_FOUND) { 655 return uriString.substring(qsi + 1); 656 } 657 658 if (fsi < qsi) { 659 // Invalid. 660 return null; 661 } 662 663 return uriString.substring(qsi + 1, fsi); 664 } 665 getQuery()666 public String getQuery() { 667 return getQueryPart().getDecoded(); 668 } 669 670 private Part fragment; 671 getFragmentPart()672 private Part getFragmentPart() { 673 return fragment == null 674 ? fragment = Part.fromEncoded(parseFragment()) : fragment; 675 } 676 getEncodedFragment()677 public String getEncodedFragment() { 678 return getFragmentPart().getEncoded(); 679 } 680 parseFragment()681 private String parseFragment() { 682 int fsi = findFragmentSeparator(); 683 return fsi == NOT_FOUND ? null : uriString.substring(fsi + 1); 684 } 685 getFragment()686 public String getFragment() { 687 return getFragmentPart().getDecoded(); 688 } 689 toString()690 public String toString() { 691 return uriString; 692 } 693 694 /** 695 * Parses an authority out of the given URI string. 696 * 697 * @param uriString URI string 698 * @param ssi scheme separator index, -1 for a relative URI 699 * 700 * @return the authority or null if none is found 701 */ parseAuthority(String uriString, int ssi)702 static String parseAuthority(String uriString, int ssi) { 703 int length = uriString.length(); 704 705 // If "//" follows the scheme separator, we have an authority. 706 if (length > ssi + 2 707 && uriString.charAt(ssi + 1) == '/' 708 && uriString.charAt(ssi + 2) == '/') { 709 // We have an authority. 710 711 // Look for the start of the path, query, or fragment, or the 712 // end of the string. 713 int end = ssi + 3; 714 LOOP: while (end < length) { 715 switch (uriString.charAt(end)) { 716 case '/': // Start of path 717 case '?': // Start of query 718 case '#': // Start of fragment 719 break LOOP; 720 } 721 end++; 722 } 723 724 return uriString.substring(ssi + 3, end); 725 } else { 726 return null; 727 } 728 729 } 730 731 /** 732 * Parses a path out of this given URI string. 733 * 734 * @param uriString URI string 735 * @param ssi scheme separator index, -1 for a relative URI 736 * 737 * @return the path 738 */ parsePath(String uriString, int ssi)739 static String parsePath(String uriString, int ssi) { 740 int length = uriString.length(); 741 742 // Find start of path. 743 int pathStart; 744 if (length > ssi + 2 745 && uriString.charAt(ssi + 1) == '/' 746 && uriString.charAt(ssi + 2) == '/') { 747 // Skip over authority to path. 748 pathStart = ssi + 3; 749 LOOP: while (pathStart < length) { 750 switch (uriString.charAt(pathStart)) { 751 case '?': // Start of query 752 case '#': // Start of fragment 753 return ""; // Empty path. 754 case '/': // Start of path! 755 break LOOP; 756 } 757 pathStart++; 758 } 759 } else { 760 // Path starts immediately after scheme separator. 761 pathStart = ssi + 1; 762 } 763 764 // Find end of path. 765 int pathEnd = pathStart; 766 LOOP: while (pathEnd < length) { 767 switch (uriString.charAt(pathEnd)) { 768 case '?': // Start of query 769 case '#': // Start of fragment 770 break LOOP; 771 } 772 pathEnd++; 773 } 774 775 return uriString.substring(pathStart, pathEnd); 776 } 777 buildUpon()778 public Builder buildUpon() { 779 if (isHierarchical()) { 780 return new Builder() 781 .scheme(getScheme()) 782 .authority(getAuthorityPart()) 783 .path(getPathPart()) 784 .query(getQueryPart()) 785 .fragment(getFragmentPart()); 786 } else { 787 return new Builder() 788 .scheme(getScheme()) 789 .opaquePart(getSsp()) 790 .fragment(getFragmentPart()); 791 } 792 } 793 } 794 795 /** 796 * Creates an opaque Uri from the given components. Encodes the ssp 797 * which means this method cannot be used to create hierarchical URIs. 798 * 799 * @param scheme of the URI 800 * @param ssp scheme-specific-part, everything between the 801 * scheme separator (':') and the fragment separator ('#'), which will 802 * get encoded 803 * @param fragment fragment, everything after the '#', null if undefined, 804 * will get encoded 805 * 806 * @throws NullPointerException if scheme or ssp is null 807 * @return Uri composed of the given scheme, ssp, and fragment 808 * 809 * @see Builder if you don't want the ssp and fragment to be encoded 810 */ fromParts(String scheme, String ssp, String fragment)811 public static Uri fromParts(String scheme, String ssp, 812 String fragment) { 813 if (scheme == null) { 814 throw new NullPointerException("scheme"); 815 } 816 if (ssp == null) { 817 throw new NullPointerException("ssp"); 818 } 819 820 return new OpaqueUri(scheme, Part.fromDecoded(ssp), 821 Part.fromDecoded(fragment)); 822 } 823 824 /** 825 * Opaque URI. 826 */ 827 private static class OpaqueUri extends Uri { 828 829 /** Used in parcelling. */ 830 static final int TYPE_ID = 2; 831 832 private final String scheme; 833 private final Part ssp; 834 private final Part fragment; 835 OpaqueUri(String scheme, Part ssp, Part fragment)836 private OpaqueUri(String scheme, Part ssp, Part fragment) { 837 this.scheme = scheme; 838 this.ssp = ssp; 839 this.fragment = fragment == null ? Part.NULL : fragment; 840 } 841 readFrom(Parcel parcel)842 static Uri readFrom(Parcel parcel) { 843 return new OpaqueUri( 844 parcel.readString(), 845 Part.readFrom(parcel), 846 Part.readFrom(parcel) 847 ); 848 } 849 describeContents()850 public int describeContents() { 851 return 0; 852 } 853 writeToParcel(Parcel parcel, int flags)854 public void writeToParcel(Parcel parcel, int flags) { 855 parcel.writeInt(TYPE_ID); 856 parcel.writeString(scheme); 857 ssp.writeTo(parcel); 858 fragment.writeTo(parcel); 859 } 860 isHierarchical()861 public boolean isHierarchical() { 862 return false; 863 } 864 isRelative()865 public boolean isRelative() { 866 return scheme == null; 867 } 868 getScheme()869 public String getScheme() { 870 return this.scheme; 871 } 872 getEncodedSchemeSpecificPart()873 public String getEncodedSchemeSpecificPart() { 874 return ssp.getEncoded(); 875 } 876 getSchemeSpecificPart()877 public String getSchemeSpecificPart() { 878 return ssp.getDecoded(); 879 } 880 getAuthority()881 public String getAuthority() { 882 return null; 883 } 884 getEncodedAuthority()885 public String getEncodedAuthority() { 886 return null; 887 } 888 getPath()889 public String getPath() { 890 return null; 891 } 892 getEncodedPath()893 public String getEncodedPath() { 894 return null; 895 } 896 getQuery()897 public String getQuery() { 898 return null; 899 } 900 getEncodedQuery()901 public String getEncodedQuery() { 902 return null; 903 } 904 getFragment()905 public String getFragment() { 906 return fragment.getDecoded(); 907 } 908 getEncodedFragment()909 public String getEncodedFragment() { 910 return fragment.getEncoded(); 911 } 912 getPathSegments()913 public List<String> getPathSegments() { 914 return Collections.emptyList(); 915 } 916 getLastPathSegment()917 public String getLastPathSegment() { 918 return null; 919 } 920 getUserInfo()921 public String getUserInfo() { 922 return null; 923 } 924 getEncodedUserInfo()925 public String getEncodedUserInfo() { 926 return null; 927 } 928 getHost()929 public String getHost() { 930 return null; 931 } 932 getPort()933 public int getPort() { 934 return -1; 935 } 936 937 private volatile String cachedString = NOT_CACHED; 938 toString()939 public String toString() { 940 @SuppressWarnings("StringEquality") 941 boolean cached = cachedString != NOT_CACHED; 942 if (cached) { 943 return cachedString; 944 } 945 946 StringBuilder sb = new StringBuilder(); 947 948 sb.append(scheme).append(':'); 949 sb.append(getEncodedSchemeSpecificPart()); 950 951 if (!fragment.isEmpty()) { 952 sb.append('#').append(fragment.getEncoded()); 953 } 954 955 return cachedString = sb.toString(); 956 } 957 buildUpon()958 public Builder buildUpon() { 959 return new Builder() 960 .scheme(this.scheme) 961 .opaquePart(this.ssp) 962 .fragment(this.fragment); 963 } 964 } 965 966 /** 967 * Wrapper for path segment array. 968 */ 969 static class PathSegments extends AbstractList<String> 970 implements RandomAccess { 971 972 static final PathSegments EMPTY = new PathSegments(null, 0); 973 974 final String[] segments; 975 final int size; 976 PathSegments(String[] segments, int size)977 PathSegments(String[] segments, int size) { 978 this.segments = segments; 979 this.size = size; 980 } 981 get(int index)982 public String get(int index) { 983 if (index >= size) { 984 throw new IndexOutOfBoundsException(); 985 } 986 987 return segments[index]; 988 } 989 size()990 public int size() { 991 return this.size; 992 } 993 } 994 995 /** 996 * Builds PathSegments. 997 */ 998 static class PathSegmentsBuilder { 999 1000 String[] segments; 1001 int size = 0; 1002 add(String segment)1003 void add(String segment) { 1004 if (segments == null) { 1005 segments = new String[4]; 1006 } else if (size + 1 == segments.length) { 1007 String[] expanded = new String[segments.length * 2]; 1008 System.arraycopy(segments, 0, expanded, 0, segments.length); 1009 segments = expanded; 1010 } 1011 1012 segments[size++] = segment; 1013 } 1014 build()1015 PathSegments build() { 1016 if (segments == null) { 1017 return PathSegments.EMPTY; 1018 } 1019 1020 try { 1021 return new PathSegments(segments, size); 1022 } finally { 1023 // Makes sure this doesn't get reused. 1024 segments = null; 1025 } 1026 } 1027 } 1028 1029 /** 1030 * Support for hierarchical URIs. 1031 */ 1032 private abstract static class AbstractHierarchicalUri extends Uri { 1033 getLastPathSegment()1034 public String getLastPathSegment() { 1035 // TODO: If we haven't parsed all of the segments already, just 1036 // grab the last one directly so we only allocate one string. 1037 1038 List<String> segments = getPathSegments(); 1039 int size = segments.size(); 1040 if (size == 0) { 1041 return null; 1042 } 1043 return segments.get(size - 1); 1044 } 1045 1046 private Part userInfo; 1047 getUserInfoPart()1048 private Part getUserInfoPart() { 1049 return userInfo == null 1050 ? userInfo = Part.fromEncoded(parseUserInfo()) : userInfo; 1051 } 1052 getEncodedUserInfo()1053 public final String getEncodedUserInfo() { 1054 return getUserInfoPart().getEncoded(); 1055 } 1056 parseUserInfo()1057 private String parseUserInfo() { 1058 String authority = getEncodedAuthority(); 1059 if (authority == null) { 1060 return null; 1061 } 1062 1063 int end = authority.indexOf('@'); 1064 return end == NOT_FOUND ? null : authority.substring(0, end); 1065 } 1066 getUserInfo()1067 public String getUserInfo() { 1068 return getUserInfoPart().getDecoded(); 1069 } 1070 1071 private volatile String host = NOT_CACHED; 1072 getHost()1073 public String getHost() { 1074 @SuppressWarnings("StringEquality") 1075 boolean cached = (host != NOT_CACHED); 1076 return cached ? host 1077 : (host = parseHost()); 1078 } 1079 parseHost()1080 private String parseHost() { 1081 String authority = getEncodedAuthority(); 1082 if (authority == null) { 1083 return null; 1084 } 1085 1086 // Parse out user info and then port. 1087 int userInfoSeparator = authority.indexOf('@'); 1088 int portSeparator = authority.indexOf(':', userInfoSeparator); 1089 1090 String encodedHost = portSeparator == NOT_FOUND 1091 ? authority.substring(userInfoSeparator + 1) 1092 : authority.substring(userInfoSeparator + 1, portSeparator); 1093 1094 return decode(encodedHost); 1095 } 1096 1097 private volatile int port = NOT_CALCULATED; 1098 getPort()1099 public int getPort() { 1100 return port == NOT_CALCULATED 1101 ? port = parsePort() 1102 : port; 1103 } 1104 parsePort()1105 private int parsePort() { 1106 String authority = getEncodedAuthority(); 1107 if (authority == null) { 1108 return -1; 1109 } 1110 1111 // Make sure we look for the port separtor *after* the user info 1112 // separator. We have URLs with a ':' in the user info. 1113 int userInfoSeparator = authority.indexOf('@'); 1114 int portSeparator = authority.indexOf(':', userInfoSeparator); 1115 1116 if (portSeparator == NOT_FOUND) { 1117 return -1; 1118 } 1119 1120 String portString = decode(authority.substring(portSeparator + 1)); 1121 try { 1122 return Integer.parseInt(portString); 1123 } catch (NumberFormatException e) { 1124 Log.w(LOG, "Error parsing port string.", e); 1125 return -1; 1126 } 1127 } 1128 } 1129 1130 /** 1131 * Hierarchical Uri. 1132 */ 1133 private static class HierarchicalUri extends AbstractHierarchicalUri { 1134 1135 /** Used in parcelling. */ 1136 static final int TYPE_ID = 3; 1137 1138 private final String scheme; // can be null 1139 private final Part authority; 1140 private final PathPart path; 1141 private final Part query; 1142 private final Part fragment; 1143 HierarchicalUri(String scheme, Part authority, PathPart path, Part query, Part fragment)1144 private HierarchicalUri(String scheme, Part authority, PathPart path, 1145 Part query, Part fragment) { 1146 this.scheme = scheme; 1147 this.authority = Part.nonNull(authority); 1148 this.path = path == null ? PathPart.NULL : path; 1149 this.query = Part.nonNull(query); 1150 this.fragment = Part.nonNull(fragment); 1151 } 1152 readFrom(Parcel parcel)1153 static Uri readFrom(Parcel parcel) { 1154 return new HierarchicalUri( 1155 parcel.readString(), 1156 Part.readFrom(parcel), 1157 PathPart.readFrom(parcel), 1158 Part.readFrom(parcel), 1159 Part.readFrom(parcel) 1160 ); 1161 } 1162 describeContents()1163 public int describeContents() { 1164 return 0; 1165 } 1166 writeToParcel(Parcel parcel, int flags)1167 public void writeToParcel(Parcel parcel, int flags) { 1168 parcel.writeInt(TYPE_ID); 1169 parcel.writeString(scheme); 1170 authority.writeTo(parcel); 1171 path.writeTo(parcel); 1172 query.writeTo(parcel); 1173 fragment.writeTo(parcel); 1174 } 1175 isHierarchical()1176 public boolean isHierarchical() { 1177 return true; 1178 } 1179 isRelative()1180 public boolean isRelative() { 1181 return scheme == null; 1182 } 1183 getScheme()1184 public String getScheme() { 1185 return scheme; 1186 } 1187 1188 private Part ssp; 1189 getSsp()1190 private Part getSsp() { 1191 return ssp == null 1192 ? ssp = Part.fromEncoded(makeSchemeSpecificPart()) : ssp; 1193 } 1194 getEncodedSchemeSpecificPart()1195 public String getEncodedSchemeSpecificPart() { 1196 return getSsp().getEncoded(); 1197 } 1198 getSchemeSpecificPart()1199 public String getSchemeSpecificPart() { 1200 return getSsp().getDecoded(); 1201 } 1202 1203 /** 1204 * Creates the encoded scheme-specific part from its sub parts. 1205 */ makeSchemeSpecificPart()1206 private String makeSchemeSpecificPart() { 1207 StringBuilder builder = new StringBuilder(); 1208 appendSspTo(builder); 1209 return builder.toString(); 1210 } 1211 appendSspTo(StringBuilder builder)1212 private void appendSspTo(StringBuilder builder) { 1213 String encodedAuthority = authority.getEncoded(); 1214 if (encodedAuthority != null) { 1215 // Even if the authority is "", we still want to append "//". 1216 builder.append("//").append(encodedAuthority); 1217 } 1218 1219 String encodedPath = path.getEncoded(); 1220 if (encodedPath != null) { 1221 builder.append(encodedPath); 1222 } 1223 1224 if (!query.isEmpty()) { 1225 builder.append('?').append(query.getEncoded()); 1226 } 1227 } 1228 getAuthority()1229 public String getAuthority() { 1230 return this.authority.getDecoded(); 1231 } 1232 getEncodedAuthority()1233 public String getEncodedAuthority() { 1234 return this.authority.getEncoded(); 1235 } 1236 getEncodedPath()1237 public String getEncodedPath() { 1238 return this.path.getEncoded(); 1239 } 1240 getPath()1241 public String getPath() { 1242 return this.path.getDecoded(); 1243 } 1244 getQuery()1245 public String getQuery() { 1246 return this.query.getDecoded(); 1247 } 1248 getEncodedQuery()1249 public String getEncodedQuery() { 1250 return this.query.getEncoded(); 1251 } 1252 getFragment()1253 public String getFragment() { 1254 return this.fragment.getDecoded(); 1255 } 1256 getEncodedFragment()1257 public String getEncodedFragment() { 1258 return this.fragment.getEncoded(); 1259 } 1260 getPathSegments()1261 public List<String> getPathSegments() { 1262 return this.path.getPathSegments(); 1263 } 1264 1265 private volatile String uriString = NOT_CACHED; 1266 1267 @Override toString()1268 public String toString() { 1269 @SuppressWarnings("StringEquality") 1270 boolean cached = (uriString != NOT_CACHED); 1271 return cached ? uriString 1272 : (uriString = makeUriString()); 1273 } 1274 makeUriString()1275 private String makeUriString() { 1276 StringBuilder builder = new StringBuilder(); 1277 1278 if (scheme != null) { 1279 builder.append(scheme).append(':'); 1280 } 1281 1282 appendSspTo(builder); 1283 1284 if (!fragment.isEmpty()) { 1285 builder.append('#').append(fragment.getEncoded()); 1286 } 1287 1288 return builder.toString(); 1289 } 1290 buildUpon()1291 public Builder buildUpon() { 1292 return new Builder() 1293 .scheme(scheme) 1294 .authority(authority) 1295 .path(path) 1296 .query(query) 1297 .fragment(fragment); 1298 } 1299 } 1300 1301 /** 1302 * Helper class for building or manipulating URI references. Not safe for 1303 * concurrent use. 1304 * 1305 * <p>An absolute hierarchical URI reference follows the pattern: 1306 * {@code <scheme>://<authority><absolute path>?<query>#<fragment>} 1307 * 1308 * <p>Relative URI references (which are always hierarchical) follow one 1309 * of two patterns: {@code <relative or absolute path>?<query>#<fragment>} 1310 * or {@code //<authority><absolute path>?<query>#<fragment>} 1311 * 1312 * <p>An opaque URI follows this pattern: 1313 * {@code <scheme>:<opaque part>#<fragment>} 1314 * 1315 * <p>Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI. 1316 */ 1317 public static final class Builder { 1318 1319 private String scheme; 1320 private Part opaquePart; 1321 private Part authority; 1322 private PathPart path; 1323 private Part query; 1324 private Part fragment; 1325 1326 /** 1327 * Constructs a new Builder. 1328 */ Builder()1329 public Builder() {} 1330 1331 /** 1332 * Sets the scheme. 1333 * 1334 * @param scheme name or {@code null} if this is a relative Uri 1335 */ scheme(String scheme)1336 public Builder scheme(String scheme) { 1337 this.scheme = scheme; 1338 return this; 1339 } 1340 opaquePart(Part opaquePart)1341 Builder opaquePart(Part opaquePart) { 1342 this.opaquePart = opaquePart; 1343 return this; 1344 } 1345 1346 /** 1347 * Encodes and sets the given opaque scheme-specific-part. 1348 * 1349 * @param opaquePart decoded opaque part 1350 */ opaquePart(String opaquePart)1351 public Builder opaquePart(String opaquePart) { 1352 return opaquePart(Part.fromDecoded(opaquePart)); 1353 } 1354 1355 /** 1356 * Sets the previously encoded opaque scheme-specific-part. 1357 * 1358 * @param opaquePart encoded opaque part 1359 */ encodedOpaquePart(String opaquePart)1360 public Builder encodedOpaquePart(String opaquePart) { 1361 return opaquePart(Part.fromEncoded(opaquePart)); 1362 } 1363 authority(Part authority)1364 Builder authority(Part authority) { 1365 // This URI will be hierarchical. 1366 this.opaquePart = null; 1367 1368 this.authority = authority; 1369 return this; 1370 } 1371 1372 /** 1373 * Encodes and sets the authority. 1374 */ authority(String authority)1375 public Builder authority(String authority) { 1376 return authority(Part.fromDecoded(authority)); 1377 } 1378 1379 /** 1380 * Sets the previously encoded authority. 1381 */ encodedAuthority(String authority)1382 public Builder encodedAuthority(String authority) { 1383 return authority(Part.fromEncoded(authority)); 1384 } 1385 path(PathPart path)1386 Builder path(PathPart path) { 1387 // This URI will be hierarchical. 1388 this.opaquePart = null; 1389 1390 this.path = path; 1391 return this; 1392 } 1393 1394 /** 1395 * Sets the path. Leaves '/' characters intact but encodes others as 1396 * necessary. 1397 * 1398 * <p>If the path is not null and doesn't start with a '/', and if 1399 * you specify a scheme and/or authority, the builder will prepend the 1400 * given path with a '/'. 1401 */ path(String path)1402 public Builder path(String path) { 1403 return path(PathPart.fromDecoded(path)); 1404 } 1405 1406 /** 1407 * Sets the previously encoded path. 1408 * 1409 * <p>If the path is not null and doesn't start with a '/', and if 1410 * you specify a scheme and/or authority, the builder will prepend the 1411 * given path with a '/'. 1412 */ encodedPath(String path)1413 public Builder encodedPath(String path) { 1414 return path(PathPart.fromEncoded(path)); 1415 } 1416 1417 /** 1418 * Encodes the given segment and appends it to the path. 1419 */ appendPath(String newSegment)1420 public Builder appendPath(String newSegment) { 1421 return path(PathPart.appendDecodedSegment(path, newSegment)); 1422 } 1423 1424 /** 1425 * Appends the given segment to the path. 1426 */ appendEncodedPath(String newSegment)1427 public Builder appendEncodedPath(String newSegment) { 1428 return path(PathPart.appendEncodedSegment(path, newSegment)); 1429 } 1430 query(Part query)1431 Builder query(Part query) { 1432 // This URI will be hierarchical. 1433 this.opaquePart = null; 1434 1435 this.query = query; 1436 return this; 1437 } 1438 1439 /** 1440 * Encodes and sets the query. 1441 */ query(String query)1442 public Builder query(String query) { 1443 return query(Part.fromDecoded(query)); 1444 } 1445 1446 /** 1447 * Sets the previously encoded query. 1448 */ encodedQuery(String query)1449 public Builder encodedQuery(String query) { 1450 return query(Part.fromEncoded(query)); 1451 } 1452 fragment(Part fragment)1453 Builder fragment(Part fragment) { 1454 this.fragment = fragment; 1455 return this; 1456 } 1457 1458 /** 1459 * Encodes and sets the fragment. 1460 */ fragment(String fragment)1461 public Builder fragment(String fragment) { 1462 return fragment(Part.fromDecoded(fragment)); 1463 } 1464 1465 /** 1466 * Sets the previously encoded fragment. 1467 */ encodedFragment(String fragment)1468 public Builder encodedFragment(String fragment) { 1469 return fragment(Part.fromEncoded(fragment)); 1470 } 1471 1472 /** 1473 * Encodes the key and value and then appends the parameter to the 1474 * query string. 1475 * 1476 * @param key which will be encoded 1477 * @param value which will be encoded 1478 */ appendQueryParameter(String key, String value)1479 public Builder appendQueryParameter(String key, String value) { 1480 // This URI will be hierarchical. 1481 this.opaquePart = null; 1482 1483 String encodedParameter = encode(key, null) + "=" 1484 + encode(value, null); 1485 1486 if (query == null) { 1487 query = Part.fromEncoded(encodedParameter); 1488 return this; 1489 } 1490 1491 String oldQuery = query.getEncoded(); 1492 if (oldQuery == null || oldQuery.length() == 0) { 1493 query = Part.fromEncoded(encodedParameter); 1494 } else { 1495 query = Part.fromEncoded(oldQuery + "&" + encodedParameter); 1496 } 1497 1498 return this; 1499 } 1500 1501 /** 1502 * Clears the the previously set query. 1503 */ clearQuery()1504 public Builder clearQuery() { 1505 return query((Part) null); 1506 } 1507 1508 /** 1509 * Constructs a Uri with the current attributes. 1510 * 1511 * @throws UnsupportedOperationException if the URI is opaque and the 1512 * scheme is null 1513 */ build()1514 public Uri build() { 1515 if (opaquePart != null) { 1516 if (this.scheme == null) { 1517 throw new UnsupportedOperationException( 1518 "An opaque URI must have a scheme."); 1519 } 1520 1521 return new OpaqueUri(scheme, opaquePart, fragment); 1522 } else { 1523 // Hierarchical URIs should not return null for getPath(). 1524 PathPart path = this.path; 1525 if (path == null || path == PathPart.NULL) { 1526 path = PathPart.EMPTY; 1527 } else { 1528 // If we have a scheme and/or authority, the path must 1529 // be absolute. Prepend it with a '/' if necessary. 1530 if (hasSchemeOrAuthority()) { 1531 path = PathPart.makeAbsolute(path); 1532 } 1533 } 1534 1535 return new HierarchicalUri( 1536 scheme, authority, path, query, fragment); 1537 } 1538 } 1539 hasSchemeOrAuthority()1540 private boolean hasSchemeOrAuthority() { 1541 return scheme != null 1542 || (authority != null && authority != Part.NULL); 1543 1544 } 1545 1546 @Override toString()1547 public String toString() { 1548 return build().toString(); 1549 } 1550 } 1551 1552 /** 1553 * Returns a set of the unique names of all query parameters. Iterating 1554 * over the set will return the names in order of their first occurrence. 1555 * 1556 * @throws UnsupportedOperationException if this isn't a hierarchical URI 1557 * 1558 * @return a set of decoded names 1559 */ getQueryParameterNames()1560 public Set<String> getQueryParameterNames() { 1561 if (isOpaque()) { 1562 throw new UnsupportedOperationException(NOT_HIERARCHICAL); 1563 } 1564 1565 String query = getEncodedQuery(); 1566 if (query == null) { 1567 return Collections.emptySet(); 1568 } 1569 1570 Set<String> names = new LinkedHashSet<String>(); 1571 int start = 0; 1572 do { 1573 int next = query.indexOf('&', start); 1574 int end = (next == -1) ? query.length() : next; 1575 1576 int separator = query.indexOf('=', start); 1577 if (separator > end || separator == -1) { 1578 separator = end; 1579 } 1580 1581 String name = query.substring(start, separator); 1582 names.add(decode(name)); 1583 1584 // Move start to end of name. 1585 start = end + 1; 1586 } while (start < query.length()); 1587 1588 return Collections.unmodifiableSet(names); 1589 } 1590 1591 /** 1592 * Searches the query string for parameter values with the given key. 1593 * 1594 * @param key which will be encoded 1595 * 1596 * @throws UnsupportedOperationException if this isn't a hierarchical URI 1597 * @throws NullPointerException if key is null 1598 * @return a list of decoded values 1599 */ getQueryParameters(String key)1600 public List<String> getQueryParameters(String key) { 1601 if (isOpaque()) { 1602 throw new UnsupportedOperationException(NOT_HIERARCHICAL); 1603 } 1604 if (key == null) { 1605 throw new NullPointerException("key"); 1606 } 1607 1608 String query = getEncodedQuery(); 1609 if (query == null) { 1610 return Collections.emptyList(); 1611 } 1612 1613 String encodedKey; 1614 try { 1615 encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING); 1616 } catch (UnsupportedEncodingException e) { 1617 throw new AssertionError(e); 1618 } 1619 1620 ArrayList<String> values = new ArrayList<String>(); 1621 1622 int start = 0; 1623 do { 1624 int nextAmpersand = query.indexOf('&', start); 1625 int end = nextAmpersand != -1 ? nextAmpersand : query.length(); 1626 1627 int separator = query.indexOf('=', start); 1628 if (separator > end || separator == -1) { 1629 separator = end; 1630 } 1631 1632 if (separator - start == encodedKey.length() 1633 && query.regionMatches(start, encodedKey, 0, encodedKey.length())) { 1634 if (separator == end) { 1635 values.add(""); 1636 } else { 1637 values.add(decode(query.substring(separator + 1, end))); 1638 } 1639 } 1640 1641 // Move start to end of name. 1642 if (nextAmpersand != -1) { 1643 start = nextAmpersand + 1; 1644 } else { 1645 break; 1646 } 1647 } while (true); 1648 1649 return Collections.unmodifiableList(values); 1650 } 1651 1652 /** 1653 * Searches the query string for the first value with the given key. 1654 * 1655 * <p><strong>Warning:</strong> Prior to Ice Cream Sandwich, this decoded 1656 * the '+' character as '+' rather than ' '. 1657 * 1658 * @param key which will be encoded 1659 * @throws UnsupportedOperationException if this isn't a hierarchical URI 1660 * @throws NullPointerException if key is null 1661 * @return the decoded value or null if no parameter is found 1662 */ getQueryParameter(String key)1663 public String getQueryParameter(String key) { 1664 if (isOpaque()) { 1665 throw new UnsupportedOperationException(NOT_HIERARCHICAL); 1666 } 1667 if (key == null) { 1668 throw new NullPointerException("key"); 1669 } 1670 1671 final String query = getEncodedQuery(); 1672 if (query == null) { 1673 return null; 1674 } 1675 1676 final String encodedKey = encode(key, null); 1677 final int length = query.length(); 1678 int start = 0; 1679 do { 1680 int nextAmpersand = query.indexOf('&', start); 1681 int end = nextAmpersand != -1 ? nextAmpersand : length; 1682 1683 int separator = query.indexOf('=', start); 1684 if (separator > end || separator == -1) { 1685 separator = end; 1686 } 1687 1688 if (separator - start == encodedKey.length() 1689 && query.regionMatches(start, encodedKey, 0, encodedKey.length())) { 1690 if (separator == end) { 1691 return ""; 1692 } else { 1693 String encodedValue = query.substring(separator + 1, end); 1694 return UriCodec.decode(encodedValue, true, StandardCharsets.UTF_8, false); 1695 } 1696 } 1697 1698 // Move start to end of name. 1699 if (nextAmpersand != -1) { 1700 start = nextAmpersand + 1; 1701 } else { 1702 break; 1703 } 1704 } while (true); 1705 return null; 1706 } 1707 1708 /** 1709 * Searches the query string for the first value with the given key and interprets it 1710 * as a boolean value. "false" and "0" are interpreted as <code>false</code>, everything 1711 * else is interpreted as <code>true</code>. 1712 * 1713 * @param key which will be decoded 1714 * @param defaultValue the default value to return if there is no query parameter for key 1715 * @return the boolean interpretation of the query parameter key 1716 */ getBooleanQueryParameter(String key, boolean defaultValue)1717 public boolean getBooleanQueryParameter(String key, boolean defaultValue) { 1718 String flag = getQueryParameter(key); 1719 if (flag == null) { 1720 return defaultValue; 1721 } 1722 flag = flag.toLowerCase(Locale.ROOT); 1723 return (!"false".equals(flag) && !"0".equals(flag)); 1724 } 1725 1726 /** 1727 * Return an equivalent URI with a lowercase scheme component. 1728 * This aligns the Uri with Android best practices for 1729 * intent filtering. 1730 * 1731 * <p>For example, "HTTP://www.android.com" becomes 1732 * "http://www.android.com" 1733 * 1734 * <p>All URIs received from outside Android (such as user input, 1735 * or external sources like Bluetooth, NFC, or the Internet) should 1736 * be normalized before they are used to create an Intent. 1737 * 1738 * <p class="note">This method does <em>not</em> validate bad URI's, 1739 * or 'fix' poorly formatted URI's - so do not use it for input validation. 1740 * A Uri will always be returned, even if the Uri is badly formatted to 1741 * begin with and a scheme component cannot be found. 1742 * 1743 * @return normalized Uri (never null) 1744 * @see {@link android.content.Intent#setData} 1745 * @see {@link #setNormalizedData} 1746 */ normalizeScheme()1747 public Uri normalizeScheme() { 1748 String scheme = getScheme(); 1749 if (scheme == null) return this; // give up 1750 String lowerScheme = scheme.toLowerCase(Locale.ROOT); 1751 if (scheme.equals(lowerScheme)) return this; // no change 1752 1753 return buildUpon().scheme(lowerScheme).build(); 1754 } 1755 1756 /** Identifies a null parcelled Uri. */ 1757 private static final int NULL_TYPE_ID = 0; 1758 1759 /** 1760 * Reads Uris from Parcels. 1761 */ 1762 public static final Parcelable.Creator<Uri> CREATOR 1763 = new Parcelable.Creator<Uri>() { 1764 public Uri createFromParcel(Parcel in) { 1765 int type = in.readInt(); 1766 switch (type) { 1767 case NULL_TYPE_ID: return null; 1768 case StringUri.TYPE_ID: return StringUri.readFrom(in); 1769 case OpaqueUri.TYPE_ID: return OpaqueUri.readFrom(in); 1770 case HierarchicalUri.TYPE_ID: 1771 return HierarchicalUri.readFrom(in); 1772 } 1773 1774 throw new IllegalArgumentException("Unknown URI type: " + type); 1775 } 1776 1777 public Uri[] newArray(int size) { 1778 return new Uri[size]; 1779 } 1780 }; 1781 1782 /** 1783 * Writes a Uri to a Parcel. 1784 * 1785 * @param out parcel to write to 1786 * @param uri to write, can be null 1787 */ writeToParcel(Parcel out, Uri uri)1788 public static void writeToParcel(Parcel out, Uri uri) { 1789 if (uri == null) { 1790 out.writeInt(NULL_TYPE_ID); 1791 } else { 1792 uri.writeToParcel(out, 0); 1793 } 1794 } 1795 1796 private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); 1797 1798 /** 1799 * Encodes characters in the given string as '%'-escaped octets 1800 * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers 1801 * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes 1802 * all other characters. 1803 * 1804 * @param s string to encode 1805 * @return an encoded version of s suitable for use as a URI component, 1806 * or null if s is null 1807 */ encode(String s)1808 public static String encode(String s) { 1809 return encode(s, null); 1810 } 1811 1812 /** 1813 * Encodes characters in the given string as '%'-escaped octets 1814 * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers 1815 * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes 1816 * all other characters with the exception of those specified in the 1817 * allow argument. 1818 * 1819 * @param s string to encode 1820 * @param allow set of additional characters to allow in the encoded form, 1821 * null if no characters should be skipped 1822 * @return an encoded version of s suitable for use as a URI component, 1823 * or null if s is null 1824 */ encode(String s, String allow)1825 public static String encode(String s, String allow) { 1826 if (s == null) { 1827 return null; 1828 } 1829 1830 // Lazily-initialized buffers. 1831 StringBuilder encoded = null; 1832 1833 int oldLength = s.length(); 1834 1835 // This loop alternates between copying over allowed characters and 1836 // encoding in chunks. This results in fewer method calls and 1837 // allocations than encoding one character at a time. 1838 int current = 0; 1839 while (current < oldLength) { 1840 // Start in "copying" mode where we copy over allowed chars. 1841 1842 // Find the next character which needs to be encoded. 1843 int nextToEncode = current; 1844 while (nextToEncode < oldLength 1845 && isAllowed(s.charAt(nextToEncode), allow)) { 1846 nextToEncode++; 1847 } 1848 1849 // If there's nothing more to encode... 1850 if (nextToEncode == oldLength) { 1851 if (current == 0) { 1852 // We didn't need to encode anything! 1853 return s; 1854 } else { 1855 // Presumably, we've already done some encoding. 1856 encoded.append(s, current, oldLength); 1857 return encoded.toString(); 1858 } 1859 } 1860 1861 if (encoded == null) { 1862 encoded = new StringBuilder(); 1863 } 1864 1865 if (nextToEncode > current) { 1866 // Append allowed characters leading up to this point. 1867 encoded.append(s, current, nextToEncode); 1868 } else { 1869 // assert nextToEncode == current 1870 } 1871 1872 // Switch to "encoding" mode. 1873 1874 // Find the next allowed character. 1875 current = nextToEncode; 1876 int nextAllowed = current + 1; 1877 while (nextAllowed < oldLength 1878 && !isAllowed(s.charAt(nextAllowed), allow)) { 1879 nextAllowed++; 1880 } 1881 1882 // Convert the substring to bytes and encode the bytes as 1883 // '%'-escaped octets. 1884 String toEncode = s.substring(current, nextAllowed); 1885 try { 1886 byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING); 1887 int bytesLength = bytes.length; 1888 for (int i = 0; i < bytesLength; i++) { 1889 encoded.append('%'); 1890 encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]); 1891 encoded.append(HEX_DIGITS[bytes[i] & 0xf]); 1892 } 1893 } catch (UnsupportedEncodingException e) { 1894 throw new AssertionError(e); 1895 } 1896 1897 current = nextAllowed; 1898 } 1899 1900 // Encoded could still be null at this point if s is empty. 1901 return encoded == null ? s : encoded.toString(); 1902 } 1903 1904 /** 1905 * Returns true if the given character is allowed. 1906 * 1907 * @param c character to check 1908 * @param allow characters to allow 1909 * @return true if the character is allowed or false if it should be 1910 * encoded 1911 */ isAllowed(char c, String allow)1912 private static boolean isAllowed(char c, String allow) { 1913 return (c >= 'A' && c <= 'Z') 1914 || (c >= 'a' && c <= 'z') 1915 || (c >= '0' && c <= '9') 1916 || "_-!.~'()*".indexOf(c) != NOT_FOUND 1917 || (allow != null && allow.indexOf(c) != NOT_FOUND); 1918 } 1919 1920 /** 1921 * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. 1922 * Replaces invalid octets with the unicode replacement character 1923 * ("\\uFFFD"). 1924 * 1925 * @param s encoded string to decode 1926 * @return the given string with escaped octets decoded, or null if 1927 * s is null 1928 */ decode(String s)1929 public static String decode(String s) { 1930 if (s == null) { 1931 return null; 1932 } 1933 return UriCodec.decode(s, false, StandardCharsets.UTF_8, false); 1934 } 1935 1936 /** 1937 * Support for part implementations. 1938 */ 1939 static abstract class AbstractPart { 1940 1941 /** 1942 * Enum which indicates which representation of a given part we have. 1943 */ 1944 static class Representation { 1945 static final int BOTH = 0; 1946 static final int ENCODED = 1; 1947 static final int DECODED = 2; 1948 } 1949 1950 volatile String encoded; 1951 volatile String decoded; 1952 AbstractPart(String encoded, String decoded)1953 AbstractPart(String encoded, String decoded) { 1954 this.encoded = encoded; 1955 this.decoded = decoded; 1956 } 1957 getEncoded()1958 abstract String getEncoded(); 1959 getDecoded()1960 final String getDecoded() { 1961 @SuppressWarnings("StringEquality") 1962 boolean hasDecoded = decoded != NOT_CACHED; 1963 return hasDecoded ? decoded : (decoded = decode(encoded)); 1964 } 1965 writeTo(Parcel parcel)1966 final void writeTo(Parcel parcel) { 1967 @SuppressWarnings("StringEquality") 1968 boolean hasEncoded = encoded != NOT_CACHED; 1969 1970 @SuppressWarnings("StringEquality") 1971 boolean hasDecoded = decoded != NOT_CACHED; 1972 1973 if (hasEncoded && hasDecoded) { 1974 parcel.writeInt(Representation.BOTH); 1975 parcel.writeString(encoded); 1976 parcel.writeString(decoded); 1977 } else if (hasEncoded) { 1978 parcel.writeInt(Representation.ENCODED); 1979 parcel.writeString(encoded); 1980 } else if (hasDecoded) { 1981 parcel.writeInt(Representation.DECODED); 1982 parcel.writeString(decoded); 1983 } else { 1984 throw new IllegalArgumentException("Neither encoded nor decoded"); 1985 } 1986 } 1987 } 1988 1989 /** 1990 * Immutable wrapper of encoded and decoded versions of a URI part. Lazily 1991 * creates the encoded or decoded version from the other. 1992 */ 1993 static class Part extends AbstractPart { 1994 1995 /** A part with null values. */ 1996 static final Part NULL = new EmptyPart(null); 1997 1998 /** A part with empty strings for values. */ 1999 static final Part EMPTY = new EmptyPart(""); 2000 Part(String encoded, String decoded)2001 private Part(String encoded, String decoded) { 2002 super(encoded, decoded); 2003 } 2004 isEmpty()2005 boolean isEmpty() { 2006 return false; 2007 } 2008 getEncoded()2009 String getEncoded() { 2010 @SuppressWarnings("StringEquality") 2011 boolean hasEncoded = encoded != NOT_CACHED; 2012 return hasEncoded ? encoded : (encoded = encode(decoded)); 2013 } 2014 readFrom(Parcel parcel)2015 static Part readFrom(Parcel parcel) { 2016 int representation = parcel.readInt(); 2017 switch (representation) { 2018 case Representation.BOTH: 2019 return from(parcel.readString(), parcel.readString()); 2020 case Representation.ENCODED: 2021 return fromEncoded(parcel.readString()); 2022 case Representation.DECODED: 2023 return fromDecoded(parcel.readString()); 2024 default: 2025 throw new IllegalArgumentException("Unknown representation: " 2026 + representation); 2027 } 2028 } 2029 2030 /** 2031 * Returns given part or {@link #NULL} if the given part is null. 2032 */ nonNull(Part part)2033 static Part nonNull(Part part) { 2034 return part == null ? NULL : part; 2035 } 2036 2037 /** 2038 * Creates a part from the encoded string. 2039 * 2040 * @param encoded part string 2041 */ fromEncoded(String encoded)2042 static Part fromEncoded(String encoded) { 2043 return from(encoded, NOT_CACHED); 2044 } 2045 2046 /** 2047 * Creates a part from the decoded string. 2048 * 2049 * @param decoded part string 2050 */ fromDecoded(String decoded)2051 static Part fromDecoded(String decoded) { 2052 return from(NOT_CACHED, decoded); 2053 } 2054 2055 /** 2056 * Creates a part from the encoded and decoded strings. 2057 * 2058 * @param encoded part string 2059 * @param decoded part string 2060 */ from(String encoded, String decoded)2061 static Part from(String encoded, String decoded) { 2062 // We have to check both encoded and decoded in case one is 2063 // NOT_CACHED. 2064 2065 if (encoded == null) { 2066 return NULL; 2067 } 2068 if (encoded.length() == 0) { 2069 return EMPTY; 2070 } 2071 2072 if (decoded == null) { 2073 return NULL; 2074 } 2075 if (decoded .length() == 0) { 2076 return EMPTY; 2077 } 2078 2079 return new Part(encoded, decoded); 2080 } 2081 2082 private static class EmptyPart extends Part { EmptyPart(String value)2083 public EmptyPart(String value) { 2084 super(value, value); 2085 } 2086 2087 @Override isEmpty()2088 boolean isEmpty() { 2089 return true; 2090 } 2091 } 2092 } 2093 2094 /** 2095 * Immutable wrapper of encoded and decoded versions of a path part. Lazily 2096 * creates the encoded or decoded version from the other. 2097 */ 2098 static class PathPart extends AbstractPart { 2099 2100 /** A part with null values. */ 2101 static final PathPart NULL = new PathPart(null, null); 2102 2103 /** A part with empty strings for values. */ 2104 static final PathPart EMPTY = new PathPart("", ""); 2105 PathPart(String encoded, String decoded)2106 private PathPart(String encoded, String decoded) { 2107 super(encoded, decoded); 2108 } 2109 getEncoded()2110 String getEncoded() { 2111 @SuppressWarnings("StringEquality") 2112 boolean hasEncoded = encoded != NOT_CACHED; 2113 2114 // Don't encode '/'. 2115 return hasEncoded ? encoded : (encoded = encode(decoded, "/")); 2116 } 2117 2118 /** 2119 * Cached path segments. This doesn't need to be volatile--we don't 2120 * care if other threads see the result. 2121 */ 2122 private PathSegments pathSegments; 2123 2124 /** 2125 * Gets the individual path segments. Parses them if necessary. 2126 * 2127 * @return parsed path segments or null if this isn't a hierarchical 2128 * URI 2129 */ getPathSegments()2130 PathSegments getPathSegments() { 2131 if (pathSegments != null) { 2132 return pathSegments; 2133 } 2134 2135 String path = getEncoded(); 2136 if (path == null) { 2137 return pathSegments = PathSegments.EMPTY; 2138 } 2139 2140 PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder(); 2141 2142 int previous = 0; 2143 int current; 2144 while ((current = path.indexOf('/', previous)) > -1) { 2145 // This check keeps us from adding a segment if the path starts 2146 // '/' and an empty segment for "//". 2147 if (previous < current) { 2148 String decodedSegment 2149 = decode(path.substring(previous, current)); 2150 segmentBuilder.add(decodedSegment); 2151 } 2152 previous = current + 1; 2153 } 2154 2155 // Add in the final path segment. 2156 if (previous < path.length()) { 2157 segmentBuilder.add(decode(path.substring(previous))); 2158 } 2159 2160 return pathSegments = segmentBuilder.build(); 2161 } 2162 appendEncodedSegment(PathPart oldPart, String newSegment)2163 static PathPart appendEncodedSegment(PathPart oldPart, 2164 String newSegment) { 2165 // If there is no old path, should we make the new path relative 2166 // or absolute? I pick absolute. 2167 2168 if (oldPart == null) { 2169 // No old path. 2170 return fromEncoded("/" + newSegment); 2171 } 2172 2173 String oldPath = oldPart.getEncoded(); 2174 2175 if (oldPath == null) { 2176 oldPath = ""; 2177 } 2178 2179 int oldPathLength = oldPath.length(); 2180 String newPath; 2181 if (oldPathLength == 0) { 2182 // No old path. 2183 newPath = "/" + newSegment; 2184 } else if (oldPath.charAt(oldPathLength - 1) == '/') { 2185 newPath = oldPath + newSegment; 2186 } else { 2187 newPath = oldPath + "/" + newSegment; 2188 } 2189 2190 return fromEncoded(newPath); 2191 } 2192 appendDecodedSegment(PathPart oldPart, String decoded)2193 static PathPart appendDecodedSegment(PathPart oldPart, String decoded) { 2194 String encoded = encode(decoded); 2195 2196 // TODO: Should we reuse old PathSegments? Probably not. 2197 return appendEncodedSegment(oldPart, encoded); 2198 } 2199 readFrom(Parcel parcel)2200 static PathPart readFrom(Parcel parcel) { 2201 int representation = parcel.readInt(); 2202 switch (representation) { 2203 case Representation.BOTH: 2204 return from(parcel.readString(), parcel.readString()); 2205 case Representation.ENCODED: 2206 return fromEncoded(parcel.readString()); 2207 case Representation.DECODED: 2208 return fromDecoded(parcel.readString()); 2209 default: 2210 throw new IllegalArgumentException("Bad representation: " + representation); 2211 } 2212 } 2213 2214 /** 2215 * Creates a path from the encoded string. 2216 * 2217 * @param encoded part string 2218 */ fromEncoded(String encoded)2219 static PathPart fromEncoded(String encoded) { 2220 return from(encoded, NOT_CACHED); 2221 } 2222 2223 /** 2224 * Creates a path from the decoded string. 2225 * 2226 * @param decoded part string 2227 */ fromDecoded(String decoded)2228 static PathPart fromDecoded(String decoded) { 2229 return from(NOT_CACHED, decoded); 2230 } 2231 2232 /** 2233 * Creates a path from the encoded and decoded strings. 2234 * 2235 * @param encoded part string 2236 * @param decoded part string 2237 */ from(String encoded, String decoded)2238 static PathPart from(String encoded, String decoded) { 2239 if (encoded == null) { 2240 return NULL; 2241 } 2242 2243 if (encoded.length() == 0) { 2244 return EMPTY; 2245 } 2246 2247 return new PathPart(encoded, decoded); 2248 } 2249 2250 /** 2251 * Prepends path values with "/" if they're present, not empty, and 2252 * they don't already start with "/". 2253 */ makeAbsolute(PathPart oldPart)2254 static PathPart makeAbsolute(PathPart oldPart) { 2255 @SuppressWarnings("StringEquality") 2256 boolean encodedCached = oldPart.encoded != NOT_CACHED; 2257 2258 // We don't care which version we use, and we don't want to force 2259 // unneccessary encoding/decoding. 2260 String oldPath = encodedCached ? oldPart.encoded : oldPart.decoded; 2261 2262 if (oldPath == null || oldPath.length() == 0 2263 || oldPath.startsWith("/")) { 2264 return oldPart; 2265 } 2266 2267 // Prepend encoded string if present. 2268 String newEncoded = encodedCached 2269 ? "/" + oldPart.encoded : NOT_CACHED; 2270 2271 // Prepend decoded string if present. 2272 @SuppressWarnings("StringEquality") 2273 boolean decodedCached = oldPart.decoded != NOT_CACHED; 2274 String newDecoded = decodedCached 2275 ? "/" + oldPart.decoded 2276 : NOT_CACHED; 2277 2278 return new PathPart(newEncoded, newDecoded); 2279 } 2280 } 2281 2282 /** 2283 * Creates a new Uri by appending an already-encoded path segment to a 2284 * base Uri. 2285 * 2286 * @param baseUri Uri to append path segment to 2287 * @param pathSegment encoded path segment to append 2288 * @return a new Uri based on baseUri with the given segment appended to 2289 * the path 2290 * @throws NullPointerException if baseUri is null 2291 */ withAppendedPath(Uri baseUri, String pathSegment)2292 public static Uri withAppendedPath(Uri baseUri, String pathSegment) { 2293 Builder builder = baseUri.buildUpon(); 2294 builder = builder.appendEncodedPath(pathSegment); 2295 return builder.build(); 2296 } 2297 2298 /** 2299 * If this {@link Uri} is {@code file://}, then resolve and return its 2300 * canonical path. Also fixes legacy emulated storage paths so they are 2301 * usable across user boundaries. Should always be called from the app 2302 * process before sending elsewhere. 2303 * 2304 * @hide 2305 */ getCanonicalUri()2306 public Uri getCanonicalUri() { 2307 if ("file".equals(getScheme())) { 2308 final String canonicalPath; 2309 try { 2310 canonicalPath = new File(getPath()).getCanonicalPath(); 2311 } catch (IOException e) { 2312 return this; 2313 } 2314 2315 if (Environment.isExternalStorageEmulated()) { 2316 final String legacyPath = Environment.getLegacyExternalStorageDirectory() 2317 .toString(); 2318 2319 // Splice in user-specific path when legacy path is found 2320 if (canonicalPath.startsWith(legacyPath)) { 2321 return Uri.fromFile(new File( 2322 Environment.getExternalStorageDirectory().toString(), 2323 canonicalPath.substring(legacyPath.length() + 1))); 2324 } 2325 } 2326 2327 return Uri.fromFile(new File(canonicalPath)); 2328 } else { 2329 return this; 2330 } 2331 } 2332 2333 /** 2334 * If this is a {@code file://} Uri, it will be reported to 2335 * {@link StrictMode}. 2336 * 2337 * @hide 2338 */ checkFileUriExposed(String location)2339 public void checkFileUriExposed(String location) { 2340 if ("file".equals(getScheme())) { 2341 StrictMode.onFileUriExposed(location); 2342 } 2343 } 2344 2345 /** 2346 * Test if this is a path prefix match against the given Uri. Verifies that 2347 * scheme, authority, and atomic path segments match. 2348 * 2349 * @hide 2350 */ isPathPrefixMatch(Uri prefix)2351 public boolean isPathPrefixMatch(Uri prefix) { 2352 if (!Objects.equals(getScheme(), prefix.getScheme())) return false; 2353 if (!Objects.equals(getAuthority(), prefix.getAuthority())) return false; 2354 2355 List<String> seg = getPathSegments(); 2356 List<String> prefixSeg = prefix.getPathSegments(); 2357 2358 final int prefixSize = prefixSeg.size(); 2359 if (seg.size() < prefixSize) return false; 2360 2361 for (int i = 0; i < prefixSize; i++) { 2362 if (!Objects.equals(seg.get(i), prefixSeg.get(i))) { 2363 return false; 2364 } 2365 } 2366 2367 return true; 2368 } 2369 } 2370