package com.ch_linghu.fanfoudroid.http;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.SSLHandshakeException;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.NoHttpResponseException;
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.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnRoutePNames;
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.HttpEntityWrapper;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicCredentialsProvider;
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.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import android.util.Log;
import com.ch_linghu.fanfoudroid.AppParam;
import com.ch_linghu.fanfoudroid.fanfou.Configuration;
import com.ch_linghu.fanfoudroid.fanfou.RefuseError;
import com.ch_linghu.fanfoudroid.util.DebugTimer;
import eriji.com.oauth.OAuthClient;
import eriji.com.oauth.OAuthClientException;
import eriji.com.oauth.OAuthFileStore;
import eriji.com.oauth.OAuthSharedPreferencesStore;
import eriji.com.oauth.OAuthStore;
import eriji.com.oauth.XAuthClient;
/**
* Wrap of org.apache.http.impl.client.DefaultHttpClient
*
* @author lds
*
*/
public class HttpClient {
private static final String TAG = "HttpClient";
private final static boolean DEBUG = Configuration.getDebug();
/** OK: Success! */
public static final int OK = 200;
/** Not Modified: There was no new data to return. */
public static final int NOT_MODIFIED = 304;
/**
* Bad Request: The request was invalid. An accompanying error message will
* explain why. This is the status code will be returned during rate
* limiting.
*/
public static final int BAD_REQUEST = 400;
/** Not Authorized: Authentication credentials were missing or incorrect. */
public static final int NOT_AUTHORIZED = 401;
/**
* Forbidden: The request is understood, but it has been refused. An
* accompanying error message will explain why.
*/
public static final int FORBIDDEN = 403;
/**
* Not Found: The URI requested is invalid or the resource requested, such
* as a user, does not exists.
*/
public static final int NOT_FOUND = 404;
/**
* Not Acceptable: Returned by the Search API when an invalid format is
* specified in the request.
*/
public static final int NOT_ACCEPTABLE = 406;
/**
* Internal Server Error: Something is broken. Please post to the group so
* the Weibo team can investigate.
*/
public static final int INTERNAL_SERVER_ERROR = 500;
/** Bad Gateway: Weibo is down or being upgraded. */
public static final int BAD_GATEWAY = 502;
/**
* Service Unavailable: The Weibo servers are up, but overloaded with
* requests. Try again later. The search and trend methods use this to
* indicate when you are being rate limited.
*/
public static final int SERVICE_UNAVAILABLE = 503;
private static final int CONNECTION_TIMEOUT_MS = 30 * 1000;
private static final int SOCKET_TIMEOUT_MS = 30 * 1000;
public static final int RETRIEVE_LIMIT = 20;
public static final int RETRIED_TIME = 3;
private static final String SERVER_HOST = "api.fanfou.com";
private DefaultHttpClient mClient;
private AuthScope mAuthScope;
private BasicHttpContext localcontext;
private String mUserId;
private String mPassword;
private OAuthClient mOAuthClient;
private String mOAuthBaseUrl = Configuration.getOAuthBaseUrl();;
private static boolean isAuthenticationEnabled = false;
public HttpClient() {
prepareHttpClient();
setOAuthConsumer(null, null, null);
}
/**
* @param user_id
* auth user
* @param password
* auth password
*/
public HttpClient(String user_id, String password) {
prepareHttpClient();
setOAuthConsumer(null, null, null);
setCredentials(user_id, password);
}
/**
* Empty the credentials
*/
public void reset() {
setCredentials("", "");
}
/**
* @return authed user id
*/
public String getUserId() {
return mUserId;
}
/**
* @return authed user password
*/
public String getPassword() {
return mPassword;
}
/**
* @param hostname
* the hostname (IP or DNS name)
* @param port
* the port number. -1 indicates the scheme default port.
* @param scheme
* the name of the scheme. null indicates the default scheme
*/
public void setProxy(String host, int port, String scheme) {
HttpHost proxy = new HttpHost(host, port, scheme);
mClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
}
public void removeProxy() {
mClient.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY);
}
private void enableDebug() {
Log.d(TAG, "enable apache.http debug");
java.util.logging.Logger.getLogger("org.apache.http").setLevel(
java.util.logging.Level.FINEST);
java.util.logging.Logger.getLogger("org.apache.http.wire").setLevel(
java.util.logging.Level.FINER);
java.util.logging.Logger.getLogger("org.apache.http.headers").setLevel(
java.util.logging.Level.OFF);
/*
* System.setProperty("log.tag.org.apache.http", "VERBOSE");
* System.setProperty("log.tag.org.apache.http.wire", "VERBOSE");
* System.setProperty("log.tag.org.apache.http.headers", "VERBOSE");
*
* 在这里使用System.setProperty设置不会生效, 原因不明, 必须在终端上输入以下命令方能开启http调试信息: > adb
* shell setprop log.tag.org.apache.http VERBOSE > adb shell setprop
* log.tag.org.apache.http.wire VERBOSE > adb shell setprop
* log.tag.org.apache.http.headers VERBOSE
*/
}
/**
* Sets the consumer key and consumer secret.<br>
*/
public void setOAuthConsumer(String consumerKey, String consumerSecret, OAuthStore store) {
mOAuthClient = XAuthClient.factory();
isAuthenticationEnabled = true;
}
/**
* Setup DefaultHttpClient
*
* Use ThreadSafeClientConnManager.
*
*/
private void prepareHttpClient() {
if (DEBUG) {
enableDebug();
}
// Create and initialize HTTP parameters
HttpParams params = new BasicHttpParams();
ConnManagerParams.setMaxTotalConnections(params, 10);
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
// Create and initialize scheme registry
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", SSLSocketFactory
.getSocketFactory(), 443));
// Create an HttpClient with the ThreadSafeClientConnManager.
ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(
params, schemeRegistry);
mClient = new DefaultHttpClient(cm, params);
// Support GZIP
mClient.addResponseInterceptor(gzipResponseIntercepter);
// TODO: need to release this connection in httpRequest()
// cm.releaseConnection(conn, validDuration, timeUnit);
// httpclient.getConnectionManager().shutdown();
}
private void setBasicAuth() {
// Setup BasicAuth
BasicScheme basicScheme = new BasicScheme();
mAuthScope = new AuthScope(SERVER_HOST, AuthScope.ANY_PORT);
// mClient.setAuthSchemes(authRegistry);
mClient.setCredentialsProvider(new BasicCredentialsProvider());
// Generate BASIC scheme object and stick it to the local
// execution context
localcontext = new BasicHttpContext();
localcontext.setAttribute("preemptive-auth", basicScheme);
// first request interceptor
mClient.addRequestInterceptor(preemptiveAuth, 0);
}
/**
* HttpRequestInterceptor for DefaultHttpClient
*/
private static HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() {
@Override
public void process(final HttpRequest request, final HttpContext context) {
AuthState authState = (AuthState) context
.getAttribute(ClientContext.TARGET_AUTH_STATE);
CredentialsProvider credsProvider = (CredentialsProvider) context
.getAttribute(ClientContext.CREDS_PROVIDER);
HttpHost targetHost = (HttpHost) context
.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
if (authState.getAuthScheme() == null) {
AuthScope authScope = new AuthScope(targetHost.getHostName(),
targetHost.getPort());
Credentials creds = credsProvider.getCredentials(authScope);
if (creds != null) {
authState.setAuthScheme(new BasicScheme());
authState.setCredentials(creds);
}
}
}
};
private static HttpResponseInterceptor gzipResponseIntercepter = new HttpResponseInterceptor() {
@Override
public void process(HttpResponse response, HttpContext context)
throws org.apache.http.HttpException, IOException {
HttpEntity entity = response.getEntity();
Header ceheader = entity.getContentEncoding();
if (ceheader != null) {
HeaderElement[] codecs = ceheader.getElements();
for (int i = 0; i < codecs.length; i++) {
if (codecs[i].getName().equalsIgnoreCase("gzip")) {
response.setEntity(new GzipDecompressingEntity(response
.getEntity()));
return;
}
}
}
}
};
static class GzipDecompressingEntity extends HttpEntityWrapper {
public GzipDecompressingEntity(final HttpEntity entity) {
super(entity);
}
@Override
public InputStream getContent() throws IOException,
IllegalStateException {
// the wrapped entity's getContent() decides about repeatability
InputStream wrappedin = wrappedEntity.getContent();
return new GZIPInputStream(wrappedin);
}
@Override
public long getContentLength() {
// length of ungzipped content is not known
return -1;
}
}
/**
* Setup Credentials for HTTP Basic Auth
*
* @param username
* @param password
*/
public void setCredentials(String username, String password) {
mUserId = username;
mPassword = password;
// FOR TEST: disable Basic auth
//mClient.getCredentialsProvider().setCredentials(mAuthScope,
// new UsernamePasswordCredentials(username, password));
isAuthenticationEnabled = true;
}
public Response post(String url, ArrayList<BasicNameValuePair> postParams,
boolean authenticated) throws HttpException {
if (null == postParams) {
postParams = new ArrayList<BasicNameValuePair>();
}
return httpRequest(url, postParams, authenticated, HttpPost.METHOD_NAME);
}
public Response post(String url, ArrayList<BasicNameValuePair> params)
throws HttpException {
return httpRequest(url, params, true, HttpPost.METHOD_NAME);
}
public Response post(String url, boolean authenticated)
throws HttpException {
return httpRequest(url, null, authenticated, HttpPost.METHOD_NAME);
}
public Response post(String url) throws HttpException {
return httpRequest(url, null, false, HttpPost.METHOD_NAME);
}
public Response post(String url, File file) throws HttpException {
return httpRequest(url, null, file, false, HttpPost.METHOD_NAME);
}
/**
* POST一个文件
*
* @param url
* @param file
* @param authenticate
* @return
* @throws HttpException
*/
public Response post(String url, File file, boolean authenticate)
throws HttpException {
return httpRequest(url, null, file, authenticate, HttpPost.METHOD_NAME);
}
public Response get(String url, ArrayList<BasicNameValuePair> params,
boolean authenticated) throws HttpException {
return httpRequest(url, params, authenticated, HttpGet.METHOD_NAME);
}
public Response get(String url, ArrayList<BasicNameValuePair> params)
throws HttpException {
return httpRequest(url, params, false, HttpGet.METHOD_NAME);
}
public Response get(String url) throws HttpException {
return httpRequest(url, null, false, HttpGet.METHOD_NAME);
}
public Response get(String url, boolean authenticated) throws HttpException {
return httpRequest(url, null, authenticated, HttpGet.METHOD_NAME);
}
public Response httpRequest(String url,
ArrayList<BasicNameValuePair> postParams, boolean authenticated,
String httpMethod) throws HttpException {
return httpRequest(url, postParams, null, authenticated, httpMethod);
}
/**
* Execute the DefaultHttpClient
*
* @param url
* target
* @param postParams
* @param file
* can be NULL
* @param authenticated
* need or not
* @param httpMethod
* HttpPost.METHOD_NAME HttpGet.METHOD_NAME
* HttpDelete.METHOD_NAME
* @return Response from server
* @throws HttpException
* 此异常包装了一系列底层异常 <br />
* <br />
* 1. 底层异常, 可使用getCause()查看: <br />
* <li>URISyntaxException, 由`new URI` 引发的.</li> <li>IOException,
* 由`createMultipartEntity` 或 `UrlEncodedFormEntity` 引发的.</li>
* <li>IOException和ClientProtocolException,
* 由`HttpClient.execute` 引发的.</li><br />
*
* 2. 当响应码不为200时报出的各种子类异常: <li>HttpRequestException,
* 通常发生在请求的错误,如请求错误了 网址导致404等, 抛出此异常, 首先检查request log,
* 确认不是人为错误导致请求失败</li> <li>HttpAuthException, 通常发生在Auth失败,
* 检查用于验证登录的用户名/密码/KEY等</li> <li>HttpRefusedException,
* 通常发生在服务器接受到请求, 但拒绝请求, 可是多种原因, 具体原因 服务器会返回拒绝理由,
* 调用HttpRefusedException#getError#getMessage查看</li> <li>
* HttpServerException, 通常发生在服务器发生错误时, 检查服务器端是否在正常提供服务</li> <li>
* HttpException, 其他未知错误.</li>
*/
public Response httpRequest(String url,
ArrayList<BasicNameValuePair> postParams, File file,
boolean authenticated, String httpMethod) throws HttpException {
Log.d(TAG, "Sending " + httpMethod + " request to " + url);
if (AppParam.DEBUG) {
DebugTimer.betweenStart("HTTP");
}
URI uri = createURI(url);
HttpResponse response = null;
Response res = null;
HttpUriRequest method = null;
// Create POST, GET or DELETE METHOD
method = createMethod(httpMethod, uri, file, postParams);
// Setup ConnectionParams, Request Headers
SetupHTTPConnectionParams(method);
// Execute Request
try {
// 加入OAuth认证信息
if (authenticated) {
mOAuthClient.signRequest(method);
}
response = mClient.execute(method, localcontext);
res = new Response(response);
} catch (ClientProtocolException e) {
Log.e(TAG, e.getMessage(), e);
throw new HttpException(e.getMessage(), e);
} catch (IOException ioe) {
throw new HttpException(ioe.getMessage(), ioe);
} catch (OAuthClientException e) {
Log.e(TAG, e.getMessage(), e);
throw new HttpException(e.getMessage(), e);
}
if (response != null) {
int statusCode = response.getStatusLine().getStatusCode();
// It will throw a weiboException while status code is not 200
HandleResponseStatusCode(statusCode, res);
} else {
Log.e(TAG, "response is null");
}
if (AppParam.DEBUG) {
DebugTimer.betweenEnd("HTTP");
}
return res;
}
/**
* CreateURI from URL string
*
* @param url
* @return request URI
* @throws HttpException
* Cause by URISyntaxException
*/
private URI createURI(String url) throws HttpException {
URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
Log.e(TAG, e.getMessage(), e);
throw new HttpException("Invalid URL.");
}
return uri;
}
/**
* 创建可带一个File的MultipartEntity
*
* @param filename
* 文件名
* @param file
* 文件
* @param postParams
* 其他POST参数
* @return 带文件和其他参数的Entity
* @throws UnsupportedEncodingException
*/
private MultipartEntity createMultipartEntity(String filename, File file,
ArrayList<BasicNameValuePair> postParams)
throws UnsupportedEncodingException {
MultipartEntity entity = new MultipartEntity();
// Don't try this. Server does not appear to support chunking.
// entity.addPart("media", new InputStreamBody(imageStream, "media"));
entity.addPart(filename, new FileBody(file));
for (BasicNameValuePair param : postParams) {
entity.addPart(param.getName(), new StringBody(param.getValue()));
}
return entity;
}
/**
* Setup HTTPConncetionParams
*
* @param method
*/
private void SetupHTTPConnectionParams(HttpUriRequest method) {
HttpConnectionParams.setConnectionTimeout(method.getParams(),
CONNECTION_TIMEOUT_MS);
HttpConnectionParams
.setSoTimeout(method.getParams(), SOCKET_TIMEOUT_MS);
mClient.setHttpRequestRetryHandler(requestRetryHandler);
method.addHeader("Accept-Encoding", "gzip, deflate");
method.addHeader("Accept-Charset", "UTF-8,*;q=0.5");
}
/**
* Create request method, such as POST, GET, DELETE
*
* @param httpMethod
* "GET","POST","DELETE"
* @param uri
* 请求的URI
* @param file
* 可为null
* @param postParams
* POST参数
* @return httpMethod Request implementations for the various HTTP methods
* like GET and POST.
* @throws HttpException
* createMultipartEntity 或 UrlEncodedFormEntity引发的IOException
*/
private HttpUriRequest createMethod(String httpMethod, URI uri, File file,
ArrayList<BasicNameValuePair> postParams) throws HttpException {
HttpUriRequest method;
if (httpMethod.equalsIgnoreCase(HttpPost.METHOD_NAME)) {
// POST METHOD
HttpPost post = new HttpPost(uri);
// See this:
// http://groups.google.com/group/twitter-development-talk/browse_thread/thread/e178b1d3d63d8e3b
post.getParams().setBooleanParameter(
"http.protocol.expect-continue", false);
try {
HttpEntity entity = null;
if (null != file) {
entity = createMultipartEntity("photo", file, postParams);
post.setEntity(entity);
} else if (null != postParams) {
entity = new UrlEncodedFormEntity(postParams, HTTP.UTF_8);
}
post.setEntity(entity);
} catch (IOException ioe) {
throw new HttpException(ioe.getMessage(), ioe);
}
method = post;
} else if (httpMethod.equalsIgnoreCase(HttpDelete.METHOD_NAME)) {
method = new HttpDelete(uri);
} else {
method = new HttpGet(uri);
}
return method;
}
/**
* 解析HTTP错误码
*
* @param statusCode
* @return
*/
private static String getCause(int statusCode) {
String cause = null;
switch (statusCode) {
case NOT_MODIFIED:
break;
case BAD_REQUEST:
cause = "The request was invalid. An accompanying error message will explain why. This is the status code will be returned during rate limiting.";
break;
case NOT_AUTHORIZED:
cause = "Authentication credentials were missing or incorrect.";
break;
case FORBIDDEN:
cause = "The request is understood, but it has been refused. An accompanying error message will explain why.";
break;
case NOT_FOUND:
cause = "The URI requested is invalid or the resource requested, such as a user, does not exists.";
break;
case NOT_ACCEPTABLE:
cause = "Returned by the Search API when an invalid format is specified in the request.";
break;
case INTERNAL_SERVER_ERROR:
cause = "Something is broken. Please post to the group so the Weibo team can investigate.";
break;
case BAD_GATEWAY:
cause = "Weibo is down or being upgraded.";
break;
case SERVICE_UNAVAILABLE:
cause = "Service Unavailable: The Weibo servers are up, but overloaded with requests. Try again later. The search and trend methods use this to indicate when you are being rate limited.";
break;
default:
cause = "";
}
return statusCode + ":" + cause;
}
public boolean isAuthenticationEnabled() {
return isAuthenticationEnabled;
}
public static void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
/**
* Handle Status code
*
* @param statusCode
* 响应的状态码
* @param res
* 服务器响应
* @throws HttpException
* 当响应码不为200时都会报出此异常:<br />
* <li>HttpRequestException, 通常发生在请求的错误,如请求错误了 网址导致404等, 抛出此异常,
* 首先检查request log, 确认不是人为错误导致请求失败</li> <li>HttpAuthException,
* 通常发生在Auth失败, 检查用于验证登录的用户名/密码/KEY等</li> <li>
* HttpRefusedException, 通常发生在服务器接受到请求, 但拒绝请求, 可是多种原因, 具体原因
* 服务器会返回拒绝理由, 调用HttpRefusedException#getError#getMessage查看</li>
* <li>HttpServerException, 通常发生在服务器发生错误时, 检查服务器端是否在正常提供服务</li>
* <li>HttpException, 其他未知错误.</li>
*/
private void HandleResponseStatusCode(int statusCode, Response res)
throws HttpException {
String msg = getCause(statusCode) + "\n";
RefuseError error = null;
switch (statusCode) {
// It's OK, do nothing
case OK:
break;
// Mine mistake, Check the Log
case NOT_MODIFIED:
case BAD_REQUEST:
case NOT_FOUND:
case NOT_ACCEPTABLE:
throw new HttpException(msg + res.asString(), statusCode);
// UserName/Password incorrect
case NOT_AUTHORIZED:
throw new HttpAuthException(msg + res.asString(), statusCode);
// Server will return a error message, use
// HttpRefusedException#getError() to see.
case FORBIDDEN:
throw new HttpRefusedException(msg, statusCode);
// Something wrong with server
case INTERNAL_SERVER_ERROR:
case BAD_GATEWAY:
case SERVICE_UNAVAILABLE:
throw new HttpServerException(msg, statusCode);
// Others
default:
throw new HttpException(msg + res.asString(), statusCode);
}
}
public static String encode(String value) throws HttpException {
try {
return URLEncoder.encode(value, HTTP.UTF_8);
} catch (UnsupportedEncodingException e_e) {
throw new HttpException(e_e.getMessage(), e_e);
}
}
public static String encodeParameters(ArrayList<BasicNameValuePair> params)
throws HttpException {
StringBuffer buf = new StringBuffer();
for (int j = 0; j < params.size(); j++) {
if (j != 0) {
buf.append("&");
}
try {
buf.append(URLEncoder.encode(params.get(j).getName(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(params.get(j).getValue(),
"UTF-8"));
} catch (java.io.UnsupportedEncodingException neverHappen) {
throw new HttpException(neverHappen.getMessage(), neverHappen);
}
}
return buf.toString();
}
/**
* 异常自动恢复处理, 使用HttpRequestRetryHandler接口实现请求的异常恢复
*/
private static HttpRequestRetryHandler requestRetryHandler = new HttpRequestRetryHandler() {
// 自定义的恢复策略
public boolean retryRequest(IOException exception, int executionCount,
HttpContext context) {
// 设置恢复策略,在发生异常时候将自动重试N次
if (executionCount >= RETRIED_TIME) {
// Do not retry if over max retry count
return false;
}
if (exception instanceof NoHttpResponseException) {
// Retry if the server dropped connection on us
return true;
}
if (exception instanceof SSLHandshakeException) {
// Do not retry on SSL handshake exception
return false;
}
HttpRequest request = (HttpRequest) context
.getAttribute(ExecutionContext.HTTP_REQUEST);
boolean idempotent = (request instanceof HttpEntityEnclosingRequest);
if (!idempotent) {
// Retry if the request is considered idempotent
return true;
}
return false;
}
};
public OAuthClient getOAuthClient() {
return mOAuthClient;
}
}