package com.akdeniz.googleplaycrawler; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.Key; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.PublicKey; import java.security.spec.RSAPublicKeySpec; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import javax.crypto.Cipher; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; 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.utils.URLEncodedUtils; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.apache.http.impl.conn.SchemeRegistryFactory; import org.apache.http.message.BasicNameValuePair; import com.akdeniz.googleplaycrawler.GooglePlay.AndroidAppDeliveryData; import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinRequest; import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinResponse; //import com.akdeniz.googleplaycrawler.GooglePlay.BrowseResponse; import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsRequest; import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsRequest.Builder; import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsResponse; import com.akdeniz.googleplaycrawler.GooglePlay.BuyResponse; import com.akdeniz.googleplaycrawler.GooglePlay.DetailsResponse; import com.akdeniz.googleplaycrawler.GooglePlay.ListResponse; import com.akdeniz.googleplaycrawler.GooglePlay.ResponseWrapper; import com.akdeniz.googleplaycrawler.GooglePlay.ReviewResponse; import com.akdeniz.googleplaycrawler.GooglePlay.SearchResponse; import com.akdeniz.googleplaycrawler.GooglePlay.UploadDeviceConfigRequest; import com.akdeniz.googleplaycrawler.GooglePlay.UploadDeviceConfigResponse; import com.akdeniz.googleplaycrawler.misc.Base64; /** * This class provides * <code>checkin, search, details, bulkDetails, browse, list and download</code> * capabilities. It uses <code>Apache Commons HttpClient</code> for POST and GET * requests. * * <p> * <b>XXX : DO NOT call checkin, login and download consecutively. To allow * server to catch up, sleep for a while before download! (5 sec will do!) Also * it is recommended to call checkin once and use generated android-id for * further operations.</b> * </p> * * @author akdeniz * */ public class GooglePlayAPI { private static final String CHECKIN_URL = "https://android.clients.google.com/checkin"; private static final String URL_LOGIN = "https://android.clients.google.com/auth"; private static final String C2DM_REGISTER_URL = "https://android.clients.google.com/c2dm/register2"; private static final String FDFE_URL = "https://android.clients.google.com/fdfe/"; private static final String LIST_URL = FDFE_URL + "list"; private static final String BROWSE_URL = FDFE_URL + "browse"; private static final String DETAILS_URL = FDFE_URL + "details"; private static final String SEARCH_URL = FDFE_URL + "search"; private static final String BULKDETAILS_URL = FDFE_URL + "bulkDetails"; private static final String PURCHASE_URL = FDFE_URL + "purchase"; private static final String REVIEWS_URL = FDFE_URL + "rev"; private static final String UPLOADDEVICECONFIG_URL = FDFE_URL + "uploadDeviceConfig"; private static final String RECOMMENDATIONS_URL = FDFE_URL + "rec"; private static final String DELIVERY_URL = FDFE_URL + "delivery"; private static final String ACCOUNT_TYPE_HOSTED_OR_GOOGLE = "HOSTED_OR_GOOGLE"; public static enum REVIEW_SORT { NEWEST(0), HIGHRATING(1), HELPFUL(2); public int value; private REVIEW_SORT(int value) { this.value = value; } } public static enum RECOMMENDATION_TYPE { ALSO_VIEWED(1), ALSO_INSTALLED(2); public int value; private RECOMMENDATION_TYPE(int value) { this.value = value; } } private String token; private String androidID; private String email; private String password; private HttpClient client; private String securityToken; private String localization; private String useragent; /** * Default constructor. ANDROID ID and Authentication token must be supplied * before any other operation. */ public GooglePlayAPI() { } /** * Constructs a ready to login {@link GooglePlayAPI}. */ public GooglePlayAPI(String email, String password, String androidID) { this(email, password); this.setAndroidID(androidID); } /** * If this constructor is used, Android ID must be generated by calling * <code>checkin()</code> or set by using <code>setAndroidID</code> before * using other abilities. */ public GooglePlayAPI(String email, String password) { this.setEmail(email); this.password = password; setClient(new DefaultHttpClient(getConnectionManager())); // setUseragent("Android-Finsky/3.10.14 (api=3,versionCode=8016014,sdk=15,device=GT-I9300,hardware=aries,product=GT-I9300)"); // setUseragent("Android-Finsky/6.5.08.D-all (versionCode=80650800,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)"); setUseragent("Android-Finsky/13.1.32-all (versionCode=81313200,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)"); } /** * Connection manager to allow concurrent connections. * * @return {@link ClientConnectionManager} instance */ public static ClientConnectionManager getConnectionManager() { PoolingClientConnectionManager connManager = new PoolingClientConnectionManager( SchemeRegistryFactory.createDefault()); connManager.setMaxTotal(100); connManager.setDefaultMaxPerRoute(30); return connManager; } /** * Performs authentication on "ac2dm" service and match up android id, * security token and email by checking them in on this server. * * This function sets check-inded android ID and that can be taken either by * using <code>getToken()</code> or from returned * {@link AndroidCheckinResponse} instance. * */ public AndroidCheckinResponse checkin() throws Exception { // this first checkin is for generating android-id AndroidCheckinResponse checkinResponse = postCheckin(Utils .generateAndroidCheckinRequest().toByteArray()); this.setAndroidID(BigInteger.valueOf(checkinResponse.getGsfId()).toString( 16)); setSecurityToken((BigInteger.valueOf(checkinResponse.getSecurityToken()) .toString(16))); String c2dmAuth = loginAC2DM(); // login(); // String c2dmAuth= getToken(); AndroidCheckinRequest.Builder checkInbuilder = AndroidCheckinRequest .newBuilder(Utils.generateAndroidCheckinRequest()); AndroidCheckinRequest build = checkInbuilder .setId(new BigInteger(this.getAndroidID(), 16).longValue()) .setSecurityToken(new BigInteger(getSecurityToken(), 16).longValue()) .addAccountCookie("[" + getEmail() + "]").addAccountCookie(c2dmAuth) .build(); // this is the second checkin to match credentials with android-id return postCheckin(build.toByteArray()); } private static int readInt(byte[] bArr, int i) { return (((((bArr[i] & 255) << 24) | 0) | ((bArr[i + 1] & 255) << 16)) | ((bArr[i + 2] & 255) << 8)) | (bArr[i + 3] & 255); } public static PublicKey createKeyFromString(String str, byte[] bArr) { try { byte[] decode = Base64.decode(str, 0); int readInt = readInt(decode, 0); byte[] obj = new byte[readInt]; System.arraycopy(decode, 4, obj, 0, readInt); BigInteger bigInteger = new BigInteger(1, obj); int readInt2 = readInt(decode, readInt + 4); byte[] obj2 = new byte[readInt2]; System.arraycopy(decode, readInt + 8, obj2, 0, readInt2); BigInteger bigInteger2 = new BigInteger(1, obj2); decode = MessageDigest.getInstance("SHA-1").digest(decode); bArr[0] = (byte) 0; System.arraycopy(decode, 0, bArr, 1, 4); return KeyFactory.getInstance("RSA").generatePublic( new RSAPublicKeySpec(bigInteger, bigInteger2)); } catch (Throwable e) { throw new RuntimeException(e); } } private static String encryptString(String str) { int i = 0; ResourceBundle bundle = PropertyResourceBundle .getBundle("com.akdeniz.googleplaycrawler.crypt"); String string = bundle.getString("key"); byte[] obj = new byte[5]; Key createKeyFromString = createKeyFromString(string, obj); if (createKeyFromString == null) { return null; } try { Cipher instance = Cipher .getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING"); byte[] bytes = str.getBytes("UTF-8"); int length = ((bytes.length - 1) / 86) + 1; byte[] obj2 = new byte[(length * 133)]; while (i < length) { instance.init(1, createKeyFromString); byte[] doFinal = instance.doFinal(bytes, i * 86, i == length + -1 ? bytes.length - (i * 86) : 86); System.arraycopy(obj, 0, obj2, i * 133, obj.length); System.arraycopy(doFinal, 0, obj2, (i * 133) + obj.length, doFinal.length); i++; } return Base64.encodeToString(obj2, 10); } catch (Throwable e) { throw new RuntimeException(e); } } /** * Logins AC2DM server and returns authentication string. */ public String loginAC2DM() throws IOException { HttpEntity c2dmResponseEntity = executePost(URL_LOGIN, new String[][] { { "Email", this.getEmail() }, { "EncryptedPasswd", encryptString(this.getEmail() + "\u0000" + this.password) }, { "add_account", "1" }, { "service", "ac2dm" }, { "accountType", ACCOUNT_TYPE_HOSTED_OR_GOOGLE }, { "has_permission", "1" }, { "source", "android" }, { "app", "com.google.android.gsf" }, { "device_country", "us" }, { "device_country", "us" }, { "lang", "en" }, { "sdk_version", "17" }, }, null); Map<String, String> c2dmAuth = Utils.parseResponse(new String(Utils .readAll(c2dmResponseEntity.getContent()))); return c2dmAuth.get("Auth"); } public Map<String, String> c2dmRegister(String application, String sender) throws IOException { String c2dmAuth = loginAC2DM(); String[][] data = new String[][] { { "app", application }, { "sender", sender }, { "device", new BigInteger(this.getAndroidID(), 16).toString() } }; HttpEntity responseEntity = executePost(C2DM_REGISTER_URL, data, getHeaderParameters(c2dmAuth, null)); return Utils.parseResponse(new String(Utils.readAll(responseEntity .getContent()))); } /** * Equivalent of <code>setToken</code>. This function does not performs * authentication, it simply sets authentication token. */ public void login(String token) throws Exception { setToken(token); } /** * Authenticates on server with given email and password and sets * authentication token. This token can be used to login instead of using * email and password every time. */ public void login() throws Exception { /* * HttpEntity responseEntity = executePost(URL_LOGIN, new String[][] { { * "Email", this.getEmail() }, { "EncryptedPasswd", * encryptString(this.getEmail()+"\u0000"+this.password) }, { "service", * "androidmarket" }, { "add_account", "1"}, { "accountType", * ACCOUNT_TYPE_HOSTED_OR_GOOGLE }, { "has_permission", "1" }, { "source", * "android" }, { "androidId", this.getAndroidID() }, { "app", * "com.android.vending" }, { "device_country", "en" }, { "lang", "en" }, { * "sdk_version", "17" }, }, null); * * Map<String, String> response = Utils.parseResponse(new * String(Utils.readAll(responseEntity .getContent()))); if * (response.containsKey("Auth")) { setToken(response.get("Auth")); } else { * throw new GooglePlayException("Authentication failed!"); } */ Identity ident = Identity.signIn(getClient(), getEmail(), password); setToken(ident.getAuthToken()); } /** * Equivalent of <code>search(query, null, null)</code> */ public SearchResponse search(String query) throws IOException { return search(query, null, null); } /** * Fetches a search results for given query. Offset and numberOfResult * parameters are optional and <code>null</code> can be passed! */ public SearchResponse search(String query, Integer offset, Integer numberOfResult) throws IOException { ResponseWrapper responseWrapper = executeGETRequest( SEARCH_URL, new String[][] { { "c", "3" }, { "q", query }, { "o", (offset == null) ? null : String.valueOf(offset) }, { "n", (numberOfResult == null) ? null : String .valueOf(numberOfResult) }, }); return responseWrapper.getPayload().getSearchResponse(); } public ResponseWrapper searchApp(String query) throws IOException { ResponseWrapper responseWrapper = executeGETRequest(SEARCH_URL, new String[][] { { "c", "3" }, { "q", query }, }); return responseWrapper; } public ResponseWrapper getList(String url) throws IOException { return executeGETRequest(FDFE_URL+url, null); } /** * Fetches detailed information about passed package name. If it is needed to * fetch information about more than one application, consider to use * <code>bulkDetails</code>. */ public DetailsResponse details(String packageName) throws IOException { ResponseWrapper responseWrapper = executeGETRequest(DETAILS_URL, new String[][] { { "doc", packageName }, }); return responseWrapper.getPayload().getDetailsResponse(); } /** Equivalent of details but bulky one! */ public BulkDetailsResponse bulkDetails(List<String> packageNames) throws IOException { Builder bulkDetailsRequestBuilder = BulkDetailsRequest.newBuilder(); bulkDetailsRequestBuilder.addAllDocid(packageNames).setIncludeDetails(true); ResponseWrapper responseWrapper = executePOSTRequest(BULKDETAILS_URL, bulkDetailsRequestBuilder.build().toByteArray(), "application/x-protobuf"); return responseWrapper.getPayload().getBulkDetailsResponse(); } /** * Fetches available categories * public BrowseResponse browse() throws * IOException { * * return browse(null, null); } * * public BrowseResponse browse(String categoryId, String subCategoryId) * throws IOException { * * ResponseWrapper responseWrapper = executeGETRequest(BROWSE_URL, new * String[][] { { "c", "3" }, { "cat", categoryId }, { "ctr", subCategoryId } * }); * * return responseWrapper.getPayload().getBrowseResponse(); }/* * * /** Equivalent of <code>list(categoryId, null, null, null)</code>. It * fetches sub-categories of given category! */ public ListResponse list(String categoryId) throws IOException { return list(categoryId, null, null, null); } /** * Fetches applications within supplied category and sub-category. If * <code>null</code> is given for sub-category, it fetches sub-categories of * passed category. * * Default values for offset and numberOfResult are "0" and "20" respectively. * These values are determined by Google Play Store. */ public ListResponse list(String categoryId, String subCategoryId, Integer offset, Integer numberOfResult) throws IOException { ResponseWrapper responseWrapper = executeGETRequest( LIST_URL, new String[][] { { "c", "3" }, { "cat", categoryId }, { "ctr", subCategoryId }, { "o", (offset == null) ? null : String.valueOf(offset) }, { "n", (numberOfResult == null) ? null : String .valueOf(numberOfResult) }, }); return responseWrapper.getPayload().getListResponse(); } public ListResponse nextPage(String url) throws IOException { ResponseWrapper responseWrapper = executeGETRequest(FDFE_URL + url, null); return responseWrapper.getPayload().getListResponse(); } /** * Downloads given application package name, version and offer type. Version * code and offer type can be fetch by <code>details</code> interface. **/ public DownloadData download(String packageName, int versionCode, int offerType) throws IOException { BuyResponse buyResponse = purchase(packageName, versionCode, offerType); return new DownloadData(this, buyResponse.getPurchaseStatusResponse() .getAppDeliveryData()); } public DownloadData delivery(String packageName, int versionCode, int offerType) throws IOException { ResponseWrapper responseWrapper = executeGETRequest(DELIVERY_URL, new String[][] { { "ot", String.valueOf(offerType) }, { "doc", packageName }, { "vc", String.valueOf(versionCode) }, }); AndroidAppDeliveryData appDeliveryData = responseWrapper.getPayload() .getDeliveryResponse().getAppDeliveryData(); return new DownloadData(this, appDeliveryData); } public DownloadData purchaseAndDeliver(String packageName, int versionCode, int offerType) throws IOException { BuyResponse buyResponse = purchase(packageName, versionCode, offerType); AndroidAppDeliveryData ada = buyResponse.getPurchaseStatusResponse() .getAppDeliveryData(); if (ada.hasDownloadUrl() && ada.getDownloadAuthCookieCount() > 0) { // This is for backwards compatibility. return new DownloadData(this, ada); } return delivery(packageName, versionCode, offerType); } /** * Posts given check-in request content and returns * {@link AndroidCheckinResponse}. */ private AndroidCheckinResponse postCheckin(byte[] request) throws IOException { HttpEntity httpEntity = executePost(CHECKIN_URL, new ByteArrayEntity( request), new String[][] { { "User-Agent", "Android-Checkin/2.0 (generic JRO03E); gzip" }, { "Host", "android.clients.google.com" }, { "Content-Type", "application/x-protobuffer" } }); return AndroidCheckinResponse.parseFrom(httpEntity.getContent()); } /** * This function is used for fetching download url and donwload cookie, rather * than actual purchasing. */ private BuyResponse purchase(String packageName, int versionCode, int offerType) throws IOException { ResponseWrapper responseWrapper = executePOSTRequest(PURCHASE_URL, new String[][] { { "ot", String.valueOf(offerType) }, { "doc", packageName }, { "vc", String.valueOf(versionCode) }, }); return responseWrapper.getPayload().getBuyResponse(); } /** * Fetches url content by executing GET request with provided cookie string. */ public InputStream executeDownload(String url, String cookie) throws IOException { String[][] headerParams = new String[][] { { "Cookie", cookie }, { "User-Agent", "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)" }, }; HttpEntity httpEntity = executeGet(url, null, headerParams); return httpEntity.getContent(); } /** * Fetches the reviews of given package name by sorting passed choice. * * Default values for offset and numberOfResult are "0" and "20" respectively. * These values are determined by Google Play Store. */ public ReviewResponse reviews(String packageName, REVIEW_SORT sort, Integer offset, Integer numberOfResult) throws IOException { ResponseWrapper responseWrapper = executeGETRequest( REVIEWS_URL, new String[][] { { "doc", packageName }, { "sort", (sort == null) ? null : String.valueOf(sort.value) }, { "o", (offset == null) ? null : String.valueOf(offset) }, { "n", (numberOfResult == null) ? null : String .valueOf(numberOfResult) } }); return responseWrapper.getPayload().getReviewResponse(); } /** * Uploads device configuration to google server so that can be seen from web * as a registered device!! * * @see https://play.google.com/store/account */ public UploadDeviceConfigResponse uploadDeviceConfig() throws Exception { UploadDeviceConfigRequest request = UploadDeviceConfigRequest.newBuilder() .setDeviceConfiguration(Utils.getDeviceConfigurationProto()) .setManufacturer("Samsung").build(); ResponseWrapper responseWrapper = executePOSTRequest( UPLOADDEVICECONFIG_URL, request.toByteArray(), "application/x-protobuf"); return responseWrapper.getPayload().getUploadDeviceConfigResponse(); } /** * Fetches the recommendations of given package name. * * Default values for offset and numberOfResult are "0" and "20" respectively. * These values are determined by Google Play Store. */ public ListResponse recommendations(String packageName, RECOMMENDATION_TYPE type, Integer offset, Integer numberOfResult) throws IOException { ResponseWrapper responseWrapper = executeGETRequest( RECOMMENDATIONS_URL, new String[][] { { "c", "3" }, { "doc", packageName }, { "rt", (type == null) ? null : String.valueOf(type.value) }, { "o", (offset == null) ? null : String.valueOf(offset) }, { "n", (numberOfResult == null) ? null : String .valueOf(numberOfResult) } }); return responseWrapper.getPayload().getListResponse(); } /* =======================Helper Functions====================== */ /** * Executes GET request and returns result as {@link ResponseWrapper}. * Standard header parameters will be used for request. * * @see getHeaderParameters * */ private ResponseWrapper executeGETRequest(String path, String[][] datapost) throws IOException { HttpEntity httpEntity = executeGet(path, datapost, getHeaderParameters(this.getToken(), null)); return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent()); } /** * Executes POST request and returns result as {@link ResponseWrapper}. * Standard header parameters will be used for request. * * @see getHeaderParameters * */ private ResponseWrapper executePOSTRequest(String path, String[][] datapost) throws IOException { HttpEntity httpEntity = executePost(path, datapost, getHeaderParameters(this.getToken(), null)); return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent()); } /** * Executes POST request and returns result as {@link ResponseWrapper}. * Content type can be specified for given byte array. */ private ResponseWrapper executePOSTRequest(String url, byte[] datapost, String contentType) throws IOException { HttpEntity httpEntity = executePost(url, new ByteArrayEntity(datapost), getHeaderParameters(this.getToken(), contentType)); return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent()); } /** * Executes POST request on given URL with POST parameters and header * parameters. */ private HttpEntity executePost(String url, String[][] postParams, String[][] headerParams) throws IOException { List<NameValuePair> formparams = new ArrayList<NameValuePair>(); for (String[] param : postParams) { if (param[0] != null && param[1] != null) { formparams.add(new BasicNameValuePair(param[0], param[1])); } } UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8"); return executePost(url, entity, headerParams); } /** * Executes POST request on given URL with {@link HttpEntity} typed POST * parameters and header parameters. */ private HttpEntity executePost(String url, HttpEntity postData, String[][] headerParams) throws IOException { HttpPost httppost = new HttpPost(url); if (headerParams != null) { for (String[] param : headerParams) { if (param[0] != null && param[1] != null) { httppost.setHeader(param[0], param[1]); } } } httppost.setEntity(postData); return executeHttpRequest(httppost); } /** * Executes GET request on given URL with GET parameters and header * parameters. */ private HttpEntity executeGet(String url, String[][] getParams, String[][] headerParams) throws IOException { if (getParams != null) { List<NameValuePair> formparams = new ArrayList<NameValuePair>(); for (String[] param : getParams) { if (param[0] != null && param[1] != null) { formparams.add(new BasicNameValuePair(param[0], param[1])); } } url = url + "?" + URLEncodedUtils.format(formparams, "UTF-8"); } HttpGet httpget = new HttpGet(url); if (headerParams != null) { for (String[] param : headerParams) { if (param[0] != null && param[1] != null) { httpget.setHeader(param[0], param[1]); } } } return executeHttpRequest(httpget); } /** Executes given GET/POST request */ private HttpEntity executeHttpRequest(HttpUriRequest request) throws ClientProtocolException, IOException { HttpResponse response = getClient().execute(request); if (response.getStatusLine().getStatusCode() != 200) { throw GooglePlayException.create(response); } return response.getEntity(); } /** * Gets header parameters for GET/POST requests. If no content type is given, * default one is used! */ private String[][] getHeaderParameters(String token, String contentType) { return new String[][] { { "Accept-Language", getLocalization() != null ? getLocalization() : "en-EN" }, { "Authorization", "GoogleLogin auth=" + token }, { "X-DFE-Enabled-Experiments", "cl:billing.select_add_instrument_by_default" }, { "X-DFE-Unsupported-Experiments", "nocache:billing.use_charging_poller,market_emails,buyer_currency,prod_baseline,checkin.set_asset_paid_app_field,shekel_test,content_ratings,buyer_currency_in_app,nocache:encrypted_apk,recent_changes" }, { "X-DFE-Device-Id", this.getAndroidID() }, { "X-DFE-Client-Id", "am-android-google" }, { "User-Agent", getUseragent() }, { "X-DFE-SmallestScreenWidthDp", "320" }, { "X-DFE-Filter-Level", "3" }, { "Host", "android.clients.google.com" }, { "Content-Type", (contentType != null) ? contentType : "application/x-www-form-urlencoded; charset=UTF-8" } }; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getAndroidID() { return androidID; } public void setAndroidID(String androidID) { this.androidID = androidID; } public String getSecurityToken() { return securityToken; } public void setSecurityToken(String securityToken) { this.securityToken = securityToken; } public HttpClient getClient() { return client; } /** * Sets {@link HttpClient} instance for internal usage of GooglePlayAPI. It is * important to note that this instance should allow concurrent connections. * * @see getConnectionManager * * @param client */ public void setClient(HttpClient client) { this.client = client; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getLocalization() { return localization; } /** * Localization string that will be used in each request to server. Using this * option you can fetch localized informations such as reviews and * descriptions. * <p> * Note that changing this value has no affect on localized application list * that server provides. It depends on only your IP location. * <p> * * @param localization * can be <b>en-EN, en-US, tr-TR, fr-FR ... (default : en-EN)</b> */ public void setLocalization(String localization) { this.localization = localization; } /** * @return the useragent */ public String getUseragent() { return useragent; } /** * @param useragent * the useragent to set */ public void setUseragent(String useragent) { this.useragent = useragent; } }