package edu.mit.mobile.android.locast.net;
/*
* Copyright (C) 2010 MIT Mobile Experience Lab
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.Vector;
import org.apache.http.HttpEntity;
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.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
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.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import edu.mit.mobile.android.locast.Constants;
import edu.mit.mobile.android.locast.accounts.AuthenticationService;
import edu.mit.mobile.android.locast.accounts.Authenticator;
import edu.mit.mobile.android.locast.data.Cast;
import edu.mit.mobile.android.locast.data.MediaProvider;
import edu.mit.mobile.android.locast.data.NoPublicPath;
import edu.mit.mobile.android.locast.notifications.ProgressNotification;
import edu.mit.mobile.android.locast.ver2.R;
import edu.mit.mobile.android.utils.StreamUtils;
/**
* An client implementation of the JSON RESTful API for the Locast project.
*
* @author stevep
*/
public class NetworkClient extends DefaultHttpClient {
private static final String TAG = NetworkClient.class.getSimpleName();
public final static String JSON_MIME_TYPE = "application/json";
private static final boolean DEBUG = Constants.DEBUG;
private final static String PATH_PAIR = "pair/", PATH_UNPAIR = "un-pair/",
PATH_USER = "user/me";
protected URI mBaseUrl;
// one of the formats from ISO 8601
public final static SimpleDateFormat dateFormat = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss'Z'");
private AuthScope mAuthScope;
protected final Context mContext;
protected final static HttpRequestInterceptor PREEMPTIVE_AUTH = new HttpRequestInterceptor() {
public void process(final HttpRequest request, final HttpContext context)
throws HttpException, IOException {
final AuthState authState = (AuthState) context
.getAttribute(ClientContext.TARGET_AUTH_STATE);
final CredentialsProvider credsProvider = (CredentialsProvider) context
.getAttribute(ClientContext.CREDS_PROVIDER);
final HttpHost targetHost = (HttpHost) context
.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
// If not auth scheme has been initialized yet
if (authState.getAuthScheme() == null) {
final AuthScope authScope = new AuthScope(targetHost.getHostName(),
targetHost.getPort());
// Obtain credentials matching the target host
final Credentials creds = credsProvider.getCredentials(authScope);
// If found, generate BasicScheme preemptively
if (creds != null) {
if (creds.getUserPrincipal() != null) {
if (DEBUG) {
Log.d("NetworkClient", "Pre-emptively authenticating as: "
+ creds.getUserPrincipal().getName());
}
}
authState.setAuthScheme(new BasicScheme());
authState.setCredentials(creds);
}
}
}
};
/**
* Adds an accept-language header based on the user's current locale.
*/
protected final static HttpRequestInterceptor ACCEPT_LANGUAGE = new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException,
IOException {
final Locale locale = Locale.getDefault();
final String language = locale.getLanguage();
if (DEBUG) {
Log.d(TAG, "added header Accept-Language: " + language);
}
request.addHeader("Accept-Language", language);
}
};
protected final static HttpRequestInterceptor REMOVE_EXPECTATIONS = new HttpRequestInterceptor() {
public void process(HttpRequest request, HttpContext context) throws HttpException,
IOException {
if (request.containsHeader("Expect")) {
request.removeHeader(request.getFirstHeader("Expect"));
}
}
};
public static final String PREF_SERVER_URL = "server_url";
public static final String PREF_LOCAST_SITE = "locast_site";
/**
* Create a new NetworkClient, authenticating with the given account.
*
* @param context
* @param account
*/
private NetworkClient(Context context, Account account) {
super();
this.mContext = context;
initClient();
loadFromExistingAccount(account);
}
/**
* Create a new NetworkClient using the baseUrl. You will need to call
* {@link #setCredentials(Credentials)} at some point if you want authentication.
*
* @param context
* @param baseUrl
* @throws MalformedURLException
*/
private NetworkClient(Context context, String baseUrl) throws MalformedURLException {
super();
this.mContext = context;
initClient();
setBaseUrl(baseUrl);
}
protected void initClient() {
this.addRequestInterceptor(PREEMPTIVE_AUTH, 0);
this.addRequestInterceptor(ACCEPT_LANGUAGE, 1);
}
@Override
protected HttpParams createHttpParams() {
final HttpParams params = super.createHttpParams();
// from AndroidHttpClient:
// 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);
HttpProtocolParams.setUseExpectContinue(params, true);
String appVersion = "unknown";
try {
appVersion = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName;
} catch (final NameNotFoundException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
}
final String userAgent = mContext.getString(R.string.app_name) + "/" + appVersion;
// Set the specified user agent and register standard protocols.
HttpProtocolParams.setUserAgent(params, userAgent);
return params;
}
@Override
protected ClientConnectionManager createClientConnectionManager() {
final SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
return new ThreadSafeClientConnManager(getParams(), registry);
}
/************************* credentials and pairing **********************/
public String getUsername() {
String username = null;
final Credentials credentials = getCredentialsProvider().getCredentials(mAuthScope);
if (credentials != null) {
username = credentials.getUserPrincipal().getName();
}
return username;
}
/**
* Set the login credentials.
*
* @param credentials
*/
protected void setCredentials(String username, String auth_secret) {
this.setCredentials(new UsernamePasswordCredentials(username, auth_secret));
}
/**
* Set the login credentials. Most often you will want to pass in a
* UsernamePasswordCredentials() object.
*
* @param credentials
*/
protected void setCredentials(Credentials credentials) {
this.getCredentialsProvider().clear();
if (credentials != null) {
this.getCredentialsProvider().setCredentials(mAuthScope, credentials);
}
}
public boolean isAuthenticated() {
return getUsername() != null;
}
/**
* @param context
* @param account
* an existing account which contains a userdata key of
* {@link AuthenticationService#USERDATA_LOCAST_API_URL} which specifies the URL to
* use.
* @param password
* @return a Bundle containing the user's profile or null if authentication failed.
* @throws IOException
* @throws JSONException
* @throws NetworkProtocolException
*/
public static Bundle authenticate(Context context, Account account, String password)
throws IOException, JSONException, NetworkProtocolException {
return authenticate(context, new NetworkClient(context, account), account.name, password);
}
/**
* @param context
* @param baseUrl
* @param username
* @param password
* @return
* @throws IOException
* @throws JSONException
* @throws NetworkProtocolException
*/
public static Bundle authenticate(Context context, String baseUrl, String username,
String password) throws IOException, JSONException, NetworkProtocolException {
return authenticate(context, new NetworkClient(context, baseUrl), username, password);
}
private static Bundle authenticate(Context context, NetworkClient nc, String username,
String password) throws IOException, JSONException, NetworkProtocolException {
nc.setCredentials(username, password);
boolean authenticated = false;
try {
final HttpResponse res = nc.get(PATH_USER);
authenticated = nc.checkStatusCode(res, false);
final HttpEntity ent = res.getEntity();
JSONObject jo = null;
if (authenticated) {
jo = new JSONObject(StreamUtils.inputStreamToString(ent.getContent()));
ent.consumeContent();
} else {
jo = null;
}
final Bundle userData = jsonObjectToBundle(jo, true);
userData.putString(AuthenticationService.USERDATA_LOCAST_API_URL, nc.getBaseUrl());
return userData;
} catch (final HttpResponseException e) {
if (e.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
return null;
} else {
throw e;
}
}
}
public static Bundle jsonObjectToBundle(JSONObject jsonObject, boolean allStrings) {
final Bundle b = new Bundle();
for (@SuppressWarnings("unchecked")
final Iterator<String> i = jsonObject.keys(); i.hasNext();) {
final String key = i.next();
final Object value = jsonObject.opt(key);
if (value == null) {
b.putSerializable(key, null);
} else if (allStrings) {
b.putString(key, String.valueOf(value));
} else if (value instanceof String) {
b.putString(key, (String) value);
} else if (value instanceof Integer) {
b.putInt(key, (Integer) value);
}
}
return b;
}
/**
* Makes a request to pair the device with the server. The server sends back a set of
* credentials which are then stored for making further queries.
*
* @param pairCode
* the unique code that is provided by the server.
* @return true if pairing process was successful, otherwise false.
* @throws IOException
* @throws JSONException
* @throws RecordStoreException
* @throws NetworkProtocolException
*/
public boolean pairDevice(String pairCode) throws IOException, JSONException,
NetworkProtocolException {
final DefaultHttpClient hc = new DefaultHttpClient();
hc.addRequestInterceptor(REMOVE_EXPECTATIONS);
final HttpPost r = new HttpPost(getFullUrlAsString(PATH_PAIR));
final List<BasicNameValuePair> parameters = new ArrayList<BasicNameValuePair>();
parameters.add(new BasicNameValuePair("auth_secret", pairCode));
r.setEntity(new UrlEncodedFormEntity(parameters));
r.setHeader("Content-Type", URLEncodedUtils.CONTENT_TYPE);
final HttpResponse c = hc.execute(r);
checkStatusCode(c, false);
// final JSONObject creds = toJsonObject(c);
return true;
}
/**
* Requests that the device be unpaired with the server.
*
* @return true if successful, false otherwise.
* @throws IOException
* @throws NetworkProtocolException
* @throws RecordStoreException
*/
public boolean unpairDevice() throws IOException, NetworkProtocolException {
final HttpPost r = new HttpPost(getFullUrlAsString(PATH_UNPAIR));
final HttpResponse c = this.execute(r);
checkStatusCode(c, false);
if (c.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return true;
} else {
return false;
}
}
/*************************** all request methods ******************/
public HttpResponse head(String path) throws IOException, JSONException,
NetworkProtocolException {
final String fullUrl = getFullUrlAsString(path);
if (DEBUG) {
Log.d(TAG, "HEAD " + fullUrl);
}
final HttpHead req = new HttpHead(fullUrl);
return this.execute(req);
}
/**
* Given a HttpResponse, checks that the return types are all correct and returns a JSONObject
* from the response.
*
* @param res
* @return the full response body as a JSONObject
* @throws IllegalStateException
* @throws IOException
* @throws NetworkProtocolException
* @throws JSONException
*/
public static JSONObject toJsonObject(HttpResponse res) throws IllegalStateException,
IOException, NetworkProtocolException, JSONException {
checkContentType(res, JSON_MIME_TYPE, false);
final HttpEntity ent = res.getEntity();
final JSONObject jo = new JSONObject(StreamUtils.inputStreamToString(ent.getContent()));
ent.consumeContent();
return jo;
}
/**
* Verifies that the HttpResponse has a good status code. Throws exceptions if they are not.
*
* @param res
* @param createdOk
* true if a 201 (CREATED) code is ok. Otherwise, only 200 (OK) is allowed.
* @throws HttpResponseException
* if the status code is 400 series.
* @throws NetworkProtocolException
* for all status codes errors.
* @throws IOException
* @return returns true upon success or throws an exception explaining what went wrong.
*/
public boolean checkStatusCode(HttpResponse res, boolean createdOk)
throws HttpResponseException, NetworkProtocolException, IOException {
final int statusCode = res.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK || (createdOk && statusCode == HttpStatus.SC_CREATED)) {
return true;
} else if (statusCode >= HttpStatus.SC_BAD_REQUEST
&& statusCode < HttpStatus.SC_INTERNAL_SERVER_ERROR) {
final HttpEntity e = res.getEntity();
if (e.getContentType().getValue().equals("text/html") || e.getContentLength() > 40) {
logDebug("Got long response body. Not showing.");
} else {
logDebug(StreamUtils.inputStreamToString(e.getContent()));
}
e.consumeContent();
throw new HttpResponseException(statusCode, res.getStatusLine().getReasonPhrase());
} else {
final HttpEntity e = res.getEntity();
if (e.getContentType().getValue().equals("text/html") || e.getContentLength() > 40) {
logDebug("Got long response body. Not showing.");
} else {
logDebug(StreamUtils.inputStreamToString(e.getContent()));
}
e.consumeContent();
throw new NetworkProtocolException("HTTP " + res.getStatusLine().getStatusCode() + " "
+ res.getStatusLine().getReasonPhrase(), res);
}
}
/**
* Verifies that the HttpResponse has a correct content type. Throws exceptions if not.
*
* @param res
* @param contentType
* @param exact
* If false, will only check to see that the result content type starts with the
* desired contentType.
* @return
* @throws NetworkProtocolException
* @throws IOException
*/
private static boolean checkContentType(HttpResponse res, String contentType, boolean exact)
throws NetworkProtocolException, IOException {
final String resContentType = res.getFirstHeader("Content-Type").getValue();
if (!(exact ? resContentType.equals(contentType) : resContentType.startsWith(contentType))) {
throw new NetworkProtocolException("Did not return content-type '" + contentType
+ "'. Got: '" + resContentType + "'", res);
}
return true;
}
/************************************ GET *******************************/
/**
* Gets an object and verifies that it got a successful response code.
*
* @param path
* @return
* @throws IOException
* @throws JSONException
* @throws NetworkProtocolException
* @throws HttpResponseException
*/
public HttpResponse get(String path) throws IOException, JSONException,
NetworkProtocolException, HttpResponseException {
final String fullUri = getFullUrlAsString(path);
final HttpGet req = new HttpGet(fullUri);
if (DEBUG) {
Log.d("NetworkClient", "GET " + fullUri);
}
final HttpResponse res = this.execute(req);
checkStatusCode(res, false);
return res;
}
/**
* Loads a JSON object from the given URI
*
* @param path
* @return
* @throws IOException
* @throws JSONException
* @throws NetworkProtocolException
*/
public JSONObject getObject(String path) throws IOException, JSONException,
NetworkProtocolException {
final HttpEntity ent = getJson(path);
final JSONObject jo = new JSONObject(StreamUtils.inputStreamToString(ent.getContent()));
ent.consumeContent();
return jo;
}
/**
* GETs a JSON Array
*
* @param path
* @return
* @throws IOException
* @throws JSONException
* @throws NetworkProtocolException
*/
public JSONArray getArray(String path) throws IOException, JSONException,
NetworkProtocolException {
final HttpEntity ent = getJson(path);
final JSONArray ja = new JSONArray(StreamUtils.inputStreamToString(ent.getContent()));
ent.consumeContent();
return ja;
}
private synchronized HttpEntity getJson(String path) throws IOException, JSONException,
NetworkProtocolException {
final HttpResponse res = get(path);
checkContentType(res, JSON_MIME_TYPE, false);
// XXX possibly untrue isAuthenticated = true; // this should only get set if we managed to
// make it here
return res.getEntity();
}
/***************************** PUT ***********************************/
/**
* PUTs a JSON object, returns an updated JSON object.
*
* @param path
* @param jsonObject
* @return
* @throws IOException
* @throws NetworkProtocolException
* @throws JSONException
* @throws IllegalStateException
*/
public JSONObject putJson(String path, JSONObject jsonObject) throws IOException,
NetworkProtocolException, IllegalStateException, JSONException {
return toJsonObject(put(path, jsonObject.toString()));
}
public HttpResponse putJson(String path, boolean jsonValue) throws IOException,
NetworkProtocolException {
return put(path, jsonValue ? "true" : "false");
}
/**
* @param path
* @param jsonString
* @return A HttpResponse that has been checked for improper response codes.
* @throws IOException
* @throws NetworkProtocolException
*/
protected synchronized HttpResponse put(String path, String jsonString) throws IOException,
NetworkProtocolException {
final String fullUri = getFullUrlAsString(path);
final HttpPut r = new HttpPut(fullUri);
if (DEBUG) {
Log.d("NetworkClient", "PUT " + fullUri);
}
r.setEntity(new StringEntity(jsonString, "utf-8"));
r.setHeader("Content-Type", JSON_MIME_TYPE);
if (DEBUG) {
Log.d("NetworkClient", "PUTting: " + jsonString);
}
final HttpResponse c = this.execute(r);
checkStatusCode(c, true);
return c;
}
protected synchronized HttpResponse put(String path, String contentType, InputStream is)
throws IOException, NetworkProtocolException {
final String fullUri = getFullUrlAsString(path);
final HttpPut r = new HttpPut(fullUri);
if (DEBUG) {
Log.d("NetworkClient", "PUT " + fullUri);
}
r.setEntity(new InputStreamEntity(is, 0));
r.setHeader("Content-Type", contentType);
final HttpResponse c = this.execute(r);
checkStatusCode(c, true);
return c;
}
public synchronized Uri getFullUrl(String path) {
Uri fullUri;
if (path.startsWith("http")) {
fullUri = Uri.parse(path);
} else {
fullUri = Uri.parse(mBaseUrl.resolve(path).normalize().toASCIIString());
if (DEBUG) {
Log.d("NetworkClient", "path: " + path + ", baseUrl: " + mBaseUrl + ", fullUri: "
+ fullUri);
}
}
return fullUri;
}
public String getBaseUrl() {
return mBaseUrl.toString();
}
public synchronized String getFullUrlAsString(String path) {
String fullUrl;
if (path.startsWith("http")) {
fullUrl = path;
} else {
fullUrl = mBaseUrl.resolve(path).normalize().toASCIIString();
if (DEBUG) {
Log.d("NetworkClient", "path: " + path + ", baseUrl: " + mBaseUrl + ", fullUrl: "
+ fullUrl);
}
}
return fullUrl;
}
/************************** POST ******************************/
/**
* @param path
* @param jsonString
* @return
* @throws IOException
* @throws NetworkProtocolException
*/
public synchronized HttpResponse post(String path, String jsonString) throws IOException,
NetworkProtocolException {
final String fullUri = getFullUrlAsString(path);
final HttpPost r = new HttpPost(fullUri);
if (DEBUG) {
Log.d("NetworkClient", "POST " + fullUri);
}
r.setEntity(new StringEntity(jsonString, "utf-8"));
r.setHeader("Content-Type", JSON_MIME_TYPE);
final HttpResponse c = this.execute(r);
logDebug("just sent: " + jsonString);
checkStatusCode(c, true);
return c;
}
public JSONObject postJson(String path, JSONObject object) throws IllegalStateException,
IOException, NetworkProtocolException, JSONException {
final HttpResponse res = post(path, object.toString());
return toJsonObject(res);
}
/*********************************** User ******************************/
/**
* Loads/returns the User for the authenticated user
*
* @return the User object for the authenticated user.
* @throws NetworkProtocolException
* @throws IOException
* @throws JSONException
*/
public JSONObject getUser() throws NetworkProtocolException, IOException, JSONException {
JSONObject user;
user = getUser("me");
return user;
}
/**
* Retrieves a User object representing the given user from the network.
*
* @param username
* @return a new instance of the user
* @throws NetworkProtocolException
* @throws IOException
* @throws JSONException
*/
public JSONObject getUser(String username) throws NetworkProtocolException, IOException,
JSONException {
if (username == null) {
return null;
}
return getObject("user/" + username + "/");
}
/**
* Listener for use with InputStreamWatcher.
*
* @author steve
*
*/
public static interface TransferProgressListener {
/**
* @param bytes
* Total bytes transferred.
*/
public void publish(long bytes);
}
public static class InputStreamWatcher extends InputStream {
private static final int GRANULARITY = 1024 * 100; // bytes; number needed to trigger a
// publish()
private final InputStream mInputStream;
private final TransferProgressListener mProgressListener;
private long mCount = 0;
private long mIncrementalCount = 0;
public InputStreamWatcher(InputStream wrappedStream,
TransferProgressListener progressListener) {
mInputStream = wrappedStream;
mProgressListener = progressListener;
}
private void incrementAndNotify(long count) {
mCount += count;
mIncrementalCount += count;
if (mIncrementalCount > GRANULARITY) {
mProgressListener.publish(mCount);
mIncrementalCount = 0;
}
}
@Override
public int read() throws IOException {
return mInputStream.read();
}
private int rcount;
@Override
public int read(byte[] b) throws IOException {
rcount = mInputStream.read(b);
incrementAndNotify(rcount);
return rcount;
}
@Override
public int read(byte[] b, int offset, int length) throws IOException {
rcount = mInputStream.read(b, offset, length);
incrementAndNotify(rcount);
return rcount;
}
@Override
public int available() throws IOException {
return mInputStream.available();
}
@Override
public void close() throws IOException {
mCount = 0;
mInputStream.close();
}
@Override
public boolean equals(Object o) {
return mInputStream.equals(o);
}
@Override
public int hashCode() {
return mInputStream.hashCode();
}
@Override
public void mark(int readlimit) {
mInputStream.mark(readlimit);
}
@Override
public boolean markSupported() {
return mInputStream.markSupported();
}
@Override
public long skip(long n) throws IOException {
final long count = mInputStream.skip(n);
incrementAndNotify(count);
return count;
}
@Override
public synchronized void reset() throws IOException {
mInputStream.reset();
}
}
public void uploadContent(Context context, TransferProgressListener progressListener,
String serverPath, Uri localFile, String contentType) throws NetworkProtocolException,
IOException {
if (localFile == null) {
throw new IOException("Cannot send. Content item does not reference a local file.");
}
final InputStream is = getFileStream(context, localFile);
// next step is to send the file contents.
final HttpPut r = new HttpPut(getFullUrlAsString(serverPath));
r.setHeader("Content-Type", contentType);
final AssetFileDescriptor afd = context.getContentResolver().openAssetFileDescriptor(
localFile, "r");
final InputStreamWatcher isw = new InputStreamWatcher(is, progressListener);
r.setEntity(new InputStreamEntity(isw, afd.getLength()));
final HttpResponse c = this.execute(r);
checkStatusCode(c, true);
c.getEntity().consumeContent();
}
public void uploadContentUsingForm(Context context, TransferProgressListener progressListener,
String serverPath, Uri localFile, String contentType) throws NetworkProtocolException,
IOException {
if (localFile == null) {
throw new IOException("Cannot send. Content item does not reference a local file.");
}
final InputStream is = getFileStream(context, localFile);
// next step is to send the file contents.
final HttpPost r = new HttpPost(getFullUrlAsString(serverPath));
final InputStreamWatcher isw = new InputStreamWatcher(is, progressListener);
final MultipartEntity reqEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
final InputStreamBody fileBody = new AndroidFileInputStreamBody(context, localFile, isw,
contentType);
Log.d(TAG, "content length: " + fileBody.getContentLength());
reqEntity.addPart("file", fileBody);
r.setEntity(reqEntity);
final HttpResponse c = this.execute(r);
checkStatusCode(c, true);
c.getEntity().consumeContent();
}
private static class AndroidFileInputStreamBody extends InputStreamBody {
private final Uri mLocalFile;
private final Context mContext;
public AndroidFileInputStreamBody(Context context, Uri localFile, InputStream in,
String mimeType) {
super(in, mimeType, localFile.getLastPathSegment());
mLocalFile = localFile;
mContext = context;
}
private long length() throws FileNotFoundException {
final AssetFileDescriptor afd = mContext.getContentResolver().openAssetFileDescriptor(
mLocalFile, "r");
return afd.getLength();
}
@Override
public long getContentLength() {
try {
return length();
} catch (final FileNotFoundException e) {
return -1;
}
}
}
/*************************** categories **************************/
/**
* @return A list of all tags, with the most popular one first.
*
* @throws JSONException
* @throws IOException
* @throws NetworkProtocolException
*/
public List<String> getTagsList() throws JSONException, IOException, NetworkProtocolException {
return getTagsList("");
}
public List<String> getRecommendedTagsList(Location near) throws JSONException, IOException,
NetworkProtocolException {
return getTagsList("?location=" + near.getLongitude() + ',' + near.getLatitude());
}
/**
* Gets a specfic type of tag list.
*
* @param path
* either 'favorite' or 'ignore'
* @return
* @throws JSONException
* @throws IOException
* @throws NetworkProtocolException
*/
public List<String> getTagsList(String path) throws JSONException, IOException,
NetworkProtocolException {
return toNativeStringList(getArray("tag/" + path));
}
protected InputStream getFileStream(String localFilename) throws IOException {
if (localFilename.startsWith("content:")) {
final ContentResolver cr = this.mContext.getContentResolver();
return cr.openInputStream(Uri.parse(localFilename));
} else {
return new FileInputStream(new File(localFilename));
}
}
protected InputStream getFileStream(Context context, Uri localFileUri) throws IOException {
return context.getContentResolver().openInputStream(localFileUri);
}
public static NetworkClient getInstance(Context context, Account account) {
return new NetworkClient(context, account);
}
public static String getBaseUrlFromPreferences(Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getString(NetworkClient.PREF_SERVER_URL,
context.getString(R.string.default_api_url));
}
private void setBaseUrl(String baseUrlString) throws MalformedURLException {
final URL baseUrl = new URL(baseUrlString);
try {
mBaseUrl = baseUrl.toURI();
mAuthScope = new AuthScope(mBaseUrl.getHost(), mBaseUrl.getPort());
} catch (final URISyntaxException e) {
final MalformedURLException me = new MalformedURLException(e.getLocalizedMessage());
me.initCause(e);
throw me;
}
}
protected synchronized void loadFromExistingAccount(Account account) {
if (account == null) {
throw new IllegalArgumentException("must specify account");
}
String baseUrlString;
final AccountManager am = AccountManager.get(mContext);
baseUrlString = am.getUserData(account, AuthenticationService.USERDATA_LOCAST_API_URL);
if (baseUrlString == null) {
Log.w(TAG, "loading base URL from preferences instead of account metadata");
baseUrlString = getBaseUrlFromPreferences(mContext);
// if it's null in the userdata, then it must be an account from before this feature
// was added.
// Store for later use.
am.setUserData(account, AuthenticationService.USERDATA_LOCAST_API_URL, baseUrlString);
}
try {
setBaseUrl(baseUrlString);
setCredentialsFromAccount(account);
} catch (final MalformedURLException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
}
}
public void setCredentialsFromAccount(Account account) {
final AccountManager am = AccountManager.get(mContext);
if (Authenticator.DEMO_ACCOUNT.equals(account.name)) {
if (DEBUG) {
Log.i(TAG, "demo account is being used");
}
return;
}
setCredentials(account.name, am.getPassword(account));
}
/**
* Perform an offline check to see if there is a pairing stored for this client. Does not block
* on network connection.
*
* @return true if the client is paired with the server.
*/
@Deprecated
public boolean isPaired() {
final AccountManager am = AccountManager.get(mContext);
final Account[] accounts = am.getAccountsByType(AuthenticationService.ACCOUNT_TYPE);
return accounts.length >= 1;
}
protected void logDebug(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
public static enum UploadType {
RAW_PUT, FORM_POST
}
public void uploadContentWithNotification(Context context, Uri cast, String serverPath,
Uri localFile, String contentType, UploadType uploadType)
throws NetworkProtocolException, IOException {
String castTitle = Cast.getTitle(context, cast);
if (castTitle == null) {
castTitle = "untitled (cast #" + cast.getLastPathSegment() + ")";
}
final ProgressNotification notification = new ProgressNotification(context,
context.getString(R.string.sync_uploading_cast, castTitle),
ProgressNotification.TYPE_UPLOAD, PendingIntent.getActivity(context, 0, new Intent(
Intent.ACTION_VIEW, cast).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0), true);
// assume fail: when successful, all will be reset.
notification.successful = false;
notification.doneTitle = context.getString(R.string.sync_upload_fail);
notification.doneText = context.getString(R.string.sync_upload_fail_message, castTitle);
notification.doneIntent = PendingIntent.getActivity(context, 0, new Intent(
Intent.ACTION_VIEW, cast).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 0);
final NotificationManager nm = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
final NotificationProgressListener tpl = new NotificationProgressListener(nm, notification,
0, (int) ContentUris.parseId(cast));
try {
final AssetFileDescriptor afd = context.getContentResolver().openAssetFileDescriptor(
localFile, "r");
final long max = afd.getLength();
tpl.setSize(max);
switch (uploadType) {
case RAW_PUT:
uploadContent(context, tpl, serverPath, localFile, contentType);
break;
case FORM_POST:
uploadContentUsingForm(context, tpl, serverPath, localFile, contentType);
break;
}
notification.doneTitle = context.getString(R.string.sync_upload_success);
notification.doneText = context.getString(R.string.sync_upload_success_message,
castTitle);
notification.successful = true;
} catch (final NetworkProtocolException e) {
notification.setUnsuccessful(e.getLocalizedMessage());
throw e;
} catch (final IOException e) {
notification.setUnsuccessful(e.getLocalizedMessage());
throw e;
} finally {
tpl.done();
}
}
/**
* @param favoritable
* @param newState
* @return the newly-set state
* @throws NetworkProtocolException
* @throws IOException
*/
public boolean setFavorite(Uri favoritable, boolean newState) throws NetworkProtocolException,
IOException {
try {
final String newStateString = "favorite=" + (newState ? "true" : "false");
final HttpResponse hr = post(MediaProvider.getPublicPath(mContext, favoritable)
+ "favorite/", newStateString);
final JSONObject serverStateObj = toJsonObject(hr);
final boolean serverState = serverStateObj.getBoolean("is_favorite");
return serverState;
} catch (final IllegalStateException e) {
throw new NetworkProtocolException(e.getLocalizedMessage());
} catch (final JSONException e) {
throw new NetworkProtocolException(e);
} catch (final NoPublicPath e) {
final NetworkProtocolException npe = new NetworkProtocolException(
"no known path to mark favorite");
npe.initCause(e);
throw npe;
}
}
/******************************** utils **************************/
public static JSONArray featureCollectionToList(JSONObject featureCollection)
throws NetworkProtocolException, JSONException {
if (!featureCollection.getString("type").equals("FeatureCollection")) {
throw new NetworkProtocolException("Expecting a FeatureCollection but received a "
+ featureCollection.getString("type"));
}
return featureCollection.getJSONArray("features");
}
public static List<String> toNativeStringList(JSONArray ja) throws JSONException {
final Vector<String> strs = new Vector<String>(ja.length());
for (int i = 0; i < ja.length(); i++) {
strs.add(i, ja.getString(i));
}
return strs;
}
/**
* Generates a query string from a hashmap of query parameters.
*
* @param parameters
* @return
*/
static public String toQueryString(HashMap<String, Object> parameters) {
final StringBuilder query = new StringBuilder();
for (final Iterator<String> i = parameters.keySet().iterator(); i.hasNext();) {
final String key = i.next();
query.append(key).append('=');
final Object val = parameters.get(key);
if (val instanceof Date) {
query.append(dateFormat.format(val));
} else {
query.append(val.toString());
}
if (i.hasNext()) {
query.append("&");
}
}
return query.toString();
}
public static Date parseDate(String dateString) throws ParseException {
/*
* if (dateString.endsWith("Z")){ dateString = dateString.substring(0,
* dateString.length()-2) + "GMT"; }
*/
return dateFormat.parse(dateString);
}
static {
dateFormat.setCalendar(Calendar.getInstance(TimeZone.getTimeZone("GMT")));
}
}