/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.RandomAccess; import java.util.Set; import android.annotation.SystemApi; import android.os.Environment; import android.os.Parcelable; import android.util.Log; /** * Immutable URI reference. A URI reference includes a URI and a fragment, the * component of the URI following a '#'. Builds and parses URI references * which conform to * RFC 2396. * *
In the interest of performance, this class performs little to no
* validation. Behavior is undefined for invalid input. This class is very
* forgiving--in the face of invalid input, it will return garbage
* rather than throw an exception unless otherwise specified.
*/
public abstract class Uri implements Parcelable, Comparable Example: "//www.google.com/search?q=android"
*
* @return the decoded scheme-specific-part
*/
public abstract String getSchemeSpecificPart();
/**
* Gets the scheme-specific part of this URI, i.e. everything between
* the scheme separator ':' and the fragment separator '#'. If this is a
* relative URI, this method returns the entire URI. Leaves escaped octets
* intact.
*
* Example: "//www.google.com/search?q=android"
*
* @return the encoded scheme-specific-part
*/
public abstract String getEncodedSchemeSpecificPart();
/**
* Gets the decoded authority part of this URI. For
* server addresses, the authority is structured as follows:
* {@code [ userinfo '@' ] host [ ':' port ]}
*
* Examples: "google.com", "bob@google.com:80"
*
* @return the authority for this URI or null if not present
*/
public abstract String getAuthority();
/**
* Gets the encoded authority part of this URI. For
* server addresses, the authority is structured as follows:
* {@code [ userinfo '@' ] host [ ':' port ]}
*
* Examples: "google.com", "bob@google.com:80"
*
* @return the authority for this URI or null if not present
*/
public abstract String getEncodedAuthority();
/**
* Gets the decoded user information from the authority.
* For example, if the authority is "nobody@google.com", this method will
* return "nobody".
*
* @return the user info for this URI or null if not present
*/
public abstract String getUserInfo();
/**
* Gets the encoded user information from the authority.
* For example, if the authority is "nobody@google.com", this method will
* return "nobody".
*
* @return the user info for this URI or null if not present
*/
public abstract String getEncodedUserInfo();
/**
* Gets the encoded host from the authority for this URI. For example,
* if the authority is "bob@google.com", this method will return
* "google.com".
*
* @return the host for this URI or null if not present
*/
public abstract String getHost();
/**
* Gets the port from the authority for this URI. For example,
* if the authority is "google.com:80", this method will return 80.
*
* @return the port for this URI or -1 if invalid or not present
*/
public abstract int getPort();
/**
* Gets the decoded path.
*
* @return the decoded path, or null if this is not a hierarchical URI
* (like "mailto:nobody@google.com") or the URI is invalid
*/
public abstract String getPath();
/**
* Gets the encoded path.
*
* @return the encoded path, or null if this is not a hierarchical URI
* (like "mailto:nobody@google.com") or the URI is invalid
*/
public abstract String getEncodedPath();
/**
* Gets the decoded query component from this URI. The query comes after
* the query separator ('?') and before the fragment separator ('#'). This
* method would return "q=android" for
* "http://www.google.com/search?q=android".
*
* @return the decoded query or null if there isn't one
*/
public abstract String getQuery();
/**
* Gets the encoded query component from this URI. The query comes after
* the query separator ('?') and before the fragment separator ('#'). This
* method would return "q=android" for
* "http://www.google.com/search?q=android".
*
* @return the encoded query or null if there isn't one
*/
public abstract String getEncodedQuery();
/**
* Gets the decoded fragment part of this URI, everything after the '#'.
*
* @return the decoded fragment or null if there isn't one
*/
public abstract String getFragment();
/**
* Gets the encoded fragment part of this URI, everything after the '#'.
*
* @return the encoded fragment or null if there isn't one
*/
public abstract String getEncodedFragment();
/**
* Gets the decoded path segments.
*
* @return decoded path segments, each without a leading or trailing '/'
*/
public abstract List Example: "file:///tmp/android.txt"
*
* @throws NullPointerException if file is null
* @return a Uri for the given file
*/
public static Uri fromFile(File file) {
if (file == null) {
throw new NullPointerException("file");
}
PathPart path = PathPart.fromDecoded(file.getAbsolutePath());
return new HierarchicalUri(
"file", Part.EMPTY, path, Part.NULL, Part.NULL);
}
/**
* An implementation which wraps a String URI. This URI can be opaque or
* hierarchical, but we extend AbstractHierarchicalUri in case we need
* the hierarchical functionality.
*/
private static class StringUri extends AbstractHierarchicalUri {
/** URI string representation. */
private final String uriString;
private StringUri(String uriString) {
if (uriString == null) {
throw new NullPointerException("uriString");
}
this.uriString = uriString;
}
/** Cached scheme separator index. */
private volatile int cachedSsi = NOT_CALCULATED;
/** Finds the first ':'. Returns -1 if none found. */
private int findSchemeSeparator() {
return cachedSsi == NOT_CALCULATED
? cachedSsi = uriString.indexOf(':')
: cachedSsi;
}
/** Cached fragment separator index. */
private volatile int cachedFsi = NOT_CALCULATED;
/** Finds the first '#'. Returns -1 if none found. */
private int findFragmentSeparator() {
return cachedFsi == NOT_CALCULATED
? cachedFsi = uriString.indexOf('#', findSchemeSeparator())
: cachedFsi;
}
public boolean isHierarchical() {
int ssi = findSchemeSeparator();
if (ssi == NOT_FOUND) {
// All relative URIs are hierarchical.
return true;
}
if (uriString.length() == ssi + 1) {
// No ssp.
return false;
}
// If the ssp starts with a '/', this is hierarchical.
return uriString.charAt(ssi + 1) == '/';
}
public boolean isRelative() {
// Note: We return true if the index is 0
return findSchemeSeparator() == NOT_FOUND;
}
private volatile String scheme = NotCachedHolder.NOT_CACHED;
public String getScheme() {
@SuppressWarnings("StringEquality")
boolean cached = (scheme != NotCachedHolder.NOT_CACHED);
return cached ? scheme : (scheme = parseScheme());
}
private String parseScheme() {
int ssi = findSchemeSeparator();
return ssi == NOT_FOUND ? null : uriString.substring(0, ssi);
}
private Part ssp;
private Part getSsp() {
return ssp == null ? ssp = Part.fromEncoded(parseSsp()) : ssp;
}
public String getEncodedSchemeSpecificPart() {
return getSsp().getEncoded();
}
public String getSchemeSpecificPart() {
return getSsp().getDecoded();
}
private String parseSsp() {
int ssi = findSchemeSeparator();
int fsi = findFragmentSeparator();
// Return everything between ssi and fsi.
return fsi == NOT_FOUND
? uriString.substring(ssi + 1)
: uriString.substring(ssi + 1, fsi);
}
private Part authority;
private Part getAuthorityPart() {
if (authority == null) {
String encodedAuthority
= parseAuthority(this.uriString, findSchemeSeparator());
return authority = Part.fromEncoded(encodedAuthority);
}
return authority;
}
public String getEncodedAuthority() {
return getAuthorityPart().getEncoded();
}
public String getAuthority() {
return getAuthorityPart().getDecoded();
}
private PathPart path;
private PathPart getPathPart() {
return path == null
? path = PathPart.fromEncoded(parsePath())
: path;
}
public String getPath() {
return getPathPart().getDecoded();
}
public String getEncodedPath() {
return getPathPart().getEncoded();
}
public List An absolute hierarchical URI reference follows the pattern:
* {@code Relative URI references (which are always hierarchical) follow one
* of two patterns: {@code An opaque URI follows this pattern:
* {@code Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI.
*/
public static final class Builder {
private String scheme;
private Part opaquePart;
private Part authority;
private PathPart path;
private Part query;
private Part fragment;
/**
* Constructs a new Builder.
*/
public Builder() {}
/**
* Sets the scheme.
*
* @param scheme name or {@code null} if this is a relative Uri
*/
public Builder scheme(String scheme) {
this.scheme = scheme;
return this;
}
Builder opaquePart(Part opaquePart) {
this.opaquePart = opaquePart;
return this;
}
/**
* Encodes and sets the given opaque scheme-specific-part.
*
* @param opaquePart decoded opaque part
*/
public Builder opaquePart(String opaquePart) {
return opaquePart(Part.fromDecoded(opaquePart));
}
/**
* Sets the previously encoded opaque scheme-specific-part.
*
* @param opaquePart encoded opaque part
*/
public Builder encodedOpaquePart(String opaquePart) {
return opaquePart(Part.fromEncoded(opaquePart));
}
Builder authority(Part authority) {
// This URI will be hierarchical.
this.opaquePart = null;
this.authority = authority;
return this;
}
/**
* Encodes and sets the authority.
*/
public Builder authority(String authority) {
return authority(Part.fromDecoded(authority));
}
/**
* Sets the previously encoded authority.
*/
public Builder encodedAuthority(String authority) {
return authority(Part.fromEncoded(authority));
}
Builder path(PathPart path) {
// This URI will be hierarchical.
this.opaquePart = null;
this.path = path;
return this;
}
/**
* Sets the path. Leaves '/' characters intact but encodes others as
* necessary.
*
* If the path is not null and doesn't start with a '/', and if
* you specify a scheme and/or authority, the builder will prepend the
* given path with a '/'.
*/
public Builder path(String path) {
return path(PathPart.fromDecoded(path));
}
/**
* Sets the previously encoded path.
*
* If the path is not null and doesn't start with a '/', and if
* you specify a scheme and/or authority, the builder will prepend the
* given path with a '/'.
*/
public Builder encodedPath(String path) {
return path(PathPart.fromEncoded(path));
}
/**
* Encodes the given segment and appends it to the path.
*/
public Builder appendPath(String newSegment) {
return path(PathPart.appendDecodedSegment(path, newSegment));
}
/**
* Appends the given segment to the path.
*/
public Builder appendEncodedPath(String newSegment) {
return path(PathPart.appendEncodedSegment(path, newSegment));
}
Builder query(Part query) {
// This URI will be hierarchical.
this.opaquePart = null;
this.query = query;
return this;
}
/**
* Encodes and sets the query.
*/
public Builder query(String query) {
return query(Part.fromDecoded(query));
}
/**
* Sets the previously encoded query.
*/
public Builder encodedQuery(String query) {
return query(Part.fromEncoded(query));
}
Builder fragment(Part fragment) {
this.fragment = fragment;
return this;
}
/**
* Encodes and sets the fragment.
*/
public Builder fragment(String fragment) {
return fragment(Part.fromDecoded(fragment));
}
/**
* Sets the previously encoded fragment.
*/
public Builder encodedFragment(String fragment) {
return fragment(Part.fromEncoded(fragment));
}
/**
* Encodes the key and value and then appends the parameter to the
* query string.
*
* @param key which will be encoded
* @param value which will be encoded
*/
public Builder appendQueryParameter(String key, String value) {
// This URI will be hierarchical.
this.opaquePart = null;
String encodedParameter = encode(key, null) + "="
+ encode(value, null);
if (query == null) {
query = Part.fromEncoded(encodedParameter);
return this;
}
String oldQuery = query.getEncoded();
if (oldQuery == null || oldQuery.length() == 0) {
query = Part.fromEncoded(encodedParameter);
} else {
query = Part.fromEncoded(oldQuery + "&" + encodedParameter);
}
return this;
}
/**
* Clears the the previously set query.
*/
public Builder clearQuery() {
return query((Part) null);
}
/**
* Constructs a Uri with the current attributes.
*
* @throws UnsupportedOperationException if the URI is opaque and the
* scheme is null
*/
public Uri build() {
if (opaquePart != null) {
if (this.scheme == null) {
throw new UnsupportedOperationException(
"An opaque URI must have a scheme.");
}
return new OpaqueUri(scheme, opaquePart, fragment);
} else {
// Hierarchical URIs should not return null for getPath().
PathPart path = this.path;
if (path == null || path == PathPart.NULL) {
path = PathPart.EMPTY;
} else {
// If we have a scheme and/or authority, the path must
// be absolute. Prepend it with a '/' if necessary.
if (hasSchemeOrAuthority()) {
path = PathPart.makeAbsolute(path);
}
}
return new HierarchicalUri(
scheme, authority, path, query, fragment);
}
}
private boolean hasSchemeOrAuthority() {
return scheme != null
|| (authority != null && authority != Part.NULL);
}
@Override
public String toString() {
return build().toString();
}
}
/**
* Returns a set of the unique names of all query parameters. Iterating
* over the set will return the names in order of their first occurrence.
*
* @throws UnsupportedOperationException if this isn't a hierarchical URI
*
* @return a set of decoded names
*/
public Set Warning: Prior to Jelly Bean, this decoded
* the '+' character as '+' rather than ' '.
*
* @param key which will be encoded
* @throws UnsupportedOperationException if this isn't a hierarchical URI
* @throws NullPointerException if key is null
* @return the decoded value or null if no parameter is found
*/
public String getQueryParameter(String key) {
if (isOpaque()) {
throw new UnsupportedOperationException(NOT_HIERARCHICAL);
}
if (key == null) {
throw new NullPointerException("key");
}
final String query = getEncodedQuery();
if (query == null) {
return null;
}
final String encodedKey = encode(key, null);
final int length = query.length();
int start = 0;
do {
int nextAmpersand = query.indexOf('&', start);
int end = nextAmpersand != -1 ? nextAmpersand : length;
int separator = query.indexOf('=', start);
if (separator > end || separator == -1) {
separator = end;
}
if (separator - start == encodedKey.length()
&& query.regionMatches(start, encodedKey, 0, encodedKey.length())) {
if (separator == end) {
return "";
} else {
String encodedValue = query.substring(separator + 1, end);
return UriCodec.decode(encodedValue, true, StandardCharsets.UTF_8, false);
}
}
// Move start to end of name.
if (nextAmpersand != -1) {
start = nextAmpersand + 1;
} else {
break;
}
} while (true);
return null;
}
/**
* Searches the query string for the first value with the given key and interprets it
* as a boolean value. "false" and "0" are interpreted as For example, "HTTP://www.android.com" becomes
* "http://www.android.com"
*
* All URIs received from outside Android (such as user input,
* or external sources like Bluetooth, NFC, or the Internet) should
* be normalized before they are used to create an Intent.
*
* This method does not validate bad URIs,
* or 'fix' poorly formatted URIs - so do not use it for input validation.
* A Uri will always be returned, even if the Uri is badly formatted to
* begin with and a scheme component cannot be found.
*
* @return normalized Uri (never null)
* @see android.content.Intent#setData
* @see android.content.Intent#setDataAndNormalize
*/
public Uri normalizeScheme() {
String scheme = getScheme();
if (scheme == null) return this; // give up
String lowerScheme = scheme.toLowerCase(Locale.ROOT);
if (scheme.equals(lowerScheme)) return this; // no change
return buildUpon().scheme(lowerScheme).build();
}
private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
/**
* Encodes characters in the given string as '%'-escaped octets
* using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers
* ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes
* all other characters.
*
* @param s string to encode
* @return an encoded version of s suitable for use as a URI component,
* or null if s is null
*/
public static String encode(String s) {
return encode(s, null);
}
/**
* Encodes characters in the given string as '%'-escaped octets
* using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers
* ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes
* all other characters with the exception of those specified in the
* allow argument.
*
* @param s string to encode
* @param allow set of additional characters to allow in the encoded form,
* null if no characters should be skipped
* @return an encoded version of s suitable for use as a URI component,
* or null if s is null
*/
public static String encode(String s, String allow) {
if (s == null) {
return null;
}
// Lazily-initialized buffers.
StringBuilder encoded = null;
int oldLength = s.length();
// This loop alternates between copying over allowed characters and
// encoding in chunks. This results in fewer method calls and
// allocations than encoding one character at a time.
int current = 0;
while (current < oldLength) {
// Start in "copying" mode where we copy over allowed chars.
// Find the next character which needs to be encoded.
int nextToEncode = current;
while (nextToEncode < oldLength
&& isAllowed(s.charAt(nextToEncode), allow)) {
nextToEncode++;
}
// If there's nothing more to encode...
if (nextToEncode == oldLength) {
if (current == 0) {
// We didn't need to encode anything!
return s;
} else {
// Presumably, we've already done some encoding.
encoded.append(s, current, oldLength);
return encoded.toString();
}
}
if (encoded == null) {
encoded = new StringBuilder();
}
if (nextToEncode > current) {
// Append allowed characters leading up to this point.
encoded.append(s, current, nextToEncode);
} else {
// assert nextToEncode == current
}
// Switch to "encoding" mode.
// Find the next allowed character.
current = nextToEncode;
int nextAllowed = current + 1;
while (nextAllowed < oldLength
&& !isAllowed(s.charAt(nextAllowed), allow)) {
nextAllowed++;
}
// Convert the substring to bytes and encode the bytes as
// '%'-escaped octets.
String toEncode = s.substring(current, nextAllowed);
try {
byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING);
int bytesLength = bytes.length;
for (int i = 0; i < bytesLength; i++) {
encoded.append('%');
encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]);
encoded.append(HEX_DIGITS[bytes[i] & 0xf]);
}
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
current = nextAllowed;
}
// Encoded could still be null at this point if s is empty.
return encoded == null ? s : encoded.toString();
}
/**
* Returns true if the given character is allowed.
*
* @param c character to check
* @param allow characters to allow
* @return true if the character is allowed or false if it should be
* encoded
*/
private static boolean isAllowed(char c, String allow) {
return (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')
|| "_-!.~'()*".indexOf(c) != NOT_FOUND
|| (allow != null && allow.indexOf(c) != NOT_FOUND);
}
/**
* Decodes '%'-escaped octets in the given string using the UTF-8 scheme.
* Replaces invalid octets with the unicode replacement character
* ("\\uFFFD").
*
* @param s encoded string to decode
* @return the given string with escaped octets decoded, or null if
* s is null
*/
public static String decode(String s) {
if (s == null) {
return null;
}
return UriCodec.decode(
s, false /* convertPlus */, StandardCharsets.UTF_8, false /* throwOnFailure */);
}
/**
* Support for part implementations.
*/
static abstract class AbstractPart {
volatile String encoded;
volatile String decoded;
AbstractPart(String encoded, String decoded) {
if (encoded != NotCachedHolder.NOT_CACHED) {
this.encoded = encoded;
this.decoded = NotCachedHolder.NOT_CACHED;
} else if (decoded != NotCachedHolder.NOT_CACHED) {
this.encoded = NotCachedHolder.NOT_CACHED;
this.decoded = decoded;
} else {
throw new IllegalArgumentException("Neither encoded nor decoded");
}
}
abstract String getEncoded();
final String getDecoded() {
@SuppressWarnings("StringEquality")
boolean hasDecoded = decoded != NotCachedHolder.NOT_CACHED;
return hasDecoded ? decoded : (decoded = decode(encoded));
}
}
/**
* Immutable wrapper of encoded and decoded versions of a URI part. Lazily
* creates the encoded or decoded version from the other.
*/
static class Part extends AbstractPart {
/** A part with null values. */
static final Part NULL = new EmptyPart(null);
/** A part with empty strings for values. */
static final Part EMPTY = new EmptyPart("");
private Part(String encoded, String decoded) {
super(encoded, decoded);
}
boolean isEmpty() {
return false;
}
String getEncoded() {
@SuppressWarnings("StringEquality")
boolean hasEncoded = encoded != NotCachedHolder.NOT_CACHED;
return hasEncoded ? encoded : (encoded = encode(decoded));
}
/**
* Returns given part or {@link #NULL} if the given part is null.
*/
static Part nonNull(Part part) {
return part == null ? NULL : part;
}
/**
* Creates a part from the encoded string.
*
* @param encoded part string
*/
static Part fromEncoded(String encoded) {
return from(encoded, NotCachedHolder.NOT_CACHED);
}
/**
* Creates a part from the decoded string.
*
* @param decoded part string
*/
static Part fromDecoded(String decoded) {
return from(NotCachedHolder.NOT_CACHED, decoded);
}
/**
* Creates a part from the encoded and decoded strings.
*
* @param encoded part string
* @param decoded part string
*/
static Part from(String encoded, String decoded) {
// We have to check both encoded and decoded in case one is
// NotCachedHolder.NOT_CACHED.
if (encoded == null) {
return NULL;
}
if (encoded.length() == 0) {
return EMPTY;
}
if (decoded == null) {
return NULL;
}
if (decoded .length() == 0) {
return EMPTY;
}
return new Part(encoded, decoded);
}
private static class EmptyPart extends Part {
public EmptyPart(String value) {
super(value, value);
if (value != null && !value.isEmpty()) {
throw new IllegalArgumentException("Expected empty value, got: " + value);
}
// Avoid having to re-calculate the non-canonical value.
encoded = decoded = value;
}
@Override
boolean isEmpty() {
return true;
}
}
}
/**
* Immutable wrapper of encoded and decoded versions of a path part. Lazily
* creates the encoded or decoded version from the other.
*/
static class PathPart extends AbstractPart {
/** A part with null values. */
static final PathPart NULL = new PathPart(null, null);
/** A part with empty strings for values. */
static final PathPart EMPTY = new PathPart("", "");
private PathPart(String encoded, String decoded) {
super(encoded, decoded);
}
String getEncoded() {
@SuppressWarnings("StringEquality")
boolean hasEncoded = encoded != NotCachedHolder.NOT_CACHED;
// Don't encode '/'.
return hasEncoded ? encoded : (encoded = encode(decoded, "/"));
}
/**
* Cached path segments. This doesn't need to be volatile--we don't
* care if other threads see the result.
*/
private PathSegments pathSegments;
/**
* Gets the individual path segments. Parses them if necessary.
*
* @return parsed path segments or null if this isn't a hierarchical
* URI
*/
PathSegments getPathSegments() {
if (pathSegments != null) {
return pathSegments;
}
String path = getEncoded();
if (path == null) {
return pathSegments = PathSegments.EMPTY;
}
PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder();
int previous = 0;
int current;
while ((current = path.indexOf('/', previous)) > -1) {
// This check keeps us from adding a segment if the path starts
// '/' and an empty segment for "//".
if (previous < current) {
String decodedSegment
= decode(path.substring(previous, current));
segmentBuilder.add(decodedSegment);
}
previous = current + 1;
}
// Add in the final path segment.
if (previous < path.length()) {
segmentBuilder.add(decode(path.substring(previous)));
}
return pathSegments = segmentBuilder.build();
}
static PathPart appendEncodedSegment(PathPart oldPart,
String newSegment) {
// If there is no old path, should we make the new path relative
// or absolute? I pick absolute.
if (oldPart == null) {
// No old path.
return fromEncoded("/" + newSegment);
}
String oldPath = oldPart.getEncoded();
if (oldPath == null) {
oldPath = "";
}
int oldPathLength = oldPath.length();
String newPath;
if (oldPathLength == 0) {
// No old path.
newPath = "/" + newSegment;
} else if (oldPath.charAt(oldPathLength - 1) == '/') {
newPath = oldPath + newSegment;
} else {
newPath = oldPath + "/" + newSegment;
}
return fromEncoded(newPath);
}
static PathPart appendDecodedSegment(PathPart oldPart, String decoded) {
String encoded = encode(decoded);
// TODO: Should we reuse old PathSegments? Probably not.
return appendEncodedSegment(oldPart, encoded);
}
/**
* Creates a path from the encoded string.
*
* @param encoded part string
*/
static PathPart fromEncoded(String encoded) {
return from(encoded, NotCachedHolder.NOT_CACHED);
}
/**
* Creates a path from the decoded string.
*
* @param decoded part string
*/
static PathPart fromDecoded(String decoded) {
return from(NotCachedHolder.NOT_CACHED, decoded);
}
/**
* Creates a path from the encoded and decoded strings.
*
* @param encoded part string
* @param decoded part string
*/
static PathPart from(String encoded, String decoded) {
if (encoded == null) {
return NULL;
}
if (encoded.length() == 0) {
return EMPTY;
}
return new PathPart(encoded, decoded);
}
/**
* Prepends path values with "/" if they're present, not empty, and
* they don't already start with "/".
*/
static PathPart makeAbsolute(PathPart oldPart) {
@SuppressWarnings("StringEquality")
boolean encodedCached = oldPart.encoded != NotCachedHolder.NOT_CACHED;
// We don't care which version we use, and we don't want to force
// unneccessary encoding/decoding.
String oldPath = encodedCached ? oldPart.encoded : oldPart.decoded;
if (oldPath == null || oldPath.length() == 0
|| oldPath.startsWith("/")) {
return oldPart;
}
// Prepend encoded string if present.
String newEncoded = encodedCached
? "/" + oldPart.encoded : NotCachedHolder.NOT_CACHED;
// Prepend decoded string if present.
@SuppressWarnings("StringEquality")
boolean decodedCached = oldPart.decoded != NotCachedHolder.NOT_CACHED;
String newDecoded = decodedCached
? "/" + oldPart.decoded
: NotCachedHolder.NOT_CACHED;
return new PathPart(newEncoded, newDecoded);
}
}
/**
* Creates a new Uri by appending an already-encoded path segment to a
* base Uri.
*
* @param baseUri Uri to append path segment to
* @param pathSegment encoded path segment to append
* @return a new Uri based on baseUri with the given segment appended to
* the path
* @throws NullPointerException if baseUri is null
*/
public static Uri withAppendedPath(Uri baseUri, String pathSegment) {
Builder builder = baseUri.buildUpon();
builder = builder.appendEncodedPath(pathSegment);
return builder.build();
}
/**
* If this {@link Uri} is {@code file://}, then resolve and return its
* canonical path. Also fixes legacy emulated storage paths so they are
* usable across user boundaries. Should always be called from the app
* process before sending elsewhere.
*
* @hide
*/
public Uri getCanonicalUri() {
if ("file".equals(getScheme())) {
final String canonicalPath;
try {
canonicalPath = new File(getPath()).getCanonicalPath();
} catch (IOException e) {
return this;
}
if (Environment.isExternalStorageEmulated()) {
final String legacyPath = Environment.getLegacyExternalStorageDirectory()
.toString();
// Splice in user-specific path when legacy path is found
if (canonicalPath.startsWith(legacyPath)) {
return Uri.fromFile(new File(
Environment.getExternalStorageDirectory().toString(),
canonicalPath.substring(legacyPath.length() + 1)));
}
}
return Uri.fromFile(new File(canonicalPath));
} else {
return this;
}
}
/**
* Test if this is a path prefix match against the given Uri. Verifies that
* scheme, authority, and atomic path segments match.
*
* @hide
*/
public boolean isPathPrefixMatch(Uri prefix) {
if (!Objects.equals(getScheme(), prefix.getScheme())) return false;
if (!Objects.equals(getAuthority(), prefix.getAuthority())) return false;
Listfalse, everything
* else is interpreted as true.
*
* @param key which will be decoded
* @param defaultValue the default value to return if there is no query parameter for key
* @return the boolean interpretation of the query parameter key
*/
public boolean getBooleanQueryParameter(String key, boolean defaultValue) {
String flag = getQueryParameter(key);
if (flag == null) {
return defaultValue;
}
flag = flag.toLowerCase(Locale.ROOT);
return (!"false".equals(flag) && !"0".equals(flag));
}
/**
* Return an equivalent URI with a lowercase scheme component.
* This aligns the Uri with Android best practices for
* intent filtering.
*
*