Package com.akdeniz.googleplaycrawler

Source Code of com.akdeniz.googleplaycrawler.GooglePlayAPI

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;
  }

}
TOP

Related Classes of com.akdeniz.googleplaycrawler.GooglePlayAPI

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.