package com.gc.android.market.api; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.StringTokenizer; import java.util.Vector; import java.util.zip.GZIPInputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import com.gc.android.market.api.model.Market.AppsRequest; import com.gc.android.market.api.model.Market.AppsResponse; import com.gc.android.market.api.model.Market.CategoriesRequest; import com.gc.android.market.api.model.Market.CategoriesResponse; import com.gc.android.market.api.model.Market.CommentsRequest; import com.gc.android.market.api.model.Market.CommentsResponse; import com.gc.android.market.api.model.Market.GetImageRequest; import com.gc.android.market.api.model.Market.GetImageResponse; import com.gc.android.market.api.model.Market.Request; import com.gc.android.market.api.model.Market.Request.RequestGroup; import com.gc.android.market.api.model.Market.RequestContext; import com.gc.android.market.api.model.Market.Response; import com.gc.android.market.api.model.Market.Response.ResponseGroup; import com.gc.android.market.api.model.Market.ResponseContext; import com.gc.android.market.api.model.Market.GetAssetRequest; import com.gc.android.market.api.model.Market.GetAssetResponse;; /** * MarketSession session = new MarketSession(); * session.login(login,password, androidId); * For asyncronous calls use append, callback and flush * session.append(xxx,yyy); * session.append(xxx,yyy); * ... * session.flush(); * For syncronous call, use the specific method */ public class MarketSession { public static interface Callback<T> { public void onResult(ResponseContext context, T response); } /* * SERVICE : Service required to the market. * Default value: android. This service must be used to query info to the Market * androidsecure: This service must be used to download apps * sierra (checkout): This service must be used for checkout (at moment unused) */ public String SERVICE = "android"; private static final String URL_LOGIN = "https://www.google.com/accounts/ClientLogin"; public static final String ACCOUNT_TYPE_GOOGLE = "GOOGLE"; public static final String ACCOUNT_TYPE_HOSTED = "HOSTED"; public static final String ACCOUNT_TYPE_HOSTED_OR_GOOGLE = "HOSTED_OR_GOOGLE"; public static final int PROTOCOL_VERSION = 2; Request.Builder request = Request.newBuilder(); RequestContext.Builder context = RequestContext.newBuilder(); public RequestContext.Builder getContext() { return context; } List<Callback<?>> callbacks = new Vector<Callback<?>>(); String authSubToken = null; public String getAuthSubToken() { return authSubToken; } /* * Login must set isSecure to false for list and download */ public MarketSession(Boolean isSecure) { if (isSecure) SERVICE = "androidsecure"; else SERVICE = "android"; context.setIsSecure(false); context.setVersion(2009011); setLocale(Locale.getDefault()); context.setDeviceAndSdkVersion("passion:9"); setOperatorTMobile(); } public void setLocale(Locale locale) { context.setUserLanguage(locale.getLanguage().toLowerCase()); context.setUserCountry(locale.getCountry().toLowerCase()); } public void setOperator(String alpha, String numeric) { setOperator(alpha, alpha, numeric, numeric); } public void setOperatorTMobile() { setOperator("T-Mobile", "310260"); } public void setOperatorSFR() { setOperator("F SFR", "20810"); } public void setOperatorO2() { setOperator("o2 - de", "26207"); } public void setOperatorSimyo() { setOperator("E-Plus", "simyo", "26203", "26203"); } public void setOperatorSunrise() { setOperator("sunrise", "22802"); } /** * http://www.2030.tk/wiki/Android_market_switch */ public void setOperator(String alpha, String simAlpha, String numeric, String simNumeric) { context.setOperatorAlpha(alpha); context.setSimOperatorAlpha(simAlpha); context.setOperatorNumeric(numeric); context.setSimOperatorNumeric(simNumeric); } public void setAuthSubToken(String authSubToken) { context.setAuthSubToken(authSubToken); this.authSubToken = authSubToken; } public void setIsSecure(Boolean isSecure) { context.setIsSecure(isSecure); } public void setAndroidId(String androidId) { context.setAndroidId(androidId); } public void login(String email, String password, String androidId) { this.login(email, password, androidId, ACCOUNT_TYPE_HOSTED_OR_GOOGLE); } public void login(String email, String password, String androidId, String accountType) { //Android ID must an unique identifier associated to the account //used in in the login setAndroidId(androidId); Map<String,String> params = new LinkedHashMap<String,String>(); params.put("Email", email); params.put("Passwd", password); params.put("service", SERVICE); // params.put("source", source); params.put("accountType", accountType); // Login at Google.com try { String data = Tools.postUrl(URL_LOGIN, params); StringTokenizer st = new StringTokenizer(data, "\n\r="); String authKey = null; while (st.hasMoreTokens()) { if (st.nextToken().equalsIgnoreCase("Auth")) { authKey = st.nextToken(); break; } } if(authKey == null) throw new RuntimeException("authKey not found in "+ data); setAuthSubToken(authKey); } catch(Tools.HttpException httpEx) { if(httpEx.getErrorCode() != 403) throw httpEx; String data = httpEx.getErrorData(); StringTokenizer st = new StringTokenizer(data, "\n\r="); String googleErrorCode = null; while (st.hasMoreTokens()) { if (st.nextToken().equalsIgnoreCase("Error")) { googleErrorCode = st.nextToken(); break; } } if(googleErrorCode == null) throw httpEx; throw new LoginException(googleErrorCode); } catch (IOException ex) { throw new RuntimeException(ex); } } public List<Object> queryApp(AppsRequest requestGroup) { List<Object> retList = new ArrayList<Object>(); request.addRequestGroup(RequestGroup.newBuilder().setAppsRequest(requestGroup)); RequestContext ctxt = context.build(); context = RequestContext.newBuilder(ctxt); request.setContext(ctxt); try { Response resp = executeProtobuf(request.build()); for(ResponseGroup grp : resp.getResponseGroupList()) { if(grp.hasAppsResponse()) retList.add(grp.getAppsResponse()); } } finally { request = Request.newBuilder(); } return retList; } public CategoriesResponse queryCategories() { RequestContext ctxt = context.build(); context = RequestContext.newBuilder(ctxt); request.setContext(ctxt); CategoriesResponse categoriesResponse = null; try { Response response = executeProtobuf(request.addRequestGroup( RequestGroup.newBuilder().setCategoriesRequest( CategoriesRequest.newBuilder().build())).setContext(ctxt).build()); categoriesResponse = response.getResponseGroup(0).getCategoriesResponse(); } finally { request = Request.newBuilder();; } return categoriesResponse; } public GetAssetResponse queryGetAssetRequest(String assetId){ setIsSecure(true); RequestContext ctxt = context.build(); context = RequestContext.newBuilder(ctxt); request.setContext(ctxt); GetAssetResponse assetResponse = null; try { Response response = executeProtobuf(request.addRequestGroup( RequestGroup.newBuilder().setGetAssetRequest( GetAssetRequest.newBuilder().setAssetId( assetId).build())).setContext(ctxt).build()); assetResponse = response.getResponseGroup(0).getGetAssetResponse(); } finally { setIsSecure(false); request = Request.newBuilder(); } return assetResponse; } public void append(AppsRequest requestGroup, Callback<AppsResponse> responseCallback) { request.addRequestGroup(RequestGroup.newBuilder().setAppsRequest(requestGroup)); callbacks.add(responseCallback); } public void append(GetImageRequest requestGroup, Callback<GetImageResponse> responseCallback) { request.addRequestGroup(RequestGroup.newBuilder().setImageRequest(requestGroup)); callbacks.add(responseCallback); } public void append(CommentsRequest requestGroup, Callback<CommentsResponse> responseCallback) { request.addRequestGroup(RequestGroup.newBuilder().setCommentsRequest(requestGroup)); callbacks.add(responseCallback); } public void append(CategoriesRequest requestGroup, Callback<CategoriesResponse> responseCallback) { request.addRequestGroup(RequestGroup.newBuilder().setCategoriesRequest(requestGroup)); callbacks.add(responseCallback); } @SuppressWarnings("unchecked") public void flush() { RequestContext ctxt = context.build(); context = RequestContext.newBuilder(ctxt); request.setContext(ctxt); try { Response resp = executeProtobuf(request.build()); int i = 0; for(ResponseGroup grp : resp.getResponseGroupList()) { Object val = null; if(grp.hasAppsResponse()) val = grp.getAppsResponse(); if(grp.hasCategoriesResponse()) val = grp.getCategoriesResponse(); if(grp.hasCommentsResponse()) val = grp.getCommentsResponse(); if(grp.hasImageResponse()) val = grp.getImageResponse(); ((Callback)callbacks.get(i)).onResult(grp.getContext(), val); i++; } } finally { request = Request.newBuilder(); callbacks.clear(); } } public ResponseGroup execute(RequestGroup requestGroup) { RequestContext ctxt = context.build(); context = RequestContext.newBuilder(ctxt); request.setContext(ctxt); Response resp = executeProtobuf(request.addRequestGroup(requestGroup).setContext(ctxt).build()); return resp.getResponseGroup(0); } private Response executeProtobuf(Request request) { byte[] requestBytes = request.toByteArray(); byte[] responseBytes = null; try { if (!context.getIsSecure()) responseBytes = executeRawHttpQuery(requestBytes); else responseBytes = executeRawHttpsQuery(requestBytes); Response r = Response.parseFrom(responseBytes); return r; } catch(Exception ex) { throw new RuntimeException(ex); } } private byte[] executeRawHttpQuery(byte[] request) { try { URL url = new URL("http://android.clients.google.com/market/api/ApiRequest"); HttpURLConnection cnx = (HttpURLConnection)url.openConnection(); cnx.setDoOutput(true); cnx.setRequestMethod("POST"); cnx.setRequestProperty("Cookie","ANDROID="+authSubToken); cnx.setRequestProperty("User-Agent", "Android-Market/2 (sapphire PLAT-RC33); gzip"); cnx.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); cnx.setRequestProperty("Accept-Charset","ISO-8859-1,utf-8;q=0.7,*;q=0.7"); String request64 = Base64.encodeBytes(request,Base64.URL_SAFE); String requestData = "version="+PROTOCOL_VERSION+"&request="+request64; cnx.setFixedLengthStreamingMode(requestData.getBytes("UTF-8").length); OutputStream os = cnx.getOutputStream(); os.write(requestData.getBytes()); os.close(); if(cnx.getResponseCode() >= 400) { throw new RuntimeException("Response code = " + cnx.getResponseCode() + ", msg = " + cnx.getResponseMessage()); } InputStream is = cnx.getInputStream(); GZIPInputStream gzIs = new GZIPInputStream(is); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; while(true) { int nb = gzIs.read(buff); if(nb < 0) break; bos.write(buff,0,nb); } is.close(); cnx.disconnect(); return bos.toByteArray(); } catch(Exception ex) { throw new RuntimeException(ex); } } private Boolean trustAll() { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } } }; try { SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HttpsURLConnection.setDefaultHostnameVerifier( new HostnameVerifier() { public boolean verify(String arg0, SSLSession arg1) { return true; } } ); return true; } catch (Exception e) { return false; } } private byte[] executeRawHttpsQuery(byte[] request){ if (request == null) return null; if (!trustAll()) return null; try { URL url = new URL("https://android.clients.google.com/market/api/ApiRequest"); HttpsURLConnection cnx = (HttpsURLConnection)url.openConnection(); cnx.setDoOutput(true); cnx.setRequestMethod("POST"); cnx.setRequestProperty("Cookie","ANDROIDSECURE=" + this.getAuthSubToken()); cnx.setRequestProperty("User-Agent", "Android-Market/2 (sapphire PLAT-RC33); gzip"); cnx.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); cnx.setRequestProperty("Accept-Charset","ISO-8859-1,utf-8;q=0.7,*;q=0.7"); String request64 = Base64.encodeBytes(request,Base64.URL_SAFE); String requestData = "version="+PROTOCOL_VERSION+"&request="+request64; cnx.setFixedLengthStreamingMode(requestData.getBytes("UTF-8").length); OutputStream os = cnx.getOutputStream(); os.write(requestData.getBytes()); os.close(); if(cnx.getResponseCode() >= 400) { cnx.disconnect(); throw new IOException("Response code = " + cnx.getResponseCode() + ", msg = " + cnx.getResponseMessage()); } InputStream is = cnx.getInputStream(); GZIPInputStream gzIs = new GZIPInputStream(is); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; while(true) { int nb = gzIs.read(buff); if(nb < 0) break; bos.write(buff,0,nb); } is.close(); cnx.disconnect(); return bos.toByteArray(); } catch(Exception ex) { throw new RuntimeException(ex); } } }