summaryrefslogtreecommitdiffstats
path: root/core/java/android/net
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/net')
-rw-r--r--core/java/android/net/ConnectivityManager.java243
-rw-r--r--core/java/android/net/Credentials.java48
-rw-r--r--core/java/android/net/DhcpInfo.aidl19
-rw-r--r--core/java/android/net/DhcpInfo.java96
-rw-r--r--core/java/android/net/IConnectivityManager.aidl47
-rw-r--r--core/java/android/net/LocalServerSocket.java117
-rw-r--r--core/java/android/net/LocalSocket.java288
-rw-r--r--core/java/android/net/LocalSocketAddress.java100
-rw-r--r--core/java/android/net/LocalSocketImpl.java490
-rw-r--r--core/java/android/net/MailTo.java172
-rw-r--r--core/java/android/net/MobileDataStateTracker.java479
-rw-r--r--core/java/android/net/NetworkConnectivityListener.java220
-rw-r--r--core/java/android/net/NetworkInfo.aidl19
-rw-r--r--core/java/android/net/NetworkInfo.java305
-rw-r--r--core/java/android/net/NetworkStateTracker.java306
-rw-r--r--core/java/android/net/NetworkUtils.java120
-rw-r--r--core/java/android/net/ParseException.java30
-rw-r--r--core/java/android/net/Proxy.java120
-rw-r--r--core/java/android/net/SSLCertificateSocketFactory.java254
-rw-r--r--core/java/android/net/SntpClient.java201
-rwxr-xr-xcore/java/android/net/Uri.aidl19
-rw-r--r--core/java/android/net/Uri.java2251
-rw-r--r--core/java/android/net/UrlQuerySanitizer.java913
-rw-r--r--core/java/android/net/WebAddress.java134
-rw-r--r--core/java/android/net/http/AndroidHttpClient.java452
-rw-r--r--core/java/android/net/http/AndroidHttpClientConnection.java464
-rw-r--r--core/java/android/net/http/CertificateChainValidator.java444
-rw-r--r--core/java/android/net/http/CertificateValidatorCache.java254
-rw-r--r--core/java/android/net/http/CharArrayBuffers.java89
-rw-r--r--core/java/android/net/http/Connection.java523
-rw-r--r--core/java/android/net/http/ConnectionThread.java137
-rw-r--r--core/java/android/net/http/DomainNameChecker.java277
-rw-r--r--core/java/android/net/http/EventHandler.java147
-rw-r--r--core/java/android/net/http/Headers.java384
-rw-r--r--core/java/android/net/http/HttpAuthHeader.java422
-rw-r--r--core/java/android/net/http/HttpConnection.java96
-rw-r--r--core/java/android/net/http/HttpLog.java44
-rw-r--r--core/java/android/net/http/HttpsConnection.java427
-rw-r--r--core/java/android/net/http/IdleCache.java175
-rw-r--r--core/java/android/net/http/LoggingEventHandler.java90
-rw-r--r--core/java/android/net/http/Request.java456
-rw-r--r--core/java/android/net/http/RequestFeeder.java42
-rw-r--r--core/java/android/net/http/RequestHandle.java402
-rw-r--r--core/java/android/net/http/RequestQueue.java647
-rw-r--r--core/java/android/net/http/SslCertificate.java251
-rw-r--r--core/java/android/net/http/SslError.java144
-rw-r--r--core/java/android/net/http/Timer.java41
-rwxr-xr-xcore/java/android/net/http/package.html2
-rwxr-xr-xcore/java/android/net/package.html5
49 files changed, 13406 insertions, 0 deletions
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
new file mode 100644
index 0000000..213813a
--- /dev/null
+++ b/core/java/android/net/ConnectivityManager.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2008 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 android.os.RemoteException;
+
+/**
+ * Class that answers queries about the state of network connectivity. It also
+ * notifies applications when network connectivity changes. Get an instance
+ * of this class by calling
+ * {@link android.content.Context#getSystemService(String) Context.getSystemService(Context.CONNECTIVITY_SERVICE)}.
+ * <p>
+ * The primary responsibilities of this class are to:
+ * <ol>
+ * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li>
+ * <li>Send broadcast intents when network connectivity changes</li>
+ * <li>Attempt to "fail over" to another network when connectivity to a network
+ * is lost</li>
+ * <li>Provide an API that allows applications to query the coarse-grained or fine-grained
+ * state of the available networks</li>
+ * </ol>
+ */
+public class ConnectivityManager
+{
+ /**
+ * A change in network connectivity has occurred. A connection has either
+ * been established or lost. The NetworkInfo for the affected network is
+ * sent as an extra; it should be consulted to see what kind of
+ * connectivity event occurred.
+ * <p/>
+ * If this is a connection that was the result of failing over from a
+ * disconnected network, then the FAILOVER_CONNECTION boolean extra is
+ * set to true.
+ * <p/>
+ * For a loss of connectivity, if the connectivity manager is attempting
+ * to connect (or has already connected) to another network, the
+ * NetworkInfo for the new network is also passed as an extra. This lets
+ * any receivers of the broadcast know that they should not necessarily
+ * tell the user that no data traffic will be possible. Instead, the
+ * reciever should expect another broadcast soon, indicating either that
+ * the failover attempt succeeded (and so there is still overall data
+ * connectivity), or that the failover attempt failed, meaning that all
+ * connectivity has been lost.
+ * <p/>
+ * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY
+ * is set to {@code true} if there are no connected networks at all.
+ */
+ public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
+ /**
+ * The lookup key for a {@link NetworkInfo} object. Retrieve with
+ * {@link android.content.Intent#getParcelableExtra(String)}.
+ */
+ public static final String EXTRA_NETWORK_INFO = "networkInfo";
+ /**
+ * The lookup key for a boolean that indicates whether a connect event
+ * is for a network to which the connectivity manager was failing over
+ * following a disconnect on another network.
+ * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+ */
+ public static final String EXTRA_IS_FAILOVER = "isFailover";
+ /**
+ * The lookup key for a {@link NetworkInfo} object. This is supplied when
+ * there is another network that it may be possible to connect to. Retrieve with
+ * {@link android.content.Intent#getParcelableExtra(String)}.
+ */
+ public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
+ /**
+ * The lookup key for a boolean that indicates whether there is a
+ * complete lack of connectivity, i.e., no network is available.
+ * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+ */
+ public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
+ /**
+ * The lookup key for a string that indicates why an attempt to connect
+ * to a network failed. The string has no particular structure. It is
+ * intended to be used in notifications presented to users. Retrieve
+ * it with {@link android.content.Intent#getStringExtra(String)}.
+ */
+ public static final String EXTRA_REASON = "reason";
+ /**
+ * The lookup key for a string that provides optionally supplied
+ * extra information about the network state. The information
+ * may be passed up from the lower networking layers, and its
+ * meaning may be specific to a particular network type. Retrieve
+ * it with {@link android.content.Intent#getStringExtra(String)}.
+ */
+ public static final String EXTRA_EXTRA_INFO = "extraInfo";
+
+ public static final int TYPE_MOBILE = 0;
+ public static final int TYPE_WIFI = 1;
+
+ public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI;
+
+ private IConnectivityManager mService;
+
+ static public boolean isNetworkTypeValid(int networkType) {
+ return networkType == TYPE_WIFI || networkType == TYPE_MOBILE;
+ }
+
+ public void setNetworkPreference(int preference) {
+ try {
+ mService.setNetworkPreference(preference);
+ } catch (RemoteException e) {
+ }
+ }
+
+ public int getNetworkPreference() {
+ try {
+ return mService.getNetworkPreference();
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ public NetworkInfo getActiveNetworkInfo() {
+ try {
+ return mService.getActiveNetworkInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ public NetworkInfo getNetworkInfo(int networkType) {
+ try {
+ return mService.getNetworkInfo(networkType);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ public NetworkInfo[] getAllNetworkInfo() {
+ try {
+ return mService.getAllNetworkInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /** {@hide} */
+ public boolean setRadios(boolean turnOn) {
+ try {
+ return mService.setRadios(turnOn);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /** {@hide} */
+ public boolean setRadio(int networkType, boolean turnOn) {
+ try {
+ return mService.setRadio(networkType, turnOn);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Tells the underlying networking system that the caller wants to
+ * begin using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param networkType specifies which network the request pertains to
+ * @param feature the name of the feature to be used
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public int startUsingNetworkFeature(int networkType, String feature) {
+ try {
+ return mService.startUsingNetworkFeature(networkType, feature);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Tells the underlying networking system that the caller is finished
+ * using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param networkType specifies which network the request pertains to
+ * @param feature the name of the feature that is no longer needed
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public int stopUsingNetworkFeature(int networkType, String feature) {
+ try {
+ return mService.stopUsingNetworkFeature(networkType, feature);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via the specified network interface. An attempt to add a route that
+ * already exists is ignored, but treated as successful.
+ * @param networkType the type of the network over which traffic to the specified
+ * host is to be routed
+ * @param hostAddress the IP address of the host to which the route is desired
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean requestRouteToHost(int networkType, int hostAddress) {
+ try {
+ return mService.requestRouteToHost(networkType, hostAddress);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Don't allow use of default constructor.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ private ConnectivityManager() {
+ }
+
+ /**
+ * {@hide}
+ */
+ public ConnectivityManager(IConnectivityManager service) {
+ if (service == null) {
+ throw new IllegalArgumentException(
+ "ConnectivityManager() cannot be constructed with null service");
+ }
+ mService = service;
+ }
+}
diff --git a/core/java/android/net/Credentials.java b/core/java/android/net/Credentials.java
new file mode 100644
index 0000000..7f6cf9d
--- /dev/null
+++ b/core/java/android/net/Credentials.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * A class for representing UNIX credentials passed via ancillary data
+ * on UNIX domain sockets. See "man 7 unix" on a desktop linux distro.
+ */
+public class Credentials {
+ /** pid of process. root peers may lie. */
+ private final int pid;
+ /** uid of process. root peers may lie. */
+ private final int uid;
+ /** gid of process. root peers may lie. */
+ private final int gid;
+
+ public Credentials (int pid, int uid, int gid) {
+ this.pid = pid;
+ this.uid = uid;
+ this.gid = gid;
+ }
+
+ public int getPid() {
+ return pid;
+ }
+
+ public int getUid() {
+ return uid;
+ }
+
+ public int getGid() {
+ return gid;
+ }
+}
diff --git a/core/java/android/net/DhcpInfo.aidl b/core/java/android/net/DhcpInfo.aidl
new file mode 100644
index 0000000..29cd21f
--- /dev/null
+++ b/core/java/android/net/DhcpInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2008, 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;
+
+parcelable DhcpInfo;
diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java
new file mode 100644
index 0000000..1178bec
--- /dev/null
+++ b/core/java/android/net/DhcpInfo.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2008 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 android.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * A simple object for retrieving the results of a DHCP request.
+ */
+public class DhcpInfo implements Parcelable {
+ public int ipAddress;
+ public int gateway;
+ public int netmask;
+
+ public int dns1;
+ public int dns2;
+
+ public int serverAddress;
+ public int leaseDuration;
+
+ public DhcpInfo() {
+ super();
+ }
+
+ public String toString() {
+ StringBuffer str = new StringBuffer();
+
+ str.append("ipaddr "); putAddress(str, ipAddress);
+ str.append(" gateway "); putAddress(str, gateway);
+ str.append(" netmask "); putAddress(str, netmask);
+ str.append(" dns1 "); putAddress(str, dns1);
+ str.append(" dns2 "); putAddress(str, dns2);
+ str.append(" DHCP server "); putAddress(str, serverAddress);
+ str.append(" lease ").append(leaseDuration).append(" seconds");
+
+ return str.toString();
+ }
+
+ private static void putAddress(StringBuffer buf, int addr) {
+ buf.append(addr & 0xff).append('.').
+ append((addr >>>= 8) & 0xff).append('.').
+ append((addr >>>= 8) & 0xff).append('.').
+ append((addr >>>= 8) & 0xff);
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(ipAddress);
+ dest.writeInt(gateway);
+ dest.writeInt(netmask);
+ dest.writeInt(dns1);
+ dest.writeInt(dns2);
+ dest.writeInt(serverAddress);
+ dest.writeInt(leaseDuration);
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public static final Creator<DhcpInfo> CREATOR =
+ new Creator<DhcpInfo>() {
+ public DhcpInfo createFromParcel(Parcel in) {
+ DhcpInfo info = new DhcpInfo();
+ info.ipAddress = in.readInt();
+ info.gateway = in.readInt();
+ info.netmask = in.readInt();
+ info.dns1 = in.readInt();
+ info.dns2 = in.readInt();
+ info.serverAddress = in.readInt();
+ info.leaseDuration = in.readInt();
+ return info;
+ }
+
+ public DhcpInfo[] newArray(int size) {
+ return new DhcpInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
new file mode 100644
index 0000000..e1d921f
--- /dev/null
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2008, 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 android.net.NetworkInfo;
+
+/**
+ * Interface that answers queries about, and allows changing, the
+ * state of network connectivity.
+ */
+/** {@hide} */
+interface IConnectivityManager
+{
+ void setNetworkPreference(int pref);
+
+ int getNetworkPreference();
+
+ NetworkInfo getActiveNetworkInfo();
+
+ NetworkInfo getNetworkInfo(int networkType);
+
+ NetworkInfo[] getAllNetworkInfo();
+
+ boolean setRadios(boolean onOff);
+
+ boolean setRadio(int networkType, boolean turnOn);
+
+ int startUsingNetworkFeature(int networkType, in String feature);
+
+ int stopUsingNetworkFeature(int networkType, in String feature);
+
+ boolean requestRouteToHost(int networkType, int hostAddress);
+}
diff --git a/core/java/android/net/LocalServerSocket.java b/core/java/android/net/LocalServerSocket.java
new file mode 100644
index 0000000..2b93fc2
--- /dev/null
+++ b/core/java/android/net/LocalServerSocket.java
@@ -0,0 +1,117 @@
+/*
+ * 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.IOException;
+import java.io.FileDescriptor;
+
+/**
+ * non-standard class for creating inbound UNIX-domain socket
+ * on the Android platform, this is created in the Linux non-filesystem
+ * namespace.
+ *
+ * On simulator platforms, this may be created in a temporary directory on
+ * the filesystem
+ */
+public class LocalServerSocket {
+ private final LocalSocketImpl impl;
+ private final LocalSocketAddress localAddress;
+
+ /** 50 seems a bit much, but it's what was here */
+ private static final int LISTEN_BACKLOG = 50;
+
+ /**
+ * Crewates a new server socket listening at specified name.
+ * On the Android platform, the name is created in the Linux
+ * abstract namespace (instead of on the filesystem).
+ *
+ * @param name address for socket
+ * @throws IOException
+ */
+ public LocalServerSocket(String name) throws IOException
+ {
+ impl = new LocalSocketImpl();
+
+ impl.create(true);
+
+ localAddress = new LocalSocketAddress(name);
+ impl.bind(localAddress);
+
+ impl.listen(LISTEN_BACKLOG);
+ }
+
+ /**
+ * Create a LocalServerSocket from a file descriptor that's already
+ * been created and bound. listen() will be called immediately on it.
+ * Used for cases where file descriptors are passed in via environment
+ * variables
+ *
+ * @param fd bound file descriptor
+ * @throws IOException
+ */
+ public LocalServerSocket(FileDescriptor fd) throws IOException
+ {
+ impl = new LocalSocketImpl(fd);
+ impl.listen(LISTEN_BACKLOG);
+ localAddress = impl.getSockAddress();
+ }
+
+ /**
+ * Obtains the socket's local address
+ *
+ * @return local address
+ */
+ public LocalSocketAddress getLocalSocketAddress()
+ {
+ return localAddress;
+ }
+
+ /**
+ * Accepts a new connection to the socket. Blocks until a new
+ * connection arrives.
+ *
+ * @return a socket representing the new connection.
+ * @throws IOException
+ */
+ public LocalSocket accept() throws IOException
+ {
+ LocalSocketImpl acceptedImpl = new LocalSocketImpl();
+
+ impl.accept (acceptedImpl);
+
+ return new LocalSocket(acceptedImpl);
+ }
+
+ /**
+ * Returns file descriptor or null if not yet open/already closed
+ *
+ * @return fd or null
+ */
+ public FileDescriptor getFileDescriptor() {
+ return impl.getFileDescriptor();
+ }
+
+ /**
+ * Closes server socket.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException
+ {
+ impl.close();
+ }
+}
diff --git a/core/java/android/net/LocalSocket.java b/core/java/android/net/LocalSocket.java
new file mode 100644
index 0000000..4039a69
--- /dev/null
+++ b/core/java/android/net/LocalSocket.java
@@ -0,0 +1,288 @@
+/*
+ * 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.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketOptions;
+
+/**
+ * Creates a (non-server) socket in the UNIX-domain namespace. The interface
+ * here is not entirely unlike that of java.net.Socket
+ */
+public class LocalSocket {
+
+ private LocalSocketImpl impl;
+ private volatile boolean implCreated;
+ private LocalSocketAddress localAddress;
+ private boolean isBound;
+ private boolean isConnected;
+
+ /**
+ * Creates a AF_LOCAL/UNIX domain stream socket.
+ */
+ public LocalSocket() {
+ this(new LocalSocketImpl());
+ isBound = false;
+ isConnected = false;
+ }
+
+ /**
+ * for use with AndroidServerSocket
+ * @param impl a SocketImpl
+ */
+ /*package*/ LocalSocket(LocalSocketImpl impl) {
+ this.impl = impl;
+ this.isConnected = false;
+ this.isBound = false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return super.toString() + " impl:" + impl;
+ }
+
+ /**
+ * It's difficult to discern from the spec when impl.create() should be
+ * called, but it seems like a reasonable rule is "as soon as possible,
+ * but not in a context where IOException cannot be thrown"
+ *
+ * @throws IOException from SocketImpl.create()
+ */
+ private void implCreateIfNeeded() throws IOException {
+ if (!implCreated) {
+ synchronized (this) {
+ if (!implCreated) {
+ implCreated = true;
+ impl.create(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Connects this socket to an endpoint. May only be called on an instance
+ * that has not yet been connected.
+ *
+ * @param endpoint endpoint address
+ * @throws IOException if socket is in invalid state or the address does
+ * not exist.
+ */
+ public void connect(LocalSocketAddress endpoint) throws IOException {
+ synchronized (this) {
+ if (isConnected) {
+ throw new IOException("already connected");
+ }
+
+ implCreateIfNeeded();
+ impl.connect(endpoint, 0);
+ isConnected = true;
+ isBound = true;
+ }
+ }
+
+ /**
+ * Binds this socket to an endpoint name. May only be called on an instance
+ * that has not yet been bound.
+ *
+ * @param bindpoint endpoint address
+ * @throws IOException
+ */
+ public void bind(LocalSocketAddress bindpoint) throws IOException {
+ implCreateIfNeeded();
+
+ synchronized (this) {
+ if (isBound) {
+ throw new IOException("already bound");
+ }
+
+ localAddress = bindpoint;
+ impl.bind(localAddress);
+ isBound = true;
+ }
+ }
+
+ /**
+ * Retrieves the name that this socket is bound to, if any.
+ *
+ * @return Local address or null if anonymous
+ */
+ public LocalSocketAddress getLocalSocketAddress() {
+ return localAddress;
+ }
+
+ /**
+ * Retrieves the input stream for this instance.
+ *
+ * @return input stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ public InputStream getInputStream() throws IOException {
+ implCreateIfNeeded();
+ return impl.getInputStream();
+ }
+
+ /**
+ * Retrieves the output stream for this instance.
+ *
+ * @return output stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ public OutputStream getOutputStream() throws IOException {
+ implCreateIfNeeded();
+ return impl.getOutputStream();
+ }
+
+ /**
+ * Closes the socket.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException {
+ implCreateIfNeeded();
+ impl.close();
+ }
+
+ /**
+ * Shuts down the input side of the socket.
+ *
+ * @throws IOException
+ */
+ public void shutdownInput() throws IOException {
+ implCreateIfNeeded();
+ impl.shutdownInput();
+ }
+
+ /**
+ * Shuts down the output side of the socket.
+ *
+ * @throws IOException
+ */
+ public void shutdownOutput() throws IOException {
+ implCreateIfNeeded();
+ impl.shutdownOutput();
+ }
+
+ public void setReceiveBufferSize(int size) throws IOException {
+ impl.setOption(SocketOptions.SO_RCVBUF, Integer.valueOf(size));
+ }
+
+ public int getReceiveBufferSize() throws IOException {
+ return ((Integer) impl.getOption(SocketOptions.SO_RCVBUF)).intValue();
+ }
+
+ public void setSoTimeout(int n) throws IOException {
+ impl.setOption(SocketOptions.SO_TIMEOUT, Integer.valueOf(n));
+ }
+
+ public int getSoTimeout() throws IOException {
+ return ((Integer) impl.getOption(SocketOptions.SO_TIMEOUT)).intValue();
+ }
+
+ public void setSendBufferSize(int n) throws IOException {
+ impl.setOption(SocketOptions.SO_SNDBUF, Integer.valueOf(n));
+ }
+
+ public int getSendBufferSize() throws IOException {
+ return ((Integer) impl.getOption(SocketOptions.SO_SNDBUF)).intValue();
+ }
+
+ //???SEC
+ public LocalSocketAddress getRemoteSocketAddress() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public synchronized boolean isConnected() {
+ return isConnected;
+ }
+
+ //???SEC
+ public boolean isClosed() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public synchronized boolean isBound() {
+ return isBound;
+ }
+
+ //???SEC
+ public boolean isOutputShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public boolean isInputShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public void connect(LocalSocketAddress endpoint, int timeout)
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Enqueues a set of file descriptors to send to the peer. The queue
+ * is one deep. The file descriptors will be sent with the next write
+ * of normal data, and will be delivered in a single ancillary message.
+ * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine.
+ *
+ * @param fds non-null; file descriptors to send.
+ */
+ public void setFileDescriptorsForSend(FileDescriptor[] fds) {
+ impl.setFileDescriptorsForSend(fds);
+ }
+
+ /**
+ * Retrieves a set of file descriptors that a peer has sent through
+ * an ancillary message. This method retrieves the most recent set sent,
+ * and then returns null until a new set arrives.
+ * File descriptors may only be passed along with regular data, so this
+ * method can only return a non-null after a read operation.
+ *
+ * @return null or file descriptor array
+ * @throws IOException
+ */
+ public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+ return impl.getAncillaryFileDescriptors();
+ }
+
+ /**
+ * Retrieves the credentials of this socket's peer. Only valid on
+ * connected sockets.
+ *
+ * @return non-null; peer credentials
+ * @throws IOException
+ */
+ public Credentials getPeerCredentials() throws IOException {
+ return impl.getPeerCredentials();
+ }
+
+ /**
+ * Returns file descriptor or null if not yet open/already closed
+ *
+ * @return fd or null
+ */
+ public FileDescriptor getFileDescriptor() {
+ return impl.getFileDescriptor();
+ }
+}
diff --git a/core/java/android/net/LocalSocketAddress.java b/core/java/android/net/LocalSocketAddress.java
new file mode 100644
index 0000000..8265b85
--- /dev/null
+++ b/core/java/android/net/LocalSocketAddress.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+/**
+ * A UNIX-domain (AF_LOCAL) socket address. For use with
+ * android.net.LocalSocket and android.net.LocalServerSocket.
+ *
+ * On the Android system, these names refer to names in the Linux
+ * abstract (non-filesystem) UNIX domain namespace.
+ */
+public class LocalSocketAddress
+{
+ /**
+ * The namespace that this address exists in. See also
+ * include/cutils/sockets.h ANDROID_SOCKET_NAMESPACE_*
+ */
+ public enum Namespace {
+ /** A socket in the Linux abstract namespace */
+ ABSTRACT(0),
+ /**
+ * A socket in the Android reserved namespace in /dev/socket.
+ * Only the init process may create a socket here.
+ */
+ RESERVED(1),
+ /**
+ * A socket named with a normal filesystem path.
+ */
+ FILESYSTEM(2);
+
+ /** The id matches with a #define in include/cutils/sockets.h */
+ private int id;
+ Namespace (int id) {
+ this.id = id;
+ }
+
+ /**
+ * @return int constant shared with native code
+ */
+ /*package*/ int getId() {
+ return id;
+ }
+ }
+
+ private final String name;
+ private final Namespace namespace;
+
+ /**
+ * Creates an instance with a given name.
+ *
+ * @param name non-null name
+ * @param namespace namespace the name should be created in.
+ */
+ public LocalSocketAddress(String name, Namespace namespace) {
+ this.name = name;
+ this.namespace = namespace;
+ }
+
+ /**
+ * Creates an instance with a given name in the {@link Namespace#ABSTRACT}
+ * namespace
+ *
+ * @param name non-null name
+ */
+ public LocalSocketAddress(String name) {
+ this(name,Namespace.ABSTRACT);
+ }
+
+ /**
+ * Retrieves the string name of this address
+ * @return string name
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns the namespace used by this address.
+ *
+ * @return non-null a namespace
+ */
+ public Namespace getNamespace() {
+ return namespace;
+ }
+}
diff --git a/core/java/android/net/LocalSocketImpl.java b/core/java/android/net/LocalSocketImpl.java
new file mode 100644
index 0000000..6c36a7d
--- /dev/null
+++ b/core/java/android/net/LocalSocketImpl.java
@@ -0,0 +1,490 @@
+/*
+ * 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.IOException;
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.io.FileDescriptor;
+import java.net.SocketOptions;
+
+/**
+ * Socket implementation used for android.net.LocalSocket and
+ * android.net.LocalServerSocket. Supports only AF_LOCAL sockets.
+ */
+class LocalSocketImpl
+{
+ private SocketInputStream fis;
+ private SocketOutputStream fos;
+ private Object readMonitor = new Object();
+ private Object writeMonitor = new Object();
+
+ /** null if closed or not yet created */
+ private FileDescriptor fd;
+
+ // These fields are accessed by native code;
+ /** file descriptor array received during a previous read */
+ FileDescriptor[] inboundFileDescriptors;
+ /** file descriptor array that should be written during next write */
+ FileDescriptor[] outboundFileDescriptors;
+
+ /**
+ * An input stream for local sockets. Needed because we may
+ * need to read ancillary data.
+ */
+ class SocketInputStream extends InputStream {
+ /** {@inheritDoc} */
+ @Override
+ public int available() throws IOException {
+ return available_native(fd);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ LocalSocketImpl.this.close();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read() throws IOException {
+ int ret;
+ synchronized (readMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+
+ ret = read_native(myFd);
+ return ret;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ synchronized (readMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+
+ if (off < 0 || len < 0 || (off + len) > b.length ) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ int ret = readba_native(b, off, len, myFd);
+
+ return ret;
+ }
+ }
+ }
+
+ /**
+ * An output stream for local sockets. Needed because we may
+ * need to read ancillary data.
+ */
+ class SocketOutputStream extends OutputStream {
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ LocalSocketImpl.this.close();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write (byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write (byte[] b, int off, int len) throws IOException {
+ synchronized (writeMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+
+ if (off < 0 || len < 0 || (off + len) > b.length ) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ writeba_native(b, off, len, myFd);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write (int b) throws IOException {
+ synchronized (writeMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+ write_native(b, myFd);
+ }
+ }
+ }
+
+ private native int available_native(FileDescriptor fd) throws IOException;
+ private native void close_native(FileDescriptor fd) throws IOException;
+ private native int read_native(FileDescriptor fd) throws IOException;
+ private native int readba_native(byte[] b, int off, int len,
+ FileDescriptor fd) throws IOException;
+ private native void writeba_native(byte[] b, int off, int len,
+ FileDescriptor fd) throws IOException;
+ private native void write_native(int b, FileDescriptor fd)
+ throws IOException;
+ private native void connectLocal(FileDescriptor fd, String name,
+ int namespace) throws IOException;
+ private native void bindLocal(FileDescriptor fd, String name, int namespace)
+ throws IOException;
+ private native FileDescriptor create_native(boolean stream)
+ throws IOException;
+ private native void listen_native(FileDescriptor fd, int backlog)
+ throws IOException;
+ private native void shutdown(FileDescriptor fd, boolean shutdownInput);
+ private native Credentials getPeerCredentials_native(
+ FileDescriptor fd) throws IOException;
+ private native int getOption_native(FileDescriptor fd, int optID)
+ throws IOException;
+ private native void setOption_native(FileDescriptor fd, int optID,
+ int b, int value) throws IOException;
+
+// private native LocalSocketAddress getSockName_native
+// (FileDescriptor fd) throws IOException;
+
+ /**
+ * Accepts a connection on a server socket.
+ *
+ * @param fd file descriptor of server socket
+ * @param s socket implementation that will become the new socket
+ * @return file descriptor of new socket
+ */
+ private native FileDescriptor accept
+ (FileDescriptor fd, LocalSocketImpl s) throws IOException;
+
+ /**
+ * Create a new instance.
+ */
+ /*package*/ LocalSocketImpl()
+ {
+ }
+
+ /**
+ * Create a new instance from a file descriptor representing
+ * a bound socket. The state of the file descriptor is not checked here
+ * but the caller can verify socket state by calling listen().
+ *
+ * @param fd non-null; bound file descriptor
+ */
+ /*package*/ LocalSocketImpl(FileDescriptor fd) throws IOException
+ {
+ this.fd = fd;
+ }
+
+ public String toString() {
+ return super.toString() + " fd:" + fd;
+ }
+
+ /**
+ * Creates a socket in the underlying OS.
+ *
+ * @param stream true if this should be a stream socket, false for
+ * datagram.
+ * @throws IOException
+ */
+ public void create (boolean stream) throws IOException {
+ // no error if socket already created
+ // need this for LocalServerSocket.accept()
+ if (fd == null) {
+ fd = create_native(stream);
+ }
+ }
+
+ /**
+ * Closes the socket.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException {
+ synchronized (LocalSocketImpl.this) {
+ if (fd == null) return;
+ close_native(fd);
+ fd = null;
+ }
+ }
+
+ /** note timeout presently ignored */
+ protected void connect(LocalSocketAddress address, int timeout)
+ throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ connectLocal(fd, address.getName(), address.getNamespace().getId());
+ }
+
+ /**
+ * Binds this socket to an endpoint name. May only be called on an instance
+ * that has not yet been bound.
+ *
+ * @param endpoint endpoint address
+ * @throws IOException
+ */
+ public void bind(LocalSocketAddress endpoint) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ bindLocal(fd, endpoint.getName(), endpoint.getNamespace().getId());
+ }
+
+ protected void listen(int backlog) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ listen_native(fd, backlog);
+ }
+
+ /**
+ * Accepts a new connection to the socket. Blocks until a new
+ * connection arrives.
+ *
+ * @param s a socket that will be used to represent the new connection.
+ * @throws IOException
+ */
+ protected void accept(LocalSocketImpl s) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ s.fd = accept(fd, s);
+ }
+
+ /**
+ * Retrieves the input stream for this instance.
+ *
+ * @return input stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ protected InputStream getInputStream() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ synchronized (this) {
+ if (fis == null) {
+ fis = new SocketInputStream();
+ }
+
+ return fis;
+ }
+ }
+
+ /**
+ * Retrieves the output stream for this instance.
+ *
+ * @return output stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ protected OutputStream getOutputStream() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ synchronized (this) {
+ if (fos == null) {
+ fos = new SocketOutputStream();
+ }
+
+ return fos;
+ }
+ }
+
+ /**
+ * Returns the number of bytes available for reading without blocking.
+ *
+ * @return >= 0 count bytes available
+ * @throws IOException
+ */
+ protected int available() throws IOException
+ {
+ return getInputStream().available();
+ }
+
+ /**
+ * Shuts down the input side of the socket.
+ *
+ * @throws IOException
+ */
+ protected void shutdownInput() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ shutdown(fd, true);
+ }
+
+ /**
+ * Shuts down the output side of the socket.
+ *
+ * @throws IOException
+ */
+ protected void shutdownOutput() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ shutdown(fd, false);
+ }
+
+ protected FileDescriptor getFileDescriptor()
+ {
+ return fd;
+ }
+
+ protected boolean supportsUrgentData()
+ {
+ return false;
+ }
+
+ protected void sendUrgentData(int data) throws IOException
+ {
+ throw new RuntimeException ("not impled");
+ }
+
+ public Object getOption(int optID) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ if (optID == SocketOptions.SO_TIMEOUT) {
+ return 0;
+ }
+
+ int value = getOption_native(fd, optID);
+ switch (optID)
+ {
+ case SocketOptions.SO_RCVBUF:
+ case SocketOptions.SO_SNDBUF:
+ return value;
+ case SocketOptions.SO_REUSEADDR:
+ default:
+ return value;
+ }
+ }
+
+ public void setOption(int optID, Object value)
+ throws IOException {
+ /*
+ * Boolean.FALSE is used to disable some options, so it
+ * is important to distinguish between FALSE and unset.
+ * We define it here that -1 is unset, 0 is FALSE, and 1
+ * is TRUE.
+ */
+ int boolValue = -1;
+ int intValue = 0;
+
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ if (value instanceof Integer) {
+ intValue = (Integer)value;
+ } else if (value instanceof Boolean) {
+ boolValue = ((Boolean) value)? 1 : 0;
+ } else {
+ throw new IOException("bad value: " + value);
+ }
+
+ setOption_native(fd, optID, boolValue, intValue);
+ }
+
+ /**
+ * Enqueues a set of file descriptors to send to the peer. The queue
+ * is one deep. The file descriptors will be sent with the next write
+ * of normal data, and will be delivered in a single ancillary message.
+ * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine.
+ *
+ * @param fds non-null; file descriptors to send.
+ * @throws IOException
+ */
+ public void setFileDescriptorsForSend(FileDescriptor[] fds) {
+ synchronized(writeMonitor) {
+ outboundFileDescriptors = fds;
+ }
+ }
+
+ /**
+ * Retrieves a set of file descriptors that a peer has sent through
+ * an ancillary message. This method retrieves the most recent set sent,
+ * and then returns null until a new set arrives.
+ * File descriptors may only be passed along with regular data, so this
+ * method can only return a non-null after a read operation.
+ *
+ * @return null or file descriptor array
+ * @throws IOException
+ */
+ public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+ synchronized(readMonitor) {
+ FileDescriptor[] result = inboundFileDescriptors;
+
+ inboundFileDescriptors = null;
+ return result;
+ }
+ }
+
+ /**
+ * Retrieves the credentials of this socket's peer. Only valid on
+ * connected sockets.
+ *
+ * @return non-null; peer credentials
+ * @throws IOException
+ */
+ public Credentials getPeerCredentials() throws IOException
+ {
+ return getPeerCredentials_native(fd);
+ }
+
+ /**
+ * Retrieves the socket name from the OS.
+ *
+ * @return non-null; socket name
+ * @throws IOException on failure
+ */
+ public LocalSocketAddress getSockAddress() throws IOException
+ {
+ return null;
+ //TODO implement this
+ //return getSockName_native(fd);
+ }
+
+ @Override
+ protected void finalize() throws IOException {
+ close();
+ }
+}
+
diff --git a/core/java/android/net/MailTo.java b/core/java/android/net/MailTo.java
new file mode 100644
index 0000000..ca28f86
--- /dev/null
+++ b/core/java/android/net/MailTo.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2008 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.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *
+ * MailTo URL parser
+ *
+ * This class parses a mailto scheme URL and then can be queried for
+ * the parsed parameters. This implements RFC 2368.
+ *
+ */
+public class MailTo {
+
+ static public final String MAILTO_SCHEME = "mailto:";
+
+ // All the parsed content is added to the headers.
+ private HashMap<String, String> mHeaders;
+
+ // Well known headers
+ static private final String TO = "to";
+ static private final String BODY = "body";
+ static private final String CC = "cc";
+ static private final String SUBJECT = "subject";
+
+
+ /**
+ * Test to see if the given string is a mailto URL
+ * @param url string to be tested
+ * @return true if the string is a mailto URL
+ */
+ public static boolean isMailTo(String url) {
+ if (url != null && url.startsWith(MAILTO_SCHEME)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Parse and decode a mailto scheme string. This parser implements
+ * RFC 2368. The returned object can be queried for the parsed parameters.
+ * @param url String containing a mailto URL
+ * @return MailTo object
+ * @exception ParseException if the scheme is not a mailto URL
+ */
+ public static MailTo parse(String url) throws ParseException {
+ if (url == null) {
+ throw new NullPointerException();
+ }
+ if (!isMailTo(url)) {
+ throw new ParseException("Not a mailto scheme");
+ }
+ // Strip the scheme as the Uri parser can't cope with it.
+ String noScheme = url.substring(MAILTO_SCHEME.length());
+ Uri email = Uri.parse(noScheme);
+ MailTo m = new MailTo();
+
+ // Parse out the query parameters
+ String query = email.getQuery();
+ if (query != null ) {
+ String[] queries = query.split("&");
+ for (String q : queries) {
+ String[] nameval = q.split("=");
+ if (nameval.length == 0) {
+ continue;
+ }
+ // insert the headers with the name in lowercase so that
+ // we can easily find common headers
+ m.mHeaders.put(Uri.decode(nameval[0]).toLowerCase(),
+ nameval.length > 1 ? Uri.decode(nameval[1]) : null);
+ }
+ }
+
+ // Address can be specified in both the headers and just after the
+ // mailto line. Join the two together.
+ String address = email.getPath();
+ if (address != null) {
+ String addr = m.getTo();
+ if (addr != null) {
+ address += ", " + addr;
+ }
+ m.mHeaders.put(TO, address);
+ }
+
+ return m;
+ }
+
+ /**
+ * Retrieve the To address line from the parsed mailto URL. This could be
+ * several email address that are comma-space delimited.
+ * If no To line was specified, then null is return
+ * @return comma delimited email addresses or null
+ */
+ public String getTo() {
+ return mHeaders.get(TO);
+ }
+
+ /**
+ * Retrieve the CC address line from the parsed mailto URL. This could be
+ * several email address that are comma-space delimited.
+ * If no CC line was specified, then null is return
+ * @return comma delimited email addresses or null
+ */
+ public String getCc() {
+ return mHeaders.get(CC);
+ }
+
+ /**
+ * Retrieve the subject line from the parsed mailto URL.
+ * If no subject line was specified, then null is return
+ * @return subject or null
+ */
+ public String getSubject() {
+ return mHeaders.get(SUBJECT);
+ }
+
+ /**
+ * Retrieve the body line from the parsed mailto URL.
+ * If no body line was specified, then null is return
+ * @return body or null
+ */
+ public String getBody() {
+ return mHeaders.get(BODY);
+ }
+
+ /**
+ * Retrieve all the parsed email headers from the mailto URL
+ * @return map containing all parsed values
+ */
+ public Map<String, String> getHeaders() {
+ return mHeaders;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(MAILTO_SCHEME);
+ sb.append('?');
+ for (Map.Entry<String,String> header : mHeaders.entrySet()) {
+ sb.append(Uri.encode(header.getKey()));
+ sb.append('=');
+ sb.append(Uri.encode(header.getValue()));
+ sb.append('&');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Private constructor. The only way to build a Mailto object is through
+ * the parse() method.
+ */
+ private MailTo() {
+ mHeaders = new HashMap<String, String>();
+ }
+}
diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java
new file mode 100644
index 0000000..ae74e6f
--- /dev/null
+++ b/core/java/android/net/MobileDataStateTracker.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright (C) 2008 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyIntents;
+import android.net.NetworkInfo.DetailedState;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.Config;
+import android.text.TextUtils;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Track the state of mobile data connectivity. This is done by
+ * receiving broadcast intents from the Phone process whenever
+ * the state of data connectivity changes.
+ *
+ * {@hide}
+ */
+public class MobileDataStateTracker extends NetworkStateTracker {
+
+ private static final String TAG = "MobileDataStateTracker";
+ private static final boolean DBG = false;
+
+ private Phone.DataState mMobileDataState;
+ private ITelephony mPhoneService;
+ private static final String[] sDnsPropNames = {
+ "net.rmnet0.dns1",
+ "net.rmnet0.dns2",
+ "net.eth0.dns1",
+ "net.eth0.dns2",
+ "net.eth0.dns3",
+ "net.eth0.dns4",
+ "net.gprs.dns1",
+ "net.gprs.dns2"
+ };
+ private List<String> mDnsServers;
+ private String mInterfaceName;
+ private int mDefaultGatewayAddr;
+ private int mLastCallingPid = -1;
+
+ /**
+ * Create a new MobileDataStateTracker
+ * @param context the application context of the caller
+ * @param target a message handler for getting callbacks about state changes
+ */
+ public MobileDataStateTracker(Context context, Handler target) {
+ super(context, target, ConnectivityManager.TYPE_MOBILE);
+ mPhoneService = null;
+ mDnsServers = new ArrayList<String>();
+ }
+
+ /**
+ * Begin monitoring mobile data connectivity.
+ */
+ public void startMonitoring() {
+
+ IntentFilter filter = new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED);
+ filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED);
+
+ Intent intent = mContext.registerReceiver(new MobileDataStateReceiver(), filter);
+ if (intent != null)
+ mMobileDataState = getMobileDataState(intent);
+ else
+ mMobileDataState = Phone.DataState.DISCONNECTED;
+ }
+
+ private static Phone.DataState getMobileDataState(Intent intent) {
+ String str = intent.getStringExtra(Phone.STATE_KEY);
+ if (str != null)
+ return Enum.valueOf(Phone.DataState.class, str);
+ else
+ return Phone.DataState.DISCONNECTED;
+ }
+
+ private class MobileDataStateReceiver extends BroadcastReceiver {
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) {
+ Phone.DataState state = getMobileDataState(intent);
+ String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY);
+ String apnName = intent.getStringExtra(Phone.DATA_APN_KEY);
+ boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, false);
+ if (DBG) Log.d(TAG, "Received " + intent.getAction() +
+ " broadcast - state = " + state
+ + ", unavailable = " + unavailable
+ + ", reason = " + (reason == null ? "(unspecified)" : reason));
+ mNetworkInfo.setIsAvailable(!unavailable);
+ if (mMobileDataState != state) {
+ mMobileDataState = state;
+
+ switch (state) {
+ case DISCONNECTED:
+ setDetailedState(DetailedState.DISCONNECTED, reason, apnName);
+ if (mInterfaceName != null) {
+ NetworkUtils.resetConnections(mInterfaceName);
+ }
+ mInterfaceName = null;
+ mDefaultGatewayAddr = 0;
+ break;
+ case CONNECTING:
+ setDetailedState(DetailedState.CONNECTING, reason, apnName);
+ break;
+ case SUSPENDED:
+ setDetailedState(DetailedState.SUSPENDED, reason, apnName);
+ break;
+ case CONNECTED:
+ mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY);
+ if (mInterfaceName == null) {
+ Log.d(TAG, "CONNECTED event did not supply interface name.");
+ }
+ setupDnsProperties();
+ setDetailedState(DetailedState.CONNECTED, reason, apnName);
+ break;
+ }
+ }
+ } else if (intent.getAction().equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) {
+ String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY);
+ String apnName = intent.getStringExtra(Phone.DATA_APN_KEY);
+ if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" +
+ reason == null ? "" : "(" + reason + ")");
+ setDetailedState(DetailedState.FAILED, reason, apnName);
+ }
+ }
+ }
+
+ /**
+ * Make sure that route(s) exist to the carrier DNS server(s).
+ */
+ public void addPrivateRoutes() {
+ if (mInterfaceName != null) {
+ for (String addrString : mDnsServers) {
+ int addr = NetworkUtils.lookupHost(addrString);
+ if (addr != -1) {
+ NetworkUtils.addHostRoute(mInterfaceName, addr);
+ }
+ }
+ }
+ }
+
+ public void removePrivateRoutes() {
+ if(mInterfaceName != null) {
+ NetworkUtils.removeHostRoutes(mInterfaceName);
+ }
+ }
+
+ public void removeDefaultRoute() {
+ if(mInterfaceName != null) {
+ mDefaultGatewayAddr = NetworkUtils.getDefaultRoute(mInterfaceName);
+ NetworkUtils.removeDefaultRoute(mInterfaceName);
+ }
+ }
+
+ public void restoreDefaultRoute() {
+ // 0 is not a valid address for a gateway
+ if (mInterfaceName != null && mDefaultGatewayAddr != 0) {
+ NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr);
+ }
+ }
+
+ private void getPhoneService(boolean forceRefresh) {
+ if ((mPhoneService == null) || forceRefresh) {
+ mPhoneService = ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
+ }
+ }
+
+ /**
+ * Report whether data connectivity is possible.
+ */
+ public boolean isAvailable() {
+ getPhoneService(false);
+
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) break;
+
+ try {
+ return mPhoneService.isDataConnectivityPossible();
+ } catch (RemoteException e) {
+ // First-time failed, get the phone service again
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the IP addresses of the DNS servers available for the mobile data
+ * network interface.
+ * @return a list of DNS addresses, with no holes.
+ */
+ public String[] getNameServers() {
+ return getNameServerList(sDnsPropNames);
+ }
+
+ /**
+ * Return the system properties name associated with the tcp buffer sizes
+ * for this network.
+ */
+ public String getTcpBufferSizesPropName() {
+ String networkTypeStr = "unknown";
+ TelephonyManager tm = new TelephonyManager(mContext);
+ switch(tm.getNetworkType()) {
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ networkTypeStr = "gprs";
+ break;
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ networkTypeStr = "edge";
+ break;
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ networkTypeStr = "umts";
+ break;
+ }
+ return "net.tcp.buffersize." + networkTypeStr;
+ }
+
+ /**
+ * Tear down mobile data connectivity, i.e., disable the ability to create
+ * mobile data connections.
+ */
+ @Override
+ public boolean teardown() {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring mobile data teardown request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ return mPhoneService.disableDataConnectivity();
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Failed to tear down mobile data connectivity");
+ return false;
+ }
+
+ /**
+ * Re-enable mobile data connectivity after a {@link #teardown()}.
+ */
+ public boolean reconnect() {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring mobile data connect request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ return mPhoneService.enableDataConnectivity();
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Failed to set up mobile data connectivity");
+ return false;
+ }
+
+ /**
+ * Turn on or off the mobile radio. No connectivity will be possible while the
+ * radio is off. The operation is a no-op if the radio is already in the desired state.
+ * @param turnOn {@code true} if the radio should be turned on, {@code false} if
+ */
+ public boolean setRadio(boolean turnOn) {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring mobile radio request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ return mPhoneService.setRadio(turnOn);
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Could not set radio power to " + (turnOn ? "on" : "off"));
+ return false;
+ }
+
+ /**
+ * Tells the phone sub-system that the caller wants to
+ * begin using the named feature. The only supported feature at
+ * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application
+ * to specify that it wants to send and/or receive MMS data.
+ * @param feature the name of the feature to be used
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is feature-specific.
+ * specific, except that the value {@code -1}
+ * always indicates failure. For {@code Phone.FEATURE_ENABLE_MMS},
+ * the other possible return values are
+ * <ul>
+ * <li>{@code Phone.APN_ALREADY_ACTIVE}</li>
+ * <li>{@code Phone.APN_REQUEST_STARTED}</li>
+ * <li>{@code Phone.APN_TYPE_NOT_AVAILABLE}</li>
+ * <li>{@code Phone.APN_REQUEST_FAILED}</li>
+ * </ul>
+ */
+ public int startUsingNetworkFeature(String feature, int callingPid, int callingUid) {
+ if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) {
+ mLastCallingPid = callingPid;
+ return setEnableApn(Phone.APN_TYPE_MMS, true);
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Tells the phone sub-system that the caller is finished is
+ * finished using the named feature. The only supported feature at
+ * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application
+ * to specify that it wants to send and/or receive MMS data.
+ * @param feature the name of the feature that is no longer needed
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is feature-specific, except that
+ * the value {@code -1} always indicates failure.
+ */
+ public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid) {
+ if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) {
+ return setEnableApn(Phone.APN_TYPE_MMS, false);
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via the mobile data network.
+ * @param hostAddress the IP address of the host to which the route is desired,
+ * in network byte order.
+ * @return {@code true} on success, {@code false} on failure
+ */
+ @Override
+ public boolean requestRouteToHost(int hostAddress) {
+ if (mInterfaceName != null && hostAddress != -1) {
+ if (DBG) {
+ Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress));
+ }
+ return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer("Mobile data state: ");
+
+ sb.append(mMobileDataState);
+ return sb.toString();
+ }
+
+ private void setupDnsProperties() {
+ mDnsServers.clear();
+ // Set up per-process DNS server list on behalf of the MMS process
+ int i = 1;
+ if (mInterfaceName != null) {
+ for (String propName : sDnsPropNames) {
+ if (propName.indexOf(mInterfaceName) != -1) {
+ String propVal = SystemProperties.get(propName);
+ if (propVal != null && propVal.length() != 0 && !propVal.equals("0.0.0.0")) {
+ mDnsServers.add(propVal);
+ if (mLastCallingPid != -1) {
+ SystemProperties.set("net.dns" + i + "." + mLastCallingPid, propVal);
+ }
+ ++i;
+ }
+ }
+ }
+ }
+ if (i == 1) {
+ Log.d(TAG, "DNS server addresses are not known.");
+ } else if (mLastCallingPid != -1) {
+ /*
+ * Bump the property that tells the name resolver library
+ * to reread the DNS server list from the properties.
+ */
+ String propVal = SystemProperties.get("net.dnschange");
+ if (propVal.length() != 0) {
+ try {
+ int n = Integer.parseInt(propVal);
+ SystemProperties.set("net.dnschange", "" + (n+1));
+ } catch (NumberFormatException e) {
+ }
+ }
+ }
+ mLastCallingPid = -1;
+ }
+
+ /**
+ * Internal method supporting the ENABLE_MMS feature.
+ * @param apnType the type of APN to be enabled or disabled (e.g., mms)
+ * @param enable {@code true} to enable the specified APN type,
+ * {@code false} to disable it.
+ * @return an integer value representing the outcome of the request.
+ */
+ private int setEnableApn(String apnType, boolean enable) {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring feature request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ if (enable) {
+ return mPhoneService.enableApnType(apnType);
+ } else {
+ return mPhoneService.disableApnType(apnType);
+ }
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Could not " + (enable ? "enable" : "disable")
+ + " APN type \"" + apnType + "\"");
+ return Phone.APN_REQUEST_FAILED;
+ }
+}
diff --git a/core/java/android/net/NetworkConnectivityListener.java b/core/java/android/net/NetworkConnectivityListener.java
new file mode 100644
index 0000000..858fc77
--- /dev/null
+++ b/core/java/android/net/NetworkConnectivityListener.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2006 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * A wrapper for a broadcast receiver that provides network connectivity
+ * state information, independent of network type (mobile, Wi-Fi, etc.).
+ * {@hide}
+ */
+public class NetworkConnectivityListener {
+ private static final String TAG = "NetworkConnectivityListener";
+ private static final boolean DBG = false;
+
+ private Context mContext;
+ private HashMap<Handler, Integer> mHandlers = new HashMap<Handler, Integer>();
+ private State mState;
+ private boolean mListening;
+ private String mReason;
+ private boolean mIsFailover;
+
+ /** Network connectivity information */
+ private NetworkInfo mNetworkInfo;
+
+ /**
+ * In case of a Disconnect, the connectivity manager may have
+ * already established, or may be attempting to establish, connectivity
+ * with another network. If so, {@code mOtherNetworkInfo} will be non-null.
+ */
+ private NetworkInfo mOtherNetworkInfo;
+
+ private ConnectivityBroadcastReceiver mReceiver;
+
+ private class ConnectivityBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION) ||
+ mListening == false) {
+ Log.w(TAG, "onReceived() called with " + mState.toString() + " and " + intent);
+ return;
+ }
+
+ boolean noConnectivity =
+ intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+
+ if (noConnectivity) {
+ mState = State.NOT_CONNECTED;
+ } else {
+ mState = State.CONNECTED;
+ }
+
+ mNetworkInfo = (NetworkInfo)
+ intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ mOtherNetworkInfo = (NetworkInfo)
+ intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);
+
+ mReason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON);
+ mIsFailover =
+ intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false);
+
+ if (DBG) {
+ Log.d(TAG, "onReceive(): mNetworkInfo=" + mNetworkInfo + " mOtherNetworkInfo = "
+ + (mOtherNetworkInfo == null ? "[none]" : mOtherNetworkInfo +
+ " noConn=" + noConnectivity) + " mState=" + mState.toString());
+ }
+
+ // Notifiy any handlers.
+ Iterator<Handler> it = mHandlers.keySet().iterator();
+ while (it.hasNext()) {
+ Handler target = it.next();
+ Message message = Message.obtain(target, mHandlers.get(target));
+ target.sendMessage(message);
+ }
+ }
+ };
+
+ public enum State {
+ UNKNOWN,
+
+ /** This state is returned if there is connectivity to any network **/
+ CONNECTED,
+ /**
+ * This state is returned if there is no connectivity to any network. This is set
+ * to true under two circumstances:
+ * <ul>
+ * <li>When connectivity is lost to one network, and there is no other available
+ * network to attempt to switch to.</li>
+ * <li>When connectivity is lost to one network, and the attempt to switch to
+ * another network fails.</li>
+ */
+ NOT_CONNECTED
+ }
+
+ /**
+ * Create a new NetworkConnectivityListener.
+ */
+ public NetworkConnectivityListener() {
+ mState = State.UNKNOWN;
+ mReceiver = new ConnectivityBroadcastReceiver();
+ }
+
+ /**
+ * This method starts listening for network connectivity state changes.
+ * @param context
+ */
+ public synchronized void startListening(Context context) {
+ if (!mListening) {
+ mContext = context;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(mReceiver, filter);
+ mListening = true;
+ }
+ }
+
+ /**
+ * This method stops this class from listening for network changes.
+ */
+ public synchronized void stopListening() {
+ if (mListening) {
+ mContext.unregisterReceiver(mReceiver);
+ mContext = null;
+ mNetworkInfo = null;
+ mOtherNetworkInfo = null;
+ mIsFailover = false;
+ mReason = null;
+ mListening = false;
+ }
+ }
+
+ /**
+ * This methods registers a Handler to be called back onto with the specified what code when
+ * the network connectivity state changes.
+ *
+ * @param target The target handler.
+ * @param what The what code to be used when posting a message to the handler.
+ */
+ public void registerHandler(Handler target, int what) {
+ mHandlers.put(target, what);
+ }
+
+ /**
+ * This methods unregisters the specified Handler.
+ * @param target
+ */
+ public void unregisterHandler(Handler target) {
+ mHandlers.remove(target);
+ }
+
+ public State getState() {
+ return mState;
+ }
+
+ /**
+ * Return the NetworkInfo associated with the most recent connectivity event.
+ * @return {@code NetworkInfo} for the network that had the most recent connectivity event.
+ */
+ public NetworkInfo getNetworkInfo() {
+ return mNetworkInfo;
+ }
+
+ /**
+ * If the most recent connectivity event was a DISCONNECT, return
+ * any information supplied in the broadcast about an alternate
+ * network that might be available. If this returns a non-null
+ * value, then another broadcast should follow shortly indicating
+ * whether connection to the other network succeeded.
+ *
+ * @return NetworkInfo
+ */
+ public NetworkInfo getOtherNetworkInfo() {
+ return mOtherNetworkInfo;
+ }
+
+ /**
+ * Returns true if the most recent event was for an attempt to switch over to
+ * a new network following loss of connectivity on another network.
+ * @return {@code true} if this was a failover attempt, {@code false} otherwise.
+ */
+ public boolean isFailover() {
+ return mIsFailover;
+ }
+
+ /**
+ * An optional reason for the connectivity state change may have been supplied.
+ * This returns it.
+ * @return the reason for the state change, if available, or {@code null}
+ * otherwise.
+ */
+ public String getReason() {
+ return mReason;
+ }
+}
diff --git a/core/java/android/net/NetworkInfo.aidl b/core/java/android/net/NetworkInfo.aidl
new file mode 100644
index 0000000..f501873
--- /dev/null
+++ b/core/java/android/net/NetworkInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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;
+
+parcelable NetworkInfo;
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
new file mode 100644
index 0000000..f776abf
--- /dev/null
+++ b/core/java/android/net/NetworkInfo.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2008 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 android.os.Parcelable;
+import android.os.Parcel;
+
+import java.util.EnumMap;
+
+/**
+ * Describes the status of a network interface of a given type
+ * (currently either Mobile or Wifi).
+ */
+public class NetworkInfo implements Parcelable {
+
+ /**
+ * Coarse-grained network state. This is probably what most applications should
+ * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}.
+ * The mapping between the two is as follows:
+ * <br/><br/>
+ * <table>
+ * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr>
+ * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr>
+ * <tr><td><code>SCANNING</code></td><td><code>CONNECTING</code></td></tr>
+ * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr>
+ * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr>
+ * <tr><td><code>CONNECTED</code></td><td<code>CONNECTED</code></td></tr>
+ * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr>
+ * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr>
+ * <tr><td><code>UNAVAILABLE</code></td><td><code>DISCONNECTED</code></td></tr>
+ * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr>
+ * </table>
+ */
+ public enum State {
+ CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN
+ }
+
+ /**
+ * The fine-grained state of a network connection. This level of detail
+ * is probably of interest to few applications. Most should use
+ * {@link android.net.NetworkInfo.State State} instead.
+ */
+ public enum DetailedState {
+ /** Ready to start data connection setup. */
+ IDLE,
+ /** Searching for an available access point. */
+ SCANNING,
+ /** Currently setting up data connection. */
+ CONNECTING,
+ /** Network link established, performing authentication. */
+ AUTHENTICATING,
+ /** Awaiting response from DHCP server in order to assign IP address information. */
+ OBTAINING_IPADDR,
+ /** IP traffic should be available. */
+ CONNECTED,
+ /** IP traffic is suspended */
+ SUSPENDED,
+ /** Currently tearing down data connection. */
+ DISCONNECTING,
+ /** IP traffic not available. */
+ DISCONNECTED,
+ /** Attempt to connect failed. */
+ FAILED
+ }
+
+ /**
+ * This is the map described in the Javadoc comment above. The positions
+ * of the elements of the array must correspond to the ordinal values
+ * of <code>DetailedState</code>.
+ */
+ private static final EnumMap<DetailedState, State> stateMap =
+ new EnumMap<DetailedState, State>(DetailedState.class);
+
+ static {
+ stateMap.put(DetailedState.IDLE, State.DISCONNECTED);
+ stateMap.put(DetailedState.SCANNING, State.DISCONNECTED);
+ stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
+ stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
+ stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
+ stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
+ stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
+ stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
+ stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED);
+ stateMap.put(DetailedState.FAILED, State.DISCONNECTED);
+ }
+
+ private int mNetworkType;
+ private State mState;
+ private DetailedState mDetailedState;
+ private String mReason;
+ private String mExtraInfo;
+ private boolean mIsFailover;
+ /**
+ * Indicates whether network connectivity is possible:
+ */
+ private boolean mIsAvailable;
+
+ public NetworkInfo(int type) {
+ if (!ConnectivityManager.isNetworkTypeValid(type)) {
+ throw new IllegalArgumentException("Invalid network type: " + type);
+ }
+ this.mNetworkType = type;
+ setDetailedState(DetailedState.IDLE, null, null);
+ mState = State.UNKNOWN;
+ mIsAvailable = true;
+ }
+
+ /**
+ * Reports the type of network (currently mobile or Wi-Fi) to which the
+ * info in this object pertains.
+ * @return the network type
+ */
+ public int getType() {
+ return mNetworkType;
+ }
+
+ /**
+ * Indicates whether network connectivity exists or is in the process
+ * of being established. This is good for applications that need to
+ * do anything related to the network other than read or write data.
+ * For the latter, call {@link #isConnected()} instead, which guarantees
+ * that the network is fully usable.
+ * @return {@code true} if network connectivity exists or is in the process
+ * of being established, {@code false} otherwise.
+ */
+ public boolean isConnectedOrConnecting() {
+ return mState == State.CONNECTED || mState == State.CONNECTING;
+ }
+
+ /**
+ * Indicates whether network connectivity exists and it is possible to establish
+ * connections and pass data.
+ * @return {@code true} if network connectivity exists, {@code false} otherwise.
+ */
+ public boolean isConnected() {
+ return mState == State.CONNECTED;
+ }
+
+ /**
+ * Indicates whether network connectivity is possible. A network is unavailable
+ * when a persistent or semi-persistent condition prevents the possibility
+ * of connecting to that network. Examples include
+ * <ul>
+ * <li>The device is out of the coverage area for any network of this type.</li>
+ * <li>The device is on a network other than the home network (i.e., roaming), and
+ * data roaming has been disabled.</li>
+ * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li>
+ * </ul>
+ * @return {@code true} if the network is available, {@code false} otherwise
+ */
+ public boolean isAvailable() {
+ return mIsAvailable;
+ }
+
+ /**
+ * Sets if the network is available, ie, if the connectivity is possible.
+ * @param isAvailable the new availability value.
+ *
+ * {@hide}
+ */
+ public void setIsAvailable(boolean isAvailable) {
+ mIsAvailable = isAvailable;
+ }
+
+ /**
+ * Indicates whether the current attempt to connect to the network
+ * resulted from the ConnectivityManager trying to fail over to this
+ * network following a disconnect from another network.
+ * @return {@code true} if this is a failover attempt, {@code false}
+ * otherwise.
+ */
+ public boolean isFailover() {
+ return mIsFailover;
+ }
+
+ /** {@hide} */
+ public void setFailover(boolean isFailover) {
+ mIsFailover = isFailover;
+ }
+
+ /**
+ * Reports the current coarse-grained state of the network.
+ * @return the coarse-grained state
+ */
+ public State getState() {
+ return mState;
+ }
+
+ /**
+ * Reports the current fine-grained state of the network.
+ * @return the fine-grained state
+ */
+ public DetailedState getDetailedState() {
+ return mDetailedState;
+ }
+
+ /**
+ * Sets the fine-grained state of the network.
+ * @param detailedState the {@link DetailedState}.
+ * @param reason a {@code String} indicating the reason for the state change,
+ * if one was supplied. May be {@code null}.
+ * @param extraInfo an optional {@code String} providing addditional network state
+ * information passed up from the lower networking layers.
+ *
+ * {@hide}
+ */
+ void setDetailedState(DetailedState detailedState, String reason, String extraInfo) {
+ this.mDetailedState = detailedState;
+ this.mState = stateMap.get(detailedState);
+ this.mReason = reason;
+ this.mExtraInfo = extraInfo;
+ }
+
+ /**
+ * Report the reason an attempt to establish connectivity failed,
+ * if one is available.
+ * @return the reason for failure, or null if not available
+ */
+ public String getReason() {
+ return mReason;
+ }
+
+ /**
+ * Report the extra information about the network state, if any was
+ * provided by the lower networking layers.,
+ * if one is available.
+ * @return the extra information, or null if not available
+ */
+ public String getExtraInfo() {
+ return mExtraInfo;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("NetworkInfo: ");
+ builder.append("type: ").append(getTypeName()).append(", state: ").append(mState).
+ append("/").append(mDetailedState).
+ append(", reason: ").append(mReason == null ? "(unspecified)" : mReason).
+ append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo).
+ append(", failover: ").append(mIsFailover).
+ append(", isAvailable: ").append(mIsAvailable);
+ return builder.toString();
+ }
+
+ public String getTypeName() {
+ switch (mNetworkType) {
+ case ConnectivityManager.TYPE_WIFI:
+ return "WIFI";
+ case ConnectivityManager.TYPE_MOBILE:
+ return "MOBILE";
+ default:
+ return "<invalid>";
+ }
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mNetworkType);
+ dest.writeString(mState.name());
+ dest.writeString(mDetailedState.name());
+ dest.writeInt(mIsFailover ? 1 : 0);
+ dest.writeInt(mIsAvailable ? 1 : 0);
+ dest.writeString(mReason);
+ dest.writeString(mExtraInfo);
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public static final Creator<NetworkInfo> CREATOR =
+ new Creator<NetworkInfo>() {
+ public NetworkInfo createFromParcel(Parcel in) {
+ int netType = in.readInt();
+ NetworkInfo netInfo = new NetworkInfo(netType);
+ netInfo.mState = State.valueOf(in.readString());
+ netInfo.mDetailedState = DetailedState.valueOf(in.readString());
+ netInfo.mIsFailover = in.readInt() != 0;
+ netInfo.mIsAvailable = in.readInt() != 0;
+ netInfo.mReason = in.readString();
+ netInfo.mExtraInfo = in.readString();
+ return netInfo;
+ }
+
+ public NetworkInfo[] newArray(int size) {
+ return new NetworkInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java
new file mode 100644
index 0000000..4e1efa6
--- /dev/null
+++ b/core/java/android/net/NetworkStateTracker.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2008 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.FileWriter;
+import java.io.IOException;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.os.PowerManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Each subclass of this class keeps track of the state of connectivity
+ * of a network interface. All state information for a network should
+ * be kept in a Tracker class. This superclass manages the
+ * network-type-independent aspects of network state.
+ *
+ * {@hide}
+ */
+public abstract class NetworkStateTracker extends Handler {
+
+ protected NetworkInfo mNetworkInfo;
+ protected Context mContext;
+ protected Handler mTarget;
+
+ private static boolean DBG = Config.LOGV;
+ private static final String TAG = "NetworkStateTracker";
+
+ public static final int EVENT_STATE_CHANGED = 1;
+ public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2;
+ /**
+ * arg1: 1 to show, 0 to hide
+ * arg2: ID of the notification
+ * obj: Notification (if showing)
+ */
+ public static final int EVENT_NOTIFICATION_CHANGED = 3;
+ public static final int EVENT_CONFIGURATION_CHANGED = 4;
+
+ public NetworkStateTracker(Context context, Handler target, int networkType) {
+ super();
+ mContext = context;
+ mTarget = target;
+ this.mNetworkInfo = new NetworkInfo(networkType);
+ }
+
+ public NetworkInfo getNetworkInfo() {
+ return mNetworkInfo;
+ }
+
+ /**
+ * Return the list of DNS servers associated with this network.
+ * @return a list of the IP addresses of the DNS servers available
+ * for the network.
+ */
+ public abstract String[] getNameServers();
+
+ /**
+ * Return the system properties name associated with the tcp buffer sizes
+ * for this network.
+ */
+ public abstract String getTcpBufferSizesPropName();
+
+ /**
+ * Return the IP addresses of the DNS servers available for this
+ * network interface.
+ * @param propertyNames the names of the system properties whose values
+ * give the IP addresses. Properties with no values are skipped.
+ * @return an array of {@code String}s containing the IP addresses
+ * of the DNS servers, in dot-notation. This may have fewer
+ * non-null entries than the list of names passed in, since
+ * some of the passed-in names may have empty values.
+ */
+ static protected String[] getNameServerList(String[] propertyNames) {
+ String[] dnsAddresses = new String[propertyNames.length];
+ int i, j;
+
+ for (i = 0, j = 0; i < propertyNames.length; i++) {
+ String value = SystemProperties.get(propertyNames[i]);
+ // The GSM layer sometimes sets a bogus DNS server address of
+ // 0.0.0.0
+ if (!TextUtils.isEmpty(value) && !TextUtils.equals(value, "0.0.0.0")) {
+ dnsAddresses[j++] = value;
+ }
+ }
+ return dnsAddresses;
+ }
+
+ /**
+ * Reads the network specific TCP buffer sizes from SystemProperties
+ * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system
+ * wide use
+ */
+ public void updateNetworkSettings() {
+ String key = getTcpBufferSizesPropName();
+ String bufferSizes = SystemProperties.get(key);
+
+ if (bufferSizes.length() == 0) {
+ Log.e(TAG, key + " not found in system properties. Using defaults");
+
+ // Setting to default values so we won't be stuck to previous values
+ key = "net.tcp.buffersize.default";
+ bufferSizes = SystemProperties.get(key);
+ }
+
+ // Set values in kernel
+ if (bufferSizes.length() != 0) {
+ if (DBG) {
+ Log.v(TAG, "Setting TCP values: [" + bufferSizes
+ + "] which comes from [" + key + "]");
+ }
+ setBufferSize(bufferSizes);
+ }
+ }
+
+ /**
+ * Release the wakelock, if any, that may be held while handling a
+ * disconnect operation.
+ */
+ public void releaseWakeLock() {
+ }
+
+ /**
+ * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max]
+ * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem
+ *
+ * @param bufferSizes in the format of "readMin, readInitial, readMax,
+ * writeMin, writeInitial, writeMax"
+ */
+ private void setBufferSize(String bufferSizes) {
+ try {
+ String[] values = bufferSizes.split(",");
+
+ if (values.length == 6) {
+ final String prefix = "/sys/kernel/ipv4/tcp_";
+ stringToFile(prefix + "rmem_min", values[0]);
+ stringToFile(prefix + "rmem_def", values[1]);
+ stringToFile(prefix + "rmem_max", values[2]);
+ stringToFile(prefix + "wmem_min", values[3]);
+ stringToFile(prefix + "wmem_def", values[4]);
+ stringToFile(prefix + "wmem_max", values[5]);
+ } else {
+ Log.e(TAG, "Invalid buffersize string: " + bufferSizes);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Can't set tcp buffer sizes:" + e);
+ }
+ }
+
+ /**
+ * Writes string to file. Basically same as "echo -n $string > $filename"
+ *
+ * @param filename
+ * @param string
+ * @throws IOException
+ */
+ private void stringToFile(String filename, String string) throws IOException {
+ FileWriter out = new FileWriter(filename);
+ try {
+ out.write(string);
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Record the detailed state of a network, and if it is a
+ * change from the previous state, send a notification to
+ * any listeners.
+ * @param state the new @{code DetailedState}
+ */
+ public void setDetailedState(NetworkInfo.DetailedState state) {
+ setDetailedState(state, null, null);
+ }
+
+ /**
+ * Record the detailed state of a network, and if it is a
+ * change from the previous state, send a notification to
+ * any listeners.
+ * @param state the new @{code DetailedState}
+ * @param reason a {@code String} indicating a reason for the state change,
+ * if one was supplied. May be {@code null}.
+ * @param extraInfo optional {@code String} providing extra information about the state change
+ */
+ public void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) {
+ if (state != mNetworkInfo.getDetailedState()) {
+ boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING);
+ String lastReason = mNetworkInfo.getReason();
+ /*
+ * If a reason was supplied when the CONNECTING state was entered, and no
+ * reason was supplied for entering the CONNECTED state, then retain the
+ * reason that was supplied when going to CONNECTING.
+ */
+ if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null
+ && lastReason != null)
+ reason = lastReason;
+ mNetworkInfo.setDetailedState(state, reason, extraInfo);
+ Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo);
+ msg.sendToTarget();
+ }
+ }
+
+ protected void setDetailedStateInternal(NetworkInfo.DetailedState state) {
+ mNetworkInfo.setDetailedState(state, null, null);
+ }
+
+ /**
+ * Send a notification that the results of a scan for network access
+ * points has completed, and results are available.
+ */
+ protected void sendScanResultsAvailable() {
+ Message msg = mTarget.obtainMessage(EVENT_SCAN_RESULTS_AVAILABLE, mNetworkInfo);
+ msg.sendToTarget();
+ }
+
+ public abstract void startMonitoring();
+
+ /**
+ * Disable connectivity to a network
+ * @return {@code true} if a teardown occurred, {@code false} if the
+ * teardown did not occur.
+ */
+ public abstract boolean teardown();
+
+ /**
+ * Reenable connectivity to a network after a {@link #teardown()}.
+ */
+ public abstract boolean reconnect();
+
+ /**
+ * Turn the wireless radio off for a network.
+ * @param turnOn {@code true} to turn the radio on, {@code false}
+ */
+ public abstract boolean setRadio(boolean turnOn);
+
+ /**
+ * Returns an indication of whether this network is available for
+ * connections. A value of {@code false} means that some quasi-permanent
+ * condition prevents connectivity to this network.
+ */
+ public abstract boolean isAvailable();
+
+ /**
+ * Tells the underlying networking system that the caller wants to
+ * begin using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param feature the name of the feature to be used
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public abstract int startUsingNetworkFeature(String feature, int callingPid, int callingUid);
+
+ /**
+ * Tells the underlying networking system that the caller is finished
+ * using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param feature the name of the feature that is no longer needed.
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public abstract int stopUsingNetworkFeature(String feature, int callingPid, int callingUid);
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via this network interface.
+ * @param hostAddress the IP address of the host to which the route is desired
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean requestRouteToHost(int hostAddress) {
+ return false;
+ }
+
+ /**
+ * Interprets scan results. This will be called at a safe time for
+ * processing, and from a safe thread.
+ */
+ public void interpretScanResultsAvailable() {
+ }
+
+}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
new file mode 100644
index 0000000..129248a
--- /dev/null
+++ b/core/java/android/net/NetworkUtils.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2008 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.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Native methods for managing network interfaces.
+ *
+ * {@hide}
+ */
+public class NetworkUtils {
+ /** Bring the named network interface down. */
+ public native static int disableInterface(String interfaceName);
+
+ /** Add a route to the specified host via the named interface. */
+ public native static int addHostRoute(String interfaceName, int hostaddr);
+
+ /** Add a default route for the named interface. */
+ public native static int setDefaultRoute(String interfaceName, int gwayAddr);
+
+ /** Return the gateway address for the default route for the named interface. */
+ public native static int getDefaultRoute(String interfaceName);
+
+ /** Remove host routes that uses the named interface. */
+ public native static int removeHostRoutes(String interfaceName);
+
+ /** Remove the default route for the named interface. */
+ public native static int removeDefaultRoute(String interfaceName);
+
+ /** Reset any sockets that are connected via the named interface. */
+ public native static int resetConnections(String interfaceName);
+
+ /**
+ * Start the DHCP client daemon, in order to have it request addresses
+ * for the named interface, and then configure the interface with those
+ * addresses. This call blocks until it obtains a result (either success
+ * or failure) from the daemon.
+ * @param interfaceName the name of the interface to configure
+ * @param ipInfo if the request succeeds, this object is filled in with
+ * the IP address information.
+ * @return {@code true} for success, {@code false} for failure
+ */
+ public native static boolean runDhcp(String interfaceName, DhcpInfo ipInfo);
+
+ /**
+ * Shut down the DHCP client daemon.
+ * @param interfaceName the name of the interface for which the daemon
+ * should be stopped
+ * @return {@code true} for success, {@code false} for failure
+ */
+ public native static boolean stopDhcp(String interfaceName);
+
+ /**
+ * Return the last DHCP-related error message that was recorded.
+ * <p/>NOTE: This string is not localized, but currently it is only
+ * used in logging.
+ * @return the most recent error message, if any
+ */
+ public native static String getDhcpError();
+
+ /**
+ * When static IP configuration has been specified, configure the network
+ * interface according to the values supplied.
+ * @param interfaceName the name of the interface to configure
+ * @param ipInfo the IP address, default gateway, and DNS server addresses
+ * with which to configure the interface.
+ * @return {@code true} for success, {@code false} for failure
+ */
+ public static boolean configureInterface(String interfaceName, DhcpInfo ipInfo) {
+ return configureNative(interfaceName,
+ ipInfo.ipAddress,
+ ipInfo.netmask,
+ ipInfo.gateway,
+ ipInfo.dns1,
+ ipInfo.dns2);
+ }
+
+ private native static boolean configureNative(
+ String interfaceName, int ipAddress, int netmask, int gateway, int dns1, int dns2);
+
+ /**
+ * Look up a host name and return the result as an int. Works if the argument
+ * is an IP address in dot notation. Obviously, this can only be used for IPv4
+ * addresses.
+ * @param hostname the name of the host (or the IP address)
+ * @return the IP address as an {@code int} in network byte order
+ */
+ public static int lookupHost(String hostname) {
+ InetAddress inetAddress;
+ try {
+ inetAddress = InetAddress.getByName(hostname);
+ } catch (UnknownHostException e) {
+ return -1;
+ }
+ byte[] addrBytes;
+ int addr;
+ addrBytes = inetAddress.getAddress();
+ addr = ((addrBytes[3] & 0xff) << 24)
+ | ((addrBytes[2] & 0xff) << 16)
+ | ((addrBytes[1] & 0xff) << 8)
+ | (addrBytes[0] & 0xff);
+ return addr;
+ }
+}
diff --git a/core/java/android/net/ParseException.java b/core/java/android/net/ParseException.java
new file mode 100644
index 0000000..000fa68
--- /dev/null
+++ b/core/java/android/net/ParseException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 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;
+
+/**
+ *
+ *
+ * When WebAddress Parser Fails, this exception is thrown
+ */
+public class ParseException extends RuntimeException {
+ public String response;
+
+ ParseException(String response) {
+ this.response = response;
+ }
+}
diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java
new file mode 100644
index 0000000..86e1d5b
--- /dev/null
+++ b/core/java/android/net/Proxy.java
@@ -0,0 +1,120 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.Context;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+/**
+ * A convenience class for accessing the user and default proxy
+ * settings.
+ */
+final public class Proxy {
+
+ static final public String PROXY_CHANGE_ACTION =
+ "android.intent.action.PROXY_CHANGE";
+
+ /**
+ * Return the proxy host set by the user.
+ * @param ctx A Context used to get the settings for the proxy host.
+ * @return String containing the host name. If the user did not set a host
+ * name it returns the default host. A null value means that no
+ * host is to be used.
+ */
+ static final public String getHost(Context ctx) {
+ ContentResolver contentResolver = ctx.getContentResolver();
+ Assert.assertNotNull(contentResolver);
+ String host = Settings.System.getString(
+ contentResolver,
+ Settings.System.HTTP_PROXY);
+ if (host != null) {
+ int i = host.indexOf(':');
+ if (i == -1) {
+ if (android.util.Config.DEBUG) {
+ Assert.assertTrue(host.length() == 0);
+ }
+ return null;
+ }
+ return host.substring(0, i);
+ }
+ return getDefaultHost();
+ }
+
+ /**
+ * Return the proxy port set by the user.
+ * @param ctx A Context used to get the settings for the proxy port.
+ * @return The port number to use or -1 if no proxy is to be used.
+ */
+ static final public int getPort(Context ctx) {
+ ContentResolver contentResolver = ctx.getContentResolver();
+ Assert.assertNotNull(contentResolver);
+ String host = Settings.System.getString(
+ contentResolver,
+ Settings.System.HTTP_PROXY);
+ if (host != null) {
+ int i = host.indexOf(':');
+ if (i == -1) {
+ if (android.util.Config.DEBUG) {
+ Assert.assertTrue(host.length() == 0);
+ }
+ return -1;
+ }
+ if (android.util.Config.DEBUG) {
+ Assert.assertTrue(i < host.length());
+ }
+ return Integer.parseInt(host.substring(i+1));
+ }
+ return getDefaultPort();
+ }
+
+ /**
+ * Return the default proxy host specified by the carrier.
+ * @return String containing the host name or null if there is no proxy for
+ * this carrier.
+ */
+ static final public String getDefaultHost() {
+ String host = SystemProperties.get("net.gprs.http-proxy");
+ if (host != null) {
+ Uri u = Uri.parse(host);
+ host = u.getHost();
+ return host;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the default proxy port specified by the carrier.
+ * @return The port number to be used with the proxy host or -1 if there is
+ * no proxy for this carrier.
+ */
+ static final public int getDefaultPort() {
+ String host = SystemProperties.get("net.gprs.http-proxy");
+ if (host != null) {
+ Uri u = Uri.parse(host);
+ return u.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+};
diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java
new file mode 100644
index 0000000..f816caa
--- /dev/null
+++ b/core/java/android/net/SSLCertificateSocketFactory.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2008 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 android.util.Log;
+import android.util.Config;
+import android.net.http.DomainNameChecker;
+import android.os.SystemProperties;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.NoSuchAlgorithmException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+public class SSLCertificateSocketFactory extends SSLSocketFactory {
+
+ private static final boolean DBG = true;
+ private static final String LOG_TAG = "SSLCertificateSocketFactory";
+
+ private static X509TrustManager sDefaultTrustManager;
+
+ private final int socketReadTimeoutForSslHandshake;
+
+ static {
+ try {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+ tmf.init((KeyStore)null);
+ TrustManager[] tms = tmf.getTrustManagers();
+ if (tms != null) {
+ for (TrustManager tm : tms) {
+ if (tm instanceof X509TrustManager) {
+ sDefaultTrustManager = (X509TrustManager)tm;
+ break;
+ }
+ }
+ }
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
+ } catch (KeyStoreException e) {
+ Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
+ }
+ }
+
+ private static final TrustManager[] TRUST_MANAGER = new TrustManager[] {
+ new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public void checkClientTrusted(X509Certificate[] certs,
+ String authType) { }
+
+ public void checkServerTrusted(X509Certificate[] certs,
+ String authType) { }
+ }
+ };
+
+ private SSLSocketFactory factory;
+
+ public SSLCertificateSocketFactory(int socketReadTimeoutForSslHandshake)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, TRUST_MANAGER, new java.security.SecureRandom());
+ factory = (SSLSocketFactory) context.getSocketFactory();
+ this.socketReadTimeoutForSslHandshake = socketReadTimeoutForSslHandshake;
+ }
+
+ /**
+ * Returns a default instantiation of a new socket factory which
+ * only allows SSL connections with valid certificates.
+ *
+ * @param socketReadTimeoutForSslHandshake the socket read timeout used for performing
+ * ssl handshake. The socket read timeout is set back to 0 after the handshake.
+ * @return a new SocketFactory, or null on error
+ */
+ public static SocketFactory getDefault(int socketReadTimeoutForSslHandshake) {
+ try {
+ return new SSLCertificateSocketFactory(socketReadTimeoutForSslHandshake);
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(LOG_TAG,
+ "SSLCertifcateSocketFactory.getDefault" +
+ " NoSuchAlgorithmException " , e);
+ return null;
+ } catch (KeyManagementException e) {
+ Log.e(LOG_TAG,
+ "SSLCertifcateSocketFactory.getDefault" +
+ " KeyManagementException " , e);
+ return null;
+ }
+ }
+
+ private boolean hasValidCertificateChain(Certificate[] certs)
+ throws IOException {
+ if (sDefaultTrustManager == null) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"hasValidCertificateChain():" +
+ " null default trust manager!");
+ }
+ throw new IOException("null default trust manager");
+ }
+
+ boolean trusted = (certs != null && (certs.length > 0));
+
+ if (trusted) {
+ try {
+ // the authtype we pass in doesn't actually matter
+ sDefaultTrustManager.checkServerTrusted((X509Certificate[]) certs, "RSA");
+ } catch (GeneralSecurityException e) {
+ String exceptionMessage = e != null ? e.getMessage() : "none";
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"hasValidCertificateChain(): sec. exception: "
+ + exceptionMessage);
+ }
+ trusted = false;
+ }
+ }
+
+ return trusted;
+ }
+
+ private void validateSocket(SSLSocket sslSock, String destHost)
+ throws IOException
+ {
+ if (Config.LOGV) {
+ Log.v(LOG_TAG,"validateSocket() to host "+destHost);
+ }
+
+ String relaxSslCheck = SystemProperties.get("socket.relaxsslcheck");
+ String secure = SystemProperties.get("ro.secure");
+
+ // only allow relaxing the ssl check on non-secure builds where the relaxation is
+ // specifically requested.
+ if ("0".equals(secure) && "yes".equals(relaxSslCheck)) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"sys prop socket.relaxsslcheck is set," +
+ " ignoring invalid certs");
+ }
+ return;
+ }
+
+ Certificate[] certs = null;
+ sslSock.setUseClientMode(true);
+ sslSock.startHandshake();
+ certs = sslSock.getSession().getPeerCertificates();
+
+ // check that the root certificate in the chain belongs to
+ // a CA we trust
+ if (certs == null) {
+ Log.e(LOG_TAG,
+ "[SSLCertificateSocketFactory] no trusted root CA");
+ throw new IOException("no trusted root CA");
+ }
+
+ if (Config.LOGV) {
+ Log.v(LOG_TAG,"validateSocket # certs = " +certs.length);
+ }
+
+ if (!hasValidCertificateChain(certs)) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"validateSocket(): certificate untrusted!");
+ }
+ throw new IOException("Certificate untrusted");
+ }
+
+ X509Certificate lastChainCert = (X509Certificate) certs[0];
+
+ if (!DomainNameChecker.match(lastChainCert, destHost)) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"validateSocket(): domain name check failed");
+ }
+ throw new IOException("Domain Name check failed");
+ }
+ }
+
+ public Socket createSocket(Socket socket, String s, int i, boolean flag)
+ throws IOException
+ {
+ throw new IOException("Cannot validate certification without a hostname");
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j)
+ throws IOException
+ {
+ throw new IOException("Cannot validate certification without a hostname");
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i) throws IOException {
+ throw new IOException("Cannot validate certification without a hostname");
+ }
+
+ public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
+ SSLSocket sslSock = (SSLSocket) factory.createSocket(s, i, inaddr, j);
+
+ if (socketReadTimeoutForSslHandshake >= 0) {
+ sslSock.setSoTimeout(socketReadTimeoutForSslHandshake);
+ }
+
+ validateSocket(sslSock,s);
+ sslSock.setSoTimeout(0);
+
+ return sslSock;
+ }
+
+ public Socket createSocket(String s, int i) throws IOException {
+ SSLSocket sslSock = (SSLSocket) factory.createSocket(s, i);
+
+ if (socketReadTimeoutForSslHandshake >= 0) {
+ sslSock.setSoTimeout(socketReadTimeoutForSslHandshake);
+ }
+
+ validateSocket(sslSock,s);
+ sslSock.setSoTimeout(0);
+
+ return sslSock;
+ }
+
+ public String[] getDefaultCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+
+ public String[] getSupportedCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+}
+
+
diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java
new file mode 100644
index 0000000..28134b2
--- /dev/null
+++ b/core/java/android/net/SntpClient.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2008 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 android.os.SystemClock;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+
+/**
+ * {@hide}
+ *
+ * Simple SNTP client class for retrieving network time.
+ *
+ * Sample usage:
+ * <pre>SntpClient client = new SntpClient();
+ * if (client.requestTime("time.foo.com")) {
+ * long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
+ * }
+ * </pre>
+ */
+public class SntpClient
+{
+ private static final String TAG = "SntpClient";
+
+ private static final int REFERENCE_TIME_OFFSET = 16;
+ private static final int ORIGINATE_TIME_OFFSET = 24;
+ private static final int RECEIVE_TIME_OFFSET = 32;
+ private static final int TRANSMIT_TIME_OFFSET = 40;
+ private static final int NTP_PACKET_SIZE = 48;
+
+ private static final int NTP_PORT = 123;
+ private static final int NTP_MODE_CLIENT = 3;
+ private static final int NTP_VERSION = 3;
+
+ // Number of seconds between Jan 1, 1900 and Jan 1, 1970
+ // 70 years plus 17 leap days
+ private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
+
+ // system time computed from NTP server response
+ private long mNtpTime;
+
+ // value of SystemClock.elapsedRealtime() corresponding to mNtpTime
+ private long mNtpTimeReference;
+
+ // round trip time in milliseconds
+ private long mRoundTripTime;
+
+ /**
+ * Sends an SNTP request to the given host and processes the response.
+ *
+ * @param host host name of the server.
+ * @param timeout network timeout in milliseconds.
+ * @return true if the transaction was successful.
+ */
+ public boolean requestTime(String host, int timeout) {
+ try {
+ DatagramSocket socket = new DatagramSocket();
+ socket.setSoTimeout(timeout);
+ InetAddress address = InetAddress.getByName(host);
+ byte[] buffer = new byte[NTP_PACKET_SIZE];
+ DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);
+
+ // set mode = 3 (client) and version = 3
+ // mode is in low 3 bits of first byte
+ // version is in bits 3-5 of first byte
+ buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
+
+ // get current time and write it to the request packet
+ long requestTime = System.currentTimeMillis();
+ long requestTicks = SystemClock.elapsedRealtime();
+ writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
+
+ socket.send(request);
+
+ // read the response
+ DatagramPacket response = new DatagramPacket(buffer, buffer.length);
+ socket.receive(response);
+ long responseTicks = SystemClock.elapsedRealtime();
+ long responseTime = requestTime + (responseTicks - requestTicks);
+ socket.close();
+
+ // extract the results
+ long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
+ long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
+ long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
+ long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
+ long clockOffset = (receiveTime - originateTime) + (transmitTime - responseTime);
+ if (Config.LOGD) Log.d(TAG, "round trip: " + roundTripTime + " ms");
+ if (Config.LOGD) Log.d(TAG, "clock offset: " + clockOffset + " ms");
+
+ // save our results
+ mNtpTime = requestTime + clockOffset;
+ mNtpTimeReference = requestTicks;
+ mRoundTripTime = roundTripTime;
+ } catch (Exception e) {
+ if (Config.LOGD) Log.d(TAG, "request time failed: " + e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the time computed from the NTP transaction.
+ *
+ * @return time value computed from NTP server response.
+ */
+ public long getNtpTime() {
+ return mNtpTime;
+ }
+
+ /**
+ * Returns the reference clock value (value of SystemClock.elapsedRealtime())
+ * corresponding to the NTP time.
+ *
+ * @return reference clock corresponding to the NTP time.
+ */
+ public long getNtpTimeReference() {
+ return mNtpTimeReference;
+ }
+
+ /**
+ * Returns the round trip time of the NTP transaction
+ *
+ * @return round trip time in milliseconds.
+ */
+ public long getRoundTripTime() {
+ return mRoundTripTime;
+ }
+
+ /**
+ * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
+ */
+ private long read32(byte[] buffer, int offset) {
+ byte b0 = buffer[offset];
+ byte b1 = buffer[offset+1];
+ byte b2 = buffer[offset+2];
+ byte b3 = buffer[offset+3];
+
+ // convert signed bytes to unsigned values
+ int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
+ int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
+ int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
+ int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
+
+ return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3;
+ }
+
+ /**
+ * Reads the NTP time stamp at the given offset in the buffer and returns
+ * it as a system time (milliseconds since January 1, 1970).
+ */
+ private long readTimeStamp(byte[] buffer, int offset) {
+ long seconds = read32(buffer, offset);
+ long fraction = read32(buffer, offset + 4);
+ return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
+ }
+
+ /**
+ * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
+ * at the given offset in the buffer.
+ */
+ private void writeTimeStamp(byte[] buffer, int offset, long time) {
+ long seconds = time / 1000L;
+ long milliseconds = time - seconds * 1000L;
+ seconds += OFFSET_1900_TO_1970;
+
+ // write seconds in big endian format
+ buffer[offset++] = (byte)(seconds >> 24);
+ buffer[offset++] = (byte)(seconds >> 16);
+ buffer[offset++] = (byte)(seconds >> 8);
+ buffer[offset++] = (byte)(seconds >> 0);
+
+ long fraction = milliseconds * 0x100000000L / 1000L;
+ // write fraction in big endian format
+ buffer[offset++] = (byte)(fraction >> 24);
+ buffer[offset++] = (byte)(fraction >> 16);
+ buffer[offset++] = (byte)(fraction >> 8);
+ // low order bits should be random data
+ buffer[offset++] = (byte)(Math.random() * 255.0);
+ }
+}
diff --git a/core/java/android/net/Uri.aidl b/core/java/android/net/Uri.aidl
new file mode 100755
index 0000000..6bd3be5
--- /dev/null
+++ b/core/java/android/net/Uri.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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;
+
+parcelable Uri;
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
new file mode 100644
index 0000000..32a26e4
--- /dev/null
+++ b/core/java/android/net/Uri.java
@@ -0,0 +1,2251 @@
+/*
+ * 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 android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.io.ByteArrayOutputStream;
+import java.net.URLEncoder;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.RandomAccess;
+
+/**
+ * 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
+ * <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC 2396</a>.
+ *
+ * <p>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<Uri> {
+
+ /*
+
+ This class aims to do as little up front work as possible. To accomplish
+ that, we vary the implementation dependending on what the user passes in.
+ For example, we have one implementation if the user passes in a
+ URI string (StringUri) and another if the user passes in the
+ individual components (OpaqueUri).
+
+ *Concurrency notes*: Like any truly immutable object, this class is safe
+ for concurrent use. This class uses a caching pattern in some places where
+ it doesn't use volatile or synchronized. This is safe to do with ints
+ because getting or setting an int is atomic. It's safe to do with a String
+ because the internal fields are final and the memory model guarantees other
+ threads won't see a partially initialized instance. We are not guaranteed
+ that some threads will immediately see changes from other threads on
+ certain platforms, but we don't mind if those threads reconstruct the
+ cached result. As a result, we get thread safe caching with no concurrency
+ overhead, which means the most common case, access from a single thread,
+ is as fast as possible.
+
+ From the Java Language spec.:
+
+ "17.5 Final Field Semantics
+
+ ... when the object is seen by another thread, that thread will always
+ see the correctly constructed version of that object's final fields.
+ It will also see versions of any object or array referenced by
+ those final fields that are at least as up-to-date as the final fields
+ are."
+
+ In that same vein, all non-transient fields within Uri
+ implementations should be final and immutable so as to ensure true
+ immutability for clients even when they don't use proper concurrency
+ control.
+
+ For reference, from RFC 2396:
+
+ "4.3. Parsing a URI Reference
+
+ A URI reference is typically parsed according to the four main
+ components and fragment identifier in order to determine what
+ components are present and whether the reference is relative or
+ absolute. The individual components are then parsed for their
+ subparts and, if not opaque, to verify their validity.
+
+ Although the BNF defines what is allowed in each component, it is
+ ambiguous in terms of differentiating between an authority component
+ and a path component that begins with two slash characters. The
+ greedy algorithm is used for disambiguation: the left-most matching
+ rule soaks up as much of the URI reference string as it is capable of
+ matching. In other words, the authority component wins."
+
+ The "four main components" of a hierarchical URI consist of
+ <scheme>://<authority><path>?<query>
+
+ */
+
+ /** Log tag. */
+ private static final String LOG = Uri.class.getSimpleName();
+
+ /**
+ * The empty URI, equivalent to "".
+ */
+ public static final Uri EMPTY = new HierarchicalUri(null, Part.NULL,
+ PathPart.EMPTY, Part.NULL, Part.NULL);
+
+ /**
+ * Prevents external subclassing.
+ */
+ private Uri() {}
+
+ /**
+ * Returns true if this URI is hierarchical like "http://google.com".
+ * Absolute URIs are hierarchical if the scheme-specific part starts with
+ * a '/'. Relative URIs are always hierarchical.
+ */
+ public abstract boolean isHierarchical();
+
+ /**
+ * Returns true if this URI is opaque like "mailto:nobody@google.com". The
+ * scheme-specific part of an opaque URI cannot start with a '/'.
+ */
+ public boolean isOpaque() {
+ return !isHierarchical();
+ }
+
+ /**
+ * Returns true if this URI is relative, i.e. if it doesn't contain an
+ * explicit scheme.
+ *
+ * @return true if this URI is relative, false if it's absolute
+ */
+ public abstract boolean isRelative();
+
+ /**
+ * Returns true if this URI is absolute, i.e. if it contains an
+ * explicit scheme.
+ *
+ * @return true if this URI is absolute, false if it's relative
+ */
+ public boolean isAbsolute() {
+ return !isRelative();
+ }
+
+ /**
+ * Gets the scheme of this URI. Example: "http"
+ *
+ * @return the scheme or null if this is a relative URI
+ */
+ public abstract String getScheme();
+
+ /**
+ * 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. Decodes escaped octets.
+ *
+ * <p>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.
+ *
+ * <p>Example: "//www.google.com/search?q=android"
+ *
+ * @return the decoded 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 ]}
+ *
+ * <p>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 ]}
+ *
+ * <p>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<String> getPathSegments();
+
+ /**
+ * Gets the decoded last segment in the path.
+ *
+ * @return the decoded last segment or null if the path is empty
+ */
+ public abstract String getLastPathSegment();
+
+ /**
+ * Compares this Uri to another object for equality. Returns true if the
+ * encoded string representations of this Uri and the given Uri are
+ * equal. Case counts. Paths are not normalized. If one Uri specifies a
+ * default port explicitly and the other leaves it implicit, they will not
+ * be considered equal.
+ */
+ public boolean equals(Object o) {
+ if (!(o instanceof Uri)) {
+ return false;
+ }
+
+ Uri other = (Uri) o;
+
+ return toString().equals(other.toString());
+ }
+
+ /**
+ * Hashes the encoded string represention of this Uri consistently with
+ * {@link #equals(Object)}.
+ */
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ /**
+ * Compares the string representation of this Uri with that of
+ * another.
+ */
+ public int compareTo(Uri other) {
+ return toString().compareTo(other.toString());
+ }
+
+ /**
+ * Returns the encoded string representation of this URI.
+ * Example: "http://google.com/"
+ */
+ public abstract String toString();
+
+ /**
+ * Constructs a new builder, copying the attributes from this Uri.
+ */
+ public abstract Builder buildUpon();
+
+ /** Index of a component which was not found. */
+ private final static int NOT_FOUND = -1;
+
+ /** Placeholder value for an index which hasn't been calculated yet. */
+ private final static int NOT_CALCULATED = -2;
+
+ /**
+ * Placeholder for strings which haven't been cached. This enables us
+ * to cache null. We intentionally create a new String instance so we can
+ * compare its identity and there is no chance we will confuse it with
+ * user data.
+ */
+ @SuppressWarnings("RedundantStringConstructorCall")
+ private static final String NOT_CACHED = new String("NOT CACHED");
+
+ /**
+ * Error message presented when a user tries to treat an opaque URI as
+ * hierarchical.
+ */
+ private static final String NOT_HIERARCHICAL
+ = "This isn't a hierarchical URI.";
+
+ /** Default encoding. */
+ private static final String DEFAULT_ENCODING = "UTF-8";
+
+ /**
+ * Creates a Uri which parses the given encoded URI string.
+ *
+ * @param uriString an RFC 3296-compliant, encoded URI
+ * @throws NullPointerException if uriString is null
+ * @return Uri for this given uri string
+ */
+ public static Uri parse(String uriString) {
+ return new StringUri(uriString);
+ }
+
+ /**
+ * Creates a Uri from a file. The URI has the form
+ * "file://<absolute path>". Encodes path characters with the exception of
+ * '/'.
+ *
+ * <p>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 {
+
+ /** Used in parcelling. */
+ static final int TYPE_ID = 1;
+
+ /** URI string representation. */
+ private final String uriString;
+
+ private StringUri(String uriString) {
+ if (uriString == null) {
+ throw new NullPointerException("uriString");
+ }
+
+ this.uriString = uriString;
+ }
+
+ static Uri readFrom(Parcel parcel) {
+ return new StringUri(parcel.readString());
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(TYPE_ID);
+ parcel.writeString(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 = NOT_CACHED;
+
+ public String getScheme() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = (scheme != 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<String> getPathSegments() {
+ return getPathPart().getPathSegments();
+ }
+
+ private String parsePath() {
+ String uriString = this.uriString;
+ int ssi = findSchemeSeparator();
+
+ // If the URI is absolute.
+ if (ssi > -1) {
+ // Is there anything after the ':'?
+ boolean schemeOnly = ssi + 1 == uriString.length();
+ if (schemeOnly) {
+ // Opaque URI.
+ return null;
+ }
+
+ // A '/' after the ':' means this is hierarchical.
+ if (uriString.charAt(ssi + 1) != '/') {
+ // Opaque URI.
+ return null;
+ }
+ } else {
+ // All relative URIs are hierarchical.
+ }
+
+ return parsePath(uriString, ssi);
+ }
+
+ private Part query;
+
+ private Part getQueryPart() {
+ return query == null
+ ? query = Part.fromEncoded(parseQuery()) : query;
+ }
+
+ public String getEncodedQuery() {
+ return getQueryPart().getEncoded();
+ }
+
+ private String parseQuery() {
+ // It doesn't make sense to cache this index. We only ever
+ // calculate it once.
+ int qsi = uriString.indexOf('?', findSchemeSeparator());
+ if (qsi == NOT_FOUND) {
+ return null;
+ }
+
+ int fsi = findFragmentSeparator();
+
+ if (fsi == NOT_FOUND) {
+ return uriString.substring(qsi + 1);
+ }
+
+ if (fsi < qsi) {
+ // Invalid.
+ return null;
+ }
+
+ return uriString.substring(qsi + 1, fsi);
+ }
+
+ public String getQuery() {
+ return getQueryPart().getDecoded();
+ }
+
+ private Part fragment;
+
+ private Part getFragmentPart() {
+ return fragment == null
+ ? fragment = Part.fromEncoded(parseFragment()) : fragment;
+ }
+
+ public String getEncodedFragment() {
+ return getFragmentPart().getEncoded();
+ }
+
+ private String parseFragment() {
+ int fsi = findFragmentSeparator();
+ return fsi == NOT_FOUND ? null : uriString.substring(fsi + 1);
+ }
+
+ public String getFragment() {
+ return getFragmentPart().getDecoded();
+ }
+
+ public String toString() {
+ return uriString;
+ }
+
+ /**
+ * Parses an authority out of the given URI string.
+ *
+ * @param uriString URI string
+ * @param ssi scheme separator index, -1 for a relative URI
+ *
+ * @return the authority or null if none is found
+ */
+ static String parseAuthority(String uriString, int ssi) {
+ int length = uriString.length();
+
+ // If "//" follows the scheme separator, we have an authority.
+ if (length > ssi + 2
+ && uriString.charAt(ssi + 1) == '/'
+ && uriString.charAt(ssi + 2) == '/') {
+ // We have an authority.
+
+ // Look for the start of the path, query, or fragment, or the
+ // end of the string.
+ int end = ssi + 3;
+ LOOP: while (end < length) {
+ switch (uriString.charAt(end)) {
+ case '/': // Start of path
+ case '?': // Start of query
+ case '#': // Start of fragment
+ break LOOP;
+ }
+ end++;
+ }
+
+ return uriString.substring(ssi + 3, end);
+ } else {
+ return null;
+ }
+
+ }
+
+ /**
+ * Parses a path out of this given URI string.
+ *
+ * @param uriString URI string
+ * @param ssi scheme separator index, -1 for a relative URI
+ *
+ * @return the path
+ */
+ static String parsePath(String uriString, int ssi) {
+ int length = uriString.length();
+
+ // Find start of path.
+ int pathStart;
+ if (length > ssi + 2
+ && uriString.charAt(ssi + 1) == '/'
+ && uriString.charAt(ssi + 2) == '/') {
+ // Skip over authority to path.
+ pathStart = ssi + 3;
+ LOOP: while (pathStart < length) {
+ switch (uriString.charAt(pathStart)) {
+ case '?': // Start of query
+ case '#': // Start of fragment
+ return ""; // Empty path.
+ case '/': // Start of path!
+ break LOOP;
+ }
+ pathStart++;
+ }
+ } else {
+ // Path starts immediately after scheme separator.
+ pathStart = ssi + 1;
+ }
+
+ // Find end of path.
+ int pathEnd = pathStart;
+ LOOP: while (pathEnd < length) {
+ switch (uriString.charAt(pathEnd)) {
+ case '?': // Start of query
+ case '#': // Start of fragment
+ break LOOP;
+ }
+ pathEnd++;
+ }
+
+ return uriString.substring(pathStart, pathEnd);
+ }
+
+ public Builder buildUpon() {
+ if (isHierarchical()) {
+ return new Builder()
+ .scheme(getScheme())
+ .authority(getAuthorityPart())
+ .path(getPathPart())
+ .query(getQueryPart())
+ .fragment(getFragmentPart());
+ } else {
+ return new Builder()
+ .scheme(getScheme())
+ .opaquePart(getSsp())
+ .fragment(getFragmentPart());
+ }
+ }
+ }
+
+ /**
+ * Creates an opaque Uri from the given components. Encodes the ssp
+ * which means this method cannot be used to create hierarchical URIs.
+ *
+ * @param scheme of the URI
+ * @param ssp scheme-specific-part, everything between the
+ * scheme separator (':') and the fragment separator ('#'), which will
+ * get encoded
+ * @param fragment fragment, everything after the '#', null if undefined,
+ * will get encoded
+ *
+ * @throws NullPointerException if scheme or ssp is null
+ * @return Uri composed of the given scheme, ssp, and fragment
+ *
+ * @see Builder if you don't want the ssp and fragment to be encoded
+ */
+ public static Uri fromParts(String scheme, String ssp,
+ String fragment) {
+ if (scheme == null) {
+ throw new NullPointerException("scheme");
+ }
+ if (ssp == null) {
+ throw new NullPointerException("ssp");
+ }
+
+ return new OpaqueUri(scheme, Part.fromDecoded(ssp),
+ Part.fromDecoded(fragment));
+ }
+
+ /**
+ * Opaque URI.
+ */
+ private static class OpaqueUri extends Uri {
+
+ /** Used in parcelling. */
+ static final int TYPE_ID = 2;
+
+ private final String scheme;
+ private final Part ssp;
+ private final Part fragment;
+
+ private OpaqueUri(String scheme, Part ssp, Part fragment) {
+ this.scheme = scheme;
+ this.ssp = ssp;
+ this.fragment = fragment == null ? Part.NULL : fragment;
+ }
+
+ static Uri readFrom(Parcel parcel) {
+ return new OpaqueUri(
+ parcel.readString(),
+ Part.readFrom(parcel),
+ Part.readFrom(parcel)
+ );
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(TYPE_ID);
+ parcel.writeString(scheme);
+ ssp.writeTo(parcel);
+ fragment.writeTo(parcel);
+ }
+
+ public boolean isHierarchical() {
+ return false;
+ }
+
+ public boolean isRelative() {
+ return scheme == null;
+ }
+
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ public String getEncodedSchemeSpecificPart() {
+ return ssp.getEncoded();
+ }
+
+ public String getSchemeSpecificPart() {
+ return ssp.getDecoded();
+ }
+
+ public String getAuthority() {
+ return null;
+ }
+
+ public String getEncodedAuthority() {
+ return null;
+ }
+
+ public String getPath() {
+ return null;
+ }
+
+ public String getEncodedPath() {
+ return null;
+ }
+
+ public String getQuery() {
+ return null;
+ }
+
+ public String getEncodedQuery() {
+ return null;
+ }
+
+ public String getFragment() {
+ return fragment.getDecoded();
+ }
+
+ public String getEncodedFragment() {
+ return fragment.getEncoded();
+ }
+
+ public List<String> getPathSegments() {
+ return Collections.emptyList();
+ }
+
+ public String getLastPathSegment() {
+ return null;
+ }
+
+ public String getUserInfo() {
+ return null;
+ }
+
+ public String getEncodedUserInfo() {
+ return null;
+ }
+
+ public String getHost() {
+ return null;
+ }
+
+ public int getPort() {
+ return -1;
+ }
+
+ private volatile String cachedString = NOT_CACHED;
+
+ public String toString() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = cachedString != NOT_CACHED;
+ if (cached) {
+ return cachedString;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(scheme).append(':');
+ sb.append(getEncodedSchemeSpecificPart());
+
+ if (!fragment.isEmpty()) {
+ sb.append('#').append(fragment.getEncoded());
+ }
+
+ return cachedString = sb.toString();
+ }
+
+ public Builder buildUpon() {
+ return new Builder()
+ .scheme(this.scheme)
+ .opaquePart(this.ssp)
+ .fragment(this.fragment);
+ }
+ }
+
+ /**
+ * Wrapper for path segment array.
+ */
+ static class PathSegments extends AbstractList<String>
+ implements RandomAccess {
+
+ static final PathSegments EMPTY = new PathSegments(null, 0);
+
+ final String[] segments;
+ final int size;
+
+ PathSegments(String[] segments, int size) {
+ this.segments = segments;
+ this.size = size;
+ }
+
+ public String get(int index) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ return segments[index];
+ }
+
+ public int size() {
+ return this.size;
+ }
+ }
+
+ /**
+ * Builds PathSegments.
+ */
+ static class PathSegmentsBuilder {
+
+ String[] segments;
+ int size = 0;
+
+ void add(String segment) {
+ if (segments == null) {
+ segments = new String[4];
+ } else if (size + 1 == segments.length) {
+ String[] expanded = new String[segments.length * 2];
+ System.arraycopy(segments, 0, expanded, 0, segments.length);
+ segments = expanded;
+ }
+
+ segments[size++] = segment;
+ }
+
+ PathSegments build() {
+ if (segments == null) {
+ return PathSegments.EMPTY;
+ }
+
+ try {
+ return new PathSegments(segments, size);
+ } finally {
+ // Makes sure this doesn't get reused.
+ segments = null;
+ }
+ }
+ }
+
+ /**
+ * Support for hierarchical URIs.
+ */
+ private abstract static class AbstractHierarchicalUri extends Uri {
+
+ public String getLastPathSegment() {
+ // TODO: If we haven't parsed all of the segments already, just
+ // grab the last one directly so we only allocate one string.
+
+ List<String> segments = getPathSegments();
+ int size = segments.size();
+ if (size == 0) {
+ return null;
+ }
+ return segments.get(size - 1);
+ }
+
+ private Part userInfo;
+
+ private Part getUserInfoPart() {
+ return userInfo == null
+ ? userInfo = Part.fromEncoded(parseUserInfo()) : userInfo;
+ }
+
+ public final String getEncodedUserInfo() {
+ return getUserInfoPart().getEncoded();
+ }
+
+ private String parseUserInfo() {
+ String authority = getEncodedAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ int end = authority.indexOf('@');
+ return end == NOT_FOUND ? null : authority.substring(0, end);
+ }
+
+ public String getUserInfo() {
+ return getUserInfoPart().getDecoded();
+ }
+
+ private volatile String host = NOT_CACHED;
+
+ public String getHost() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = (host != NOT_CACHED);
+ return cached ? host
+ : (host = parseHost());
+ }
+
+ private String parseHost() {
+ String authority = getAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ // Parse out user info and then port.
+ int userInfoSeparator = authority.indexOf('@');
+ int portSeparator = authority.indexOf(':', userInfoSeparator);
+
+ return portSeparator == NOT_FOUND
+ ? authority.substring(userInfoSeparator + 1)
+ : authority.substring(userInfoSeparator + 1, portSeparator);
+ }
+
+ private volatile int port = NOT_CALCULATED;
+
+ public int getPort() {
+ return port == NOT_CALCULATED
+ ? port = parsePort()
+ : port;
+ }
+
+ private int parsePort() {
+ String authority = getAuthority();
+ if (authority == null) {
+ return -1;
+ }
+
+ // Make sure we look for the port separtor *after* the user info
+ // separator. We have URLs with a ':' in the user info.
+ int userInfoSeparator = authority.indexOf('@');
+ int portSeparator = authority.indexOf(':', userInfoSeparator);
+
+ if (portSeparator == NOT_FOUND) {
+ return -1;
+ }
+
+ String portString = authority.substring(portSeparator + 1);
+ try {
+ return Integer.parseInt(portString);
+ } catch (NumberFormatException e) {
+ Log.w(LOG, "Error parsing port string.", e);
+ return -1;
+ }
+ }
+ }
+
+ /**
+ * Hierarchical Uri.
+ */
+ private static class HierarchicalUri extends AbstractHierarchicalUri {
+
+ /** Used in parcelling. */
+ static final int TYPE_ID = 3;
+
+ private final String scheme;
+ private final Part authority;
+ private final PathPart path;
+ private final Part query;
+ private final Part fragment;
+
+ private HierarchicalUri(String scheme, Part authority, PathPart path,
+ Part query, Part fragment) {
+ this.scheme = scheme;
+ this.authority = authority;
+ this.path = path;
+ this.query = query;
+ this.fragment = fragment;
+ }
+
+ static Uri readFrom(Parcel parcel) {
+ return new HierarchicalUri(
+ parcel.readString(),
+ Part.readFrom(parcel),
+ PathPart.readFrom(parcel),
+ Part.readFrom(parcel),
+ Part.readFrom(parcel)
+ );
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(TYPE_ID);
+ parcel.writeString(scheme);
+ authority.writeTo(parcel);
+ path.writeTo(parcel);
+ query.writeTo(parcel);
+ fragment.writeTo(parcel);
+ }
+
+ public boolean isHierarchical() {
+ return true;
+ }
+
+ public boolean isRelative() {
+ return scheme == null;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ private Part ssp;
+
+ private Part getSsp() {
+ return ssp == null
+ ? ssp = Part.fromEncoded(makeSchemeSpecificPart()) : ssp;
+ }
+
+ public String getEncodedSchemeSpecificPart() {
+ return getSsp().getEncoded();
+ }
+
+ public String getSchemeSpecificPart() {
+ return getSsp().getDecoded();
+ }
+
+ /**
+ * Creates the encoded scheme-specific part from its sub parts.
+ */
+ private String makeSchemeSpecificPart() {
+ StringBuilder builder = new StringBuilder();
+ appendSspTo(builder);
+ return builder.toString();
+ }
+
+ private void appendSspTo(StringBuilder builder) {
+ if (authority != null) {
+ String encodedAuthority = authority.getEncoded();
+ if (encodedAuthority != null) {
+ // Even if the authority is "", we still want to append "//".
+ builder.append("//").append(encodedAuthority);
+ }
+ }
+
+ // path is never null.
+ String encodedPath = path.getEncoded();
+ if (encodedPath != null) {
+ builder.append(encodedPath);
+ }
+
+ if (query != null && !query.isEmpty()) {
+ builder.append('?').append(query.getEncoded());
+ }
+ }
+
+ public String getAuthority() {
+ return this.authority.getDecoded();
+ }
+
+ public String getEncodedAuthority() {
+ return this.authority.getEncoded();
+ }
+
+ public String getEncodedPath() {
+ return this.path.getEncoded();
+ }
+
+ public String getPath() {
+ return this.path.getDecoded();
+ }
+
+ public String getQuery() {
+ return this.query.getDecoded();
+ }
+
+ public String getEncodedQuery() {
+ return this.query.getEncoded();
+ }
+
+ public String getFragment() {
+ return this.fragment.getDecoded();
+ }
+
+ public String getEncodedFragment() {
+ return this.fragment.getEncoded();
+ }
+
+ public List<String> getPathSegments() {
+ return this.path.getPathSegments();
+ }
+
+ private volatile String uriString = NOT_CACHED;
+
+ @Override
+ public String toString() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = (uriString != NOT_CACHED);
+ return cached ? uriString
+ : (uriString = makeUriString());
+ }
+
+ private String makeUriString() {
+ StringBuilder builder = new StringBuilder();
+
+ if (scheme != null) {
+ builder.append(scheme).append(':');
+ }
+
+ appendSspTo(builder);
+
+ if (fragment != null && !fragment.isEmpty()) {
+ builder.append('#').append(fragment.getEncoded());
+ }
+
+ return builder.toString();
+ }
+
+ public Builder buildUpon() {
+ return new Builder()
+ .scheme(scheme)
+ .authority(authority)
+ .path(path)
+ .query(query)
+ .fragment(fragment);
+ }
+ }
+
+ /**
+ * Helper class for building or manipulating URI references. Not safe for
+ * concurrent use.
+ *
+ * <p>An absolute hierarchical URI reference follows the pattern:
+ * {@code &lt;scheme&gt;://&lt;authority&gt;&lt;absolute path&gt;?&lt;query&gt;#&lt;fragment&gt;}
+ *
+ * <p>Relative URI references (which are always hierarchical) follow one
+ * of two patterns: {@code &lt;relative or absolute path&gt;?&lt;query&gt;#&lt;fragment&gt;}
+ * or {@code //&lt;authority&gt;&lt;absolute path&gt;?&lt;query&gt;#&lt;fragment&gt;}
+ *
+ * <p>An opaque URI follows this pattern:
+ * {@code &lt;scheme&gt;:&lt;opaque part&gt;#&lt;fragment&gt;}
+ */
+ 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.
+ *
+ * <p>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.
+ *
+ * <p>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;
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Searches the query string for parameter values with the given key.
+ *
+ * @param key which will be encoded
+ *
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ * @throws NullPointerException if key is null
+ *
+ * @return a list of decoded values
+ */
+ public List<String> getQueryParameters(String key) {
+ if (isOpaque()) {
+ throw new UnsupportedOperationException(NOT_HIERARCHICAL);
+ }
+
+ String query = getQuery();
+ if (query == null) {
+ return Collections.emptyList();
+ }
+
+ String encodedKey;
+ try {
+ encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+
+ // Prepend query with "&" making the first parameter the same as the
+ // rest.
+ query = "&" + query;
+
+ // Parameter prefix.
+ String prefix = "&" + encodedKey + "=";
+
+ ArrayList<String> values = new ArrayList<String>();
+
+ int start = 0;
+ int length = query.length();
+ while (start < length) {
+ start = query.indexOf(prefix, start);
+
+ if (start == -1) {
+ // No more values.
+ break;
+ }
+
+ // Move start to start of value.
+ start += prefix.length();
+
+ // Find end of value.
+ int end = query.indexOf('&', start);
+ if (end == -1) {
+ end = query.length();
+ }
+
+ String value = query.substring(start, end);
+ values.add(decode(value));
+
+ start = end;
+ }
+
+ return Collections.unmodifiableList(values);
+ }
+
+ /**
+ * Searches the query string for the first value with the given key.
+ *
+ * @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);
+ }
+
+ String query = getQuery();
+
+ if (query == null) {
+ return null;
+ }
+
+ String encodedKey;
+ try {
+ encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+
+ String prefix = encodedKey + "=";
+
+ if (query.length() < prefix.length()) {
+ return null;
+ }
+
+ int start;
+ if (query.startsWith(prefix)) {
+ // It's the first parameter.
+ start = prefix.length();
+ } else {
+ // It must be later in the query string.
+ prefix = "&" + prefix;
+ start = query.indexOf(prefix);
+
+ if (start == -1) {
+ // Not found.
+ return null;
+ }
+
+ start += prefix.length();
+ }
+
+ // Find end of value.
+ int end = query.indexOf('&', start);
+ if (end == -1) {
+ end = query.length();
+ }
+
+ String value = query.substring(start, end);
+ return decode(value);
+ }
+
+ /** Identifies a null parcelled Uri. */
+ private static final int NULL_TYPE_ID = 0;
+
+ /**
+ * Reads Uris from Parcels.
+ */
+ public static final Parcelable.Creator<Uri> CREATOR
+ = new Parcelable.Creator<Uri>() {
+ public Uri createFromParcel(Parcel in) {
+ int type = in.readInt();
+ switch (type) {
+ case NULL_TYPE_ID: return null;
+ case StringUri.TYPE_ID: return StringUri.readFrom(in);
+ case OpaqueUri.TYPE_ID: return OpaqueUri.readFrom(in);
+ case HierarchicalUri.TYPE_ID:
+ return HierarchicalUri.readFrom(in);
+ }
+
+ throw new AssertionError("Unknown URI type: " + type);
+ }
+
+ public Uri[] newArray(int size) {
+ return new Uri[size];
+ }
+ };
+
+ /**
+ * Writes a Uri to a Parcel.
+ *
+ * @param out parcel to write to
+ * @param uri to write, can be null
+ */
+ public static void writeToParcel(Parcel out, Uri uri) {
+ if (uri == null) {
+ out.writeInt(NULL_TYPE_ID);
+ } else {
+ uri.writeToParcel(out, 0);
+ }
+ }
+
+ 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);
+ }
+
+ /** Unicode replacement character: \\uFFFD. */
+ private static final byte[] REPLACEMENT = { (byte) 0xFF, (byte) 0xFD };
+
+ /**
+ * 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) {
+ /*
+ Compared to java.net.URLEncoderDecoder.decode(), this method decodes a
+ chunk at a time instead of one character at a time, and it doesn't
+ throw exceptions. It also only allocates memory when necessary--if
+ there's nothing to decode, this method won't do much.
+ */
+
+ if (s == null) {
+ return null;
+ }
+
+ // Lazily-initialized buffers.
+ StringBuilder decoded = null;
+ ByteArrayOutputStream out = null;
+
+ int oldLength = s.length();
+
+ // This loop alternates between copying over normal characters and
+ // escaping in chunks. This results in fewer method calls and
+ // allocations than decoding one character at a time.
+ int current = 0;
+ while (current < oldLength) {
+ // Start in "copying" mode where we copy over normal characters.
+
+ // Find the next escape sequence.
+ int nextEscape = s.indexOf('%', current);
+
+ if (nextEscape == NOT_FOUND) {
+ if (decoded == null) {
+ // We didn't actually decode anything.
+ return s;
+ } else {
+ // Append the remainder and return the decoded string.
+ decoded.append(s, current, oldLength);
+ return decoded.toString();
+ }
+ }
+
+ // Prepare buffers.
+ if (decoded == null) {
+ // Looks like we're going to need the buffers...
+ // We know the new string will be shorter. Using the old length
+ // may overshoot a bit, but it will save us from resizing the
+ // buffer.
+ decoded = new StringBuilder(oldLength);
+ out = new ByteArrayOutputStream(4);
+ } else {
+ // Clear decoding buffer.
+ out.reset();
+ }
+
+ // Append characters leading up to the escape.
+ if (nextEscape > current) {
+ decoded.append(s, current, nextEscape);
+
+ current = nextEscape;
+ } else {
+ // assert current == nextEscape
+ }
+
+ // Switch to "decoding" mode where we decode a string of escape
+ // sequences.
+
+ // Decode and append escape sequences. Escape sequences look like
+ // "%ab" where % is literal and a and b are hex digits.
+ try {
+ do {
+ if (current + 2 >= oldLength) {
+ // Truncated escape sequence.
+ out.write(REPLACEMENT);
+ } else {
+ int a = Character.digit(s.charAt(current + 1), 16);
+ int b = Character.digit(s.charAt(current + 2), 16);
+
+ if (a == -1 || b == -1) {
+ // Non hex digits.
+ out.write(REPLACEMENT);
+ } else {
+ // Combine the hex digits into one byte and write.
+ out.write((a << 4) + b);
+ }
+ }
+
+ // Move passed the escape sequence.
+ current += 3;
+ } while (current < oldLength && s.charAt(current) == '%');
+
+ // Decode UTF-8 bytes into a string and append it.
+ decoded.append(out.toString(DEFAULT_ENCODING));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ // If we don't have a buffer, we didn't have to decode anything.
+ return decoded == null ? s : decoded.toString();
+ }
+
+ /**
+ * Support for part implementations.
+ */
+ static abstract class AbstractPart {
+
+ /**
+ * Enum which indicates which representation of a given part we have.
+ */
+ static class Representation {
+ static final int BOTH = 0;
+ static final int ENCODED = 1;
+ static final int DECODED = 2;
+ }
+
+ volatile String encoded;
+ volatile String decoded;
+
+ AbstractPart(String encoded, String decoded) {
+ this.encoded = encoded;
+ this.decoded = decoded;
+ }
+
+ abstract String getEncoded();
+
+ final String getDecoded() {
+ @SuppressWarnings("StringEquality")
+ boolean hasDecoded = decoded != NOT_CACHED;
+ return hasDecoded ? decoded : (decoded = decode(encoded));
+ }
+
+ final void writeTo(Parcel parcel) {
+ @SuppressWarnings("StringEquality")
+ boolean hasEncoded = encoded != NOT_CACHED;
+
+ @SuppressWarnings("StringEquality")
+ boolean hasDecoded = decoded != NOT_CACHED;
+
+ if (hasEncoded && hasDecoded) {
+ parcel.writeInt(Representation.BOTH);
+ parcel.writeString(encoded);
+ parcel.writeString(decoded);
+ } else if (hasEncoded) {
+ parcel.writeInt(Representation.ENCODED);
+ parcel.writeString(encoded);
+ } else if (hasDecoded) {
+ parcel.writeInt(Representation.DECODED);
+ parcel.writeString(decoded);
+ } else {
+ throw new AssertionError();
+ }
+ }
+ }
+
+ /**
+ * 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 != NOT_CACHED;
+ return hasEncoded ? encoded : (encoded = encode(decoded));
+ }
+
+ static Part readFrom(Parcel parcel) {
+ int representation = parcel.readInt();
+ switch (representation) {
+ case Representation.BOTH:
+ return from(parcel.readString(), parcel.readString());
+ case Representation.ENCODED:
+ return fromEncoded(parcel.readString());
+ case Representation.DECODED:
+ return fromDecoded(parcel.readString());
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * 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, NOT_CACHED);
+ }
+
+ /**
+ * Creates a part from the decoded string.
+ *
+ * @param decoded part string
+ */
+ static Part fromDecoded(String decoded) {
+ return from(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
+ // 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);
+ }
+
+ @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 != 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);
+ }
+
+ static PathPart readFrom(Parcel parcel) {
+ int representation = parcel.readInt();
+ switch (representation) {
+ case Representation.BOTH:
+ return from(parcel.readString(), parcel.readString());
+ case Representation.ENCODED:
+ return fromEncoded(parcel.readString());
+ case Representation.DECODED:
+ return fromDecoded(parcel.readString());
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Creates a path from the encoded string.
+ *
+ * @param encoded part string
+ */
+ static PathPart fromEncoded(String encoded) {
+ return from(encoded, NOT_CACHED);
+ }
+
+ /**
+ * Creates a path from the decoded string.
+ *
+ * @param decoded part string
+ */
+ static PathPart fromDecoded(String decoded) {
+ return from(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 != 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 : NOT_CACHED;
+
+ // Prepend decoded string if present.
+ @SuppressWarnings("StringEquality")
+ boolean decodedCached = oldPart.decoded != NOT_CACHED;
+ String newDecoded = decodedCached
+ ? "/" + oldPart.decoded
+ : NOT_CACHED;
+
+ return new PathPart(newEncoded, newDecoded);
+ }
+ }
+
+ /**
+ * Creates a new Uri by encoding and appending a path segment to a base Uri.
+ *
+ * @param baseUri Uri to append path segment to
+ * @param pathSegment to encode and append
+ * @return a new Uri based on baseUri with the given segment encoded and
+ * 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();
+ }
+}
diff --git a/core/java/android/net/UrlQuerySanitizer.java b/core/java/android/net/UrlQuerySanitizer.java
new file mode 100644
index 0000000..70e50b7
--- /dev/null
+++ b/core/java/android/net/UrlQuerySanitizer.java
@@ -0,0 +1,913 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+/**
+ *
+ * Sanitizes the Query portion of a URL. Simple example:
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
+ * sanitizer.setAllowUnregisteredParamaters(true);
+ * sanitizer.parseUrl("http://example.com/?name=Joe+User");
+ * String name = sanitizer.getValue("name"));
+ * // name now contains "Joe_User"
+ * </code>
+ *
+ * Register ValueSanitizers to customize the way individual
+ * parameters are sanitized:
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
+ * sanitizer.registerParamater("name", UrlQuerySanitizer.createSpaceLegal());
+ * sanitizer.parseUrl("http://example.com/?name=Joe+User");
+ * String name = sanitizer.getValue("name"));
+ * // name now contains "Joe User". (The string is first decoded, which
+ * // converts the '+' to a ' '. Then the string is sanitized, which
+ * // converts the ' ' to an '_'. (The ' ' is converted because the default
+ * unregistered parameter sanitizer does not allow any special characters,
+ * and ' ' is a special character.)
+ * </code>
+ *
+ * There are several ways to create ValueSanitizers. In order of increasing
+ * sophistication:
+ * <ol>
+ * <li>Call one of the UrlQuerySanitizer.createXXX() methods.
+ * <li>Construct your own instance of
+ * UrlQuerySanitizer.IllegalCharacterValueSanitizer.
+ * <li>Subclass UrlQuerySanitizer.ValueSanitizer to define your own value
+ * sanitizer.
+ * </ol>
+ *
+ */
+public class UrlQuerySanitizer {
+
+ /**
+ * A simple tuple that holds parameter-value pairs.
+ *
+ */
+ public class ParameterValuePair {
+ /**
+ * Construct a parameter-value tuple.
+ * @param parameter an unencoded parameter
+ * @param value an unencoded value
+ */
+ public ParameterValuePair(String parameter,
+ String value) {
+ mParameter = parameter;
+ mValue = value;
+ }
+ /**
+ * The unencoded parameter
+ */
+ public String mParameter;
+ /**
+ * The unencoded value
+ */
+ public String mValue;
+ }
+
+ final private HashMap<String, ValueSanitizer> mSanitizers =
+ new HashMap<String, ValueSanitizer>();
+ final private HashMap<String, String> mEntries =
+ new HashMap<String, String>();
+ final private ArrayList<ParameterValuePair> mEntriesList =
+ new ArrayList<ParameterValuePair>();
+ private boolean mAllowUnregisteredParamaters;
+ private boolean mPreferFirstRepeatedParameter;
+ private ValueSanitizer mUnregisteredParameterValueSanitizer =
+ getAllIllegal();
+
+ /**
+ * A functor used to sanitize a single query value.
+ *
+ */
+ public static interface ValueSanitizer {
+ /**
+ * Sanitize an unencoded value.
+ * @param value
+ * @return the sanitized unencoded value
+ */
+ public String sanitize(String value);
+ }
+
+ /**
+ * Sanitize values based on which characters they contain. Illegal
+ * characters are replaced with either space or '_', depending upon
+ * whether space is a legal character or not.
+ */
+ public static class IllegalCharacterValueSanitizer implements
+ ValueSanitizer {
+ private int mFlags;
+
+ /**
+ * Allow space (' ') characters.
+ */
+ public final static int SPACE_OK = 1 << 0;
+ /**
+ * Allow whitespace characters other than space. The
+ * other whitespace characters are
+ * '\t' '\f' '\n' '\r' and '\0x000b' (vertical tab)
+ */
+ public final static int OTHER_WHITESPACE_OK = 1 << 1;
+ /**
+ * Allow characters with character codes 128 to 255.
+ */
+ public final static int NON_7_BIT_ASCII_OK = 1 << 2;
+ /**
+ * Allow double quote characters. ('"')
+ */
+ public final static int DQUOTE_OK = 1 << 3;
+ /**
+ * Allow single quote characters. ('\'')
+ */
+ public final static int SQUOTE_OK = 1 << 4;
+ /**
+ * Allow less-than characters. ('<')
+ */
+ public final static int LT_OK = 1 << 5;
+ /**
+ * Allow greater-than characters. ('>')
+ */
+ public final static int GT_OK = 1 << 6;
+ /**
+ * Allow ampersand characters ('&')
+ */
+ public final static int AMP_OK = 1 << 7;
+ /**
+ * Allow percent-sign characters ('%')
+ */
+ public final static int PCT_OK = 1 << 8;
+ /**
+ * Allow nul characters ('\0')
+ */
+ public final static int NUL_OK = 1 << 9;
+ /**
+ * Allow text to start with a script URL
+ * such as "javascript:" or "vbscript:"
+ */
+ public final static int SCRIPT_URL_OK = 1 << 10;
+
+ /**
+ * Mask with all fields set to OK
+ */
+ public final static int ALL_OK = 0x7ff;
+
+ /**
+ * Mask with both regular space and other whitespace OK
+ */
+ public final static int ALL_WHITESPACE_OK =
+ SPACE_OK | OTHER_WHITESPACE_OK;
+
+
+ // Common flag combinations:
+
+ /**
+ * <ul>
+ * <li>Deny all special characters.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int ALL_ILLEGAL =
+ 0;
+ /**
+ * <ul>
+ * <li>Allow all special characters except Nul. ('\0').
+ * <li>Allow script URLs.
+ * </ul>
+ */
+ public final static int ALL_BUT_NUL_LEGAL =
+ ALL_OK & ~NUL_OK;
+ /**
+ * <ul>
+ * <li>Allow all special characters except for:
+ * <ul>
+ * <li>whitespace characters
+ * <li>Nul ('\0')
+ * </ul>
+ * <li>Allow script URLs.
+ * </ul>
+ */
+ public final static int ALL_BUT_WHITESPACE_LEGAL =
+ ALL_OK & ~(ALL_WHITESPACE_OK | NUL_OK);
+ /**
+ * <ul>
+ * <li>Allow characters used by encoded URLs.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int URL_LEGAL =
+ NON_7_BIT_ASCII_OK | SQUOTE_OK | AMP_OK | PCT_OK;
+ /**
+ * <ul>
+ * <li>Allow characters used by encoded URLs.
+ * <li>Allow spaces.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int URL_AND_SPACE_LEGAL =
+ URL_LEGAL | SPACE_OK;
+ /**
+ * <ul>
+ * <li>Allow ampersand.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int AMP_LEGAL =
+ AMP_OK;
+ /**
+ * <ul>
+ * <li>Allow ampersand.
+ * <li>Allow space.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int AMP_AND_SPACE_LEGAL =
+ AMP_OK | SPACE_OK;
+ /**
+ * <ul>
+ * <li>Allow space.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int SPACE_LEGAL =
+ SPACE_OK;
+ /**
+ * <ul>
+ * <li>Allow all but.
+ * <ul>
+ * <li>Nul ('\0')
+ * <li>Angle brackets ('<', '>')
+ * </ul>
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL =
+ ALL_OK & ~(NUL_OK | LT_OK | GT_OK);
+
+ /**
+ * Script URL definitions
+ */
+
+ private final static String JAVASCRIPT_PREFIX = "javascript:";
+
+ private final static String VBSCRIPT_PREFIX = "vbscript:";
+
+ private final static int MIN_SCRIPT_PREFIX_LENGTH = Math.min(
+ JAVASCRIPT_PREFIX.length(), VBSCRIPT_PREFIX.length());
+
+ /**
+ * Construct a sanitizer. The parameters set the behavior of the
+ * sanitizer.
+ * @param flags some combination of the XXX_OK flags.
+ */
+ public IllegalCharacterValueSanitizer(
+ int flags) {
+ mFlags = flags;
+ }
+ /**
+ * Sanitize a value.
+ * <ol>
+ * <li>If script URLs are not OK, the will be removed.
+ * <li>If neither spaces nor other white space is OK, then
+ * white space will be trimmed from the beginning and end of
+ * the URL. (Just the actual white space characters are trimmed, not
+ * other control codes.)
+ * <li> Illegal characters will be replaced with
+ * either ' ' or '_', depending on whether a space is itself a
+ * legal character.
+ * </ol>
+ * @param value
+ * @return the sanitized value
+ */
+ public String sanitize(String value) {
+ if (value == null) {
+ return null;
+ }
+ int length = value.length();
+ if ((mFlags & SCRIPT_URL_OK) != 0) {
+ if (length >= MIN_SCRIPT_PREFIX_LENGTH) {
+ String asLower = value.toLowerCase();
+ if (asLower.startsWith(JAVASCRIPT_PREFIX) ||
+ asLower.startsWith(VBSCRIPT_PREFIX)) {
+ return "";
+ }
+ }
+ }
+
+ // If whitespace isn't OK, get rid of whitespace at beginning
+ // and end of value.
+ if ( (mFlags & ALL_WHITESPACE_OK) == 0) {
+ value = trimWhitespace(value);
+ // The length could have changed, so we need to correct
+ // the length variable.
+ length = value.length();
+ }
+
+ StringBuilder stringBuilder = new StringBuilder(length);
+ for(int i = 0; i < length; i++) {
+ char c = value.charAt(i);
+ if (!characterIsLegal(c)) {
+ if ((mFlags & SPACE_OK) != 0) {
+ c = ' ';
+ }
+ else {
+ c = '_';
+ }
+ }
+ stringBuilder.append(c);
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Trim whitespace from the beginning and end of a string.
+ * <p>
+ * Note: can't use {@link String#trim} because {@link String#trim} has a
+ * different definition of whitespace than we want.
+ * @param value the string to trim
+ * @return the trimmed string
+ */
+ private String trimWhitespace(String value) {
+ int start = 0;
+ int last = value.length() - 1;
+ int end = last;
+ while (start <= end && isWhitespace(value.charAt(start))) {
+ start++;
+ }
+ while (end >= start && isWhitespace(value.charAt(end))) {
+ end--;
+ }
+ if (start == 0 && end == last) {
+ return value;
+ }
+ return value.substring(start, end + 1);
+ }
+
+ /**
+ * Check if c is whitespace.
+ * @param c character to test
+ * @return true if c is a whitespace character
+ */
+ private boolean isWhitespace(char c) {
+ switch(c) {
+ case ' ':
+ case '\t':
+ case '\f':
+ case '\n':
+ case '\r':
+ case 11: /* VT */
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check whether an individual character is legal. Uses the
+ * flag bit-set passed into the constructor.
+ * @param c
+ * @return true if c is a legal character
+ */
+ private boolean characterIsLegal(char c) {
+ switch(c) {
+ case ' ' : return (mFlags & SPACE_OK) != 0;
+ case '\t': case '\f': case '\n': case '\r': case 11: /* VT */
+ return (mFlags & OTHER_WHITESPACE_OK) != 0;
+ case '\"': return (mFlags & DQUOTE_OK) != 0;
+ case '\'': return (mFlags & SQUOTE_OK) != 0;
+ case '<' : return (mFlags & LT_OK) != 0;
+ case '>' : return (mFlags & GT_OK) != 0;
+ case '&' : return (mFlags & AMP_OK) != 0;
+ case '%' : return (mFlags & PCT_OK) != 0;
+ case '\0': return (mFlags & NUL_OK) != 0;
+ default : return (c >= 32 && c < 127) ||
+ (c >= 128 && c <= 255 && ((mFlags & NON_7_BIT_ASCII_OK) != 0));
+ }
+ }
+ }
+
+ /**
+ * Get the current value sanitizer used when processing
+ * unregistered parameter values.
+ * <p>
+ * <b>Note:</b> The default unregistered parameter value sanitizer is
+ * one that doesn't allow any special characters, similar to what
+ * is returned by calling createAllIllegal.
+ *
+ * @return the current ValueSanitizer used to sanitize unregistered
+ * parameter values.
+ */
+ public ValueSanitizer getUnregisteredParameterValueSanitizer() {
+ return mUnregisteredParameterValueSanitizer;
+ }
+
+ /**
+ * Set the value sanitizer used when processing unregistered
+ * parameter values.
+ * @param sanitizer set the ValueSanitizer used to sanitize unregistered
+ * parameter values.
+ */
+ public void setUnregisteredParameterValueSanitizer(
+ ValueSanitizer sanitizer) {
+ mUnregisteredParameterValueSanitizer = sanitizer;
+ }
+
+
+ // Private fields for singleton sanitizers:
+
+ private static final ValueSanitizer sAllIllegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_ILLEGAL);
+
+ private static final ValueSanitizer sAllButNulLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_BUT_NUL_LEGAL);
+
+ private static final ValueSanitizer sAllButWhitespaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_BUT_WHITESPACE_LEGAL);
+
+ private static final ValueSanitizer sURLLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.URL_LEGAL);
+
+ private static final ValueSanitizer sUrlAndSpaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.URL_AND_SPACE_LEGAL);
+
+ private static final ValueSanitizer sAmpLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.AMP_LEGAL);
+
+ private static final ValueSanitizer sAmpAndSpaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.AMP_AND_SPACE_LEGAL);
+
+ private static final ValueSanitizer sSpaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.SPACE_LEGAL);
+
+ private static final ValueSanitizer sAllButNulAndAngleBracketsLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL);
+
+ /**
+ * Return a value sanitizer that does not allow any special characters,
+ * and also does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllIllegal() {
+ return sAllIllegal;
+ }
+
+ /**
+ * Return a value sanitizer that allows everything except Nul ('\0')
+ * characters. Script URLs are allowed.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllButNulLegal() {
+ return sAllButNulLegal;
+ }
+ /**
+ * Return a value sanitizer that allows everything except Nul ('\0')
+ * characters, space (' '), and other whitespace characters.
+ * Script URLs are allowed.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllButWhitespaceLegal() {
+ return sAllButWhitespaceLegal;
+ }
+ /**
+ * Return a value sanitizer that allows all the characters used by
+ * encoded URLs. Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getUrlLegal() {
+ return sURLLegal;
+ }
+ /**
+ * Return a value sanitizer that allows all the characters used by
+ * encoded URLs and allows spaces, which are not technically legal
+ * in encoded URLs, but commonly appear anyway.
+ * Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getUrlAndSpaceLegal() {
+ return sUrlAndSpaceLegal;
+ }
+ /**
+ * Return a value sanitizer that does not allow any special characters
+ * except ampersand ('&'). Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAmpLegal() {
+ return sAmpLegal;
+ }
+ /**
+ * Return a value sanitizer that does not allow any special characters
+ * except ampersand ('&') and space (' '). Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAmpAndSpaceLegal() {
+ return sAmpAndSpaceLegal;
+ }
+ /**
+ * Return a value sanitizer that does not allow any special characters
+ * except space (' '). Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getSpaceLegal() {
+ return sSpaceLegal;
+ }
+ /**
+ * Return a value sanitizer that allows any special characters
+ * except angle brackets ('<' and '>') and Nul ('\0').
+ * Allows script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllButNulAndAngleBracketsLegal() {
+ return sAllButNulAndAngleBracketsLegal;
+ }
+
+ /**
+ * Constructs a UrlQuerySanitizer.
+ * <p>
+ * Defaults:
+ * <ul>
+ * <li>unregistered parameters are not allowed.
+ * <li>the last instance of a repeated parameter is preferred.
+ * <li>The default value sanitizer is an AllIllegal value sanitizer.
+ * <ul>
+ */
+ public UrlQuerySanitizer() {
+ }
+
+ /**
+ * Constructs a UrlQuerySanitizer and parse a URL.
+ * This constructor is provided for convenience when the
+ * default parsing behavior is acceptable.
+ * <p>
+ * Because the URL is parsed before the constructor returns, there isn't
+ * a chance to configure the sanitizer to change the parsing behavior.
+ * <p>
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(myUrl);
+ * String name = sanitizer.getValue("name");
+ * </code>
+ * <p>
+ * Defaults:
+ * <ul>
+ * <li>unregistered parameters <em>are</em> allowed.
+ * <li>the last instance of a repeated parameter is preferred.
+ * <li>The default value sanitizer is an AllIllegal value sanitizer.
+ * <ul>
+ */
+ public UrlQuerySanitizer(String url) {
+ setAllowUnregisteredParamaters(true);
+ parseUrl(url);
+ }
+
+ /**
+ * Parse the query parameters out of an encoded URL.
+ * Works by extracting the query portion from the URL and then
+ * calling parseQuery(). If there is no query portion it is
+ * treated as if the query portion is an empty string.
+ * @param url the encoded URL to parse.
+ */
+ public void parseUrl(String url) {
+ int queryIndex = url.indexOf('?');
+ String query;
+ if (queryIndex >= 0) {
+ query = url.substring(queryIndex + 1);
+ }
+ else {
+ query = "";
+ }
+ parseQuery(query);
+ }
+
+ /**
+ * Parse a query. A query string is any number of parameter-value clauses
+ * separated by any non-zero number of ampersands. A parameter-value clause
+ * is a parameter followed by an equal sign, followed by a value. If the
+ * equal sign is missing, the value is assumed to be the empty string.
+ * @param query the query to parse.
+ */
+ public void parseQuery(String query) {
+ clear();
+ // Split by '&'
+ StringTokenizer tokenizer = new StringTokenizer(query, "&");
+ while(tokenizer.hasMoreElements()) {
+ String attributeValuePair = tokenizer.nextToken();
+ if (attributeValuePair.length() > 0) {
+ int assignmentIndex = attributeValuePair.indexOf('=');
+ if (assignmentIndex < 0) {
+ // No assignment found, treat as if empty value
+ parseEntry(attributeValuePair, "");
+ }
+ else {
+ parseEntry(attributeValuePair.substring(0, assignmentIndex),
+ attributeValuePair.substring(assignmentIndex + 1));
+ }
+ }
+ }
+ }
+
+ /**
+ * Get a set of all of the parameters found in the sanitized query.
+ * <p>
+ * Note: Do not modify this set. Treat it as a read-only set.
+ * @return all the parameters found in the current query.
+ */
+ public Set<String> getParameterSet() {
+ return mEntries.keySet();
+ }
+
+ /**
+ * An array list of all of the parameter value pairs in the sanitized
+ * query, in the order they appeared in the query. May contain duplicate
+ * parameters.
+ * <p class="note"><b>Note:</b> Do not modify this list. Treat it as a read-only list.</p>
+ */
+ public List<ParameterValuePair> getParameterList() {
+ return mEntriesList;
+ }
+
+ /**
+ * Check if a parameter exists in the current sanitized query.
+ * @param parameter the unencoded name of a parameter.
+ * @return true if the paramater exists in the current sanitized queary.
+ */
+ public boolean hasParameter(String parameter) {
+ return mEntries.containsKey(parameter);
+ }
+
+ /**
+ * Get the value for a parameter in the current sanitized query.
+ * Returns null if the parameter does not
+ * exit.
+ * @param parameter the unencoded name of a parameter.
+ * @return the sanitized unencoded value of the parameter,
+ * or null if the parameter does not exist.
+ */
+ public String getValue(String parameter) {
+ return mEntries.get(parameter);
+ }
+
+ /**
+ * Register a value sanitizer for a particular parameter. Can also be used
+ * to replace or remove an already-set value sanitizer.
+ * <p>
+ * Registering a non-null value sanitizer for a particular parameter
+ * makes that parameter a registered parameter.
+ * @param parameter an unencoded parameter name
+ * @param valueSanitizer the value sanitizer to use for a particular
+ * parameter. May be null in order to unregister that parameter.
+ * @see #getAllowUnregisteredParamaters()
+ */
+ public void registerParameter(String parameter,
+ ValueSanitizer valueSanitizer) {
+ if (valueSanitizer == null) {
+ mSanitizers.remove(parameter);
+ }
+ mSanitizers.put(parameter, valueSanitizer);
+ }
+
+ /**
+ * Register a value sanitizer for an array of parameters.
+ * @param parameters An array of unencoded parameter names.
+ * @param valueSanitizer
+ * @see #registerParameter
+ */
+ public void registerParameters(String[] parameters,
+ ValueSanitizer valueSanitizer) {
+ int length = parameters.length;
+ for(int i = 0; i < length; i++) {
+ mSanitizers.put(parameters[i], valueSanitizer);
+ }
+ }
+
+ /**
+ * Set whether or not unregistered parameters are allowed. If they
+ * are not allowed, then they will be dropped when a query is sanitized.
+ * <p>
+ * Defaults to false.
+ * @param allowUnregisteredParamaters true to allow unregistered parameters.
+ * @see #getAllowUnregisteredParamaters()
+ */
+ public void setAllowUnregisteredParamaters(
+ boolean allowUnregisteredParamaters) {
+ mAllowUnregisteredParamaters = allowUnregisteredParamaters;
+ }
+
+ /**
+ * Get whether or not unregistered parameters are allowed. If not
+ * allowed, they will be dropped when a query is parsed.
+ * @return true if unregistered parameters are allowed.
+ * @see #setAllowUnregisteredParamaters(boolean)
+ */
+ public boolean getAllowUnregisteredParamaters() {
+ return mAllowUnregisteredParamaters;
+ }
+
+ /**
+ * Set whether or not the first occurrence of a repeated parameter is
+ * preferred. True means the first repeated parameter is preferred.
+ * False means that the last repeated parameter is preferred.
+ * <p>
+ * The preferred parameter is the one that is returned when getParameter
+ * is called.
+ * <p>
+ * defaults to false.
+ * @param preferFirstRepeatedParameter True if the first repeated
+ * parameter is preferred.
+ * @see #getPreferFirstRepeatedParameter()
+ */
+ public void setPreferFirstRepeatedParameter(
+ boolean preferFirstRepeatedParameter) {
+ mPreferFirstRepeatedParameter = preferFirstRepeatedParameter;
+ }
+
+ /**
+ * Get whether or not the first occurrence of a repeated parameter is
+ * preferred.
+ * @return true if the first occurrence of a repeated parameter is
+ * preferred.
+ * @see #setPreferFirstRepeatedParameter(boolean)
+ */
+ public boolean getPreferFirstRepeatedParameter() {
+ return mPreferFirstRepeatedParameter;
+ }
+
+ /**
+ * Parse an escaped parameter-value pair. The default implementation
+ * unescapes both the parameter and the value, then looks up the
+ * effective value sanitizer for the parameter and uses it to sanitize
+ * the value. If all goes well then addSanitizedValue is called with
+ * the unescaped parameter and the sanitized unescaped value.
+ * @param parameter an escaped parameter
+ * @param value an unsanitzied escaped value
+ */
+ protected void parseEntry(String parameter, String value) {
+ String unescapedParameter = unescape(parameter);
+ ValueSanitizer valueSanitizer =
+ getEffectiveValueSanitizer(unescapedParameter);
+
+ if (valueSanitizer == null) {
+ return;
+ }
+ String unescapedValue = unescape(value);
+ String sanitizedValue = valueSanitizer.sanitize(unescapedValue);
+ addSanitizedEntry(unescapedParameter, sanitizedValue);
+ }
+
+ /**
+ * Record a sanitized parameter-value pair. Override if you want to
+ * do additional filtering or validation.
+ * @param parameter an unescaped parameter
+ * @param value a sanitized unescaped value
+ */
+ protected void addSanitizedEntry(String parameter, String value) {
+ mEntriesList.add(
+ new ParameterValuePair(parameter, value));
+ if (mPreferFirstRepeatedParameter) {
+ if (mEntries.containsKey(parameter)) {
+ return;
+ }
+ }
+ mEntries.put(parameter, value);
+ }
+
+ /**
+ * Get the value sanitizer for a parameter. Returns null if there
+ * is no value sanitizer registered for the parameter.
+ * @param parameter the unescaped parameter
+ * @return the currently registered value sanitizer for this parameter.
+ * @see #registerParameter(String, android.net.UrlQuerySanitizer.ValueSanitizer)
+ */
+ public ValueSanitizer getValueSanitizer(String parameter) {
+ return mSanitizers.get(parameter);
+ }
+
+ /**
+ * Get the effective value sanitizer for a parameter. Like getValueSanitizer,
+ * except if there is no value sanitizer registered for a parameter, and
+ * unregistered paramaters are allowed, then the default value sanitizer is
+ * returned.
+ * @param parameter an unescaped parameter
+ * @return the effective value sanitizer for a parameter.
+ */
+ public ValueSanitizer getEffectiveValueSanitizer(String parameter) {
+ ValueSanitizer sanitizer = getValueSanitizer(parameter);
+ if (sanitizer == null && mAllowUnregisteredParamaters) {
+ sanitizer = getUnregisteredParameterValueSanitizer();
+ }
+ return sanitizer;
+ }
+
+ /**
+ * Unescape an escaped string.
+ * <ul>
+ * <li>'+' characters are replaced by
+ * ' ' characters.
+ * <li>Valid "%xx" escape sequences are replaced by the
+ * corresponding unescaped character.
+ * <li>Invalid escape sequences such as %1z", are passed through unchanged.
+ * <ol>
+ * @param string the escaped string
+ * @return the unescaped string.
+ */
+ public String unescape(String string) {
+ // Early exit if no escaped characters.
+ int firstEscape = string.indexOf('%');
+ if ( firstEscape < 0) {
+ firstEscape = string.indexOf('+');
+ if (firstEscape < 0) {
+ return string;
+ }
+ }
+
+ int length = string.length();
+
+ StringBuilder stringBuilder = new StringBuilder(length);
+ stringBuilder.append(string.substring(0, firstEscape));
+ for (int i = firstEscape; i < length; i++) {
+ char c = string.charAt(i);
+ if (c == '+') {
+ c = ' ';
+ }
+ else if ( c == '%' && i + 2 < length) {
+ char c1 = string.charAt(i + 1);
+ char c2 = string.charAt(i + 2);
+ if (isHexDigit(c1) && isHexDigit(c2)) {
+ c = (char) (decodeHexDigit(c1) * 16 + decodeHexDigit(c2));
+ i += 2;
+ }
+ }
+ stringBuilder.append(c);
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Test if a character is a hexidecimal digit. Both upper case and lower
+ * case hex digits are allowed.
+ * @param c the character to test
+ * @return true if c is a hex digit.
+ */
+ protected boolean isHexDigit(char c) {
+ return decodeHexDigit(c) >= 0;
+ }
+
+ /**
+ * Convert a character that represents a hexidecimal digit into an integer.
+ * If the character is not a hexidecimal digit, then -1 is returned.
+ * Both upper case and lower case hex digits are allowed.
+ * @param c the hexidecimal digit.
+ * @return the integer value of the hexidecimal digit.
+ */
+
+ protected int decodeHexDigit(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ else if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ else if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ else {
+ return -1;
+ }
+ }
+
+ /**
+ * Clear the existing entries. Called to get ready to parse a new
+ * query string.
+ */
+ protected void clear() {
+ mEntries.clear();
+ mEntriesList.clear();
+ }
+}
+
diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java
new file mode 100644
index 0000000..f4a2a6a
--- /dev/null
+++ b/core/java/android/net/WebAddress.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2006 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.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ *
+ * Web Address Parser
+ *
+ * This is called WebAddress, rather than URL or URI, because it
+ * attempts to parse the stuff that a user will actually type into a
+ * browser address widget.
+ *
+ * Unlike java.net.uri, this parser will not choke on URIs missing
+ * schemes. It will only throw a ParseException if the input is
+ * really hosed.
+ *
+ * If given an https scheme but no port, fills in port
+ *
+ */
+public class WebAddress {
+
+ private final static String LOGTAG = "http";
+
+ public String mScheme;
+ public String mHost;
+ public int mPort;
+ public String mPath;
+ public String mAuthInfo;
+
+ static final int MATCH_GROUP_SCHEME = 1;
+ static final int MATCH_GROUP_AUTHORITY = 2;
+ static final int MATCH_GROUP_HOST = 3;
+ static final int MATCH_GROUP_PORT = 4;
+ static final int MATCH_GROUP_PATH = 5;
+
+ static Pattern sAddressPattern = Pattern.compile(
+ /* scheme */ "(?:(http|HTTP|https|HTTPS|file|FILE)\\:\\/\\/)?" +
+ /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
+ /* host */ "([-A-Za-z0-9%]+(?:\\.[-A-Za-z0-9%]+)*)?" +
+ /* port */ "(?:\\:([0-9]+))?" +
+ /* path */ "(\\/?.*)?");
+
+ /** parses given uriString. */
+ public WebAddress(String address) throws ParseException {
+ if (address == null) {
+ throw new NullPointerException();
+ }
+
+ // android.util.Log.d(LOGTAG, "WebAddress: " + address);
+
+ mScheme = "";
+ mHost = "";
+ mPort = -1;
+ mPath = "/";
+ mAuthInfo = "";
+
+ Matcher m = sAddressPattern.matcher(address);
+ String t;
+ if (m.matches()) {
+ t = m.group(MATCH_GROUP_SCHEME);
+ if (t != null) mScheme = t;
+ t = m.group(MATCH_GROUP_AUTHORITY);
+ if (t != null) mAuthInfo = t;
+ t = m.group(MATCH_GROUP_HOST);
+ if (t != null) mHost = t;
+ t = m.group(MATCH_GROUP_PORT);
+ if (t != null) {
+ try {
+ mPort = Integer.parseInt(t);
+ } catch (NumberFormatException ex) {
+ throw new ParseException("Bad port");
+ }
+ }
+ t = m.group(MATCH_GROUP_PATH);
+ if (t != null && t.length() > 0) {
+ /* handle busted myspace frontpage redirect with
+ missing initial "/" */
+ if (t.charAt(0) == '/') {
+ mPath = t;
+ } else {
+ mPath = "/" + t;
+ }
+ }
+
+ } else {
+ // nothing found... outa here
+ throw new ParseException("Bad address");
+ }
+
+ /* Get port from scheme or scheme from port, if necessary and
+ possible */
+ if (mPort == 443 && mScheme.equals("")) {
+ mScheme = "https";
+ } else if (mPort == -1) {
+ if (mScheme.equals("https"))
+ mPort = 443;
+ else
+ mPort = 80; // default
+ }
+ if (mScheme.equals("")) mScheme = "http";
+ }
+
+ public String toString() {
+ String port = "";
+ if ((mPort != 443 && mScheme.equals("https")) ||
+ (mPort != 80 && mScheme.equals("http"))) {
+ port = ":" + Integer.toString(mPort);
+ }
+ String authInfo = "";
+ if (mAuthInfo.length() > 0) {
+ authInfo = mAuthInfo + "@";
+ }
+
+ return mScheme + "://" + authInfo + mHost + port + mPath;
+ }
+}
diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java
new file mode 100644
index 0000000..01442ae
--- /dev/null
+++ b/core/java/android/net/http/AndroidHttpClient.java
@@ -0,0 +1,452 @@
+/*
+ * 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.http;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.protocol.ClientContext;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.RequestWrapper;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.protocol.BasicHttpProcessor;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.BasicHttpContext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import android.util.Log;
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+/**
+ * Subclass of the Apache {@link DefaultHttpClient} that is configured with
+ * reasonable default settings and registered schemes for Android, and
+ * also lets the user add {@link HttpRequestInterceptor} classes.
+ * Don't create this directly, use the {@link #newInstance} factory method.
+ *
+ * <p>This client processes cookies but does not retain them by default.
+ * To retain cookies, simply add a cookie store to the HttpContext:</p>
+ *
+ * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
+ *
+ * {@hide}
+ */
+public final class AndroidHttpClient implements HttpClient {
+
+ // Gzip of data shorter than this probably won't be worthwhile
+ public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
+
+ private static final String TAG = "AndroidHttpClient";
+
+
+ /** Set if HTTP requests are blocked from being executed on this thread */
+ private static final ThreadLocal<Boolean> sThreadBlocked =
+ new ThreadLocal<Boolean>();
+
+ /** Interceptor throws an exception if the executing thread is blocked */
+ private static final HttpRequestInterceptor sThreadCheckInterceptor =
+ new HttpRequestInterceptor() {
+ public void process(HttpRequest request, HttpContext context) {
+ if (sThreadBlocked.get() != null && sThreadBlocked.get()) {
+ throw new RuntimeException("This thread forbids HTTP requests");
+ }
+ }
+ };
+
+ /**
+ * Create a new HttpClient with reasonable defaults (which you can update).
+ * @param userAgent to report in your HTTP requests.
+ * @return AndroidHttpClient for you to use for all your requests.
+ */
+ public static AndroidHttpClient newInstance(String userAgent) {
+ HttpParams params = new BasicHttpParams();
+
+ // Turn off stale checking. Our connections break all the time anyway,
+ // and it's not worth it to pay the penalty of checking every time.
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+
+ // Default connection and socket timeout of 20 seconds. Tweak to taste.
+ HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
+ HttpConnectionParams.setSoTimeout(params, 20 * 1000);
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+
+ // Don't handle redirects -- return them to the caller. Our code
+ // often wants to re-POST after a redirect, which we must do ourselves.
+ HttpClientParams.setRedirecting(params, false);
+
+ // Set the specified user agent and register standard protocols.
+ HttpProtocolParams.setUserAgent(params, userAgent);
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80));
+ schemeRegistry.register(new Scheme("https",
+ SSLSocketFactory.getSocketFactory(), 443));
+ ClientConnectionManager manager =
+ new ThreadSafeClientConnManager(params, schemeRegistry);
+
+ // We use a factory method to modify superclass initialization
+ // parameters without the funny call-a-static-method dance.
+ return new AndroidHttpClient(manager, params);
+ }
+
+ private final HttpClient delegate;
+
+ private RuntimeException mLeakedException = new IllegalStateException(
+ "AndroidHttpClient created and never closed");
+
+ private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
+ this.delegate = new DefaultHttpClient(ccm, params) {
+ @Override
+ protected BasicHttpProcessor createHttpProcessor() {
+ // Add interceptor to prevent making requests from main thread.
+ BasicHttpProcessor processor = super.createHttpProcessor();
+ processor.addRequestInterceptor(sThreadCheckInterceptor);
+ processor.addRequestInterceptor(new CurlLogger());
+
+ return processor;
+ }
+
+ @Override
+ protected HttpContext createHttpContext() {
+ // Same as DefaultHttpClient.createHttpContext() minus the
+ // cookie store.
+ HttpContext context = new BasicHttpContext();
+ context.setAttribute(
+ ClientContext.AUTHSCHEME_REGISTRY,
+ getAuthSchemes());
+ context.setAttribute(
+ ClientContext.COOKIESPEC_REGISTRY,
+ getCookieSpecs());
+ context.setAttribute(
+ ClientContext.CREDS_PROVIDER,
+ getCredentialsProvider());
+ return context;
+ }
+ };
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ if (mLeakedException != null) {
+ Log.e(TAG, "Leak found", mLeakedException);
+ mLeakedException = null;
+ }
+ }
+
+ /**
+ * Block this thread from executing HTTP requests.
+ * Used to guard against HTTP requests blocking the main application thread.
+ * @param blocked if HTTP requests run on this thread should be denied
+ */
+ public static void setThreadBlocked(boolean blocked) {
+ sThreadBlocked.set(blocked);
+ }
+
+ /**
+ * Modifies a request to indicate to the server that we would like a
+ * gzipped response. (Uses the "Accept-Encoding" HTTP header.)
+ * @param request the request to modify
+ * @see #getUngzippedContent
+ */
+ public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
+ request.addHeader("Accept-Encoding", "gzip");
+ }
+
+ /**
+ * Gets the input stream from a response entity. If the entity is gzipped
+ * then this will get a stream over the uncompressed data.
+ *
+ * @param entity the entity whose content should be read
+ * @return the input stream to read from
+ * @throws IOException
+ */
+ public static InputStream getUngzippedContent(HttpEntity entity)
+ throws IOException {
+ InputStream responseStream = entity.getContent();
+ if (responseStream == null) return responseStream;
+ Header header = entity.getContentEncoding();
+ if (header == null) return responseStream;
+ String contentEncoding = header.getValue();
+ if (contentEncoding == null) return responseStream;
+ if (contentEncoding.contains("gzip")) responseStream
+ = new GZIPInputStream(responseStream);
+ return responseStream;
+ }
+
+ /**
+ * Release resources associated with this client. You must call this,
+ * or significant resources (sockets and memory) may be leaked.
+ */
+ public void close() {
+ if (mLeakedException != null) {
+ getConnectionManager().shutdown();
+ mLeakedException = null;
+ }
+ }
+
+ public HttpParams getParams() {
+ return delegate.getParams();
+ }
+
+ public ClientConnectionManager getConnectionManager() {
+ return delegate.getConnectionManager();
+ }
+
+ public HttpResponse execute(HttpUriRequest request) throws IOException {
+ return delegate.execute(request);
+ }
+
+ public HttpResponse execute(HttpUriRequest request, HttpContext context)
+ throws IOException {
+ return delegate.execute(request, context);
+ }
+
+ public HttpResponse execute(HttpHost target, HttpRequest request)
+ throws IOException {
+ return delegate.execute(target, request);
+ }
+
+ public HttpResponse execute(HttpHost target, HttpRequest request,
+ HttpContext context) throws IOException {
+ return delegate.execute(target, request, context);
+ }
+
+ public <T> T execute(HttpUriRequest request,
+ ResponseHandler<? extends T> responseHandler)
+ throws IOException, ClientProtocolException {
+ return delegate.execute(request, responseHandler);
+ }
+
+ public <T> T execute(HttpUriRequest request,
+ ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return delegate.execute(request, responseHandler, context);
+ }
+
+ public <T> T execute(HttpHost target, HttpRequest request,
+ ResponseHandler<? extends T> responseHandler) throws IOException,
+ ClientProtocolException {
+ return delegate.execute(target, request, responseHandler);
+ }
+
+ public <T> T execute(HttpHost target, HttpRequest request,
+ ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return delegate.execute(target, request, responseHandler, context);
+ }
+
+ /**
+ * Compress data to send to server.
+ * Creates a Http Entity holding the gzipped data.
+ * The data will not be compressed if it is too short.
+ * @param data The bytes to compress
+ * @return Entity holding the data
+ */
+ public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
+ throws IOException {
+ AbstractHttpEntity entity;
+ if (data.length < getMinGzipSize(resolver)) {
+ entity = new ByteArrayEntity(data);
+ } else {
+ ByteArrayOutputStream arr = new ByteArrayOutputStream();
+ OutputStream zipper = new GZIPOutputStream(arr);
+ zipper.write(data);
+ zipper.close();
+ entity = new ByteArrayEntity(arr.toByteArray());
+ entity.setContentEncoding("gzip");
+ }
+ return entity;
+ }
+
+ /**
+ * Retrieves the minimum size for compressing data.
+ * Shorter data will not be compressed.
+ */
+ public static long getMinGzipSize(ContentResolver resolver) {
+ String sMinGzipBytes = Settings.Gservices.getString(resolver,
+ Settings.Gservices.SYNC_MIN_GZIP_BYTES);
+
+ if (!TextUtils.isEmpty(sMinGzipBytes)) {
+ try {
+ return Long.parseLong(sMinGzipBytes);
+ } catch (NumberFormatException nfe) {
+ Log.w(TAG, "Unable to parse " +
+ Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " +
+ sMinGzipBytes, nfe);
+ }
+ }
+ return DEFAULT_SYNC_MIN_GZIP_BYTES;
+ }
+
+ /* cURL logging support. */
+
+ /**
+ * Logging tag and level.
+ */
+ private static class LoggingConfiguration {
+
+ private final String tag;
+ private final int level;
+
+ private LoggingConfiguration(String tag, int level) {
+ this.tag = tag;
+ this.level = level;
+ }
+
+ /**
+ * Returns true if logging is turned on for this configuration.
+ */
+ private boolean isLoggable() {
+ return Log.isLoggable(tag, level);
+ }
+
+ /**
+ * Prints a message using this configuration.
+ */
+ private void println(String message) {
+ Log.println(level, tag, message);
+ }
+ }
+
+ /** cURL logging configuration. */
+ private volatile LoggingConfiguration curlConfiguration;
+
+ /**
+ * Enables cURL request logging for this client.
+ *
+ * @param name to log messages with
+ * @param level at which to log messages (see {@link android.util.Log})
+ */
+ public void enableCurlLogging(String name, int level) {
+ if (name == null) {
+ throw new NullPointerException("name");
+ }
+ if (level < Log.VERBOSE || level > Log.ASSERT) {
+ throw new IllegalArgumentException("Level is out of range ["
+ + Log.VERBOSE + ".." + Log.ASSERT + "]");
+ }
+
+ curlConfiguration = new LoggingConfiguration(name, level);
+ }
+
+ /**
+ * Disables cURL logging for this client.
+ */
+ public void disableCurlLogging() {
+ curlConfiguration = null;
+ }
+
+ /**
+ * Logs cURL commands equivalent to requests.
+ */
+ private class CurlLogger implements HttpRequestInterceptor {
+ public void process(HttpRequest request, HttpContext context)
+ throws HttpException, IOException {
+ LoggingConfiguration configuration = curlConfiguration;
+ if (configuration != null
+ && configuration.isLoggable()
+ && request instanceof HttpUriRequest) {
+ configuration.println(toCurl((HttpUriRequest) request));
+ }
+ }
+ }
+
+ /**
+ * Generates a cURL command equivalent to the given request.
+ */
+ private static String toCurl(HttpUriRequest request) throws IOException {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("curl ");
+
+ for (Header header: request.getAllHeaders()) {
+ builder.append("--header \"");
+ builder.append(header.toString().trim());
+ builder.append("\" ");
+ }
+
+ URI uri = request.getURI();
+
+ // If this is a wrapped request, use the URI from the original
+ // request instead. getURI() on the wrapper seems to return a
+ // relative URI. We want an absolute URI.
+ if (request instanceof RequestWrapper) {
+ HttpRequest original = ((RequestWrapper) request).getOriginal();
+ if (original instanceof HttpUriRequest) {
+ uri = ((HttpUriRequest) original).getURI();
+ }
+ }
+
+ builder.append("\"");
+ builder.append(uri);
+ builder.append("\"");
+
+ if (request instanceof HttpEntityEnclosingRequest) {
+ HttpEntityEnclosingRequest entityRequest =
+ (HttpEntityEnclosingRequest) request;
+ HttpEntity entity = entityRequest.getEntity();
+ if (entity != null && entity.isRepeatable()) {
+ if (entity.getContentLength() < 1024) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ entity.writeTo(stream);
+ String entityString = stream.toString();
+
+ // TODO: Check the content type, too.
+ builder.append(" --data-ascii \"")
+ .append(entityString)
+ .append("\"");
+ } else {
+ builder.append(" [TOO MUCH DATA TO INCLUDE]");
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/core/java/android/net/http/AndroidHttpClientConnection.java b/core/java/android/net/http/AndroidHttpClientConnection.java
new file mode 100644
index 0000000..eb96679
--- /dev/null
+++ b/core/java/android/net/http/AndroidHttpClientConnection.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2008 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.http;
+
+import org.apache.http.Header;
+
+import org.apache.http.HttpConnection;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpConnectionMetrics;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpInetConnection;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpResponseFactory;
+import org.apache.http.NoHttpResponseException;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.entity.ContentLengthStrategy;
+import org.apache.http.impl.DefaultHttpResponseFactory;
+import org.apache.http.impl.HttpConnectionMetricsImpl;
+import org.apache.http.impl.entity.EntitySerializer;
+import org.apache.http.impl.entity.StrictContentLengthStrategy;
+import org.apache.http.impl.io.ChunkedInputStream;
+import org.apache.http.impl.io.ContentLengthInputStream;
+import org.apache.http.impl.io.HttpRequestWriter;
+import org.apache.http.impl.io.IdentityInputStream;
+import org.apache.http.impl.io.SocketInputBuffer;
+import org.apache.http.impl.io.SocketOutputBuffer;
+import org.apache.http.io.HttpMessageWriter;
+import org.apache.http.io.SessionInputBuffer;
+import org.apache.http.io.SessionOutputBuffer;
+import org.apache.http.message.BasicLineParser;
+import org.apache.http.message.ParserCursor;
+import org.apache.http.params.CoreConnectionPNames;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.ParseException;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+
+/**
+ * A alternate class for (@link DefaultHttpClientConnection).
+ * It has better performance than DefaultHttpClientConnection
+ *
+ * {@hide}
+ */
+public class AndroidHttpClientConnection
+ implements HttpInetConnection, HttpConnection {
+
+ private SessionInputBuffer inbuffer = null;
+ private SessionOutputBuffer outbuffer = null;
+ private int maxHeaderCount;
+ // store CoreConnectionPNames.MAX_LINE_LENGTH for performance
+ private int maxLineLength;
+
+ private final EntitySerializer entityserializer;
+
+ private HttpMessageWriter requestWriter = null;
+ private HttpConnectionMetricsImpl metrics = null;
+ private volatile boolean open;
+ private Socket socket = null;
+
+ public AndroidHttpClientConnection() {
+ this.entityserializer = new EntitySerializer(
+ new StrictContentLengthStrategy());
+ }
+
+ /**
+ * Bind socket and set HttpParams to AndroidHttpClientConnection
+ * @param socket outgoing socket
+ * @param params HttpParams
+ * @throws IOException
+ */
+ public void bind(
+ final Socket socket,
+ final HttpParams params) throws IOException {
+ if (socket == null) {
+ throw new IllegalArgumentException("Socket may not be null");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ assertNotOpen();
+ socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params));
+ socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params));
+
+ int linger = HttpConnectionParams.getLinger(params);
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+ this.socket = socket;
+
+ int buffersize = HttpConnectionParams.getSocketBufferSize(params);
+ this.inbuffer = new SocketInputBuffer(socket, buffersize, params);
+ this.outbuffer = new SocketOutputBuffer(socket, buffersize, params);
+
+ maxHeaderCount = params.getIntParameter(
+ CoreConnectionPNames.MAX_HEADER_COUNT, -1);
+ maxLineLength = params.getIntParameter(
+ CoreConnectionPNames.MAX_LINE_LENGTH, -1);
+
+ this.requestWriter = new HttpRequestWriter(outbuffer, null, params);
+
+ this.metrics = new HttpConnectionMetricsImpl(
+ inbuffer.getMetrics(),
+ outbuffer.getMetrics());
+
+ this.open = true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(getClass().getSimpleName()).append("[");
+ if (isOpen()) {
+ buffer.append(getRemotePort());
+ } else {
+ buffer.append("closed");
+ }
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+
+ private void assertNotOpen() {
+ if (this.open) {
+ throw new IllegalStateException("Connection is already open");
+ }
+ }
+
+ private void assertOpen() {
+ if (!this.open) {
+ throw new IllegalStateException("Connection is not open");
+ }
+ }
+
+ public boolean isOpen() {
+ // to make this method useful, we want to check if the socket is connected
+ return (this.open && this.socket != null && this.socket.isConnected());
+ }
+
+ public InetAddress getLocalAddress() {
+ if (this.socket != null) {
+ return this.socket.getLocalAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getLocalPort() {
+ if (this.socket != null) {
+ return this.socket.getLocalPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public InetAddress getRemoteAddress() {
+ if (this.socket != null) {
+ return this.socket.getInetAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getRemotePort() {
+ if (this.socket != null) {
+ return this.socket.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public void setSocketTimeout(int timeout) {
+ assertOpen();
+ if (this.socket != null) {
+ try {
+ this.socket.setSoTimeout(timeout);
+ } catch (SocketException ignore) {
+ // It is not quite clear from the original documentation if there are any
+ // other legitimate cases for a socket exception to be thrown when setting
+ // SO_TIMEOUT besides the socket being already closed
+ }
+ }
+ }
+
+ public int getSocketTimeout() {
+ if (this.socket != null) {
+ try {
+ return this.socket.getSoTimeout();
+ } catch (SocketException ignore) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public void shutdown() throws IOException {
+ this.open = false;
+ Socket tmpsocket = this.socket;
+ if (tmpsocket != null) {
+ tmpsocket.close();
+ }
+ }
+
+ public void close() throws IOException {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ doFlush();
+ try {
+ try {
+ this.socket.shutdownOutput();
+ } catch (IOException ignore) {
+ }
+ try {
+ this.socket.shutdownInput();
+ } catch (IOException ignore) {
+ }
+ } catch (UnsupportedOperationException ignore) {
+ // if one isn't supported, the other one isn't either
+ }
+ this.socket.close();
+ }
+
+ /**
+ * Sends the request line and all headers over the connection.
+ * @param request the request whose headers to send.
+ * @throws HttpException
+ * @throws IOException
+ */
+ public void sendRequestHeader(final HttpRequest request)
+ throws HttpException, IOException {
+ if (request == null) {
+ throw new IllegalArgumentException("HTTP request may not be null");
+ }
+ assertOpen();
+ this.requestWriter.write(request);
+ this.metrics.incrementRequestCount();
+ }
+
+ /**
+ * Sends the request entity over the connection.
+ * @param request the request whose entity to send.
+ * @throws HttpException
+ * @throws IOException
+ */
+ public void sendRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ if (request == null) {
+ throw new IllegalArgumentException("HTTP request may not be null");
+ }
+ assertOpen();
+ if (request.getEntity() == null) {
+ return;
+ }
+ this.entityserializer.serialize(
+ this.outbuffer,
+ request,
+ request.getEntity());
+ }
+
+ protected void doFlush() throws IOException {
+ this.outbuffer.flush();
+ }
+
+ public void flush() throws IOException {
+ assertOpen();
+ doFlush();
+ }
+
+ /**
+ * Parses the response headers and adds them to the
+ * given {@code headers} object, and returns the response StatusLine
+ * @param headers store parsed header to headers.
+ * @throws IOException
+ * @return StatusLine
+ * @see HttpClientConnection#receiveResponseHeader()
+ */
+ public StatusLine parseResponseHeader(Headers headers)
+ throws IOException, ParseException {
+ assertOpen();
+
+ CharArrayBuffer current = new CharArrayBuffer(64);
+
+ if (inbuffer.readLine(current) == -1) {
+ throw new NoHttpResponseException("The target server failed to respond");
+ }
+
+ // Create the status line from the status string
+ StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine(
+ current, new ParserCursor(0, current.length()));
+
+ if (HttpLog.LOGV) HttpLog.v("read: " + statusline);
+ int statusCode = statusline.getStatusCode();
+
+ // Parse header body
+ CharArrayBuffer previous = null;
+ int headerNumber = 0;
+ while(true) {
+ if (current == null) {
+ current = new CharArrayBuffer(64);
+ } else {
+ // This must be he buffer used to parse the status
+ current.clear();
+ }
+ int l = inbuffer.readLine(current);
+ if (l == -1 || current.length() < 1) {
+ break;
+ }
+ // Parse the header name and value
+ // Check for folded headers first
+ // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2
+ // discussion on folded headers
+ char first = current.charAt(0);
+ if ((first == ' ' || first == '\t') && previous != null) {
+ // we have continuation folded header
+ // so append value
+ int start = 0;
+ int length = current.length();
+ while (start < length) {
+ char ch = current.charAt(start);
+ if (ch != ' ' && ch != '\t') {
+ break;
+ }
+ start++;
+ }
+ if (maxLineLength > 0 &&
+ previous.length() + 1 + current.length() - start >
+ maxLineLength) {
+ throw new IOException("Maximum line length limit exceeded");
+ }
+ previous.append(' ');
+ previous.append(current, start, current.length() - start);
+ } else {
+ if (previous != null) {
+ headers.parseHeader(previous);
+ }
+ headerNumber++;
+ previous = current;
+ current = null;
+ }
+ if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) {
+ throw new IOException("Maximum header count exceeded");
+ }
+ }
+
+ if (previous != null) {
+ headers.parseHeader(previous);
+ }
+
+ if (statusCode >= 200) {
+ this.metrics.incrementResponseCount();
+ }
+ return statusline;
+ }
+
+ /**
+ * Return the next response entity.
+ * @param headers contains values for parsing entity
+ * @see HttpClientConnection#receiveResponseEntity(HttpResponse response)
+ */
+ public HttpEntity receiveResponseEntity(final Headers headers) {
+ assertOpen();
+ BasicHttpEntity entity = new BasicHttpEntity();
+
+ long len = determineLength(headers);
+ if (len == ContentLengthStrategy.CHUNKED) {
+ entity.setChunked(true);
+ entity.setContentLength(-1);
+ entity.setContent(new ChunkedInputStream(inbuffer));
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ entity.setChunked(false);
+ entity.setContentLength(-1);
+ entity.setContent(new IdentityInputStream(inbuffer));
+ } else {
+ entity.setChunked(false);
+ entity.setContentLength(len);
+ entity.setContent(new ContentLengthInputStream(inbuffer, len));
+ }
+
+ String contentTypeHeader = headers.getContentType();
+ if (contentTypeHeader != null) {
+ entity.setContentType(contentTypeHeader);
+ }
+ String contentEncodingHeader = headers.getContentEncoding();
+ if (contentEncodingHeader != null) {
+ entity.setContentEncoding(contentEncodingHeader);
+ }
+
+ return entity;
+ }
+
+ private long determineLength(final Headers headers) {
+ long transferEncoding = headers.getTransferEncoding();
+ // We use Transfer-Encoding if present and ignore Content-Length.
+ // RFC2616, 4.4 item number 3
+ if (transferEncoding < Headers.NO_TRANSFER_ENCODING) {
+ return transferEncoding;
+ } else {
+ long contentlen = headers.getContentLength();
+ if (contentlen > Headers.NO_CONTENT_LENGTH) {
+ return contentlen;
+ } else {
+ return ContentLengthStrategy.IDENTITY;
+ }
+ }
+ }
+
+ /**
+ * Checks whether this connection has gone down.
+ * Network connections may get closed during some time of inactivity
+ * for several reasons. The next time a read is attempted on such a
+ * connection it will throw an IOException.
+ * This method tries to alleviate this inconvenience by trying to
+ * find out if a connection is still usable. Implementations may do
+ * that by attempting a read with a very small timeout. Thus this
+ * method may block for a small amount of time before returning a result.
+ * It is therefore an <i>expensive</i> operation.
+ *
+ * @return <code>true</code> if attempts to use this connection are
+ * likely to succeed, or <code>false</code> if they are likely
+ * to fail and this connection should be closed
+ */
+ public boolean isStale() {
+ assertOpen();
+ try {
+ this.inbuffer.isDataAvailable(1);
+ return false;
+ } catch (IOException ex) {
+ return true;
+ }
+ }
+
+ /**
+ * Returns a collection of connection metrcis
+ * @return HttpConnectionMetrics
+ */
+ public HttpConnectionMetrics getMetrics() {
+ return this.metrics;
+ }
+}
diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java
new file mode 100644
index 0000000..b7f7368
--- /dev/null
+++ b/core/java/android/net/http/CertificateChainValidator.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2008 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.http;
+
+import android.os.SystemClock;
+
+import java.io.IOException;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Enumeration;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.HttpHost;
+
+import org.bouncycastle.asn1.x509.X509Name;
+
+/**
+ * Class responsible for all server certificate validation functionality
+ *
+ * {@hide}
+ */
+class CertificateChainValidator {
+
+ private static long sTotal = 0;
+ private static long sTotalReused = 0;
+
+ /**
+ * The singleton instance of the certificate chain validator
+ */
+ private static CertificateChainValidator sInstance;
+
+ /**
+ * Default trust manager (used to perform CA certificate validation)
+ */
+ private X509TrustManager mDefaultTrustManager;
+
+ /**
+ * @return The singleton instance of the certificator chain validator
+ */
+ public static CertificateChainValidator getInstance() {
+ if (sInstance == null) {
+ sInstance = new CertificateChainValidator();
+ }
+
+ return sInstance;
+ }
+
+ /**
+ * Creates a new certificate chain validator. This is a pivate constructor.
+ * If you need a Certificate chain validator, call getInstance().
+ */
+ private CertificateChainValidator() {
+ try {
+ TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance("X509");
+ trustManagerFactory.init((KeyStore)null);
+ TrustManager[] trustManagers =
+ trustManagerFactory.getTrustManagers();
+ if (trustManagers != null && trustManagers.length > 0) {
+ for (TrustManager trustManager : trustManagers) {
+ if (trustManager instanceof X509TrustManager) {
+ mDefaultTrustManager = (X509TrustManager)(trustManager);
+ break;
+ }
+ }
+ }
+ } catch (Exception exc) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("CertificateChainValidator():" +
+ " failed to initialize the trust manager");
+ }
+ }
+ }
+
+ /**
+ * Performs the handshake and server certificates validation
+ * @param sslSocket The secure connection socket
+ * @param domain The website domain
+ * @return An SSL error object if there is an error and null otherwise
+ */
+ public SslError doHandshakeAndValidateServerCertificates(
+ HttpsConnection connection, SSLSocket sslSocket, String domain)
+ throws SSLHandshakeException, IOException {
+
+ ++sTotal;
+
+ SSLContext sslContext = HttpsConnection.getContext();
+ if (sslContext == null) {
+ closeSocketThrowException(sslSocket, "SSL context is null");
+ }
+
+ X509Certificate[] serverCertificates = null;
+
+ long sessionBeforeHandshakeLastAccessedTime = 0;
+ byte[] sessionBeforeHandshakeId = null;
+
+ SSLSession sessionAfterHandshake = null;
+
+ synchronized(sslContext) {
+ // get SSL session before the handshake
+ SSLSession sessionBeforeHandshake =
+ getSSLSession(sslContext, connection.getHost());
+ if (sessionBeforeHandshake != null) {
+ sessionBeforeHandshakeLastAccessedTime =
+ sessionBeforeHandshake.getLastAccessedTime();
+
+ sessionBeforeHandshakeId =
+ sessionBeforeHandshake.getId();
+ }
+
+ // start handshake, close the socket if we fail
+ try {
+ sslSocket.setUseClientMode(true);
+ sslSocket.startHandshake();
+ } catch (IOException e) {
+ closeSocketThrowException(
+ sslSocket, e.getMessage(),
+ "failed to perform SSL handshake");
+ }
+
+ // retrieve the chain of the server peer certificates
+ Certificate[] peerCertificates =
+ sslSocket.getSession().getPeerCertificates();
+
+ if (peerCertificates == null || peerCertificates.length <= 0) {
+ closeSocketThrowException(
+ sslSocket, "failed to retrieve peer certificates");
+ } else {
+ serverCertificates =
+ new X509Certificate[peerCertificates.length];
+ for (int i = 0; i < peerCertificates.length; ++i) {
+ serverCertificates[i] =
+ (X509Certificate)(peerCertificates[i]);
+ }
+
+ // update the SSL certificate associated with the connection
+ if (connection != null) {
+ if (serverCertificates[0] != null) {
+ connection.setCertificate(
+ new SslCertificate(serverCertificates[0]));
+ }
+ }
+ }
+
+ // get SSL session after the handshake
+ sessionAfterHandshake =
+ getSSLSession(sslContext, connection.getHost());
+ }
+
+ if (sessionBeforeHandshakeLastAccessedTime != 0 &&
+ sessionAfterHandshake != null &&
+ Arrays.equals(
+ sessionBeforeHandshakeId, sessionAfterHandshake.getId()) &&
+ sessionBeforeHandshakeLastAccessedTime <
+ sessionAfterHandshake.getLastAccessedTime()) {
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("SSL session was reused: total reused: "
+ + sTotalReused
+ + " out of total of: " + sTotal);
+
+ ++sTotalReused;
+ }
+
+ // no errors!!!
+ return null;
+ }
+
+ // check if the first certificate in the chain is for this site
+ X509Certificate currCertificate = serverCertificates[0];
+ if (currCertificate == null) {
+ closeSocketThrowException(
+ sslSocket, "certificate for this site is null");
+ } else {
+ if (!DomainNameChecker.match(currCertificate, domain)) {
+ String errorMessage = "certificate not for this host: " + domain;
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ sslSocket.getSession().invalidate();
+ return new SslError(
+ SslError.SSL_IDMISMATCH, currCertificate);
+ }
+ }
+
+ //
+ // first, we validate the chain using the standard validation
+ // solution; if we do not find any errors, we are done; if we
+ // fail the standard validation, we re-validate again below,
+ // this time trying to retrieve any individual errors we can
+ // report back to the user.
+ //
+ try {
+ synchronized (mDefaultTrustManager) {
+ mDefaultTrustManager.checkServerTrusted(
+ serverCertificates, "RSA");
+
+ // no errors!!!
+ return null;
+ }
+ } catch (CertificateException e) {
+ if (HttpLog.LOGV) {
+ HttpLog.v(
+ "failed to pre-validate the certificate chain, error: " +
+ e.getMessage());
+ }
+ }
+
+ sslSocket.getSession().invalidate();
+
+ SslError error = null;
+
+ // we check the root certificate separately from the rest of the
+ // chain; this is because we need to know what certificate in
+ // the chain resulted in an error if any
+ currCertificate =
+ serverCertificates[serverCertificates.length - 1];
+ if (currCertificate == null) {
+ closeSocketThrowException(
+ sslSocket, "root certificate is null");
+ }
+
+ // check if the last certificate in the chain (root) is trusted
+ X509Certificate[] rootCertificateChain = { currCertificate };
+ try {
+ synchronized (mDefaultTrustManager) {
+ mDefaultTrustManager.checkServerTrusted(
+ rootCertificateChain, "RSA");
+ }
+ } catch (CertificateExpiredException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "root certificate has expired";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ error = new SslError(
+ SslError.SSL_EXPIRED, currCertificate);
+ } catch (CertificateNotYetValidException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "root certificate not valid yet";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ error = new SslError(
+ SslError.SSL_NOTYETVALID, currCertificate);
+ } catch (CertificateException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "root certificate not trusted";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ return new SslError(
+ SslError.SSL_UNTRUSTED, currCertificate);
+ }
+
+ // Then go through the certificate chain checking that each
+ // certificate trusts the next and that each certificate is
+ // within its valid date range. Walk the chain in the order
+ // from the CA to the end-user
+ X509Certificate prevCertificate =
+ serverCertificates[serverCertificates.length - 1];
+
+ for (int i = serverCertificates.length - 2; i >= 0; --i) {
+ currCertificate = serverCertificates[i];
+
+ // if a certificate is null, we cannot verify the chain
+ if (currCertificate == null) {
+ closeSocketThrowException(
+ sslSocket, "null certificate in the chain");
+ }
+
+ // verify if trusted by chain
+ if (!prevCertificate.getSubjectDN().equals(
+ currCertificate.getIssuerDN())) {
+ String errorMessage = "not trusted by chain";
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ return new SslError(
+ SslError.SSL_UNTRUSTED, currCertificate);
+ }
+
+ try {
+ currCertificate.verify(prevCertificate.getPublicKey());
+ } catch (GeneralSecurityException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "not trusted by chain";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ return new SslError(
+ SslError.SSL_UNTRUSTED, currCertificate);
+ }
+
+ // verify if the dates are valid
+ try {
+ currCertificate.checkValidity();
+ } catch (CertificateExpiredException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "certificate expired";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ if (error == null ||
+ error.getPrimaryError() < SslError.SSL_EXPIRED) {
+ error = new SslError(
+ SslError.SSL_EXPIRED, currCertificate);
+ }
+ } catch (CertificateNotYetValidException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "certificate not valid yet";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ if (error == null ||
+ error.getPrimaryError() < SslError.SSL_NOTYETVALID) {
+ error = new SslError(
+ SslError.SSL_NOTYETVALID, currCertificate);
+ }
+ }
+
+ prevCertificate = currCertificate;
+ }
+
+ // if we do not have an error to report back to the user, throw
+ // an exception (a generic error will be reported instead)
+ if (error == null) {
+ closeSocketThrowException(
+ sslSocket,
+ "failed to pre-validate the certificate chain due to a non-standard error");
+ }
+
+ return error;
+ }
+
+ private void closeSocketThrowException(
+ SSLSocket socket, String errorMessage, String defaultErrorMessage)
+ throws SSLHandshakeException, IOException {
+ closeSocketThrowException(
+ socket, errorMessage != null ? errorMessage : defaultErrorMessage);
+ }
+
+ private void closeSocketThrowException(SSLSocket socket, String errorMessage)
+ throws SSLHandshakeException, IOException {
+ if (HttpLog.LOGV) {
+ HttpLog.v("validation error: " + errorMessage);
+ }
+
+ if (socket != null) {
+ SSLSession session = socket.getSession();
+ if (session != null) {
+ session.invalidate();
+ }
+
+ socket.close();
+ }
+
+ throw new SSLHandshakeException(errorMessage);
+ }
+
+ /**
+ * @param sslContext The SSL context shared accross all the SSL sessions
+ * @param host The host associated with the session
+ * @return A suitable SSL session from the SSL context
+ */
+ private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) {
+ if (sslContext != null && host != null) {
+ Enumeration en = sslContext.getClientSessionContext().getIds();
+ while (en.hasMoreElements()) {
+ byte[] id = (byte[]) en.nextElement();
+ if (id != null) {
+ SSLSession session =
+ sslContext.getClientSessionContext().getSession(id);
+ if (session.isValid() &&
+ host.getHostName().equals(session.getPeerHost()) &&
+ host.getPort() == session.getPeerPort()) {
+ return session;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/core/java/android/net/http/CertificateValidatorCache.java b/core/java/android/net/http/CertificateValidatorCache.java
new file mode 100644
index 0000000..54a1dbe
--- /dev/null
+++ b/core/java/android/net/http/CertificateValidatorCache.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2008 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.http;
+
+import android.os.SystemClock;
+
+import android.security.Sha1MessageDigest;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertPath;
+import java.security.GeneralSecurityException;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Random;
+
+
+/**
+ * Validator cache used to speed-up certificate chain validation. The idea is
+ * to keep each secure domain name associated with a cryptographically secure
+ * hash of the certificate chain successfully used to validate the domain. If
+ * we establish connection with the domain more than once and each time receive
+ * the same list of certificates, we do not have to re-validate.
+ *
+ * {@hide}
+ */
+class CertificateValidatorCache {
+
+ // TODO: debug only!
+ public static long mSave = 0;
+ public static long mCost = 0;
+ // TODO: debug only!
+
+ /**
+ * The cache-entry lifetime in milliseconds (here, 10 minutes)
+ */
+ private static final long CACHE_ENTRY_LIFETIME = 10 * 60 * 1000;
+
+ /**
+ * The certificate factory
+ */
+ private static CertificateFactory sCertificateFactory;
+
+ /**
+ * The certificate validator cache map (domain to a cache entry)
+ */
+ private HashMap<Integer, CacheEntry> mCacheMap;
+
+ /**
+ * Random salt
+ */
+ private int mBigScrew;
+
+ /**
+ * @param certificate The array of server certificates to compute a
+ * secure hash from
+ * @return The secure hash computed from server certificates
+ */
+ public static byte[] secureHash(Certificate[] certificates) {
+ byte[] secureHash = null;
+
+ // TODO: debug only!
+ long beg = SystemClock.uptimeMillis();
+ // TODO: debug only!
+
+ if (certificates != null && certificates.length != 0) {
+ byte[] encodedCertPath = null;
+ try {
+ synchronized (CertificateValidatorCache.class) {
+ if (sCertificateFactory == null) {
+ try {
+ sCertificateFactory =
+ CertificateFactory.getInstance("X.509");
+ } catch(GeneralSecurityException e) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("CertificateValidatorCache:" +
+ " failed to create the certificate factory");
+ }
+ }
+ }
+ }
+
+ CertPath certPath =
+ sCertificateFactory.generateCertPath(Arrays.asList(certificates));
+ if (certPath != null) {
+ encodedCertPath = certPath.getEncoded();
+ if (encodedCertPath != null) {
+ Sha1MessageDigest messageDigest =
+ new Sha1MessageDigest();
+ secureHash = messageDigest.digest(encodedCertPath);
+ }
+ }
+ } catch (GeneralSecurityException e) {}
+ }
+
+ // TODO: debug only!
+ long end = SystemClock.uptimeMillis();
+ mCost += (end - beg);
+ // TODO: debug only!
+
+ return secureHash;
+ }
+
+ /**
+ * Creates a new certificate-validator cache
+ */
+ public CertificateValidatorCache() {
+ Random random = new Random();
+ mBigScrew = random.nextInt();
+
+ mCacheMap = new HashMap<Integer, CacheEntry>();
+ }
+
+ /**
+ * @param domain The domain to check against
+ * @param secureHash The secure hash to check against
+ * @return True iff there is a valid (not expired) cache entry
+ * associated with the domain and the secure hash
+ */
+ public boolean has(String domain, byte[] secureHash) {
+ boolean rval = false;
+
+ if (domain != null && domain.length() != 0) {
+ if (secureHash != null && secureHash.length != 0) {
+ CacheEntry cacheEntry = (CacheEntry)mCacheMap.get(
+ new Integer(mBigScrew ^ domain.hashCode()));
+ if (cacheEntry != null) {
+ if (!cacheEntry.expired()) {
+ rval = cacheEntry.has(domain, secureHash);
+ // TODO: debug only!
+ if (rval) {
+ mSave += cacheEntry.mSave;
+ }
+ // TODO: debug only!
+ } else {
+ mCacheMap.remove(cacheEntry);
+ }
+ }
+ }
+ }
+
+ return rval;
+ }
+
+ /**
+ * Adds the (domain, secureHash) tuple to the cache
+ * @param domain The domain to be added to the cache
+ * @param secureHash The secure hash to be added to the cache
+ * @return True iff succeeds
+ */
+ public boolean put(String domain, byte[] secureHash, long save) {
+ if (domain != null && domain.length() != 0) {
+ if (secureHash != null && secureHash.length != 0) {
+ mCacheMap.put(
+ new Integer(mBigScrew ^ domain.hashCode()),
+ new CacheEntry(domain, secureHash, save));
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Certificate-validator cache entry. We have one per domain
+ */
+ private class CacheEntry {
+
+ /**
+ * The hash associated with this cache entry
+ */
+ private byte[] mHash;
+
+ /**
+ * The time associated with this cache entry
+ */
+ private long mTime;
+
+ // TODO: debug only!
+ public long mSave;
+ // TODO: debug only!
+
+ /**
+ * The host associated with this cache entry
+ */
+ private String mDomain;
+
+ /**
+ * Creates a new certificate-validator cache entry
+ * @param domain The domain to be associated with this cache entry
+ * @param secureHash The secure hash to be associated with this cache
+ * entry
+ */
+ public CacheEntry(String domain, byte[] secureHash, long save) {
+ mDomain = domain;
+ mHash = secureHash;
+ // TODO: debug only!
+ mSave = save;
+ // TODO: debug only!
+ mTime = SystemClock.uptimeMillis();
+ }
+
+ /**
+ * @return True iff the cache item has expired
+ */
+ public boolean expired() {
+ return CACHE_ENTRY_LIFETIME < SystemClock.uptimeMillis() - mTime;
+ }
+
+ /**
+ * @param domain The domain to check
+ * @param secureHash The secure hash to check
+ * @return True iff the given domain and hash match those associated
+ * with this entry
+ */
+ public boolean has(String domain, byte[] secureHash) {
+ if (domain != null && 0 < domain.length()) {
+ if (!mDomain.equals(domain)) {
+ return false;
+ }
+ }
+
+ int hashLength = secureHash.length;
+ if (secureHash != null && 0 < hashLength) {
+ if (hashLength == mHash.length) {
+ for (int i = 0; i < hashLength; ++i) {
+ if (secureHash[i] != mHash[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+};
diff --git a/core/java/android/net/http/CharArrayBuffers.java b/core/java/android/net/http/CharArrayBuffers.java
new file mode 100644
index 0000000..77d45f6
--- /dev/null
+++ b/core/java/android/net/http/CharArrayBuffers.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 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.http;
+
+import org.apache.http.util.CharArrayBuffer;
+import org.apache.http.protocol.HTTP;
+
+/**
+ * Utility methods for working on CharArrayBuffers.
+ *
+ * {@hide}
+ */
+class CharArrayBuffers {
+
+ static final char uppercaseAddon = 'a' - 'A';
+
+ /**
+ * Returns true if the buffer contains the given string. Ignores leading
+ * whitespace and case.
+ *
+ * @param buffer to search
+ * @param beginIndex index at which we should start
+ * @param str to search for
+ */
+ static boolean containsIgnoreCaseTrimmed(CharArrayBuffer buffer,
+ int beginIndex, final String str) {
+ int len = buffer.length();
+ char[] chars = buffer.buffer();
+ while (beginIndex < len && HTTP.isWhitespace(chars[beginIndex])) {
+ beginIndex++;
+ }
+ int size = str.length();
+ boolean ok = len >= beginIndex + size;
+ for (int j=0; ok && (j<size); j++) {
+ char a = chars[beginIndex+j];
+ char b = str.charAt(j);
+ if (a != b) {
+ a = toLower(a);
+ b = toLower(b);
+ ok = a == b;
+ }
+ }
+ return ok;
+ }
+
+ /**
+ * Returns index of first occurence ch. Lower cases characters leading up
+ * to first occurrence of ch.
+ */
+ static int setLowercaseIndexOf(CharArrayBuffer buffer, final int ch) {
+
+ int beginIndex = 0;
+ int endIndex = buffer.length();
+ char[] chars = buffer.buffer();
+
+ for (int i = beginIndex; i < endIndex; i++) {
+ char current = chars[i];
+ if (current == ch) {
+ return i;
+ } else if (current >= 'A' && current <= 'Z'){
+ // make lower case
+ current += uppercaseAddon;
+ chars[i] = current;
+ }
+ }
+ return -1;
+ }
+
+ private static char toLower(char c) {
+ if (c >= 'A' && c <= 'Z'){
+ c += uppercaseAddon;
+ }
+ return c;
+ }
+}
diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java
new file mode 100644
index 0000000..2c82582
--- /dev/null
+++ b/core/java/android/net/http/Connection.java
@@ -0,0 +1,523 @@
+/*
+ * 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.http;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.ListIterator;
+import java.util.LinkedList;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import org.apache.http.ConnectionReuseStrategy;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpVersion;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.BasicHttpContext;
+
+/**
+ * {@hide}
+ */
+abstract class Connection {
+
+ /**
+ * Allow a TCP connection 60 idle seconds before erroring out
+ */
+ static final int SOCKET_TIMEOUT = 60000;
+
+ private static final int SEND = 0;
+ private static final int READ = 1;
+ private static final int DRAIN = 2;
+ private static final int DONE = 3;
+ private static final String[] states = {"SEND", "READ", "DRAIN", "DONE"};
+
+ Context mContext;
+
+ /** The low level connection */
+ protected AndroidHttpClientConnection mHttpClientConnection = null;
+
+ /**
+ * The server SSL certificate associated with this connection
+ * (null if the connection is not secure)
+ * It would be nice to store the whole certificate chain, but
+ * we want to keep things as light-weight as possible
+ */
+ protected SslCertificate mCertificate = null;
+
+ /**
+ * The host this connection is connected to. If using proxy,
+ * this is set to the proxy address
+ */
+ HttpHost mHost;
+
+ /** true if the connection can be reused for sending more requests */
+ private boolean mCanPersist;
+
+ /** context required by ConnectionReuseStrategy. */
+ private HttpContext mHttpContext;
+
+ /** set when cancelled */
+ private static int STATE_NORMAL = 0;
+ private static int STATE_CANCEL_REQUESTED = 1;
+ private int mActive = STATE_NORMAL;
+
+ /** The number of times to try to re-connect (if connect fails). */
+ private final static int RETRY_REQUEST_LIMIT = 2;
+
+ private static final int MIN_PIPE = 2;
+ private static final int MAX_PIPE = 3;
+
+ /**
+ * Doesn't seem to exist anymore in the new HTTP client, so copied here.
+ */
+ private static final String HTTP_CONNECTION = "http.connection";
+
+ RequestQueue.ConnectionManager mConnectionManager;
+ RequestFeeder mRequestFeeder;
+
+ /**
+ * Buffer for feeding response blocks to webkit. One block per
+ * connection reduces memory churn.
+ */
+ private byte[] mBuf;
+
+ protected Connection(Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ mContext = context;
+ mHost = host;
+ mConnectionManager = connectionManager;
+ mRequestFeeder = requestFeeder;
+
+ mCanPersist = false;
+ mHttpContext = new BasicHttpContext(null);
+ }
+
+ HttpHost getHost() {
+ return mHost;
+ }
+
+ /**
+ * connection factory: returns an HTTP or HTTPS connection as
+ * necessary
+ */
+ static Connection getConnection(
+ Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+
+ if (host.getSchemeName().equals("http")) {
+ return new HttpConnection(context, host, connectionManager,
+ requestFeeder);
+ }
+
+ // Otherwise, default to https
+ return new HttpsConnection(context, host, connectionManager,
+ requestFeeder);
+ }
+
+ /**
+ * @return The server SSL certificate associated with this
+ * connection (null if the connection is not secure)
+ */
+ /* package */ SslCertificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Close current network connection
+ * Note: this runs in non-network thread
+ */
+ void cancel() {
+ mActive = STATE_CANCEL_REQUESTED;
+ closeConnection();
+ if (HttpLog.LOGV) HttpLog.v(
+ "Connection.cancel(): connection closed " + mHost);
+ }
+
+ /**
+ * Process requests in queue
+ * pipelines requests
+ */
+ void processRequests(Request firstRequest) {
+ Request req = null;
+ boolean empty;
+ int error = EventHandler.OK;
+ Exception exception = null;
+
+ LinkedList<Request> pipe = new LinkedList<Request>();
+
+ int minPipe = MIN_PIPE, maxPipe = MAX_PIPE;
+ int state = SEND;
+
+ while (state != DONE) {
+ if (HttpLog.LOGV) HttpLog.v(
+ states[state] + " pipe " + pipe.size());
+
+ /* If a request was cancelled, give other cancel requests
+ some time to go through so we don't uselessly restart
+ connections */
+ if (mActive == STATE_CANCEL_REQUESTED) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException x) { /* ignore */ }
+ mActive = STATE_NORMAL;
+ }
+
+ switch (state) {
+ case SEND: {
+ if (pipe.size() == maxPipe) {
+ state = READ;
+ break;
+ }
+ /* get a request */
+ if (firstRequest == null) {
+ req = mRequestFeeder.getRequest(mHost);
+ } else {
+ req = firstRequest;
+ firstRequest = null;
+ }
+ if (req == null) {
+ state = DRAIN;
+ break;
+ }
+ req.setConnection(this);
+
+ /* Don't work on cancelled requests. */
+ if (req.mCancelled) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests(): skipping cancelled request "
+ + req);
+ req.complete();
+ break;
+ }
+
+ if (mHttpClientConnection == null ||
+ !mHttpClientConnection.isOpen()) {
+ /* If this call fails, the address is bad or
+ the net is down. Punt for now.
+
+ FIXME: blow out entire queue here on
+ connection failure if net up? */
+
+ if (!openHttpConnection(req)) {
+ state = DONE;
+ break;
+ }
+ }
+
+ try {
+ /* FIXME: don't increment failure count if old
+ connection? There should not be a penalty for
+ attempting to reuse an old connection */
+ req.sendRequest(mHttpClientConnection);
+ } catch (HttpException e) {
+ exception = e;
+ error = EventHandler.ERROR;
+ } catch (IOException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IllegalStateException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ }
+ if (exception != null) {
+ if (httpFailure(req, error, exception) &&
+ !req.mCancelled) {
+ /* retry request if not permanent failure
+ or cancelled */
+ pipe.addLast(req);
+ }
+ exception = null;
+ state = (clearPipe(pipe) ||
+ !mConnectionManager.isNetworkConnected()) ?
+ DONE : SEND;
+ minPipe = maxPipe = 1;
+ break;
+ }
+
+ pipe.addLast(req);
+ if (!mCanPersist) state = READ;
+ break;
+
+ }
+ case DRAIN:
+ case READ: {
+ empty = !mRequestFeeder.haveRequest(mHost);
+ int pipeSize = pipe.size();
+ if (state != DRAIN && pipeSize < minPipe &&
+ !empty && mCanPersist) {
+ state = SEND;
+ break;
+ } else if (pipeSize == 0) {
+ /* Done if no other work to do */
+ state = empty ? DONE : SEND;
+ break;
+ }
+
+ req = (Request)pipe.removeFirst();
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests() reading " + req);
+
+ try {
+ req.readResponse(mHttpClientConnection);
+ } catch (ParseException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IOException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IllegalStateException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ }
+ if (exception != null) {
+ if (httpFailure(req, error, exception) &&
+ !req.mCancelled) {
+ /* retry request if not permanent failure
+ or cancelled */
+ req.reset();
+ pipe.addFirst(req);
+ }
+ exception = null;
+ mCanPersist = false;
+ }
+ if (!mCanPersist) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests(): no persist, closing " +
+ mHost);
+
+ closeConnection();
+
+ mHttpContext.removeAttribute(HTTP_CONNECTION);
+ clearPipe(pipe);
+ minPipe = maxPipe = 1;
+ /* If network active continue to service this queue */
+ state = mConnectionManager.isNetworkConnected() ?
+ SEND : DONE;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * After a send/receive failure, any pipelined requests must be
+ * cleared back to the mRequest queue
+ * @return true if mRequests is empty after pipe cleared
+ */
+ private boolean clearPipe(LinkedList<Request> pipe) {
+ boolean empty = true;
+ if (HttpLog.LOGV) HttpLog.v(
+ "Connection.clearPipe(): clearing pipe " + pipe.size());
+ synchronized (mRequestFeeder) {
+ Request tReq;
+ while (!pipe.isEmpty()) {
+ tReq = (Request)pipe.removeLast();
+ if (HttpLog.LOGV) HttpLog.v(
+ "clearPipe() adding back " + mHost + " " + tReq);
+ mRequestFeeder.requeueRequest(tReq);
+ empty = false;
+ }
+ if (empty) empty = mRequestFeeder.haveRequest(mHost);
+ }
+ return empty;
+ }
+
+ /**
+ * @return true on success
+ */
+ private boolean openHttpConnection(Request req) {
+
+ long now = SystemClock.uptimeMillis();
+ int error = EventHandler.OK;
+ Exception exception = null;
+
+ try {
+ // reset the certificate to null before opening a connection
+ mCertificate = null;
+ mHttpClientConnection = openConnection(req);
+ if (mHttpClientConnection != null) {
+ mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT);
+ mHttpContext.setAttribute(HTTP_CONNECTION,
+ mHttpClientConnection);
+ } else {
+ // we tried to do SSL tunneling, failed,
+ // and need to drop the request;
+ // we have already informed the handler
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ return false;
+ }
+ } catch (UnknownHostException e) {
+ if (HttpLog.LOGV) HttpLog.v("Failed to open connection");
+ error = EventHandler.ERROR_LOOKUP;
+ exception = e;
+ } catch (SSLConnectionClosedByUserException e) {
+ // hack: if we have an SSL connection failure,
+ // we don't want to reconnect
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ // no error message
+ return false;
+ } catch (SSLHandshakeException e) {
+ // hack: if we have an SSL connection failure,
+ // we don't want to reconnect
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ if (HttpLog.LOGV) HttpLog.v(
+ "SSL exception performing handshake");
+ error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE;
+ exception = e;
+ } catch (IOException e) {
+ error = EventHandler.ERROR_CONNECT;
+ exception = e;
+ }
+
+ if (HttpLog.LOGV) {
+ long now2 = SystemClock.uptimeMillis();
+ HttpLog.v("Connection.openHttpConnection() " +
+ (now2 - now) + " " + mHost);
+ }
+
+ if (error == EventHandler.OK) {
+ return true;
+ } else {
+ if (mConnectionManager.isNetworkConnected() == false ||
+ req.mFailCount < RETRY_REQUEST_LIMIT) {
+ // requeue
+ mRequestFeeder.requeueRequest(req);
+ req.mFailCount++;
+ } else {
+ httpFailure(req, error, exception);
+ }
+ return error == EventHandler.OK;
+ }
+ }
+
+ /**
+ * Helper. Calls the mEventHandler's error() method only if
+ * request failed permanently. Increments mFailcount on failure.
+ *
+ * Increments failcount only if the network is believed to be
+ * connected
+ *
+ * @return true if request can be retried (less than
+ * RETRY_REQUEST_LIMIT failures have occurred).
+ */
+ private boolean httpFailure(Request req, int errorId, Exception e) {
+ boolean ret = true;
+ boolean networkConnected = mConnectionManager.isNetworkConnected();
+
+ // e.printStackTrace();
+ if (HttpLog.LOGV) HttpLog.v(
+ "httpFailure() ******* " + e + " count " + req.mFailCount +
+ " networkConnected " + networkConnected + " " + mHost + " " + req.getUri());
+
+ if (networkConnected && ++req.mFailCount >= RETRY_REQUEST_LIMIT) {
+ ret = false;
+ String error;
+ if (errorId < 0) {
+ error = mContext.getText(
+ EventHandler.errorStringResources[-errorId]).toString();
+ } else {
+ Throwable cause = e.getCause();
+ error = cause != null ? cause.toString() : e.getMessage();
+ }
+ req.mEventHandler.error(errorId, error);
+ req.complete();
+ }
+
+ closeConnection();
+ mHttpContext.removeAttribute(HTTP_CONNECTION);
+
+ return ret;
+ }
+
+ HttpContext getHttpContext() {
+ return mHttpContext;
+ }
+
+ /**
+ * Use same logic as ConnectionReuseStrategy
+ * @see ConnectionReuseStrategy
+ */
+ private boolean keepAlive(HttpEntity entity,
+ ProtocolVersion ver, int connType, final HttpContext context) {
+ org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection)
+ context.getAttribute(ExecutionContext.HTTP_CONNECTION);
+
+ if (conn != null && !conn.isOpen())
+ return false;
+ // do NOT check for stale connection, that is an expensive operation
+
+ if (entity != null) {
+ if (entity.getContentLength() < 0) {
+ if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ // if the content length is not known and is not chunk
+ // encoded, the connection cannot be reused
+ return false;
+ }
+ }
+ }
+ // Check for 'Connection' directive
+ if (connType == Headers.CONN_CLOSE) {
+ return false;
+ } else if (connType == Headers.CONN_KEEP_ALIVE) {
+ return true;
+ }
+ // Resorting to protocol version default close connection policy
+ return !ver.lessEquals(HttpVersion.HTTP_1_0);
+ }
+
+ void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) {
+ mCanPersist = keepAlive(entity, ver, connType, mHttpContext);
+ }
+
+ void setCanPersist(boolean canPersist) {
+ mCanPersist = canPersist;
+ }
+
+ boolean getCanPersist() {
+ return mCanPersist;
+ }
+
+ /** typically http or https... set by subclass */
+ abstract String getScheme();
+ abstract void closeConnection();
+ abstract AndroidHttpClientConnection openConnection(Request req) throws IOException;
+
+ /**
+ * Prints request queue to log, for debugging.
+ * returns request count
+ */
+ public synchronized String toString() {
+ return mHost.toString();
+ }
+
+ byte[] getBuf() {
+ if (mBuf == null) mBuf = new byte[8192];
+ return mBuf;
+ }
+
+}
diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java
new file mode 100644
index 0000000..8e759e2
--- /dev/null
+++ b/core/java/android/net/http/ConnectionThread.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2008 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.http;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import org.apache.http.HttpHost;
+
+import java.lang.Thread;
+
+/**
+ * {@hide}
+ */
+class ConnectionThread extends Thread {
+
+ static final int WAIT_TIMEOUT = 5000;
+ static final int WAIT_TICK = 1000;
+
+ // Performance probe
+ long mStartThreadTime;
+ long mCurrentThreadTime;
+
+ private boolean mWaiting;
+ private volatile boolean mRunning = true;
+ private Context mContext;
+ private RequestQueue.ConnectionManager mConnectionManager;
+ private RequestFeeder mRequestFeeder;
+
+ private int mId;
+ Connection mConnection;
+
+ ConnectionThread(Context context,
+ int id,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ super();
+ mContext = context;
+ setName("http" + id);
+ mId = id;
+ mConnectionManager = connectionManager;
+ mRequestFeeder = requestFeeder;
+ }
+
+ void requestStop() {
+ synchronized (mRequestFeeder) {
+ mRunning = false;
+ mRequestFeeder.notify();
+ }
+ }
+
+ /**
+ * Loop until app shutdown. Runs connections in priority
+ * order.
+ */
+ public void run() {
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
+
+ mStartThreadTime = -1;
+ mCurrentThreadTime = SystemClock.currentThreadTimeMillis();
+
+ while (mRunning) {
+ Request request;
+
+ /* Get a request to process */
+ request = mRequestFeeder.getRequest();
+
+ /* wait for work */
+ if (request == null) {
+ synchronized(mRequestFeeder) {
+ if (HttpLog.LOGV) HttpLog.v("ConnectionThread: Waiting for work");
+ mWaiting = true;
+ try {
+ if (mStartThreadTime != -1) {
+ mCurrentThreadTime = SystemClock
+ .currentThreadTimeMillis();
+ }
+ mRequestFeeder.wait();
+ } catch (InterruptedException e) {
+ }
+ mWaiting = false;
+ }
+ } else {
+ if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " +
+ request.mHost + " " + request );
+
+ HttpHost proxy = mConnectionManager.getProxyHost();
+
+ HttpHost host;
+ if (false) {
+ // Allow https proxy
+ host = proxy == null ? request.mHost : proxy;
+ } else {
+ // Disallow https proxy -- tmob proxy server
+ // serves a request loop for https reqs
+ host = (proxy == null ||
+ request.mHost.getSchemeName().equals("https")) ?
+ request.mHost : proxy;
+ }
+ mConnection = mConnectionManager.getConnection(mContext, host);
+ mConnection.processRequests(request);
+ if (mConnection.getCanPersist()) {
+ if (!mConnectionManager.recycleConnection(host,
+ mConnection)) {
+ mConnection.closeConnection();
+ }
+ } else {
+ mConnection.closeConnection();
+ }
+ mConnection = null;
+ }
+
+ }
+ }
+
+ public synchronized String toString() {
+ String con = mConnection == null ? "" : mConnection.toString();
+ String active = mWaiting ? "w" : "a";
+ return "cid " + mId + " " + active + " " + con;
+ }
+
+}
diff --git a/core/java/android/net/http/DomainNameChecker.java b/core/java/android/net/http/DomainNameChecker.java
new file mode 100644
index 0000000..e4c8009
--- /dev/null
+++ b/core/java/android/net/http/DomainNameChecker.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2008 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.http;
+
+import org.bouncycastle.asn1.x509.X509Name;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.cert.X509Certificate;
+import java.security.cert.CertificateParsingException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.Vector;
+
+/**
+ * Implements basic domain-name validation as specified by RFC2818.
+ *
+ * {@hide}
+ */
+public class DomainNameChecker {
+ private static Pattern QUICK_IP_PATTERN;
+ static {
+ try {
+ QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$");
+ } catch (PatternSyntaxException e) {}
+ }
+
+ private static final int ALT_DNS_NAME = 2;
+ private static final int ALT_IPA_NAME = 7;
+
+ /**
+ * Checks the site certificate against the domain name of the site being visited
+ * @param certificate The certificate to check
+ * @param thisDomain The domain name of the site being visited
+ * @return True iff if there is a domain match as specified by RFC2818
+ */
+ public static boolean match(X509Certificate certificate, String thisDomain) {
+ if (certificate == null || thisDomain == null || thisDomain.length() == 0) {
+ return false;
+ }
+
+ thisDomain = thisDomain.toLowerCase();
+ if (!isIpAddress(thisDomain)) {
+ return matchDns(certificate, thisDomain);
+ } else {
+ return matchIpAddress(certificate, thisDomain);
+ }
+ }
+
+ /**
+ * @return True iff the domain name is specified as an IP address
+ */
+ private static boolean isIpAddress(String domain) {
+ boolean rval = (domain != null && domain.length() != 0);
+ if (rval) {
+ try {
+ // do a quick-dirty IP match first to avoid DNS lookup
+ rval = QUICK_IP_PATTERN.matcher(domain).matches();
+ if (rval) {
+ rval = domain.equals(
+ InetAddress.getByName(domain).getHostAddress());
+ }
+ } catch (UnknownHostException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "unknown host exception";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.isIpAddress(): " + errorMessage);
+ }
+
+ rval = false;
+ }
+ }
+
+ return rval;
+ }
+
+ /**
+ * Checks the site certificate against the IP domain name of the site being visited
+ * @param certificate The certificate to check
+ * @param thisDomain The DNS domain name of the site being visited
+ * @return True iff if there is a domain match as specified by RFC2818
+ */
+ private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.matchIpAddress(): this domain: " + thisDomain);
+ }
+
+ try {
+ Collection subjectAltNames = certificate.getSubjectAlternativeNames();
+ if (subjectAltNames != null) {
+ Iterator i = subjectAltNames.iterator();
+ while (i.hasNext()) {
+ List altNameEntry = (List)(i.next());
+ if (altNameEntry != null && 2 <= altNameEntry.size()) {
+ Integer altNameType = (Integer)(altNameEntry.get(0));
+ if (altNameType != null) {
+ if (altNameType.intValue() == ALT_IPA_NAME) {
+ String altName = (String)(altNameEntry.get(1));
+ if (altName != null) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("alternative IP: " + altName);
+ }
+ if (thisDomain.equalsIgnoreCase(altName)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (CertificateParsingException e) {}
+
+ return false;
+ }
+
+ /**
+ * Checks the site certificate against the DNS domain name of the site being visited
+ * @param certificate The certificate to check
+ * @param thisDomain The DNS domain name of the site being visited
+ * @return True iff if there is a domain match as specified by RFC2818
+ */
+ private static boolean matchDns(X509Certificate certificate, String thisDomain) {
+ boolean hasDns = false;
+ try {
+ Collection subjectAltNames = certificate.getSubjectAlternativeNames();
+ if (subjectAltNames != null) {
+ Iterator i = subjectAltNames.iterator();
+ while (i.hasNext()) {
+ List altNameEntry = (List)(i.next());
+ if (altNameEntry != null && 2 <= altNameEntry.size()) {
+ Integer altNameType = (Integer)(altNameEntry.get(0));
+ if (altNameType != null) {
+ if (altNameType.intValue() == ALT_DNS_NAME) {
+ hasDns = true;
+ String altName = (String)(altNameEntry.get(1));
+ if (altName != null) {
+ if (matchDns(thisDomain, altName)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (CertificateParsingException e) {
+ // one way we can get here is if an alternative name starts with
+ // '*' character, which is contrary to one interpretation of the
+ // spec (a valid DNS name must start with a letter); there is no
+ // good way around this, and in order to be compatible we proceed
+ // to check the common name (ie, ignore alternative names)
+ if (HttpLog.LOGV) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "failed to parse certificate";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.matchDns(): " + errorMessage);
+ }
+ }
+ }
+
+ if (!hasDns) {
+ X509Name xName = new X509Name(certificate.getSubjectDN().getName());
+ Vector val = xName.getValues();
+ Vector oid = xName.getOIDs();
+ for (int i = 0; i < oid.size(); i++) {
+ if (oid.elementAt(i).equals(X509Name.CN)) {
+ return matchDns(thisDomain, (String)(val.elementAt(i)));
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param thisDomain The domain name of the site being visited
+ * @param thatDomain The domain name from the certificate
+ * @return True iff thisDomain matches thatDomain as specified by RFC2818
+ */
+ private static boolean matchDns(String thisDomain, String thatDomain) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.matchDns():" +
+ " this domain: " + thisDomain +
+ " that domain: " + thatDomain);
+ }
+
+ if (thisDomain == null || thisDomain.length() == 0 ||
+ thatDomain == null || thatDomain.length() == 0) {
+ return false;
+ }
+
+ thatDomain = thatDomain.toLowerCase();
+
+ // (a) domain name strings are equal, ignoring case: X matches X
+ boolean rval = thisDomain.equals(thatDomain);
+ if (!rval) {
+ String[] thisDomainTokens = thisDomain.split("\\.");
+ String[] thatDomainTokens = thatDomain.split("\\.");
+
+ int thisDomainTokensNum = thisDomainTokens.length;
+ int thatDomainTokensNum = thatDomainTokens.length;
+
+ // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X
+ if (thisDomainTokensNum >= thatDomainTokensNum) {
+ for (int i = thatDomainTokensNum - 1; i >= 0; --i) {
+ rval = thisDomainTokens[i].equals(thatDomainTokens[i]);
+ if (!rval) {
+ // (c) OR we have a special *-match:
+ // Z.Y.X matches *.Y.X but does not match *.X
+ rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum);
+ if (rval) {
+ rval = thatDomainTokens[0].equals("*");
+ if (!rval) {
+ // (d) OR we have a *-component match:
+ // f*.com matches foo.com but not bar.com
+ rval = domainTokenMatch(
+ thisDomainTokens[0], thatDomainTokens[0]);
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ return rval;
+ }
+
+ /**
+ * @param thisDomainToken The domain token from the current domain name
+ * @param thatDomainToken The domain token from the certificate
+ * @return True iff thisDomainToken matches thatDomainToken, using the
+ * wildcard match as specified by RFC2818-3.1. For example, f*.com must
+ * match foo.com but not bar.com
+ */
+ private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) {
+ if (thisDomainToken != null && thatDomainToken != null) {
+ int starIndex = thatDomainToken.indexOf('*');
+ if (starIndex >= 0) {
+ if (thatDomainToken.length() - 1 <= thisDomainToken.length()) {
+ String prefix = thatDomainToken.substring(0, starIndex);
+ String suffix = thatDomainToken.substring(starIndex + 1);
+
+ return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix);
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/core/java/android/net/http/EventHandler.java b/core/java/android/net/http/EventHandler.java
new file mode 100644
index 0000000..830d1f1
--- /dev/null
+++ b/core/java/android/net/http/EventHandler.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+
+/**
+ * Callbacks in this interface are made as an HTTP request is
+ * processed. The normal order of callbacks is status(), headers(),
+ * then multiple data() then endData(). handleSslErrorRequest(), if
+ * there is an SSL certificate error. error() can occur anywhere
+ * in the transaction.
+ *
+ * {@hide}
+ */
+
+public interface EventHandler {
+
+ /**
+ * Error codes used in the error() callback. Positive error codes
+ * are reserved for codes sent by http servers. Negative error
+ * codes are connection/parsing failures, etc.
+ */
+
+ /** Success */
+ public static final int OK = 0;
+ /** Generic error */
+ public static final int ERROR = -1;
+ /** Server or proxy hostname lookup failed */
+ public static final int ERROR_LOOKUP = -2;
+ /** Unsupported authentication scheme (ie, not basic or digest) */
+ public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3;
+ /** User authentication failed on server */
+ public static final int ERROR_AUTH = -4;
+ /** User authentication failed on proxy */
+ public static final int ERROR_PROXYAUTH = -5;
+ /** Could not connect to server */
+ public static final int ERROR_CONNECT = -6;
+ /** Failed to write to or read from server */
+ public static final int ERROR_IO = -7;
+ /** Connection timed out */
+ public static final int ERROR_TIMEOUT = -8;
+ /** Too many redirects */
+ public static final int ERROR_REDIRECT_LOOP = -9;
+ /** Unsupported URI scheme (ie, not http, https, etc) */
+ public static final int ERROR_UNSUPPORTED_SCHEME = -10;
+ /** Failed to perform SSL handshake */
+ public static final int ERROR_FAILED_SSL_HANDSHAKE = -11;
+ /** Bad URL */
+ public static final int ERROR_BAD_URL = -12;
+ /** Generic file error for file:/// loads */
+ public static final int FILE_ERROR = -13;
+ /** File not found error for file:/// loads */
+ public static final int FILE_NOT_FOUND_ERROR = -14;
+ /** Too many requests queued */
+ public static final int TOO_MANY_REQUESTS_ERROR = -15;
+
+ final static int[] errorStringResources = {
+ com.android.internal.R.string.httpErrorOk,
+ com.android.internal.R.string.httpError,
+ com.android.internal.R.string.httpErrorLookup,
+ com.android.internal.R.string.httpErrorUnsupportedAuthScheme,
+ com.android.internal.R.string.httpErrorAuth,
+ com.android.internal.R.string.httpErrorProxyAuth,
+ com.android.internal.R.string.httpErrorConnect,
+ com.android.internal.R.string.httpErrorIO,
+ com.android.internal.R.string.httpErrorTimeout,
+ com.android.internal.R.string.httpErrorRedirectLoop,
+ com.android.internal.R.string.httpErrorUnsupportedScheme,
+ com.android.internal.R.string.httpErrorFailedSslHandshake,
+ com.android.internal.R.string.httpErrorBadUrl,
+ com.android.internal.R.string.httpErrorFile,
+ com.android.internal.R.string.httpErrorFileNotFound,
+ com.android.internal.R.string.httpErrorTooManyRequests
+ };
+
+ /**
+ * Called after status line has been sucessfully processed.
+ * @param major_version HTTP version advertised by server. major
+ * is the part before the "."
+ * @param minor_version HTTP version advertised by server. minor
+ * is the part after the "."
+ * @param code HTTP Status code. See RFC 2616.
+ * @param reason_phrase Textual explanation sent by server
+ */
+ public void status(int major_version,
+ int minor_version,
+ int code,
+ String reason_phrase);
+
+ /**
+ * Called after all headers are successfully processed.
+ */
+ public void headers(Headers headers);
+
+ /**
+ * An array containing all or part of the http body as read from
+ * the server.
+ * @param data A byte array containing the content
+ * @param len The length of valid content in data
+ *
+ * Note: chunked and compressed encodings are handled within
+ * android.net.http. Decoded data is passed through this
+ * interface.
+ */
+ public void data(byte[] data, int len);
+
+ /**
+ * Called when the document is completely read. No more data()
+ * callbacks will be made after this call
+ */
+ public void endData();
+
+ /**
+ * SSL certificate callback called every time a resource is
+ * loaded via a secure connection
+ */
+ public void certificate(SslCertificate certificate);
+
+ /**
+ * There was trouble.
+ * @param id One of the error codes defined below
+ * @param description of error
+ */
+ public void error(int id, String description);
+
+ /**
+ * SSL certificate error callback. Handles SSL error(s) on the way
+ * up to the user. The callback has to make sure that restartConnection() is called,
+ * otherwise the connection will be suspended indefinitely.
+ */
+ public void handleSslErrorRequest(SslError error);
+
+}
diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java
new file mode 100644
index 0000000..5d85ba4
--- /dev/null
+++ b/core/java/android/net/http/Headers.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import android.util.Config;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+import org.apache.http.HeaderElement;
+import org.apache.http.entity.ContentLengthStrategy;
+import org.apache.http.message.BasicHeaderValueParser;
+import org.apache.http.message.ParserCursor;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.CharArrayBuffer;
+
+/**
+ * Manages received headers
+ *
+ * {@hide}
+ */
+public final class Headers {
+ private static final String LOGTAG = "Http";
+
+ // header parsing constant
+ /**
+ * indicate HTTP 1.0 connection close after the response
+ */
+ public final static int CONN_CLOSE = 1;
+ /**
+ * indicate HTTP 1.1 connection keep alive
+ */
+ public final static int CONN_KEEP_ALIVE = 2;
+
+ // initial values.
+ public final static int NO_CONN_TYPE = 0;
+ public final static long NO_TRANSFER_ENCODING = 0;
+ public final static long NO_CONTENT_LENGTH = -1;
+
+ // header string
+ public final static String TRANSFER_ENCODING = "transfer-encoding";
+ public final static String CONTENT_LEN = "content-length";
+ public final static String CONTENT_TYPE = "content-type";
+ public final static String CONTENT_ENCODING = "content-encoding";
+ public final static String CONN_DIRECTIVE = "connection";
+
+ public final static String LOCATION = "location";
+ public final static String PROXY_CONNECTION = "proxy-connection";
+
+ public final static String WWW_AUTHENTICATE = "www-authenticate";
+ public final static String PROXY_AUTHENTICATE = "proxy-authenticate";
+ public final static String CONTENT_DISPOSITION = "content-disposition";
+ public final static String ACCEPT_RANGES = "accept-ranges";
+ public final static String EXPIRES = "expires";
+ public final static String CACHE_CONTROL = "cache-control";
+ public final static String LAST_MODIFIED = "last-modified";
+ public final static String ETAG = "etag";
+ public final static String SET_COOKIE = "set-cookie";
+ public final static String PRAGMA = "pragma";
+ public final static String REFRESH = "refresh";
+
+ // following hash are generated by String.hashCode()
+ private final static int HASH_TRANSFER_ENCODING = 1274458357;
+ private final static int HASH_CONTENT_LEN = -1132779846;
+ private final static int HASH_CONTENT_TYPE = 785670158;
+ private final static int HASH_CONTENT_ENCODING = 2095084583;
+ private final static int HASH_CONN_DIRECTIVE = -775651618;
+ private final static int HASH_LOCATION = 1901043637;
+ private final static int HASH_PROXY_CONNECTION = 285929373;
+ private final static int HASH_WWW_AUTHENTICATE = -243037365;
+ private final static int HASH_PROXY_AUTHENTICATE = -301767724;
+ private final static int HASH_CONTENT_DISPOSITION = -1267267485;
+ private final static int HASH_ACCEPT_RANGES = 1397189435;
+ private final static int HASH_EXPIRES = -1309235404;
+ private final static int HASH_CACHE_CONTROL = -208775662;
+ private final static int HASH_LAST_MODIFIED = 150043680;
+ private final static int HASH_ETAG = 3123477;
+ private final static int HASH_SET_COOKIE = 1237214767;
+ private final static int HASH_PRAGMA = -980228804;
+ private final static int HASH_REFRESH = 1085444827;
+
+ private long transferEncoding;
+ private long contentLength; // Content length of the incoming data
+ private int connectionType;
+
+ private String contentType;
+ private String contentEncoding;
+ private String location;
+ private String wwwAuthenticate;
+ private String proxyAuthenticate;
+ private String contentDisposition;
+ private String acceptRanges;
+ private String expires;
+ private String cacheControl;
+ private String lastModified;
+ private String etag;
+ private String pragma;
+ private String refresh;
+ private ArrayList<String> cookies = new ArrayList<String>(2);
+
+ public Headers() {
+ transferEncoding = NO_TRANSFER_ENCODING;
+ contentLength = NO_CONTENT_LENGTH;
+ connectionType = NO_CONN_TYPE;
+ }
+
+ public void parseHeader(CharArrayBuffer buffer) {
+ int pos = CharArrayBuffers.setLowercaseIndexOf(buffer, ':');
+ if (pos == -1) {
+ return;
+ }
+ String name = buffer.substringTrimmed(0, pos);
+ if (name.length() == 0) {
+ return;
+ }
+ pos++;
+
+ if (HttpLog.LOGV) {
+ String val = buffer.substringTrimmed(pos, buffer.length());
+ HttpLog.v("hdr " + buffer.length() + " " + buffer);
+ }
+
+ switch (name.hashCode()) {
+ case HASH_TRANSFER_ENCODING:
+ if (name.equals(TRANSFER_ENCODING)) {
+ // headers.transferEncoding =
+ HeaderElement[] encodings = BasicHeaderValueParser.DEFAULT
+ .parseElements(buffer, new ParserCursor(pos,
+ buffer.length()));
+ // The chunked encoding must be the last one applied RFC2616,
+ // 14.41
+ int len = encodings.length;
+ if (HTTP.IDENTITY_CODING.equalsIgnoreCase(buffer
+ .substringTrimmed(pos, buffer.length()))) {
+ transferEncoding = ContentLengthStrategy.IDENTITY;
+ } else if ((len > 0)
+ && (HTTP.CHUNK_CODING
+ .equalsIgnoreCase(encodings[len - 1].getName()))) {
+ transferEncoding = ContentLengthStrategy.CHUNKED;
+ } else {
+ transferEncoding = ContentLengthStrategy.IDENTITY;
+ }
+ }
+ break;
+ case HASH_CONTENT_LEN:
+ if (name.equals(CONTENT_LEN)) {
+ try {
+ contentLength = Long.parseLong(buffer.substringTrimmed(pos,
+ buffer.length()));
+ } catch (NumberFormatException e) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "Headers.headers(): error parsing"
+ + " content length: " + buffer.toString());
+ }
+ }
+ }
+ break;
+ case HASH_CONTENT_TYPE:
+ if (name.equals(CONTENT_TYPE)) {
+ contentType = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_CONTENT_ENCODING:
+ if (name.equals(CONTENT_ENCODING)) {
+ contentEncoding = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_CONN_DIRECTIVE:
+ if (name.equals(CONN_DIRECTIVE)) {
+ setConnectionType(buffer, pos);
+ }
+ break;
+ case HASH_LOCATION:
+ if (name.equals(LOCATION)) {
+ location = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_PROXY_CONNECTION:
+ if (name.equals(PROXY_CONNECTION)) {
+ setConnectionType(buffer, pos);
+ }
+ break;
+ case HASH_WWW_AUTHENTICATE:
+ if (name.equals(WWW_AUTHENTICATE)) {
+ wwwAuthenticate = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_PROXY_AUTHENTICATE:
+ if (name.equals(PROXY_AUTHENTICATE)) {
+ proxyAuthenticate = buffer.substringTrimmed(pos, buffer
+ .length());
+ }
+ break;
+ case HASH_CONTENT_DISPOSITION:
+ if (name.equals(CONTENT_DISPOSITION)) {
+ contentDisposition = buffer.substringTrimmed(pos, buffer
+ .length());
+ }
+ break;
+ case HASH_ACCEPT_RANGES:
+ if (name.equals(ACCEPT_RANGES)) {
+ acceptRanges = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_EXPIRES:
+ if (name.equals(EXPIRES)) {
+ expires = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_CACHE_CONTROL:
+ if (name.equals(CACHE_CONTROL)) {
+ cacheControl = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_LAST_MODIFIED:
+ if (name.equals(LAST_MODIFIED)) {
+ lastModified = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_ETAG:
+ if (name.equals(ETAG)) {
+ etag = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_SET_COOKIE:
+ if (name.equals(SET_COOKIE)) {
+ cookies.add(buffer.substringTrimmed(pos, buffer.length()));
+ }
+ break;
+ case HASH_PRAGMA:
+ if (name.equals(PRAGMA)) {
+ pragma = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_REFRESH:
+ if (name.equals(REFRESH)) {
+ refresh = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ default:
+ // ignore
+ }
+ }
+
+ public long getTransferEncoding() {
+ return transferEncoding;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public int getConnectionType() {
+ return connectionType;
+ }
+
+ private void setConnectionType(CharArrayBuffer buffer, int pos) {
+ if (CharArrayBuffers.containsIgnoreCaseTrimmed(
+ buffer, pos, HTTP.CONN_CLOSE)) {
+ connectionType = CONN_CLOSE;
+ } else if (CharArrayBuffers.containsIgnoreCaseTrimmed(
+ buffer, pos, HTTP.CONN_KEEP_ALIVE)) {
+ connectionType = CONN_KEEP_ALIVE;
+ }
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public String getContentEncoding() {
+ return this.contentEncoding;
+ }
+
+ public String getLocation() {
+ return this.location;
+ }
+
+ public String getWwwAuthenticate() {
+ return this.wwwAuthenticate;
+ }
+
+ public String getProxyAuthenticate() {
+ return this.proxyAuthenticate;
+ }
+
+ public String getContentDisposition() {
+ return this.contentDisposition;
+ }
+
+ public String getAcceptRanges() {
+ return this.acceptRanges;
+ }
+
+ public String getExpires() {
+ return this.expires;
+ }
+
+ public String getCacheControl() {
+ return this.cacheControl;
+ }
+
+ public String getLastModified() {
+ return this.lastModified;
+ }
+
+ public String getEtag() {
+ return this.etag;
+ }
+
+ public ArrayList<String> getSetCookie() {
+ return this.cookies;
+ }
+
+ public String getPragma() {
+ return this.pragma;
+ }
+
+ public String getRefresh() {
+ return this.refresh;
+ }
+
+ public void setContentLength(long value) {
+ this.contentLength = value;
+ }
+
+ public void setContentType(String value) {
+ this.contentType = value;
+ }
+
+ public void setContentEncoding(String value) {
+ this.contentEncoding = value;
+ }
+
+ public void setLocation(String value) {
+ this.location = value;
+ }
+
+ public void setWwwAuthenticate(String value) {
+ this.wwwAuthenticate = value;
+ }
+
+ public void setProxyAuthenticate(String value) {
+ this.proxyAuthenticate = value;
+ }
+
+ public void setContentDisposition(String value) {
+ this.contentDisposition = value;
+ }
+
+ public void setAcceptRanges(String value) {
+ this.acceptRanges = value;
+ }
+
+ public void setExpires(String value) {
+ this.expires = value;
+ }
+
+ public void setCacheControl(String value) {
+ this.cacheControl = value;
+ }
+
+ public void setLastModified(String value) {
+ this.lastModified = value;
+ }
+
+ public void setEtag(String value) {
+ this.etag = value;
+ }
+}
diff --git a/core/java/android/net/http/HttpAuthHeader.java b/core/java/android/net/http/HttpAuthHeader.java
new file mode 100644
index 0000000..d41284c
--- /dev/null
+++ b/core/java/android/net/http/HttpAuthHeader.java
@@ -0,0 +1,422 @@
+/*
+ * 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.http;
+
+/**
+ * HttpAuthHeader: a class to store HTTP authentication-header parameters.
+ * For more information, see: RFC 2617: HTTP Authentication.
+ *
+ * {@hide}
+ */
+public class HttpAuthHeader {
+ /**
+ * Possible HTTP-authentication header tokens to search for:
+ */
+ public final static String BASIC_TOKEN = "Basic";
+ public final static String DIGEST_TOKEN = "Digest";
+
+ private final static String REALM_TOKEN = "realm";
+ private final static String NONCE_TOKEN = "nonce";
+ private final static String STALE_TOKEN = "stale";
+ private final static String OPAQUE_TOKEN = "opaque";
+ private final static String QOP_TOKEN = "qop";
+ private final static String ALGORITHM_TOKEN = "algorithm";
+
+ /**
+ * An authentication scheme. We currently support two different schemes:
+ * HttpAuthHeader.BASIC - basic, and
+ * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth").
+ */
+ private int mScheme;
+
+ public static final int UNKNOWN = 0;
+ public static final int BASIC = 1;
+ public static final int DIGEST = 2;
+
+ /**
+ * A flag, indicating that the previous request from the client was
+ * rejected because the nonce value was stale. If stale is TRUE
+ * (case-insensitive), the client may wish to simply retry the request
+ * with a new encrypted response, without reprompting the user for a
+ * new username and password.
+ */
+ private boolean mStale;
+
+ /**
+ * A string to be displayed to users so they know which username and
+ * password to use.
+ */
+ private String mRealm;
+
+ /**
+ * A server-specified data string which should be uniquely generated
+ * each time a 401 response is made.
+ */
+ private String mNonce;
+
+ /**
+ * A string of data, specified by the server, which should be returned
+ * by the client unchanged in the Authorization header of subsequent
+ * requests with URIs in the same protection space.
+ */
+ private String mOpaque;
+
+ /**
+ * This directive is optional, but is made so only for backward
+ * compatibility with RFC 2069 [6]; it SHOULD be used by all
+ * implementations compliant with this version of the Digest scheme.
+ * If present, it is a quoted string of one or more tokens indicating
+ * the "quality of protection" values supported by the server. The
+ * value "auth" indicates authentication; the value "auth-int"
+ * indicates authentication with integrity protection.
+ */
+ private String mQop;
+
+ /**
+ * A string indicating a pair of algorithms used to produce the digest
+ * and a checksum. If this is not present it is assumed to be "MD5".
+ */
+ private String mAlgorithm;
+
+ /**
+ * Is this authentication request a proxy authentication request?
+ */
+ private boolean mIsProxy;
+
+ /**
+ * Username string we get from the user.
+ */
+ private String mUsername;
+
+ /**
+ * Password string we get from the user.
+ */
+ private String mPassword;
+
+ /**
+ * Creates a new HTTP-authentication header object from the
+ * input header string.
+ * The header string is assumed to contain parameters of at
+ * most one authentication-scheme (ensured by the caller).
+ */
+ public HttpAuthHeader(String header) {
+ if (header != null) {
+ parseHeader(header);
+ }
+ }
+
+ /**
+ * @return True iff this is a proxy authentication header.
+ */
+ public boolean isProxy() {
+ return mIsProxy;
+ }
+
+ /**
+ * Marks this header as a proxy authentication header.
+ */
+ public void setProxy() {
+ mIsProxy = true;
+ }
+
+ /**
+ * @return The username string.
+ */
+ public String getUsername() {
+ return mUsername;
+ }
+
+ /**
+ * Sets the username string.
+ */
+ public void setUsername(String username) {
+ mUsername = username;
+ }
+
+ /**
+ * @return The password string.
+ */
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /**
+ * Sets the password string.
+ */
+ public void setPassword(String password) {
+ mPassword = password;
+ }
+
+ /**
+ * @return True iff this is the BASIC-authentication request.
+ */
+ public boolean isBasic () {
+ return mScheme == BASIC;
+ }
+
+ /**
+ * @return True iff this is the DIGEST-authentication request.
+ */
+ public boolean isDigest() {
+ return mScheme == DIGEST;
+ }
+
+ /**
+ * @return The authentication scheme requested. We currently
+ * support two schemes:
+ * HttpAuthHeader.BASIC - basic, and
+ * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth").
+ */
+ public int getScheme() {
+ return mScheme;
+ }
+
+ /**
+ * @return True if indicating that the previous request from
+ * the client was rejected because the nonce value was stale.
+ */
+ public boolean getStale() {
+ return mStale;
+ }
+
+ /**
+ * @return The realm value or null if there is none.
+ */
+ public String getRealm() {
+ return mRealm;
+ }
+
+ /**
+ * @return The nonce value or null if there is none.
+ */
+ public String getNonce() {
+ return mNonce;
+ }
+
+ /**
+ * @return The opaque value or null if there is none.
+ */
+ public String getOpaque() {
+ return mOpaque;
+ }
+
+ /**
+ * @return The QOP ("quality-of_protection") value or null if
+ * there is none. The QOP value is always lower-case.
+ */
+ public String getQop() {
+ return mQop;
+ }
+
+ /**
+ * @return The name of the algorithm used or null if there is
+ * none. By default, MD5 is used.
+ */
+ public String getAlgorithm() {
+ return mAlgorithm;
+ }
+
+ /**
+ * @return True iff the authentication scheme requested by the
+ * server is supported; currently supported schemes:
+ * BASIC,
+ * DIGEST (only algorithm="md5", no qop or qop="auth).
+ */
+ public boolean isSupportedScheme() {
+ // it is a good idea to enforce non-null realms!
+ if (mRealm != null) {
+ if (mScheme == BASIC) {
+ return true;
+ } else {
+ if (mScheme == DIGEST) {
+ return
+ mAlgorithm.equals("md5") &&
+ (mQop == null || mQop.equals("auth"));
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses the header scheme name and then scheme parameters if
+ * the scheme is supported.
+ */
+ private void parseHeader(String header) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpAuthHeader.parseHeader(): header: " + header);
+ }
+
+ if (header != null) {
+ String parameters = parseScheme(header);
+ if (parameters != null) {
+ // if we have a supported scheme
+ if (mScheme != UNKNOWN) {
+ parseParameters(parameters);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the authentication scheme name. If we have a Digest
+ * scheme, sets the algorithm value to the default of MD5.
+ * @return The authentication scheme parameters string to be
+ * parsed later (if the scheme is supported) or null if failed
+ * to parse the scheme (the header value is null?).
+ */
+ private String parseScheme(String header) {
+ if (header != null) {
+ int i = header.indexOf(' ');
+ if (i >= 0) {
+ String scheme = header.substring(0, i).trim();
+ if (scheme.equalsIgnoreCase(DIGEST_TOKEN)) {
+ mScheme = DIGEST;
+
+ // md5 is the default algorithm!!!
+ mAlgorithm = "md5";
+ } else {
+ if (scheme.equalsIgnoreCase(BASIC_TOKEN)) {
+ mScheme = BASIC;
+ }
+ }
+
+ return header.substring(i + 1);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses a comma-separated list of authentification scheme
+ * parameters.
+ */
+ private void parseParameters(String parameters) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpAuthHeader.parseParameters():" +
+ " parameters: " + parameters);
+ }
+
+ if (parameters != null) {
+ int i;
+ do {
+ i = parameters.indexOf(',');
+ if (i < 0) {
+ // have only one parameter
+ parseParameter(parameters);
+ } else {
+ parseParameter(parameters.substring(0, i));
+ parameters = parameters.substring(i + 1);
+ }
+ } while (i >= 0);
+ }
+ }
+
+ /**
+ * Parses a single authentication scheme parameter. The parameter
+ * string is expected to follow the format: PARAMETER=VALUE.
+ */
+ private void parseParameter(String parameter) {
+ if (parameter != null) {
+ // here, we are looking for the 1st occurence of '=' only!!!
+ int i = parameter.indexOf('=');
+ if (i >= 0) {
+ String token = parameter.substring(0, i).trim();
+ String value =
+ trimDoubleQuotesIfAny(parameter.substring(i + 1).trim());
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpAuthHeader.parseParameter():" +
+ " token: " + token +
+ " value: " + value);
+ }
+
+ if (token.equalsIgnoreCase(REALM_TOKEN)) {
+ mRealm = value;
+ } else {
+ if (mScheme == DIGEST) {
+ parseParameter(token, value);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * If the token is a known parameter name, parses and initializes
+ * the token value.
+ */
+ private void parseParameter(String token, String value) {
+ if (token != null && value != null) {
+ if (token.equalsIgnoreCase(NONCE_TOKEN)) {
+ mNonce = value;
+ return;
+ }
+
+ if (token.equalsIgnoreCase(STALE_TOKEN)) {
+ parseStale(value);
+ return;
+ }
+
+ if (token.equalsIgnoreCase(OPAQUE_TOKEN)) {
+ mOpaque = value;
+ return;
+ }
+
+ if (token.equalsIgnoreCase(QOP_TOKEN)) {
+ mQop = value.toLowerCase();
+ return;
+ }
+
+ if (token.equalsIgnoreCase(ALGORITHM_TOKEN)) {
+ mAlgorithm = value.toLowerCase();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parses and initializes the 'stale' paramer value. Any value
+ * different from case-insensitive "true" is considered "false".
+ */
+ private void parseStale(String value) {
+ if (value != null) {
+ if (value.equalsIgnoreCase("true")) {
+ mStale = true;
+ }
+ }
+ }
+
+ /**
+ * Trims double-quotes around a parameter value if there are any.
+ * @return The string value without the outermost pair of double-
+ * quotes or null if the original value is null.
+ */
+ static private String trimDoubleQuotesIfAny(String value) {
+ if (value != null) {
+ int len = value.length();
+ if (len > 2 &&
+ value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') {
+ return value.substring(1, len - 1);
+ }
+ }
+
+ return value;
+ }
+}
diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java
new file mode 100644
index 0000000..8b12d0b
--- /dev/null
+++ b/core/java/android/net/http/HttpConnection.java
@@ -0,0 +1,96 @@
+/*
+ * 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.http;
+
+import android.content.Context;
+
+import java.net.Socket;
+import java.io.IOException;
+
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpHost;
+import org.apache.http.impl.DefaultHttpClientConnection;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+
+/**
+ * A requestConnection connecting to a normal (non secure) http server
+ *
+ * {@hide}
+ */
+class HttpConnection extends Connection {
+
+ HttpConnection(Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ super(context, host, connectionManager, requestFeeder);
+ }
+
+ /**
+ * Opens the connection to a http server
+ *
+ * @return the opened low level connection
+ * @throws IOException if the connection fails for any reason.
+ */
+ @Override
+ AndroidHttpClientConnection openConnection(Request req) throws IOException {
+
+ // Update the certificate info (connection not secure - set to null)
+ EventHandler eventHandler = req.getEventHandler();
+ mCertificate = null;
+ eventHandler.certificate(mCertificate);
+
+ AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
+ BasicHttpParams params = new BasicHttpParams();
+ Socket sock = new Socket(mHost.getHostName(), mHost.getPort());
+ params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
+ conn.bind(sock, params);
+ return conn;
+ }
+
+ /**
+ * Closes the low level connection.
+ *
+ * If an exception is thrown then it is assumed that the
+ * connection will have been closed (to the extent possible)
+ * anyway and the caller does not need to take any further action.
+ *
+ */
+ void closeConnection() {
+ try {
+ if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
+ mHttpClientConnection.close();
+ }
+ } catch (IOException e) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "closeConnection(): failed closing connection " +
+ mHost);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Restart a secure connection suspended waiting for user interaction.
+ */
+ void restartConnection(boolean abort) {
+ // not required for plain http connections
+ }
+
+ String getScheme() {
+ return "http";
+ }
+}
diff --git a/core/java/android/net/http/HttpLog.java b/core/java/android/net/http/HttpLog.java
new file mode 100644
index 0000000..30bf647
--- /dev/null
+++ b/core/java/android/net/http/HttpLog.java
@@ -0,0 +1,44 @@
+/*
+ * 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-level logging flag
+ */
+
+package android.net.http;
+
+import android.os.SystemClock;
+
+import android.util.Log;
+import android.util.Config;
+
+/**
+ * {@hide}
+ */
+class HttpLog {
+ private final static String LOGTAG = "http";
+
+ private static final boolean DEBUG = false;
+ static final boolean LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ static void v(String logMe) {
+ Log.v(LOGTAG, SystemClock.uptimeMillis() + " " + Thread.currentThread().getName() + " " + logMe);
+ }
+
+ static void e(String logMe) {
+ Log.e(LOGTAG, logMe);
+ }
+}
diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java
new file mode 100644
index 0000000..fe02d3e
--- /dev/null
+++ b/core/java/android/net/http/HttpsConnection.java
@@ -0,0 +1,427 @@
+/*
+ * 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.http;
+
+import android.content.Context;
+
+import junit.framework.Assert;
+
+import java.io.IOException;
+
+import java.security.cert.X509Certificate;
+
+import java.net.Socket;
+import java.net.InetSocketAddress;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.Header;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.impl.DefaultHttpClientConnection;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpConnectionParams;
+
+/**
+ * Simple exception we throw if the SSL connection is closed by the user.
+ *
+ * {@hide}
+ */
+class SSLConnectionClosedByUserException extends SSLException {
+
+ public SSLConnectionClosedByUserException(String reason) {
+ super(reason);
+ }
+}
+
+/**
+ * A Connection connecting to a secure http server or tunneling through
+ * a http proxy server to a https server.
+ */
+class HttpsConnection extends Connection {
+
+ /**
+ * SSL context
+ */
+ private static SSLContext mSslContext = null;
+
+ /**
+ * SSL socket factory
+ */
+ private static SSLSocketFactory mSslSocketFactory = null;
+
+ static {
+ // initialize the socket factory
+ try {
+ mSslContext = SSLContext.getInstance("TLS");
+ if (mSslContext != null) {
+ // here, trust managers is a single trust-all manager
+ TrustManager[] trustManagers = new TrustManager[] {
+ new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public void checkClientTrusted(
+ X509Certificate[] certs, String authType) {
+ }
+
+ public void checkServerTrusted(
+ X509Certificate[] certs, String authType) {
+ }
+ }
+ };
+
+ mSslContext.init(null, trustManagers, null);
+ mSslSocketFactory = mSslContext.getSocketFactory();
+ }
+ } catch (Exception t) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpsConnection: failed to initialize the socket factory");
+ }
+ }
+ }
+
+ /**
+ * @return The shared SSL context.
+ */
+ /*package*/ static SSLContext getContext() {
+ return mSslContext;
+ }
+
+ /**
+ * Object to wait on when suspending the SSL connection
+ */
+ private Object mSuspendLock = new Object();
+
+ /**
+ * True if the connection is suspended pending the result of asking the
+ * user about an error.
+ */
+ private boolean mSuspended = false;
+
+ /**
+ * True if the connection attempt should be aborted due to an ssl
+ * error.
+ */
+ private boolean mAborted = false;
+
+ /**
+ * Contructor for a https connection.
+ */
+ HttpsConnection(Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ super(context, host, connectionManager, requestFeeder);
+ }
+
+ /**
+ * Sets the server SSL certificate associated with this
+ * connection.
+ * @param certificate The SSL certificate
+ */
+ /* package */ void setCertificate(SslCertificate certificate) {
+ mCertificate = certificate;
+ }
+
+ /**
+ * Opens the connection to a http server or proxy.
+ *
+ * @return the opened low level connection
+ * @throws IOException if the connection fails for any reason.
+ */
+ @Override
+ AndroidHttpClientConnection openConnection(Request req) throws IOException {
+ SSLSocket sslSock = null;
+
+ HttpHost proxyHost = mConnectionManager.getProxyHost();
+ if (proxyHost != null) {
+ // If we have a proxy set, we first send a CONNECT request
+ // to the proxy; if the proxy returns 200 OK, we negotiate
+ // a secure connection to the target server via the proxy.
+ // If the request fails, we drop it, but provide the event
+ // handler with the response status and headers. The event
+ // handler is then responsible for cancelling the load or
+ // issueing a new request.
+ AndroidHttpClientConnection proxyConnection = null;
+ Socket proxySock = null;
+ try {
+ proxySock = new Socket
+ (proxyHost.getHostName(), proxyHost.getPort());
+
+ proxySock.setSoTimeout(60 * 1000);
+
+ proxyConnection = new AndroidHttpClientConnection();
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+
+ proxyConnection.bind(proxySock, params);
+ } catch(IOException e) {
+ if (proxyConnection != null) {
+ proxyConnection.close();
+ }
+
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to establish a connection to the proxy";
+ }
+
+ throw new IOException(errorMessage);
+ }
+
+ StatusLine statusLine = null;
+ int statusCode = 0;
+ Headers headers = new Headers();
+ try {
+ BasicHttpRequest proxyReq = new BasicHttpRequest
+ ("CONNECT", mHost.toHostString());
+
+ // add all 'proxy' headers from the original request
+ for (Header h : req.mHttpRequest.getAllHeaders()) {
+ String headerName = h.getName().toLowerCase();
+ if (headerName.startsWith("proxy") || headerName.equals("keep-alive")) {
+ proxyReq.addHeader(h);
+ }
+ }
+
+ proxyConnection.sendRequestHeader(proxyReq);
+ proxyConnection.flush();
+
+ // it is possible to receive informational status
+ // codes prior to receiving actual headers;
+ // all those status codes are smaller than OK 200
+ // a loop is a standard way of dealing with them
+ do {
+ statusLine = proxyConnection.parseResponseHeader(headers);
+ statusCode = statusLine.getStatusCode();
+ } while (statusCode < HttpStatus.SC_OK);
+ } catch (ParseException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to send a CONNECT request";
+ }
+
+ throw new IOException(errorMessage);
+ } catch (HttpException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to send a CONNECT request";
+ }
+
+ throw new IOException(errorMessage);
+ } catch (IOException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to send a CONNECT request";
+ }
+
+ throw new IOException(errorMessage);
+ }
+
+ if (statusCode == HttpStatus.SC_OK) {
+ try {
+ synchronized (mSslSocketFactory) {
+ sslSock = (SSLSocket) mSslSocketFactory.createSocket(
+ proxySock, mHost.getHostName(), mHost.getPort(), true);
+ }
+ } catch(IOException e) {
+ if (sslSock != null) {
+ sslSock.close();
+ }
+
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to create an SSL socket";
+ }
+ throw new IOException(errorMessage);
+ }
+ } else {
+ // if the code is not OK, inform the event handler
+ ProtocolVersion version = statusLine.getProtocolVersion();
+
+ req.mEventHandler.status(version.getMajor(),
+ version.getMinor(),
+ statusCode,
+ statusLine.getReasonPhrase());
+ req.mEventHandler.headers(headers);
+ req.mEventHandler.endData();
+
+ proxyConnection.close();
+
+ // here, we return null to indicate that the original
+ // request needs to be dropped
+ return null;
+ }
+ } else {
+ // if we do not have a proxy, we simply connect to the host
+ try {
+ synchronized (mSslSocketFactory) {
+ sslSock = (SSLSocket) mSslSocketFactory.createSocket();
+
+ sslSock.setSoTimeout(SOCKET_TIMEOUT);
+ sslSock.connect(new InetSocketAddress(mHost.getHostName(),
+ mHost.getPort()));
+
+ }
+ } catch(IOException e) {
+ if (sslSock != null) {
+ sslSock.close();
+ }
+
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "failed to create an SSL socket";
+ }
+
+ throw new IOException(errorMessage);
+ }
+ }
+
+ // do handshake and validate server certificates
+ SslError error = CertificateChainValidator.getInstance().
+ doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName());
+
+ EventHandler eventHandler = req.getEventHandler();
+
+ // Update the certificate info (to be consistent, it is better to do it
+ // here, before we start handling SSL errors, if any)
+ eventHandler.certificate(mCertificate);
+
+ // Inform the user if there is a problem
+ if (error != null) {
+ // handleSslErrorRequest may immediately unsuspend if it wants to
+ // allow the certificate anyway.
+ // So we mark the connection as suspended, call handleSslErrorRequest
+ // then check if we're still suspended and only wait if we actually
+ // need to.
+ synchronized (mSuspendLock) {
+ mSuspended = true;
+ }
+ // don't hold the lock while calling out to the event handler
+ eventHandler.handleSslErrorRequest(error);
+ synchronized (mSuspendLock) {
+ if (mSuspended) {
+ try {
+ // Put a limit on how long we are waiting; if the timeout
+ // expires (which should never happen unless you choose
+ // to ignore the SSL error dialog for a very long time),
+ // we wake up the thread and abort the request. This is
+ // to prevent us from stalling the network if things go
+ // very bad.
+ mSuspendLock.wait(10 * 60 * 1000);
+ if (mSuspended) {
+ // mSuspended is true if we have not had a chance to
+ // restart the connection yet (ie, the wait timeout
+ // has expired)
+ mSuspended = false;
+ mAborted = true;
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpsConnection.openConnection():" +
+ " SSL timeout expired and request was cancelled!!!");
+ }
+ }
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ if (mAborted) {
+ // The user decided not to use this unverified connection
+ // so close it immediately.
+ sslSock.close();
+ throw new SSLConnectionClosedByUserException("connection closed by the user");
+ }
+ }
+ }
+
+ // All went well, we have an open, verified connection.
+ AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
+ BasicHttpParams params = new BasicHttpParams();
+ params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
+ conn.bind(sslSock, params);
+ return conn;
+ }
+
+ /**
+ * Closes the low level connection.
+ *
+ * If an exception is thrown then it is assumed that the connection will
+ * have been closed (to the extent possible) anyway and the caller does not
+ * need to take any further action.
+ *
+ */
+ @Override
+ void closeConnection() {
+ // if the connection has been suspended due to an SSL error
+ if (mSuspended) {
+ // wake up the network thread
+ restartConnection(false);
+ }
+
+ try {
+ if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
+ mHttpClientConnection.close();
+ }
+ } catch (IOException e) {
+ if (HttpLog.LOGV)
+ HttpLog.v("HttpsConnection.closeConnection():" +
+ " failed closing connection " + mHost);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Restart a secure connection suspended waiting for user interaction.
+ */
+ void restartConnection(boolean proceed) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpsConnection.restartConnection():" +
+ " proceed: " + proceed);
+ }
+
+ synchronized (mSuspendLock) {
+ if (mSuspended) {
+ mSuspended = false;
+ mAborted = !proceed;
+ mSuspendLock.notify();
+ }
+ }
+ }
+
+ @Override
+ String getScheme() {
+ return "https";
+ }
+}
diff --git a/core/java/android/net/http/IdleCache.java b/core/java/android/net/http/IdleCache.java
new file mode 100644
index 0000000..fda6009
--- /dev/null
+++ b/core/java/android/net/http/IdleCache.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+/**
+ * Hangs onto idle live connections for a little while
+ */
+
+package android.net.http;
+
+import org.apache.http.HttpHost;
+
+import android.os.SystemClock;
+
+/**
+ * {@hide}
+ */
+class IdleCache {
+
+ class Entry {
+ HttpHost mHost;
+ Connection mConnection;
+ long mTimeout;
+ };
+
+ private final static int IDLE_CACHE_MAX = 8;
+
+ /* Allow five consecutive empty queue checks before shutdown */
+ private final static int EMPTY_CHECK_MAX = 5;
+
+ /* six second timeout for connections */
+ private final static int TIMEOUT = 6 * 1000;
+ private final static int CHECK_INTERVAL = 2 * 1000;
+ private Entry[] mEntries = new Entry[IDLE_CACHE_MAX];
+
+ private int mCount = 0;
+
+ private IdleReaper mThread = null;
+
+ /* stats */
+ private int mCached = 0;
+ private int mReused = 0;
+
+ IdleCache() {
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ mEntries[i] = new Entry();
+ }
+ }
+
+ /**
+ * Caches connection, if there is room.
+ * @return true if connection cached
+ */
+ synchronized boolean cacheConnection(
+ HttpHost host, Connection connection) {
+
+ boolean ret = false;
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("IdleCache size " + mCount + " host " + host);
+ }
+
+ if (mCount < IDLE_CACHE_MAX) {
+ long time = SystemClock.uptimeMillis();
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ if (entry.mHost == null) {
+ entry.mHost = host;
+ entry.mConnection = connection;
+ entry.mTimeout = time + TIMEOUT;
+ mCount++;
+ if (HttpLog.LOGV) mCached++;
+ ret = true;
+ if (mThread == null) {
+ mThread = new IdleReaper();
+ mThread.start();
+ }
+ break;
+ }
+ }
+ }
+ return ret;
+ }
+
+ synchronized Connection getConnection(HttpHost host) {
+ Connection ret = null;
+
+ if (mCount > 0) {
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ HttpHost eHost = entry.mHost;
+ if (eHost != null && eHost.equals(host)) {
+ ret = entry.mConnection;
+ entry.mHost = null;
+ entry.mConnection = null;
+ mCount--;
+ if (HttpLog.LOGV) mReused++;
+ break;
+ }
+ }
+ }
+ return ret;
+ }
+
+ synchronized void clear() {
+ for (int i = 0; mCount > 0 && i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ if (entry.mHost != null) {
+ entry.mHost = null;
+ entry.mConnection.closeConnection();
+ entry.mConnection = null;
+ mCount--;
+ }
+ }
+ }
+
+ private synchronized void clearIdle() {
+ if (mCount > 0) {
+ long time = SystemClock.uptimeMillis();
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ if (entry.mHost != null && time > entry.mTimeout) {
+ entry.mHost = null;
+ entry.mConnection.closeConnection();
+ entry.mConnection = null;
+ mCount--;
+ }
+ }
+ }
+ }
+
+ private class IdleReaper extends Thread {
+
+ public void run() {
+ int check = 0;
+
+ setName("IdleReaper");
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ synchronized (IdleCache.this) {
+ while (check < EMPTY_CHECK_MAX) {
+ try {
+ IdleCache.this.wait(CHECK_INTERVAL);
+ } catch (InterruptedException ex) {
+ }
+ if (mCount == 0) {
+ check++;
+ } else {
+ check = 0;
+ clearIdle();
+ }
+ }
+ mThread = null;
+ }
+ if (HttpLog.LOGV) {
+ HttpLog.v("IdleCache IdleReaper shutdown: cached " + mCached +
+ " reused " + mReused);
+ mCached = 0;
+ mReused = 0;
+ }
+ }
+ }
+}
diff --git a/core/java/android/net/http/LoggingEventHandler.java b/core/java/android/net/http/LoggingEventHandler.java
new file mode 100644
index 0000000..1b18651
--- /dev/null
+++ b/core/java/android/net/http/LoggingEventHandler.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2006 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.
+ */
+
+/**
+ * A test EventHandler: Logs everything received
+ */
+
+package android.net.http;
+
+import android.net.http.Headers;
+
+/**
+ * {@hide}
+ */
+public class LoggingEventHandler implements EventHandler {
+
+ public void requestSent() {
+ HttpLog.v("LoggingEventHandler:requestSent()");
+ }
+
+ public void status(int major_version,
+ int minor_version,
+ int code, /* Status-Code value */
+ String reason_phrase) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler:status() major: " + major_version +
+ " minor: " + minor_version +
+ " code: " + code +
+ " reason: " + reason_phrase);
+ }
+ }
+
+ public void headers(Headers headers) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler:headers()");
+ HttpLog.v(headers.toString());
+ }
+ }
+
+ public void locationChanged(String newLocation, boolean permanent) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: locationChanged() " + newLocation +
+ " permanent " + permanent);
+ }
+ }
+
+ public void data(byte[] data, int len) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: data() " + len + " bytes");
+ }
+ // HttpLog.v(new String(data, 0, len));
+ }
+ public void endData() {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: endData() called");
+ }
+ }
+
+ public void certificate(SslCertificate certificate) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: certificate(): " + certificate);
+ }
+ }
+
+ public void error(int id, String description) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: error() called Id:" + id +
+ " description " + description);
+ }
+ }
+
+ public void handleSslErrorRequest(SslError error) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: handleSslErrorRequest():" + error);
+ }
+ }
+}
diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java
new file mode 100644
index 0000000..bcbecf0
--- /dev/null
+++ b/core/java/android/net/http/Request.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.zip.GZIPInputStream;
+
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.Header;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+
+import org.apache.http.StatusLine;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.protocol.RequestContent;
+
+/**
+ * Represents an HTTP request for a given host.
+ *
+ * {@hide}
+ */
+
+class Request {
+
+ /** The eventhandler to call as the request progresses */
+ EventHandler mEventHandler;
+
+ private Connection mConnection;
+
+ /** The Apache http request */
+ BasicHttpRequest mHttpRequest;
+
+ /** The path component of this request */
+ String mPath;
+
+ /** Host serving this request */
+ HttpHost mHost;
+
+ /** Set if I'm using a proxy server */
+ HttpHost mProxyHost;
+
+ /** True if request is .html, .js, .css */
+ boolean mHighPriority;
+
+ /** True if request has been cancelled */
+ volatile boolean mCancelled = false;
+
+ int mFailCount = 0;
+
+ private InputStream mBodyProvider;
+ private int mBodyLength;
+
+ private final static String HOST_HEADER = "Host";
+ private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
+ private final static String CONTENT_LENGTH_HEADER = "content-length";
+
+ /* Used to synchronize waitUntilComplete() requests */
+ private final Object mClientResource = new Object();
+
+ /**
+ * Processor used to set content-length and transfer-encoding
+ * headers.
+ */
+ private static RequestContent requestContentProcessor =
+ new RequestContent();
+
+ /**
+ * Instantiates a new Request.
+ * @param method GET/POST/PUT
+ * @param host The server that will handle this request
+ * @param path path part of URI
+ * @param bodyProvider InputStream providing HTTP body, null if none
+ * @param bodyLength length of body, must be 0 if bodyProvider is null
+ * @param eventHandler request will make progress callbacks on
+ * this interface
+ * @param headers reqeust headers
+ * @param highPriority true for .html, css, .cs
+ */
+ Request(String method, HttpHost host, HttpHost proxyHost, String path,
+ InputStream bodyProvider, int bodyLength,
+ EventHandler eventHandler,
+ Map<String, String> headers, boolean highPriority) {
+ mEventHandler = eventHandler;
+ mHost = host;
+ mProxyHost = proxyHost;
+ mPath = path;
+ mHighPriority = highPriority;
+ mBodyProvider = bodyProvider;
+ mBodyLength = bodyLength;
+
+ if (bodyProvider == null) {
+ mHttpRequest = new BasicHttpRequest(method, getUri());
+ } else {
+ mHttpRequest = new BasicHttpEntityEnclosingRequest(
+ method, getUri());
+ setBodyProvider(bodyProvider, bodyLength);
+ }
+ addHeader(HOST_HEADER, getHostPort());
+
+ /* FIXME: if webcore will make the root document a
+ high-priority request, we can ask for gzip encoding only on
+ high priority reqs (saving the trouble for images, etc) */
+ addHeader(ACCEPT_ENCODING_HEADER, "gzip");
+ addHeaders(headers);
+ }
+
+ /**
+ * @param connection Request served by this connection
+ */
+ void setConnection(Connection connection) {
+ mConnection = connection;
+ }
+
+ /* package */ EventHandler getEventHandler() {
+ return mEventHandler;
+ }
+
+ /**
+ * Add header represented by given pair to request. Header will
+ * be formatted in request as "name: value\r\n".
+ * @param name of header
+ * @param value of header
+ */
+ void addHeader(String name, String value) {
+ if (name == null) {
+ String damage = "Null http header name";
+ HttpLog.e(damage);
+ throw new NullPointerException(damage);
+ }
+ if (value == null || value.length() == 0) {
+ String damage = "Null or empty value for header \"" + name + "\"";
+ HttpLog.e(damage);
+ throw new RuntimeException(damage);
+ }
+ mHttpRequest.addHeader(name, value);
+ }
+
+ /**
+ * Add all headers in given map to this request. This is a helper
+ * method: it calls addHeader for each pair in the map.
+ */
+ void addHeaders(Map<String, String> headers) {
+ if (headers == null) {
+ return;
+ }
+
+ Entry<String, String> entry;
+ Iterator<Entry<String, String>> i = headers.entrySet().iterator();
+ while (i.hasNext()) {
+ entry = i.next();
+ addHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Send the request line and headers
+ */
+ void sendRequest(AndroidHttpClientConnection httpClientConnection)
+ throws HttpException, IOException {
+
+ if (mCancelled) return; // don't send cancelled requests
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
+ // HttpLog.v(mHttpRequest.getRequestLine().toString());
+ if (false) {
+ Iterator i = mHttpRequest.headerIterator();
+ while (i.hasNext()) {
+ Header header = (Header)i.next();
+ HttpLog.v(header.getName() + ": " + header.getValue());
+ }
+ }
+ }
+
+ requestContentProcessor.process(mHttpRequest,
+ mConnection.getHttpContext());
+ httpClientConnection.sendRequestHeader(mHttpRequest);
+ if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
+ httpClientConnection.sendRequestEntity(
+ (HttpEntityEnclosingRequest) mHttpRequest);
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
+ }
+ }
+
+
+ /**
+ * Receive a single http response.
+ *
+ * @param httpClientConnection the request to receive the response for.
+ */
+ void readResponse(AndroidHttpClientConnection httpClientConnection)
+ throws IOException, ParseException {
+
+ if (mCancelled) return; // don't send cancelled requests
+
+ StatusLine statusLine = null;
+ boolean hasBody = false;
+ boolean reuse = false;
+ httpClientConnection.flush();
+ int statusCode = 0;
+
+ Headers header = new Headers();
+ do {
+ statusLine = httpClientConnection.parseResponseHeader(header);
+ statusCode = statusLine.getStatusCode();
+ } while (statusCode < HttpStatus.SC_OK);
+ if (HttpLog.LOGV) HttpLog.v(
+ "Request.readResponseStatus() " +
+ statusLine.toString().length() + " " + statusLine);
+
+ ProtocolVersion v = statusLine.getProtocolVersion();
+ mEventHandler.status(v.getMajor(), v.getMinor(),
+ statusCode, statusLine.getReasonPhrase());
+ mEventHandler.headers(header);
+ HttpEntity entity = null;
+ hasBody = canResponseHaveBody(mHttpRequest, statusCode);
+
+ if (hasBody)
+ entity = httpClientConnection.receiveResponseEntity(header);
+
+ if (entity != null) {
+ InputStream is = entity.getContent();
+
+ // process gzip content encoding
+ Header contentEncoding = entity.getContentEncoding();
+ InputStream nis = null;
+ try {
+ if (contentEncoding != null &&
+ contentEncoding.getValue().equals("gzip")) {
+ nis = new GZIPInputStream(is);
+ } else {
+ nis = is;
+ }
+
+ /* accumulate enough data to make it worth pushing it
+ * up the stack */
+ byte[] buf = mConnection.getBuf();
+ int len = 0;
+ int count = 0;
+ int lowWater = buf.length / 2;
+ while (len != -1) {
+ len = nis.read(buf, count, buf.length - count);
+ if (len != -1) {
+ count += len;
+ }
+ if (len == -1 || count >= lowWater) {
+ if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
+ mEventHandler.data(buf, count);
+ count = 0;
+ }
+ }
+ } catch(IOException e) {
+ // don't throw if we have a non-OK status code
+ if (statusCode == HttpStatus.SC_OK) {
+ throw e;
+ }
+ } finally {
+ if (nis != null) {
+ nis.close();
+ }
+ }
+ }
+ mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
+ header.getConnectionType());
+ mEventHandler.endData();
+ complete();
+
+ if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
+ mHost.getSchemeName() + "://" + getHostPort() + mPath);
+ }
+
+ /**
+ * Data will not be sent to or received from server after cancel()
+ * call. Does not close connection--use close() below for that.
+ *
+ * Called by RequestHandle from non-network thread
+ */
+ void cancel() {
+ if (HttpLog.LOGV) {
+ HttpLog.v("Request.cancel(): " + getUri());
+ }
+ mCancelled = true;
+ if (mConnection != null) {
+ mConnection.cancel();
+ }
+ }
+
+ String getHostPort() {
+ String myScheme = mHost.getSchemeName();
+ int myPort = mHost.getPort();
+
+ // Only send port when we must... many servers can't deal with it
+ if (myPort != 80 && myScheme.equals("http") ||
+ myPort != 443 && myScheme.equals("https")) {
+ return mHost.toHostString();
+ } else {
+ return mHost.getHostName();
+ }
+ }
+
+ String getUri() {
+ if (mProxyHost == null ||
+ mHost.getSchemeName().equals("https")) {
+ return mPath;
+ }
+ return mHost.getSchemeName() + "://" + getHostPort() + mPath;
+ }
+
+ /**
+ * for debugging
+ */
+ public String toString() {
+ return (mHighPriority ? "P*" : "") + mPath;
+ }
+
+
+ /**
+ * If this request has been sent once and failed, it must be reset
+ * before it can be sent again.
+ */
+ void reset() {
+ /* clear content-length header */
+ mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
+
+ if (mBodyProvider != null) {
+ try {
+ mBodyProvider.reset();
+ } catch (IOException ex) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "failed to reset body provider " +
+ getUri());
+ }
+ setBodyProvider(mBodyProvider, mBodyLength);
+ }
+ }
+
+ /**
+ * Pause thread request completes. Used for synchronous requests,
+ * and testing
+ */
+ void waitUntilComplete() {
+ synchronized (mClientResource) {
+ try {
+ if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
+ mClientResource.wait();
+ if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ void complete() {
+ synchronized (mClientResource) {
+ mClientResource.notifyAll();
+ }
+ }
+
+ /**
+ * Decide whether a response comes with an entity.
+ * The implementation in this class is based on RFC 2616.
+ * Unknown methods and response codes are supposed to
+ * indicate responses with an entity.
+ * <br/>
+ * Derived executors can override this method to handle
+ * methods and response codes not specified in RFC 2616.
+ *
+ * @param request the request, to obtain the executed method
+ * @param response the response, to obtain the status code
+ */
+
+ private static boolean canResponseHaveBody(final HttpRequest request,
+ final int status) {
+
+ if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
+ return false;
+ }
+ return status >= HttpStatus.SC_OK
+ && status != HttpStatus.SC_NO_CONTENT
+ && status != HttpStatus.SC_NOT_MODIFIED
+ && status != HttpStatus.SC_RESET_CONTENT;
+ }
+
+ /**
+ * Supply an InputStream that provides the body of a request. It's
+ * not great that the caller must also provide the length of the data
+ * returned by that InputStream, but the client needs to know up
+ * front, and I'm not sure how to get this out of the InputStream
+ * itself without a costly readthrough. I'm not sure skip() would
+ * do what we want. If you know a better way, please let me know.
+ */
+ private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
+ if (!bodyProvider.markSupported()) {
+ throw new IllegalArgumentException(
+ "bodyProvider must support mark()");
+ }
+ // Mark beginning of stream
+ bodyProvider.mark(Integer.MAX_VALUE);
+
+ ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
+ new InputStreamEntity(bodyProvider, bodyLength));
+ }
+
+
+ /**
+ * Handles SSL error(s) on the way down from the user (the user
+ * has already provided their feedback).
+ */
+ public void handleSslErrorResponse(boolean proceed) {
+ HttpsConnection connection = (HttpsConnection)(mConnection);
+ if (connection != null) {
+ connection.restartConnection(proceed);
+ }
+ }
+
+ /**
+ * Helper: calls error() on eventhandler with appropriate message
+ * This should not be called before the mConnection is set.
+ */
+ void error(int errorId, int resourceId) {
+ mEventHandler.error(
+ errorId,
+ mConnection.mContext.getText(
+ resourceId).toString());
+ }
+
+}
diff --git a/core/java/android/net/http/RequestFeeder.java b/core/java/android/net/http/RequestFeeder.java
new file mode 100644
index 0000000..34ca267
--- /dev/null
+++ b/core/java/android/net/http/RequestFeeder.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 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.
+ */
+
+/**
+ * Supplies Requests to a Connection
+ */
+
+package android.net.http;
+
+import org.apache.http.HttpHost;
+
+/**
+ * {@hide}
+ */
+interface RequestFeeder {
+
+ Request getRequest();
+ Request getRequest(HttpHost host);
+
+ /**
+ * @return true if a request for this host is available
+ */
+ boolean haveRequest(HttpHost host);
+
+ /**
+ * Put request back on head of queue
+ */
+ void requeueRequest(Request request);
+}
diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java
new file mode 100644
index 0000000..5d81250
--- /dev/null
+++ b/core/java/android/net/http/RequestHandle.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import android.net.ParseException;
+import android.net.WebAddress;
+import android.security.Md5MessageDigest;
+import junit.framework.Assert;
+import android.webkit.CookieManager;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.InputStream;
+import java.lang.Math;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * RequestHandle: handles a request session that may include multiple
+ * redirects, HTTP authentication requests, etc.
+ *
+ * {@hide}
+ */
+public class RequestHandle {
+
+ private String mUrl;
+ private WebAddress mUri;
+ private String mMethod;
+ private Map<String, String> mHeaders;
+
+ private RequestQueue mRequestQueue;
+
+ private Request mRequest;
+
+ private InputStream mBodyProvider;
+ private int mBodyLength;
+
+ private int mRedirectCount = 0;
+
+ private final static String AUTHORIZATION_HEADER = "Authorization";
+ private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+ private final static int MAX_REDIRECT_COUNT = 16;
+
+ /**
+ * Creates a new request session.
+ */
+ public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri,
+ String method, Map<String, String> headers,
+ InputStream bodyProvider, int bodyLength, Request request) {
+
+ if (headers == null) {
+ headers = new HashMap<String, String>();
+ }
+ mHeaders = headers;
+ mBodyProvider = bodyProvider;
+ mBodyLength = bodyLength;
+ mMethod = method == null? "GET" : method;
+
+ mUrl = url;
+ mUri = uri;
+
+ mRequestQueue = requestQueue;
+
+ mRequest = request;
+ }
+
+ /**
+ * Cancels this request
+ */
+ public void cancel() {
+ if (mRequest != null) {
+ mRequest.cancel();
+ }
+ }
+
+ /**
+ * Handles SSL error(s) on the way down from the user (the user
+ * has already provided their feedback).
+ */
+ public void handleSslErrorResponse(boolean proceed) {
+ if (mRequest != null) {
+ mRequest.handleSslErrorResponse(proceed);
+ }
+ }
+
+ /**
+ * @return true if we've hit the max redirect count
+ */
+ public boolean isRedirectMax() {
+ return mRedirectCount >= MAX_REDIRECT_COUNT;
+ }
+
+ /**
+ * Create and queue a redirect request.
+ *
+ * @param redirectTo URL to redirect to
+ * @param statusCode HTTP status code returned from original request
+ * @param cacheHeaders Cache header for redirect URL
+ * @return true if setup succeeds, false otherwise (redirect loop
+ * count exceeded)
+ */
+ public boolean setupRedirect(String redirectTo, int statusCode,
+ Map<String, String> cacheHeaders) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("RequestHandle.setupRedirect(): redirectCount " +
+ mRedirectCount);
+ }
+
+ // be careful and remove authentication headers, if any
+ mHeaders.remove(AUTHORIZATION_HEADER);
+ mHeaders.remove(PROXY_AUTHORIZATION_HEADER);
+
+ if (++mRedirectCount == MAX_REDIRECT_COUNT) {
+ // Way too many redirects -- fail out
+ if (HttpLog.LOGV) HttpLog.v(
+ "RequestHandle.setupRedirect(): too many redirects " +
+ mRequest);
+ mRequest.error(EventHandler.ERROR_REDIRECT_LOOP,
+ com.android.internal.R.string.httpErrorRedirectLoop);
+ return false;
+ }
+
+ if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) {
+ // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3
+ if (HttpLog.LOGV) {
+ HttpLog.v("blowing away the referer on an https -> http redirect");
+ }
+ mHeaders.remove("Referer");
+ }
+
+ mUrl = redirectTo;
+ try {
+ mUri = new WebAddress(mUrl);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ // update the "cookie" header based on the redirected url
+ mHeaders.remove("cookie");
+ String cookie = CookieManager.getInstance().getCookie(mUri);
+ if (cookie != null && cookie.length() > 0) {
+ mHeaders.put("cookie", cookie);
+ }
+
+ if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("replacing POST with GET on redirect to " + redirectTo);
+ }
+ mMethod = "GET";
+ }
+ mHeaders.remove("Content-Type");
+ mBodyProvider = null;
+
+ // Update the cache headers for this URL
+ mHeaders.putAll(cacheHeaders);
+
+ createAndQueueNewRequest();
+ return true;
+ }
+
+ /**
+ * Create and queue an HTTP authentication-response (basic) request.
+ */
+ public void setupBasicAuthResponse(boolean isProxy, String username, String password) {
+ String response = computeBasicAuthResponse(username, password);
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupBasicAuthResponse(): response: " + response);
+ }
+ mHeaders.put(authorizationHeader(isProxy), "Basic " + response);
+ setupAuthResponse();
+ }
+
+ /**
+ * Create and queue an HTTP authentication-response (digest) request.
+ */
+ public void setupDigestAuthResponse(boolean isProxy,
+ String username,
+ String password,
+ String realm,
+ String nonce,
+ String QOP,
+ String algorithm,
+ String opaque) {
+
+ String response = computeDigestAuthResponse(
+ username, password, realm, nonce, QOP, algorithm, opaque);
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupDigestAuthResponse(): response: " + response);
+ }
+ mHeaders.put(authorizationHeader(isProxy), "Digest " + response);
+ setupAuthResponse();
+ }
+
+ private void setupAuthResponse() {
+ try {
+ if (mBodyProvider != null) mBodyProvider.reset();
+ } catch (java.io.IOException ex) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupAuthResponse() failed to reset body provider");
+ }
+ }
+ createAndQueueNewRequest();
+ }
+
+ /**
+ * @return HTTP request method (GET, PUT, etc).
+ */
+ public String getMethod() {
+ return mMethod;
+ }
+
+ /**
+ * @return Basic-scheme authentication response: BASE64(username:password).
+ */
+ public static String computeBasicAuthResponse(String username, String password) {
+ Assert.assertNotNull(username);
+ Assert.assertNotNull(password);
+
+ // encode username:password to base64
+ return new String(Base64.encodeBase64((username + ':' + password).getBytes()));
+ }
+
+ public void waitUntilComplete() {
+ mRequest.waitUntilComplete();
+ }
+
+ /**
+ * @return Digest-scheme authentication response.
+ */
+ private String computeDigestAuthResponse(String username,
+ String password,
+ String realm,
+ String nonce,
+ String QOP,
+ String algorithm,
+ String opaque) {
+
+ Assert.assertNotNull(username);
+ Assert.assertNotNull(password);
+ Assert.assertNotNull(realm);
+
+ String A1 = username + ":" + realm + ":" + password;
+ String A2 = mMethod + ":" + mUrl;
+
+ // because we do not preemptively send authorization headers, nc is always 1
+ String nc = "000001";
+ String cnonce = computeCnonce();
+ String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce);
+
+ String response = "";
+ response += "username=" + doubleQuote(username) + ", ";
+ response += "realm=" + doubleQuote(realm) + ", ";
+ response += "nonce=" + doubleQuote(nonce) + ", ";
+ response += "uri=" + doubleQuote(mUrl) + ", ";
+ response += "response=" + doubleQuote(digest) ;
+
+ if (opaque != null) {
+ response += ", opaque=" + doubleQuote(opaque);
+ }
+
+ if (algorithm != null) {
+ response += ", algorithm=" + algorithm;
+ }
+
+ if (QOP != null) {
+ response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce);
+ }
+
+ return response;
+ }
+
+ /**
+ * @return The right authorization header (dependeing on whether it is a proxy or not).
+ */
+ public static String authorizationHeader(boolean isProxy) {
+ if (!isProxy) {
+ return AUTHORIZATION_HEADER;
+ } else {
+ return PROXY_AUTHORIZATION_HEADER;
+ }
+ }
+
+ /**
+ * @return Double-quoted MD5 digest.
+ */
+ private String computeDigest(
+ String A1, String A2, String nonce, String QOP, String nc, String cnonce) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("computeDigest(): QOP: " + QOP);
+ }
+
+ if (QOP == null) {
+ return KD(H(A1), nonce + ":" + H(A2));
+ } else {
+ if (QOP.equalsIgnoreCase("auth")) {
+ return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return MD5 hash of concat(secret, ":", data).
+ */
+ private String KD(String secret, String data) {
+ return H(secret + ":" + data);
+ }
+
+ /**
+ * @return MD5 hash of param.
+ */
+ private String H(String param) {
+ if (param != null) {
+ Md5MessageDigest md5 = new Md5MessageDigest();
+
+ byte[] d = md5.digest(param.getBytes());
+ if (d != null) {
+ return bufferToHex(d);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return HEX buffer representation.
+ */
+ private String bufferToHex(byte[] buffer) {
+ final char hexChars[] =
+ { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
+
+ if (buffer != null) {
+ int length = buffer.length;
+ if (length > 0) {
+ StringBuilder hex = new StringBuilder(2 * length);
+
+ for (int i = 0; i < length; ++i) {
+ byte l = (byte) (buffer[i] & 0x0F);
+ byte h = (byte)((buffer[i] & 0xF0) >> 4);
+
+ hex.append(hexChars[h]);
+ hex.append(hexChars[l]);
+ }
+
+ return hex.toString();
+ } else {
+ return "";
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes a random cnonce value based on the current time.
+ */
+ private String computeCnonce() {
+ Random rand = new Random();
+ int nextInt = rand.nextInt();
+ nextInt = (nextInt == Integer.MIN_VALUE) ?
+ Integer.MAX_VALUE : Math.abs(nextInt);
+ return Integer.toString(nextInt, 16);
+ }
+
+ /**
+ * "Double-quotes" the argument.
+ */
+ private String doubleQuote(String param) {
+ if (param != null) {
+ return "\"" + param + "\"";
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates and queues new request.
+ */
+ private void createAndQueueNewRequest() {
+ mRequest = mRequestQueue.queueRequest(
+ mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
+ mBodyProvider,
+ mBodyLength, mRequest.mHighPriority).mRequest;
+ }
+}
diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java
new file mode 100644
index 0000000..d592995
--- /dev/null
+++ b/core/java/android/net/http/RequestQueue.java
@@ -0,0 +1,647 @@
+/*
+ * Copyright (C) 2006 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.
+ */
+
+/**
+ * High level HTTP Interface
+ * Queues requests as necessary
+ */
+
+package android.net.http;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkConnectivityListener;
+import android.net.NetworkInfo;
+import android.net.Proxy;
+import android.net.WebAddress;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.apache.http.HttpHost;
+
+/**
+ * {@hide}
+ */
+public class RequestQueue implements RequestFeeder {
+
+ private Context mContext;
+
+ /**
+ * Requests, indexed by HttpHost (scheme, host, port)
+ */
+ private LinkedHashMap<HttpHost, LinkedList<Request>> mPending;
+
+ /* Support for notifying a client when queue is empty */
+ private boolean mClientWaiting = false;
+
+ /** true if connected */
+ boolean mNetworkConnected = true;
+
+ private HttpHost mProxyHost = null;
+ private BroadcastReceiver mProxyChangeReceiver;
+
+ private ActivePool mActivePool;
+
+ /* default simultaneous connection count */
+ private static final int CONNECTION_COUNT = 4;
+
+ /**
+ * This intent broadcast when http is paused or unpaused due to
+ * net availability toggling
+ */
+ public final static String HTTP_NETWORK_STATE_CHANGED_INTENT =
+ "android.net.http.NETWORK_STATE";
+ public final static String HTTP_NETWORK_STATE_UP = "up";
+
+ /**
+ * Listen to platform network state. On a change,
+ * (1) kick stack on or off as appropriate
+ * (2) send an intent to my host app telling
+ * it what I've done
+ */
+ private NetworkStateTracker mNetworkStateTracker;
+ class NetworkStateTracker {
+
+ final static int EVENT_DATA_STATE_CHANGED = 100;
+
+ Context mContext;
+ NetworkConnectivityListener mConnectivityListener;
+ NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED;
+ int mCurrentNetworkType;
+
+ NetworkStateTracker(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * register for updates
+ */
+ protected void enable() {
+ if (mConnectivityListener == null) {
+ /*
+ * Initializing the network type is really unnecessary,
+ * since as soon as we register with the NCL, we'll
+ * get a CONNECTED event for the active network, and
+ * we'll configure the HTTP proxy accordingly. However,
+ * as a fallback in case that doesn't happen for some
+ * reason, initializing to type WIFI would mean that
+ * we'd start out without a proxy. This seems better
+ * than thinking we have a proxy (which is probably
+ * private to the carrier network and therefore
+ * unreachable outside of that network) when we really
+ * shouldn't.
+ */
+ mCurrentNetworkType = ConnectivityManager.TYPE_WIFI;
+ mConnectivityListener = new NetworkConnectivityListener();
+ mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED);
+ mConnectivityListener.startListening(mContext);
+ }
+ }
+
+ protected void disable() {
+ if (mConnectivityListener != null) {
+ mConnectivityListener.unregisterHandler(mHandler);
+ mConnectivityListener.stopListening();
+ mConnectivityListener = null;
+ }
+ }
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_DATA_STATE_CHANGED:
+ networkStateChanged();
+ break;
+ }
+ }
+ };
+
+ int getCurrentNetworkType() {
+ return mCurrentNetworkType;
+ }
+
+ void networkStateChanged() {
+ if (mConnectivityListener == null)
+ return;
+
+
+ NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState();
+ NetworkInfo info = mConnectivityListener.getNetworkInfo();
+ if (info == null) {
+ /**
+ * We've been seeing occasional NPEs here. I believe recent changes
+ * have made this impossible, but in the interest of being totally
+ * paranoid, check and log this here.
+ */
+ HttpLog.v("NetworkStateTracker: connectivity broadcast"
+ + " has null network info - ignoring");
+ return;
+ }
+ NetworkInfo.State state = info.getState();
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("NetworkStateTracker " + info.getTypeName() +
+ " state= " + state + " last= " + mLastNetworkState +
+ " connectivityState= " + connectivityState.toString());
+ }
+
+ boolean newConnection =
+ state != mLastNetworkState && state == NetworkInfo.State.CONNECTED;
+
+ if (state == NetworkInfo.State.CONNECTED) {
+ mCurrentNetworkType = info.getType();
+ setProxyConfig();
+ }
+
+ mLastNetworkState = state;
+ if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) {
+ setNetworkState(false);
+ broadcastState(false);
+ } else if (newConnection) {
+ setNetworkState(true);
+ broadcastState(true);
+ }
+
+ }
+
+ void broadcastState(boolean connected) {
+ Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT);
+ intent.putExtra(HTTP_NETWORK_STATE_UP, connected);
+ mContext.sendBroadcast(intent);
+ }
+ }
+
+ /**
+ * This class maintains active connection threads
+ */
+ class ActivePool implements ConnectionManager {
+ /** Threads used to process requests */
+ ConnectionThread[] mThreads;
+
+ IdleCache mIdleCache;
+
+ private int mTotalRequest;
+ private int mTotalConnection;
+ private int mConnectionCount;
+
+ ActivePool(int connectionCount) {
+ mIdleCache = new IdleCache();
+ mConnectionCount = connectionCount;
+ mThreads = new ConnectionThread[mConnectionCount];
+
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i] = new ConnectionThread(
+ mContext, i, this, RequestQueue.this);
+ }
+ }
+
+ void startup() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i].start();
+ }
+ }
+
+ void shutdown() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i].requestStop();
+ }
+ }
+
+ public boolean isNetworkConnected() {
+ return mNetworkConnected;
+ }
+
+ void startConnectionThread() {
+ synchronized (RequestQueue.this) {
+ RequestQueue.this.notify();
+ }
+ }
+
+ public void startTiming() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime;
+ }
+ mTotalRequest = 0;
+ mTotalConnection = 0;
+ }
+
+ public void stopTiming() {
+ int totalTime = 0;
+ for (int i = 0; i < mConnectionCount; i++) {
+ ConnectionThread rt = mThreads[i];
+ totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime);
+ rt.mStartThreadTime = -1;
+ }
+ Log.d("Http", "Http thread used " + totalTime + " ms " + " for "
+ + mTotalRequest + " requests and " + mTotalConnection
+ + " connections");
+ }
+
+ void logState() {
+ StringBuilder dump = new StringBuilder();
+ for (int i = 0; i < mConnectionCount; i++) {
+ dump.append(mThreads[i] + "\n");
+ }
+ HttpLog.v(dump.toString());
+ }
+
+
+ public HttpHost getProxyHost() {
+ return mProxyHost;
+ }
+
+ /**
+ * Turns off persistence on all live connections
+ */
+ void disablePersistence() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ Connection connection = mThreads[i].mConnection;
+ if (connection != null) connection.setCanPersist(false);
+ }
+ mIdleCache.clear();
+ }
+
+ /* Linear lookup -- okay for small thread counts. Might use
+ private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap;
+ if this turns out to be a hotspot */
+ ConnectionThread getThread(HttpHost host) {
+ synchronized(RequestQueue.this) {
+ for (int i = 0; i < mThreads.length; i++) {
+ ConnectionThread ct = mThreads[i];
+ Connection connection = ct.mConnection;
+ if (connection != null && connection.mHost.equals(host)) {
+ return ct;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Connection getConnection(Context context, HttpHost host) {
+ Connection con = mIdleCache.getConnection(host);
+ if (con == null) {
+ mTotalConnection++;
+ con = Connection.getConnection(
+ mContext, host, this, RequestQueue.this);
+ }
+ return con;
+ }
+ public boolean recycleConnection(HttpHost host, Connection connection) {
+ return mIdleCache.cacheConnection(host, connection);
+ }
+
+ }
+
+ /**
+ * A RequestQueue class instance maintains a set of queued
+ * requests. It orders them, makes the requests against HTTP
+ * servers, and makes callbacks to supplied eventHandlers as data
+ * is read. It supports request prioritization, connection reuse
+ * and pipelining.
+ *
+ * @param context application context
+ */
+ public RequestQueue(Context context) {
+ this(context, CONNECTION_COUNT);
+ }
+
+ /**
+ * A RequestQueue class instance maintains a set of queued
+ * requests. It orders them, makes the requests against HTTP
+ * servers, and makes callbacks to supplied eventHandlers as data
+ * is read. It supports request prioritization, connection reuse
+ * and pipelining.
+ *
+ * @param context application context
+ * @param connectionCount The number of simultaneous connections
+ */
+ public RequestQueue(Context context, int connectionCount) {
+ mContext = context;
+
+ mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32);
+
+ mActivePool = new ActivePool(connectionCount);
+ mActivePool.startup();
+ }
+
+ /**
+ * Enables data state and proxy tracking
+ */
+ public synchronized void enablePlatformNotifications() {
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network");
+
+ if (mProxyChangeReceiver == null) {
+ mProxyChangeReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context ctx, Intent intent) {
+ setProxyConfig();
+ }
+ };
+ mContext.registerReceiver(mProxyChangeReceiver,
+ new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
+ }
+
+ /* Network state notification is broken on the simulator
+ don't register for notifications on SIM */
+ String device = SystemProperties.get("ro.product.device");
+ boolean simulation = TextUtils.isEmpty(device);
+
+ if (!simulation) {
+ if (mNetworkStateTracker == null) {
+ mNetworkStateTracker = new NetworkStateTracker(mContext);
+ }
+ mNetworkStateTracker.enable();
+ }
+ }
+
+ /**
+ * If platform notifications have been enabled, call this method
+ * to disable before destroying RequestQueue
+ */
+ public synchronized void disablePlatformNotifications() {
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network");
+
+ if (mNetworkStateTracker != null) {
+ mNetworkStateTracker.disable();
+ }
+
+ if (mProxyChangeReceiver != null) {
+ mContext.unregisterReceiver(mProxyChangeReceiver);
+ mProxyChangeReceiver = null;
+ }
+ }
+
+ /**
+ * Because our IntentReceiver can run within a different thread,
+ * synchronize setting the proxy
+ */
+ private synchronized void setProxyConfig() {
+ if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) {
+ mProxyHost = null;
+ } else {
+ String host = Proxy.getHost(mContext);
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host);
+ if (host == null) {
+ mProxyHost = null;
+ } else {
+ mActivePool.disablePersistence();
+ mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http");
+ }
+ }
+ }
+
+ /**
+ * used by webkit
+ * @return proxy host if set, null otherwise
+ */
+ public HttpHost getProxyHost() {
+ return mProxyHost;
+ }
+
+ /**
+ * Queues an HTTP request
+ * @param url The url to load.
+ * @param method "GET" or "POST."
+ * @param headers A hashmap of http headers.
+ * @param eventHandler The event handler for handling returned
+ * data. Callbacks will be made on the supplied instance.
+ * @param bodyProvider InputStream providing HTTP body, null if none
+ * @param bodyLength length of body, must be 0 if bodyProvider is null
+ * @param highPriority If true, queues before low priority
+ * requests if possible
+ */
+ public RequestHandle queueRequest(
+ String url, String method,
+ Map<String, String> headers, EventHandler eventHandler,
+ InputStream bodyProvider, int bodyLength, boolean highPriority) {
+ WebAddress uri = new WebAddress(url);
+ return queueRequest(url, uri, method, headers, eventHandler,
+ bodyProvider, bodyLength, highPriority);
+ }
+
+ /**
+ * Queues an HTTP request
+ * @param url The url to load.
+ * @param uri The uri of the url to load.
+ * @param method "GET" or "POST."
+ * @param headers A hashmap of http headers.
+ * @param eventHandler The event handler for handling returned
+ * data. Callbacks will be made on the supplied instance.
+ * @param bodyProvider InputStream providing HTTP body, null if none
+ * @param bodyLength length of body, must be 0 if bodyProvider is null
+ * @param highPriority If true, queues before low priority
+ * requests if possible
+ */
+ public RequestHandle queueRequest(
+ String url, WebAddress uri, String method, Map<String, String> headers,
+ EventHandler eventHandler,
+ InputStream bodyProvider, int bodyLength,
+ boolean highPriority) {
+
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri);
+
+ // Ensure there is an eventHandler set
+ if (eventHandler == null) {
+ eventHandler = new LoggingEventHandler();
+ }
+
+ /* Create and queue request */
+ Request req;
+ HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme);
+
+ // set up request
+ req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider,
+ bodyLength, eventHandler, headers, highPriority);
+
+ queueRequest(req, highPriority);
+
+ mActivePool.mTotalRequest++;
+
+ // dump();
+ mActivePool.startConnectionThread();
+
+ return new RequestHandle(
+ this, url, uri, method, headers, bodyProvider, bodyLength,
+ req);
+ }
+
+ /**
+ * Called by the NetworkStateTracker -- updates when network connectivity
+ * is lost/restored.
+ *
+ * If isNetworkConnected is true, start processing requests
+ */
+ public void setNetworkState(boolean isNetworkConnected) {
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected);
+ mNetworkConnected = isNetworkConnected;
+ if (isNetworkConnected)
+ mActivePool.startConnectionThread();
+ }
+
+ /**
+ * @return true iff there are any non-active requests pending
+ */
+ synchronized boolean requestsPending() {
+ return !mPending.isEmpty();
+ }
+
+
+ /**
+ * debug tool: prints request queue to log
+ */
+ synchronized void dump() {
+ HttpLog.v("dump()");
+ StringBuilder dump = new StringBuilder();
+ int count = 0;
+ Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter;
+
+ // mActivePool.log(dump);
+
+ if (!mPending.isEmpty()) {
+ iter = mPending.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
+ String hostName = entry.getKey().getHostName();
+ StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " ");
+
+ LinkedList<Request> reqList = entry.getValue();
+ ListIterator reqIter = reqList.listIterator(0);
+ while (iter.hasNext()) {
+ Request request = (Request)iter.next();
+ line.append(request + " ");
+ }
+ dump.append(line);
+ dump.append("\n");
+ }
+ }
+ HttpLog.v(dump.toString());
+ }
+
+ /*
+ * RequestFeeder implementation
+ */
+ public synchronized Request getRequest() {
+ Request ret = null;
+
+ if (mNetworkConnected && !mPending.isEmpty()) {
+ ret = removeFirst(mPending);
+ }
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret);
+ return ret;
+ }
+
+ /**
+ * @return a request for given host if possible
+ */
+ public synchronized Request getRequest(HttpHost host) {
+ Request ret = null;
+
+ if (mNetworkConnected && mPending.containsKey(host)) {
+ LinkedList<Request> reqList = mPending.get(host);
+ ret = reqList.removeFirst();
+ if (reqList.isEmpty()) {
+ mPending.remove(host);
+ }
+ }
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret);
+ return ret;
+ }
+
+ /**
+ * @return true if a request for this host is available
+ */
+ public synchronized boolean haveRequest(HttpHost host) {
+ return mPending.containsKey(host);
+ }
+
+ /**
+ * Put request back on head of queue
+ */
+ public void requeueRequest(Request request) {
+ queueRequest(request, true);
+ }
+
+ /**
+ * This must be called to cleanly shutdown RequestQueue
+ */
+ public void shutdown() {
+ mActivePool.shutdown();
+ }
+
+ protected synchronized void queueRequest(Request request, boolean head) {
+ HttpHost host = request.mHost;
+ LinkedList<Request> reqList;
+ if (mPending.containsKey(host)) {
+ reqList = mPending.get(host);
+ } else {
+ reqList = new LinkedList<Request>();
+ mPending.put(host, reqList);
+ }
+ if (head) {
+ reqList.addFirst(request);
+ } else {
+ reqList.add(request);
+ }
+ }
+
+
+ public void startTiming() {
+ mActivePool.startTiming();
+ }
+
+ public void stopTiming() {
+ mActivePool.stopTiming();
+ }
+
+ /* helper */
+ private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) {
+ Request ret = null;
+ Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator();
+ if (iter.hasNext()) {
+ Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
+ LinkedList<Request> reqList = entry.getValue();
+ ret = reqList.removeFirst();
+ if (reqList.isEmpty()) {
+ requestQueue.remove(entry.getKey());
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * This interface is exposed to each connection
+ */
+ interface ConnectionManager {
+ boolean isNetworkConnected();
+ HttpHost getProxyHost();
+ Connection getConnection(Context context, HttpHost host);
+ boolean recycleConnection(HttpHost host, Connection connection);
+ }
+}
diff --git a/core/java/android/net/http/SslCertificate.java b/core/java/android/net/http/SslCertificate.java
new file mode 100644
index 0000000..46b2bee
--- /dev/null
+++ b/core/java/android/net/http/SslCertificate.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import android.os.Bundle;
+
+import java.text.DateFormat;
+import java.util.Vector;
+
+import java.security.cert.X509Certificate;
+
+import org.bouncycastle.asn1.DERObjectIdentifier;
+import org.bouncycastle.asn1.x509.X509Name;
+
+/**
+ * SSL certificate info (certificate details) class
+ */
+public class SslCertificate {
+
+ /**
+ * Name of the entity this certificate is issued to
+ */
+ private DName mIssuedTo;
+
+ /**
+ * Name of the entity this certificate is issued by
+ */
+ private DName mIssuedBy;
+
+ /**
+ * Not-before date from the validity period
+ */
+ private String mValidNotBefore;
+
+ /**
+ * Not-after date from the validity period
+ */
+ private String mValidNotAfter;
+
+ /**
+ * Bundle key names
+ */
+ private static final String ISSUED_TO = "issued-to";
+ private static final String ISSUED_BY = "issued-by";
+ private static final String VALID_NOT_BEFORE = "valid-not-before";
+ private static final String VALID_NOT_AFTER = "valid-not-after";
+
+ /**
+ * Saves the certificate state to a bundle
+ * @param certificate The SSL certificate to store
+ * @return A bundle with the certificate stored in it or null if fails
+ */
+ public static Bundle saveState(SslCertificate certificate) {
+ Bundle bundle = null;
+
+ if (certificate != null) {
+ bundle = new Bundle();
+
+ bundle.putString(ISSUED_TO, certificate.getIssuedTo().getDName());
+ bundle.putString(ISSUED_BY, certificate.getIssuedBy().getDName());
+
+ bundle.putString(VALID_NOT_BEFORE, certificate.getValidNotBefore());
+ bundle.putString(VALID_NOT_AFTER, certificate.getValidNotAfter());
+ }
+
+ return bundle;
+ }
+
+ /**
+ * Restores the certificate stored in the bundle
+ * @param bundle The bundle with the certificate state stored in it
+ * @return The SSL certificate stored in the bundle or null if fails
+ */
+ public static SslCertificate restoreState(Bundle bundle) {
+ if (bundle != null) {
+ return new SslCertificate(
+ bundle.getString(ISSUED_TO),
+ bundle.getString(ISSUED_BY),
+ bundle.getString(VALID_NOT_BEFORE),
+ bundle.getString(VALID_NOT_AFTER));
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a new SSL certificate object
+ * @param issuedTo The entity this certificate is issued to
+ * @param issuedBy The entity that issued this certificate
+ * @param validNotBefore The not-before date from the certificate validity period
+ * @param validNotAfter The not-after date from the certificate validity period
+ */
+ public SslCertificate(
+ String issuedTo, String issuedBy, String validNotBefore, String validNotAfter) {
+ mIssuedTo = new DName(issuedTo);
+ mIssuedBy = new DName(issuedBy);
+
+ mValidNotBefore = validNotBefore;
+ mValidNotAfter = validNotAfter;
+ }
+
+ /**
+ * Creates a new SSL certificate object from an X509 certificate
+ * @param certificate X509 certificate
+ */
+ public SslCertificate(X509Certificate certificate) {
+ this(certificate.getSubjectDN().getName(),
+ certificate.getIssuerDN().getName(),
+ DateFormat.getInstance().format(certificate.getNotBefore()),
+ DateFormat.getInstance().format(certificate.getNotAfter()));
+ }
+
+ /**
+ * @return Not-before date from the certificate validity period or
+ * "" if none has been set
+ */
+ public String getValidNotBefore() {
+ return mValidNotBefore != null ? mValidNotBefore : "";
+ }
+
+ /**
+ * @return Not-after date from the certificate validity period or
+ * "" if none has been set
+ */
+ public String getValidNotAfter() {
+ return mValidNotAfter != null ? mValidNotAfter : "";
+ }
+
+ /**
+ * @return Issued-to distinguished name or null if none has been set
+ */
+ public DName getIssuedTo() {
+ return mIssuedTo;
+ }
+
+ /**
+ * @return Issued-by distinguished name or null if none has been set
+ */
+ public DName getIssuedBy() {
+ return mIssuedBy;
+ }
+
+ /**
+ * @return A string representation of this certificate for debugging
+ */
+ public String toString() {
+ return
+ "Issued to: " + mIssuedTo.getDName() + ";\n" +
+ "Issued by: " + mIssuedBy.getDName() + ";\n";
+ }
+
+ /**
+ * A distinguished name helper class: a 3-tuple of:
+ * - common name (CN),
+ * - organization (O),
+ * - organizational unit (OU)
+ */
+ public class DName {
+ /**
+ * Distinguished name (normally includes CN, O, and OU names)
+ */
+ private String mDName;
+
+ /**
+ * Common-name (CN) component of the name
+ */
+ private String mCName;
+
+ /**
+ * Organization (O) component of the name
+ */
+ private String mOName;
+
+ /**
+ * Organizational Unit (OU) component of the name
+ */
+ private String mUName;
+
+ /**
+ * Creates a new distinguished name
+ * @param dName The distinguished name
+ */
+ public DName(String dName) {
+ if (dName != null) {
+ X509Name x509Name = new X509Name(mDName = dName);
+
+ Vector val = x509Name.getValues();
+ Vector oid = x509Name.getOIDs();
+
+ for (int i = 0; i < oid.size(); i++) {
+ if (oid.elementAt(i).equals(X509Name.CN)) {
+ mCName = (String) val.elementAt(i);
+ continue;
+ }
+
+ if (oid.elementAt(i).equals(X509Name.O)) {
+ mOName = (String) val.elementAt(i);
+ continue;
+ }
+
+ if (oid.elementAt(i).equals(X509Name.OU)) {
+ mUName = (String) val.elementAt(i);
+ continue;
+ }
+ }
+ }
+ }
+
+ /**
+ * @return The distinguished name (normally includes CN, O, and OU names)
+ */
+ public String getDName() {
+ return mDName != null ? mDName : "";
+ }
+
+ /**
+ * @return The Common-name (CN) component of this name
+ */
+ public String getCName() {
+ return mCName != null ? mCName : "";
+ }
+
+ /**
+ * @return The Organization (O) component of this name
+ */
+ public String getOName() {
+ return mOName != null ? mOName : "";
+ }
+
+ /**
+ * @return The Organizational Unit (OU) component of this name
+ */
+ public String getUName() {
+ return mUName != null ? mUName : "";
+ }
+ }
+}
diff --git a/core/java/android/net/http/SslError.java b/core/java/android/net/http/SslError.java
new file mode 100644
index 0000000..2788cb1
--- /dev/null
+++ b/core/java/android/net/http/SslError.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * One or more individual SSL errors and the associated SSL certificate
+ *
+ * {@hide}
+ */
+public class SslError {
+
+ /**
+ * Individual SSL errors (in the order from the least to the most severe):
+ */
+
+ /**
+ * The certificate is not yet valid
+ */
+ public static final int SSL_NOTYETVALID = 0;
+ /**
+ * The certificate has expired
+ */
+ public static final int SSL_EXPIRED = 1;
+ /**
+ * Hostname mismatch
+ */
+ public static final int SSL_IDMISMATCH = 2;
+ /**
+ * The certificate authority is not trusted
+ */
+ public static final int SSL_UNTRUSTED = 3;
+
+
+ /**
+ * The number of different SSL errors (update if you add a new SSL error!!!)
+ */
+ public static final int SSL_MAX_ERROR = 4;
+
+ /**
+ * The SSL error set bitfield (each individual error is an bit index;
+ * multiple individual errors can be OR-ed)
+ */
+ int mErrors;
+
+ /**
+ * The SSL certificate associated with the error set
+ */
+ SslCertificate mCertificate;
+
+ /**
+ * Creates a new SSL error set object
+ * @param error The SSL error
+ * @param certificate The associated SSL certificate
+ */
+ public SslError(int error, SslCertificate certificate) {
+ addError(error);
+ mCertificate = certificate;
+ }
+
+ /**
+ * Creates a new SSL error set object
+ * @param error The SSL error
+ * @param certificate The associated SSL certificate
+ */
+ public SslError(int error, X509Certificate certificate) {
+ addError(error);
+ mCertificate = new SslCertificate(certificate);
+ }
+
+ /**
+ * @return The SSL certificate associated with the error set
+ */
+ public SslCertificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Adds the SSL error to the error set
+ * @param error The SSL error to add
+ * @return True iff the error being added is a known SSL error
+ */
+ public boolean addError(int error) {
+ boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR);
+ if (rval) {
+ mErrors |= (0x1 << error);
+ }
+
+ return rval;
+ }
+
+ /**
+ * @param error The SSL error to check
+ * @return True iff the set includes the error
+ */
+ public boolean hasError(int error) {
+ boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR);
+ if (rval) {
+ rval = ((mErrors & (0x1 << error)) != 0);
+ }
+
+ return rval;
+ }
+
+ /**
+ * @return The primary, most severe, SSL error in the set
+ */
+ public int getPrimaryError() {
+ if (mErrors != 0) {
+ // go from the most to the least severe errors
+ for (int error = SslError.SSL_MAX_ERROR - 1; error >= 0; --error) {
+ if ((mErrors & (0x1 << error)) != 0) {
+ return error;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * @return A String representation of this SSL error object
+ * (used mostly for debugging).
+ */
+ public String toString() {
+ return "primary error: " + getPrimaryError() +
+ " certificate: " + getCertificate();
+ }
+}
diff --git a/core/java/android/net/http/Timer.java b/core/java/android/net/http/Timer.java
new file mode 100644
index 0000000..cc15a30
--- /dev/null
+++ b/core/java/android/net/http/Timer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import android.os.SystemClock;
+
+/**
+ * {@hide}
+ * Debugging tool
+ */
+class Timer {
+
+ private long mStart;
+ private long mLast;
+
+ public Timer() {
+ mStart = mLast = SystemClock.uptimeMillis();
+ }
+
+ public void mark(String message) {
+ long now = SystemClock.uptimeMillis();
+ if (HttpLog.LOGV) {
+ HttpLog.v(message + " " + (now - mLast) + " total " + (now - mStart));
+ }
+ mLast = now;
+ }
+}
diff --git a/core/java/android/net/http/package.html b/core/java/android/net/http/package.html
new file mode 100755
index 0000000..a81cbce
--- /dev/null
+++ b/core/java/android/net/http/package.html
@@ -0,0 +1,2 @@
+<body>
+</body>
diff --git a/core/java/android/net/package.html b/core/java/android/net/package.html
new file mode 100755
index 0000000..47c57e6
--- /dev/null
+++ b/core/java/android/net/package.html
@@ -0,0 +1,5 @@
+<body>
+
+Classes that help with network access, beyond the normal java.net.* APIs.
+
+</body>