diff --git a/src/api-impl/android/content/ContentResolver.java b/src/api-impl/android/content/ContentResolver.java index 8ec52425..c5595a94 100644 --- a/src/api-impl/android/content/ContentResolver.java +++ b/src/api-impl/android/content/ContentResolver.java @@ -24,7 +24,7 @@ public class ContentResolver { } public ParcelFileDescriptor openFileDescriptor(Uri uri, String mode) throws FileNotFoundException { - return ParcelFileDescriptor.open(new File(uri.uri), ParcelFileDescriptor.parseMode(mode)); + return ParcelFileDescriptor.open(new File(uri.toString()), ParcelFileDescriptor.parseMode(mode)); } public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { diff --git a/src/api-impl/android/net/Uri.java b/src/api-impl/android/net/Uri.java index bb8b34aa..d48ed4f0 100644 --- a/src/api-impl/android/net/Uri.java +++ b/src/api-impl/android/net/Uri.java @@ -1,58 +1,1752 @@ +/* + * 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.net.URI; -import java.net.URISyntaxException; - -import libcore.net.UriCodec; - 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; -public class Uri implements Parcelable { +/** + * 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);
}
/**
- * -- FROM ANDROID, Licensed under the Apache License, Version 2.0 --
- * 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 final static int NOT_FOUND = -1;
- 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);
- }
-
- /**
- * -- FROM ANDROID, Licensed under the Apache License, Version 2.0 --
* 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
@@ -65,8 +1759,6 @@ public class Uri implements Parcelable {
* @return an encoded version of s suitable for use as a URI component,
* or null if s is null
*/
- private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
- private static final String DEFAULT_ENCODING = "UTF-8";
public static String encode(String s, String allow) {
if (s == null) {
return null;
@@ -86,7 +1778,8 @@ public class Uri implements Parcelable {
// Find the next character which needs to be encoded.
int nextToEncode = current;
- while (nextToEncode < oldLength && isAllowed(s.charAt(nextToEncode), allow)) {
+ while (nextToEncode < oldLength
+ && isAllowed(s.charAt(nextToEncode), allow)) {
nextToEncode++;
}
@@ -118,7 +1811,8 @@ public class Uri implements Parcelable {
// Find the next allowed character.
current = nextToEncode;
int nextAllowed = current + 1;
- while (nextAllowed < oldLength && !isAllowed(s.charAt(nextAllowed), allow)) {
+ while (nextAllowed < oldLength
+ && !isAllowed(s.charAt(nextAllowed), allow)) {
nextAllowed++;
}
@@ -144,105 +1838,410 @@ public class Uri implements Parcelable {
return encoded == null ? s : encoded.toString();
}
- public Builder buildUpon() {
- Builder builder = new Builder();
- builder.scheme = getScheme();
- builder.authority = getAuthority();
- builder.path = getPath();
- builder.query = uri.getQuery();
- return builder;
+ /**
+ * 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);
}
- public static final class Builder {
- private String scheme;
- private String authority;
- private String path;
- private String query;
-
- public Builder appendQueryParameter(String key, String value) {
- this.query = (this.query != null ? this.query + "&" : "") + key + "=" + value;
- return this;
+ /**
+ * 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 */);
+ }
- public Builder scheme(String scheme) {
- this.scheme = scheme;
- return this;
- }
+ /**
+ * Support for part implementations.
+ */
+ static abstract class AbstractPart {
- public Builder authority(String authority) {
- this.authority = authority;
- return this;
- }
+ volatile String encoded;
+ volatile String decoded;
- public Builder encodedPath(String encodedPath) {
- this.path = decode(encodedPath);
- return this;
- }
-
- public Builder appendPath(String path) {
- this.path = (this.path != null ? this.path : "") + "/" + path;
- return this;
- }
-
- public Builder path(String path) {
- this.path = "/" + path;
- return this;
- }
-
- public Uri build() throws URISyntaxException {
- if ("content".equals(scheme)) { // hack: content providers not yet supported
- scheme = "file";
- authority = null;
- path = path.substring(path.indexOf("/"));
+ 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");
}
- Uri ret = new Uri();
- ret.uri = new URI(scheme, authority, path, query, null);
- return ret;
}
- public String toString() {
+ 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 {
- return build().toString();
- } catch (URISyntaxException e) {
- return super.toString();
+ 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;
}
}
- public String getScheme() {
- return uri.getScheme();
- }
+ /**
+ * 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;
- public String getPath() {
- return uri.getPath();
- }
+ 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.
+ *
+ *