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