package com.jackpf.apkdownloader.Service; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Base64; import com.google.protobuf.ByteString; import com.jackpf.apkdownloader.R; import com.jackpf.apkdownloader.Entity.App; import com.jackpf.apkdownloader.Exception.AuthenticationException; import com.jackpf.apkdownloader.Exception.PlayApiException; import com.jackpf.apkdownloader.Proto.Play; public class PlayApi { /** * Authenticator */ private Authenticator authenticator; /** * Request vars */ private final int SDK_VERSION;; private final String DEVICE_AND_SDK_VERSION; private final String OPERATOR; private final String OPERATOR_NUMERIC; private final String LOCALE; private final String COUNTRY; /** * Request url */ private final String REQUEST_URL = "https://android.clients.google.com/market/api/ApiRequest"; /** * Request version */ private final int REQUEST_VERSION = 2; /** * Constructor * * @param context * @param authenticator */ public PlayApi(Context context, Authenticator authenticator) { this.authenticator = authenticator; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SDK_VERSION = Integer.parseInt(prefs.getString(context.getString(R.string.pref_sdk_version_key), context.getString(R.string.pref_sdk_version_default))); DEVICE_AND_SDK_VERSION = prefs.getString(context.getString(R.string.pref_device_and_sdk_version_key), context.getString(R.string.pref_device_and_sdk_version_default)); OPERATOR = prefs.getString(context.getString(R.string.pref_operator_key), context.getString(R.string.pref_operator_default)); OPERATOR_NUMERIC = prefs.getString(context.getString(R.string.pref_operator_numeric_key), context.getString(R.string.pref_operator_numeric_default)); LOCALE = prefs.getString(context.getString(R.string.pref_locale_key), context.getString(R.string.pref_locale_default)); COUNTRY = prefs.getString(context.getString(R.string.pref_country_key), context.getString(R.string.pref_country_default)); } /** * Get app by package name * * @param packageName * @return * @throws PlayApiException */ public App getApp(String packageName) throws PlayApiException, AuthenticationException { byte[] protoBuf = buildProtoBuf(packageName); try { HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(REQUEST_URL); post.addHeader("Content-Type", "application/x-www-form-urlencoded"); post.setEntity( new StringEntity(String.format( "version=%d&request=%s", REQUEST_VERSION, Base64.encodeToString(protoBuf, Base64.DEFAULT) )) ); HttpResponse response = client.execute(post); if (response.getStatusLine().getStatusCode() != 200) { throw new PlayApiException(String.format("Server responded with status code %d", response.getStatusLine().getStatusCode())); } byte[] bin = EntityUtils.toByteArray(response.getEntity()); ByteArrayInputStream bais = new ByteArrayInputStream(bin); GZIPInputStream gzis = new GZIPInputStream(bais); InputStreamReader reader = new InputStreamReader(gzis); BufferedReader in = new BufferedReader(reader); String line; StringBuilder sb = new StringBuilder(); while ((line = in.readLine()) != null) { sb.append(line); } return new App(packageName, extractDownloadPath(sb.toString()), extractMarketDA(sb.toString())); } catch (Exception e) { throw new PlayApiException(e.getMessage()); } } /** * Manually build a protobuf request * * @param packageName * @return */ private byte[] buildProtoBuf(String packageName) throws AuthenticationException { // Generate byte array from the auth subtoken byte[] authTokenBytes = authenticator.getToken().getBytes(), authExtraBytes = {16, 1}, // Not sure why these have to be appended, but hey ho authBytes = new byte[authTokenBytes.length + authExtraBytes.length] ; System.arraycopy(authTokenBytes, 0, authBytes, 0, authTokenBytes.length); System.arraycopy(authExtraBytes, 0, authBytes, authTokenBytes.length, authExtraBytes.length); // Build a (mostly correct) request protobuf Play.RequestContext proto = Play.RequestContext.newBuilder().addApp( Play.RequestContext.newBuilder().addAppBuilder() .setAuthSubToken(ByteString.copyFrom(authBytes)) .setVersion(SDK_VERSION) .setAndroidId(authenticator.getGsfId()) .setDeviceAndSdkVersion(DEVICE_AND_SDK_VERSION) .setUserLanguage(LOCALE) .setUserCountry(COUNTRY) .setOperatorAlpha(OPERATOR) .setSimOperatorAlpha(OPERATOR) .setOperatorNumeric(OPERATOR_NUMERIC) .setSimOperatorNumeric(OPERATOR_NUMERIC) ).build(); byte partialProtoBytes[] = proto.toByteArray(); partialProtoBytes[4] -= 2; ArrayList<Byte> packageNameByteList = new ArrayList<Byte>(); packageNameByteList.add((byte) 19); packageNameByteList.add((byte) 82); packageNameByteList.add((byte) (packageName.length() + 2)); packageNameByteList.add((byte) 10); packageNameByteList.add((byte) packageName.length()); for (int i = 0; i < packageName.length(); i++) { packageNameByteList.add((byte) packageName.charAt(i)); } packageNameByteList.add((byte) 20); byte[] packageNameBytes = new byte[packageNameByteList.size()]; for (int i = 0; i < packageNameByteList.size(); i++) { packageNameBytes[i] = packageNameByteList.get(i); } byte protoBytes[] = new byte[partialProtoBytes.length + packageNameBytes.length]; System.arraycopy(partialProtoBytes, 0, protoBytes, 0, partialProtoBytes.length); System.arraycopy(packageNameBytes, 0, protoBytes, partialProtoBytes.length, packageNameBytes.length); return protoBytes; } /** * Manually extract the download path from the protobuf response * * @param str * @return * @throws PlayApiException */ private String extractDownloadPath(String str) throws PlayApiException { Pattern p = Pattern.compile("(?i)https?://[^:]+"); Matcher m = p.matcher(str); if (!m.find()) { throw new PlayApiException("App not found"); } return m.group(0); } /** * Manually extract the market da from the protobuf response * * @param str * @return */ private String extractMarketDA(String str) { boolean capture = false; StringBuilder sb = new StringBuilder(); for (int i = str.lastIndexOf(0x014); i < str.length(); i++) { byte b = (byte) str.charAt(i); if (b == 0x014) { capture = true; } else if (capture && b == 0x0c) { break; } else if (capture) { sb.append(str.charAt(i)); } } return sb.toString(); } }