content: package com.akdeniz.googleplaycrawler;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.math.BigInteger;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  
  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.HttpCookie;
  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;
  
  /**
   * 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 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;
  
      /**
       * 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()));
      }
  
      /**
       * 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.getAndroidId()).toString(16));
  	setSecurityToken((BigInteger.valueOf(checkinResponse.getSecurityToken()).toString(16)));
  
  	String c2dmAuth = loginAC2DM();
  
  	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());
      }
      
      /**
       * Logins AC2DM server and returns authentication string.
       *
       * <p>
       * client_sig is SHA1 digest of encoded certificate on 
       * <i>GoogleLoginService(package name : com.google.android.gsf)</i> system APK. 
       * But google doesn't seem to care of value of this parameter. 
       */
      public String loginAC2DM() throws IOException{
  	HttpEntity c2dmResponseEntity = executePost(URL_LOGIN, new String[][] { { "Email", this.getEmail() },
  		{ "Passwd", this.password }, { "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", "16" }, { "client_sig", "38918a453d07199354f8b19af05ec6562ced5788" }, }, 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() }, { "Passwd", this.password },
  		{ "service", "androidmarket" }, { "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", "16" }, { "client_sig", "38918a453d07199354f8b19af05ec6562ced5788" }, }, 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!");
  	}
      }
  
      /**
       * 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();
      }
  
      /**
       * 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);
  
  	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();
      }
  
      /**
       * Downloads given application package name, version and offer type. Version
       * code and offer type can be fetch by <code>details</code> interface.
       **/
      public InputStream download(String packageName, int versionCode, int offerType) throws IOException {
  
  	BuyResponse buyResponse = purchase(packageName, versionCode, offerType);
  
  	AndroidAppDeliveryData appDeliveryData = buyResponse.getPurchaseStatusResponse().getAppDeliveryData();
  
  	String downloadUrl = appDeliveryData.getDownloadUrl();
  	HttpCookie downloadAuthCookie = appDeliveryData.getDownloadAuthCookie(0);
  
  	return executeDownload(downloadUrl, downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue());
  
      }
  
      /**
       * 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()).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 new GooglePlayException(new String(Utils.readAll(response.getEntity().getContent())));
  	}
  
  	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",
  			"Android-Finsky/3.10.14 (api=3,versionCode=8016014,sdk=15,device=GT-I9300,hardware=aries,product=GT-I9300)" },
  		{ "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;
  	}
  
  }
  