/** * */ package com.taobao.top.android; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.text.TextUtils; import android.util.Log; import com.taobao.top.android.api.ApiError; import com.taobao.top.android.api.TaobaoUtils; import com.taobao.top.android.api.TopApiListener; import com.taobao.top.android.api.TopTqlListener; import com.taobao.top.android.api.WebUtils; import com.taobao.top.android.auth.AccessToken; import com.taobao.top.android.auth.AuthError; import com.taobao.top.android.auth.AuthException; import com.taobao.top.android.auth.AuthorizeListener; /** * top客户端实现,封装了用户授权和API调用<br> * 一个客户端支持多个appkey * * @author junyan.hj * */ public class TopAndroidClient { private static final ConcurrentHashMap<String, TopAndroidClient> CLIENT_STORE = new ConcurrentHashMap<String, TopAndroidClient>(); private static final String OAUTH_CLIENT_ID = "client_id"; private static final String OAUTH_REDIRECT_URI = "redirect_uri"; private static final String OAUTH_CLIENT_SECRET = "client_secret"; private static final String OAUTH_REFRESH_TOKEN = "refresh_token"; private static final String SDK_TRACK_ID = "track-id"; private static final String SDK_DEVICE_UUID = "device-uuid"; private static final String SDK_TIMESTAMP = "timestamp"; private static final String SDK_CLIENT_SYSVERSION = "client-sysVersion"; private static final String SDK_CLIENT_SYSNAME = "client-sysName"; private static final String SDK_VERSION = "sdk-version"; private static final String SYS_NAME = "Android"; private static final String SESSION_DIR = "top.session"; private static final String LOG_TAG = "TopAndroidClient"; private String appKey; private String appSecret; private String redirectURI; private ConcurrentHashMap<Long, AccessToken> tokenStore = new ConcurrentHashMap<Long, AccessToken>(); private Context context; private Env env; private int connectTimeout = 10000;// 10秒 private int readTimeout = 30000;// 30秒 /** * 注册client所需信息。appKey/appSecret/redirectURI的值必须和开放平台开发者中心里注册的信息保持一致 * * @param context * @param appKey * @param appSecret * @param redirectURI */ public static void registerAndroidClient(Context context, String appKey, String appSecret, String redirectURI) { registerAndroidClient(context, appKey, appSecret, redirectURI, Env.PRODUCTION); } /** * 注册client所需信息。appKey/appSecret/redirectURI的值必须和开放平台开发者中心里注册的信息保持一致 * @see Env * @param context * @param appKey * @param appSecret * @param redirectURI * @param env * appKey对应的运行环境 */ public static void registerAndroidClient(Context context, String appKey, String appSecret, String redirectURI, Env env) { if (context == null) { throw new IllegalArgumentException("context must not null."); } if (TextUtils.isEmpty(appKey) || TextUtils.isEmpty(appSecret) || TextUtils.isEmpty(redirectURI)) { throw new IllegalArgumentException( "appKey,appSecret and redirectURI must not null."); } TopAndroidClient client = new TopAndroidClient(); client.setAppKey(appKey); client.setAppSecret(appSecret); client.setRedirectURI(redirectURI); client.setContext(context); if (env == null) { env = Env.PRODUCTION; } client.setEnv(env); client.revertAccessToken(); CLIENT_STORE.put(appKey, client); } /** * 获得事先已经注册的client对象。如果事先没有注册的话返回null * * @see #registerAndroidClient(Context, String, String, String) * @see #registerAndroidClient(Context, String, String, String, Env) * @param appKey * @return */ public static TopAndroidClient getAndroidClientByAppKey(String appKey) { return CLIENT_STORE.get(appKey); } private TopAndroidClient() { } /** * 刷新access token的操作。 * * @see #authorize(Activity) * @see #addAccessToken(AccessToken) * @see AuthorizeListener * @param userId * 不能为null,通过userId确定需要刷新哪个access token。如果对应的access * token不存在会抛出IllegalArgumentException * @param listener * 不能为null,处理刷新结果的回调函数。 * @param async * 是否是异步执行刷新操作 * @throws IllegalArgumentException */ public void refreshToken(Long userId, final AuthorizeListener listener, boolean async) { if (userId == null) { throw new IllegalArgumentException("userId must not null."); } if (listener == null) { throw new IllegalArgumentException("listener must not null."); } final AccessToken token = this.tokenStore.get(userId); if (token == null) { throw new IllegalArgumentException("userId:" + userId + " can't found access token."); } if (async) { new Thread() { @Override public void run() { doRefresh(token, listener); } }.start(); } else {// 同步 doRefresh(token, listener); } } /** * 调用浏览器显示授权页面。 * * @param activity * 通过{@code Intent}方式在浏览器上打开授权页面的{@code Activity},不能为null */ public void authorize(Activity activity) { if (activity == null) { throw new IllegalArgumentException("activity must not null."); } Map<String, String> params = getProtocolParams(); params.put(OAUTH_CLIENT_ID, appKey); params.put(OAUTH_REDIRECT_URI, redirectURI); Random random = new Random(); params.put("rand", String.valueOf(random.nextInt())); String str = ""; try { URL url = WebUtils.buildGetUrl(env.getAuthUrl(), params, null); str = url.toString(); } catch (IOException e) { throw new RuntimeException(e);// won't happen } Uri uri = Uri.parse(str); Intent it = new Intent(Intent.ACTION_VIEW, uri); try { ComponentName name = new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"); // 判断系统自带浏览器是否安装 context.getPackageManager().getActivityInfo(name, PackageManager.GET_INTENT_FILTERS); it.setComponent(name); } catch (Exception e) { /* * if an activity with the given class name can not be found on the * system */ Log.e(LOG_TAG, e.getMessage(), e); } activity.startActivity(it); } public String getAuthorizeLink(){ Map<String, String> params = getProtocolParams(); params.put(OAUTH_CLIENT_ID, appKey); params.put(OAUTH_REDIRECT_URI, redirectURI); Random random = new Random(); params.put("rand", String.valueOf(random.nextInt())); String str = ""; try { URL url = WebUtils.buildGetUrl(env.getAuthUrl(), params, null); str = url.toString(); } catch (IOException e) { throw new RuntimeException(e);// won't happen } return str; } /** * 调用TOP API * * @see TopApiListener * @param params * 系统及业务参数 * @param userId * 需要使用哪个用户授权的access token来调用api,当API不需要session key时此参数可以为null * @param listener * api调用回调处理监听器,不能为null * @param async * true:异步调用;false:同步调用。Android 3.0以后会限制在UI主线程中同步访问网络,使用同步方式需谨慎 * @throws IllegalArgumentException * 当参数<code>params</code>或<code>listener</code>为null时 */ public void api(final TopParameters params, final Long userId, final TopApiListener listener, final boolean async) { if (params == null) { throw new IllegalArgumentException("params must not null."); } if (listener == null) { throw new IllegalArgumentException("listener must not null."); } final AccessToken tk = getStoredAccessToken(userId); if (async) {// 异步调用 new Thread() { @Override public void run() { invokeApi(params, listener, tk); } }.start(); } else {// 同步 invokeApi(params, listener, tk); } } /** * useId为null时返回null,如果userId对应的access token不存在则抛出异常 * * @param userId * @return * @throws IllegalArgumentException */ private AccessToken getStoredAccessToken(final Long userId) { AccessToken token = null; if (userId != null) { token = this.tokenStore.get(userId); if (token == null) { throw new IllegalArgumentException("userId:" + userId + " can't found access token."); } } return token; } /** * 调用TQL服务 * * @see TopApiListener * @param ql * @param userId * 需要使用哪个用户授权的access token来调用api,当API不需要session key时此参数可以为null * @param listener * tql调用回调处理监听器,不能为null * @param async * true:异步调用;false:同步调用。Android 3.0以后会限制在UI主线程中同步访问网络,使用同步方式需谨慎 * @throws IllegalArgumentException * 当参数<code>ql</code>或<code>listener</code>为null时 */ public void tql(final String ql, final Long userId, final TopTqlListener listener, final boolean async) { if (TextUtils.isEmpty(ql)) { throw new IllegalArgumentException("ql must not null."); } if (listener == null) { throw new IllegalArgumentException("listener must not null."); } final AccessToken token = getStoredAccessToken(userId); if (async) {// 异步调用 new Thread() { @Override public void run() { invokeTql(ql, token, listener); } }.start(); } else {// 同步 invokeTql(ql, token, listener); } } private void invokeTql(final String ql, AccessToken token, TopTqlListener listener) { TreeMap<String, String> params = new TreeMap<String, String>(); params.put("ql", ql); params.put("app_key", appKey); params.put("sign_method", "hmac"); params.put("top_tql_seperator","true"); if (token != null) { params.put("session", token.getValue()); } String sign; try { sign = TaobaoUtils.signTopRequestNew(params, appSecret); params.put("sign", sign); String jsonStr = WebUtils.doPost(context, env.getTqlUrl(), params, this.getProtocolParams(), connectTimeout, readTimeout, false); Log.d(LOG_TAG, jsonStr); listener.onComplete(jsonStr); } catch (Exception e) { Log.e(LOG_TAG, e.getMessage(), e); listener.onException(e); } } /** * 获取淘宝系统时间 * 注意这里使用的是同步的方式调用api * * @return */ public Date getTime() { TopParameters params = new TopParameters(); params.setMethod("taobao.time.get "); final List<Date> list = new ArrayList<Date>(); this.api(params, null, new TopApiListener() { @Override public void onComplete(JSONObject json) { JSONObject j = json.optJSONObject("time_get_response"); if (j != null) { String timeStr = j.optString("time"); if (!TextUtils.isEmpty(timeStr)) { SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); try { Date date = sdf.parse(timeStr); list.add(date); } catch (ParseException e) { Log.e(LOG_TAG, e.getMessage(), e); } } } } @Override public void onError(ApiError error) { } @Override public void onException(Exception e) { } }, false); if (list.size() > 0) { return list.get(0); } return new Date(); } private void doRefresh(AccessToken accessToken, AuthorizeListener listener) { Map<String, String> header = getProtocolParams(); Map<String, String> params = new HashMap<String, String>(); params.put(OAUTH_CLIENT_ID, appKey); params.put(OAUTH_CLIENT_SECRET, appSecret); params.put(OAUTH_REDIRECT_URI, redirectURI); params.put(OAUTH_REFRESH_TOKEN, accessToken.getRefreshToken() .getValue()); try { String jsonStr = WebUtils.doPost(context, env.getRefreshUrl(), params, header, connectTimeout, readTimeout, true); JSONObject json = new JSONObject(jsonStr); String error = json.optString("error"); if (!TextUtils.isEmpty(error)) { Log.e(LOG_TAG, jsonStr); AuthError authError = new AuthError(); authError.setError(error); authError.setErrorDescription(json .optString("error_description")); listener.onError(authError); } else { AccessToken token = TOPUtils.convertToAccessToken(json); token.setStartDate(this.getTime()); addAccessToken(token); listener.onComplete(token); } } catch (Exception e) { Log.e(LOG_TAG, e.getMessage(), e); listener.onAuthException(new AuthException(e)); } } private void invokeApi(TopParameters params, TopApiListener listener, AccessToken token) { try { String jsonStr = WebUtils.doPost(context, env.getApiUrl(), this.generateApiParams(params, token), this.getProtocolParams(), params.getAttachments(), connectTimeout, readTimeout); Log.d(LOG_TAG, jsonStr); handleApiResponse(listener, jsonStr); } catch (Exception e) { Log.e(LOG_TAG, e.getMessage(), e); listener.onException(e); } } private void handleApiResponse(TopApiListener listener, String jsonStr) throws JSONException { JSONObject json = new JSONObject(jsonStr); ApiError error = this.parseError(json); if (error != null) {// failed Log.e(LOG_TAG, jsonStr); listener.onError(error); } else { listener.onComplete(json); } } private ApiError parseError(JSONObject json) throws JSONException { JSONObject resp = json.optJSONObject("error_response"); if (resp == null) { return null; } String code = resp.optString("code"); String msg = resp.optString("msg"); String sub_code = resp.optString("sub_code"); String sub_msg = resp.optString("sub_msg"); ApiError error = null; if (!TextUtils.isEmpty(code) || !TextUtils.isEmpty(sub_code)) { error = new ApiError(); error.setErrorCode(code); error.setMsg(msg); error.setSubCode(sub_code); error.setSubMsg(sub_msg); } return error; } private Map<String, String> generateApiParams(TopParameters topParameters, AccessToken token) throws IOException { TreeMap<String, String> params = new TreeMap<String, String>(); params.put("timestamp", String.valueOf(System.currentTimeMillis())); params.put("v", "2.0"); params.put("app_key", appKey); params.put("partner_id", "top-android-sdk"); params.put("format", "json"); if (token != null) { params.put("session", token.getValue()); } params.put("sign_method", "hmac"); params.put("method", topParameters.getMethod()); Map<String, String> map = topParameters.getParams(); if (map != null) { Set<Entry<String, String>> set = map.entrySet(); for (Entry<String, String> entry : set) { params.put(entry.getKey(), entry.getValue()); } } List<String> list = topParameters.getFields(); if (list != null) { String fileds = TextUtils.join(",", list); if (!TextUtils.isEmpty(fileds)) { params.put("fields", fileds); } } String sign = TaobaoUtils.signTopRequestNew(params, appSecret); params.put("sign", sign); return params; } private Map<String, String> getProtocolParams() { String sign = JNIUtils.getTrackId(context, appKey, appSecret); String trackId = sign.substring(0, sign.indexOf("|")); String timestamp = sign.substring(sign.indexOf("|") + 1); Map<String, String> params = new HashMap<String, String>(); params.put(SDK_CLIENT_SYSNAME, SYS_NAME); params.put(SDK_CLIENT_SYSVERSION, android.os.Build.VERSION.RELEASE); params.put(SDK_DEVICE_UUID, TOPUtils.getDeviceId(context)); params.put(SDK_TRACK_ID, trackId); params.put(SDK_TIMESTAMP, timestamp); params.put(SDK_VERSION, JNIUtils.getSDKVersion()); return params; } private void revertAccessToken() { File sessionDir = new File(context.getFilesDir(), SESSION_DIR); if (!sessionDir.exists()) { return; } File[] tokenFiles = sessionDir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { String prefix = new StringBuilder(appKey).append("_") .toString(); return name.startsWith(prefix); } }); if (tokenFiles != null && tokenFiles.length > 0) { for (File tokenFile : tokenFiles) { FileInputStream in = null; ObjectInputStream objIn = null; try { in = new FileInputStream(tokenFile); objIn = new ObjectInputStream(in); AccessToken token = (AccessToken) objIn.readObject(); Long userId = getUserIdFromAccessToken(token); if (userId != null) { tokenStore.put(userId, token); } } catch (Exception e) { // 出现异常先跳过,只记录日志 Log.e(LOG_TAG, e.getMessage(), e); } finally { if (objIn != null) { try { objIn.close(); } catch (IOException e) { Log.e(LOG_TAG, e.getMessage(), e); } } if (in != null) { try { in.close(); } catch (IOException e) { Log.e(LOG_TAG, e.getMessage(), e); } } } } } } private void persistenceAccessToken(String appkey, AccessToken token) throws IOException { if (TextUtils.isEmpty(appkey)) { throw new IllegalArgumentException("appkey must not empty."); } if (token == null) { throw new IllegalArgumentException("token must not null."); } File sessionDir = new File(context.getFilesDir(), SESSION_DIR); if (!sessionDir.exists()) { sessionDir.mkdir(); } Long userId = getUserIdFromAccessToken(token); String fileName = new StringBuilder(appkey).append("_").append(userId) .toString(); File tokenFile = new File(sessionDir, fileName); FileOutputStream output = null; ObjectOutputStream objOutput = null; try { output = new FileOutputStream(tokenFile); objOutput = new ObjectOutputStream(output); objOutput.writeObject(token); objOutput.flush(); } finally { if (output != null) { output.close(); } if (objOutput != null) { objOutput.close(); } } } private Long getUserIdFromAccessToken(AccessToken accessToken) { String idStr = accessToken.getAdditionalInformation().get( AccessToken.KEY_SUB_TAOBAO_USER_ID); Long userId = idStr == null ? null : Long.valueOf(idStr); if (userId == null) { idStr = accessToken.getAdditionalInformation().get( AccessToken.KEY_TAOBAO_USER_ID); userId = idStr == null ? null : Long.parseLong(idStr); } return userId; } public String getAppKey() { return appKey; } public void setAppKey(String appKey) { this.appKey = appKey; } public String getAppSecret() { return appSecret; } public Context getContext() { return context; } public void setContext(Context context) { this.context = context; } public void setAppSecret(String appSecret) { this.appSecret = appSecret; } public String getRedirectURI() { return redirectURI; } public void setRedirectURI(String redirectURI) { this.redirectURI = redirectURI; } public AccessToken getAccessToken(Long userId) { return tokenStore.get(userId); } /** * 保存access token在内存和文件系统中 * @see #getAccessToken(Long) * @param accessToken * @throws IOException */ public void addAccessToken(AccessToken accessToken) throws IOException { Long userId = getUserIdFromAccessToken(accessToken); if (userId == null) { return; } tokenStore.put(userId, accessToken); persistenceAccessToken(appKey, accessToken); } public int getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; } public int getReadTimeout() { return readTimeout; } public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; } public ConcurrentHashMap<Long, AccessToken> getTokenStore() { return tokenStore; } public void setTokenStore(ConcurrentHashMap<Long, AccessToken> tokenStore) { this.tokenStore = tokenStore; } public Env getEnv() { return env; } public void setEnv(Env env) { this.env = env; } /** * client运行环境枚举值 * @author junyan.hj * */ public static enum Env { /** * 生产环境 */ PRODUCTION, /** * 沙箱环境 */ SANDBOX, /** * 淘宝内部测试环境,isv禁止使用 */ DAILY; private static final Map<String, String> URL_CONFIG = new HashMap<String, String>(); static { URL_CONFIG.put("CONTAINER_URL_PRODUCTION", "https://oauth.taobao.com"); URL_CONFIG.put("CONTAINER_URL_SANDBOX", "https://oauth.tbsandbox.com"); URL_CONFIG.put("CONTAINER_URL_DAILY", "https://oauth.daily.taobao.net"); URL_CONFIG.put("TOP_URL_PRODUCTION", "http://gw.api.taobao.com"); URL_CONFIG.put("TOP_URL_SANDBOX", "http://gw.api.tbsandbox.com"); URL_CONFIG.put("TOP_URL_DAILY", "http://10.232.127.144"); } private String getConfigedValue(String keyPrefix) { String key = new StringBuilder(keyPrefix).append(toString()) .toString(); String url = URL_CONFIG.get(key); return url; } public String getApiUrl() { String url = getConfigedValue("TOP_URL_"); return new StringBuilder(url).append("/router/rest").toString(); } public String getTqlUrl() { String url = getConfigedValue("TOP_URL_"); return new StringBuilder(url).append("/tql/2.0/json").toString(); } public String getAuthUrl() { String url = getConfigedValue("CONTAINER_URL_"); return new StringBuilder(url).append( "/authorize?response_type=token&view=wap").toString(); } public String getRefreshUrl() { String url = getConfigedValue("CONTAINER_URL_"); return new StringBuilder(url).append( "/token?grant_type=refresh_token").toString(); } } }