diff --git a/.gitignore b/.gitignore index f6b286ce..e3f7aca7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.apk *.ap_ -# Files for the ART/Dalvik VM +# Files for the Dalvik VM *.dex # Java class files @@ -11,11 +11,11 @@ # Generated files bin/ gen/ -out/ # Gradle files .gradle/ build/ +/*/build/ # Local configuration file (sdk path, etc) local.properties @@ -26,15 +26,10 @@ proguard/ # Log Files *.log -# Android Studio Navigation editor temp files -.navigation/ - -# Android Studio captures folder -captures/ - -# Intellij +# From .gitignore generated by Android Studio *.iml -.idea/workspace.xml - -# Keystore files -*.jks +.DS_Store +/captures +/.idea +/gradle* +/.gradle* diff --git a/README.md b/README.md index 43a20364..7f25e89c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ -# YalpStore -Download apks from Google Play Store +# Yalp Store + +## What does it do? +Yalp Store lets you download apps from Google Play Store **as apk files**. It searches for **updates** of installed apps when it starts and lets you **search** for other apps. Thats it. Yalp saves downloaded apks to your default download folder so you can later open it in your favorite file manager app and tap each one to install the apps. + +## Why would I use it? +If you are content with Google Play Store app, you will not need this app. + +The point of Yalp Store is to be small and independent from Google Services Framework. As time passed, Google Services Framework and Google Play Store apps grew in size, which made them almost too big for old phones (Nexus One has 150Mb memory available for apps, half of it would be taken by Google apps). Another reason to use Yalp Store is if you frequently flash experimental ROMs. This often breaks gapps and even prevents their reinstallation. In this situation Yalp will still work. + +## How does it work? +Yalp Store uses the same (protobuf) API the android Play Store app uses. You are going to need a google account to use it. Please, keep in mind that technically **Yalp Store violates** [Android Market Terms of Service](https://www.google.com/intl/en_us/mobile/android/market-tos.html) (§3.3). In theory, you might get your account disabled by using Yalp Store. Thats why you might want to register a separate gmail account and use it at least once to log in to the Play Store android app on any device. + +In practice, though, software like Yalp, Google Play Crawler and Raccoon has been used for years and it seems to be safe. + +Yalp Store is derived from the following projects: +* https://github.com/Akdeniz/google-play-crawler +* https://github.com/onyxbits/Raccoon \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..7fa51066 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,70 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.google.protobuf' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.1" + + defaultConfig { + applicationId "com.github.yeriomin.yalpstore" + minSdkVersion 10 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + disable 'GoogleAppIndexingWarning','GoogleAppIndexingApiWarning' + } + useLibrary 'org.apache.http.legacy' + sourceSets { + debug { + java { + srcDirs 'src/main/java', "${buildDir}/generated/source/proto/debug/javalite" + } + } + release { + java { + srcDirs 'src/main/java', "${buildDir}/generated/source/proto/release/javalite" + } + } + main { + proto { + srcDir 'src/main/proto' + } + } + } +} + +dependencies { + compile group: 'com.google.protobuf', name: 'protobuf-lite', version: '3.0.0' +} + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.0.0' + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.0.1' + } + javalite { + artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' + } + } + generateProtoTasks { + all()*.plugins { + javalite { } + } + ofNonTest()*.plugins { + grpc { + option 'lite' + } + } + } +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..0d5027ad --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in F:\android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ae92baf9 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/DeviceInfoProvider.java b/app/src/main/java/com/github/yeriomin/playstoreapi/DeviceInfoProvider.java new file mode 100644 index 00000000..661d1342 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/DeviceInfoProvider.java @@ -0,0 +1,9 @@ +package com.github.yeriomin.playstoreapi; + +public interface DeviceInfoProvider { + + AndroidCheckinRequest generateAndroidCheckinRequest(); + DeviceConfigurationProto getDeviceConfigurationProto(); + String getUserAgentString(); + int getSdkVersion(); +} diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/GooglePlayAPI.java b/app/src/main/java/com/github/yeriomin/playstoreapi/GooglePlayAPI.java new file mode 100644 index 00000000..6e501121 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/GooglePlayAPI.java @@ -0,0 +1,436 @@ +package com.github.yeriomin.playstoreapi; + +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnPerRouteBean; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * This class provides + * checkin, search, details, bulkDetails, browse, list and download + * capabilities. It uses Apache Commons HttpClient for POST and GET + * requests. + *

+ *

+ * 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. + *

+ * + * @author akdeniz + */ +public class GooglePlayAPI { + + private static final String SCHEME = "https://"; + private static final String HOST = "android.clients.google.com"; + private static final String CHECKIN_URL = SCHEME + HOST + "/checkin"; + private static final String URL_LOGIN = SCHEME + HOST + "/auth"; + private static final String C2DM_REGISTER_URL = SCHEME + HOST + "/c2dm/register2"; + private static final String FDFE_URL = SCHEME + HOST + "/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 enum REVIEW_SORT { + NEWEST(0), HIGHRATING(1), HELPFUL(2); + + public int value; + + REVIEW_SORT(int value) { + this.value = value; + } + } + + public enum RECOMMENDATION_TYPE { + ALSO_VIEWED(1), ALSO_INSTALLED(2); + + public int value; + + RECOMMENDATION_TYPE(int value) { + this.value = value; + } + } + + private String token; + private String gsfId; + private String email; + private String password; + private ThrottledHttpClient client; + private Locale locale; + private DeviceInfoProvider deviceInfoProvider; + private Map searchNextPage = new HashMap<>(); + + public void setToken(String token) { + this.token = token; + } + + public void setGsfId(String gsfId) { + this.gsfId = gsfId; + } + + public void setDeviceInfoProvider(DeviceInfoProvider deviceInfoProvider) { + this.deviceInfoProvider = deviceInfoProvider; + } + + private ThrottledHttpClient getClient() { + if (this.client == null) { + HttpParams httpParams = new BasicHttpParams(); + ConnManagerParams.setTimeout(httpParams, 3000); + ConnManagerParams.setMaxTotalConnections(httpParams, 100); + ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRouteBean(30)); + this.client = new ThrottledHttpClient(httpParams); + } + return this.client; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * If this constructor is used, Android ID must be generated by calling + * checkin() or set by using setGsfId before + * using other abilities. + */ + public GooglePlayAPI(String email, String password) { + this.email = email; + this.password = password; + } + + /** + * 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 getToken() or from returned + * {@link AndroidCheckinResponse} instance. + */ + public String getGsfId() throws IOException { + AndroidCheckinRequest request = this.deviceInfoProvider.generateAndroidCheckinRequest(); + + // this first checkin is for generating android-id + AndroidCheckinResponse checkinResponse = checkin(request.toByteArray()); + this.gsfId = BigInteger.valueOf(checkinResponse.getAndroidId()).toString(16); + String securityToken = BigInteger.valueOf(checkinResponse.getSecurityToken()).toString(16); + + AndroidCheckinRequest.Builder checkInbuilder = AndroidCheckinRequest.newBuilder(request); + String AC2DMToken = getAC2DMToken(); + AndroidCheckinRequest build = checkInbuilder + .setId(new BigInteger(this.gsfId, 16).longValue()) + .setSecurityToken(new BigInteger(securityToken, 16).longValue()) + .addAccountCookie("[" + this.email + "]") + .addAccountCookie(AC2DMToken) + .build(); + // this is the second checkin to match credentials with android-id + checkin(build.toByteArray()); + return this.gsfId; + } + + /** + * Posts given check-in request content and returns + * {@link AndroidCheckinResponse}. + */ + private AndroidCheckinResponse checkin(byte[] request) throws IOException { + Map headers = getDefaultHeaders(); + headers.put("Content-Type", "application/x-protobuffer"); + byte[] content = getClient().post(CHECKIN_URL, new ByteArrayEntity(request), headers); + return AndroidCheckinResponse.parseFrom(content); + } + + /** + * 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 String getToken() throws IOException { + Map params = getDefaultLoginParams(); + params.put("service", "androidmarket"); + params.put("app", "com.android.vending"); + params.put("androidId", this.getGsfId()); + byte[] responseBytes = getClient().post(URL_LOGIN, params, getDefaultHeaders()); + Map response = parseResponse(new String(responseBytes)); + if (response.containsKey("Auth")) { + return response.get("Auth"); + } else { + throw new GooglePlayException("Authentication failed! (login)"); + } + } + + /** + * Logins AC2DM server and returns authentication string. + *

+ *

+ * client_sig is SHA1 digest of encoded certificate on + * GoogleLoginService(package name : com.google.android.gsf) system APK. + * But google doesn't seem to care of value of this parameter. + */ + public String getAC2DMToken() throws IOException { + Map params = getDefaultLoginParams(); + params.put("service", "ac2dm"); + params.put("add_account", "1"); + params.put("app", "com.google.android.gsf"); + byte[] responseBytes = getClient().post(URL_LOGIN, params, getDefaultHeaders()); + Map response = parseResponse(new String(responseBytes)); + if (response.containsKey("Auth")) { + return response.get("Auth"); + } else { + throw new GooglePlayException("Authentication failed! (loginAC2DM)"); + } + } + + public Map c2dmRegister(String application, String sender) throws IOException { + Map params = new HashMap<>(); + params.put("app", application); + params.put("sender", sender); + params.put("device", new BigInteger(this.getGsfId(), 16).toString()); + Map headers = getDefaultHeaders(); + headers.put("Authorization", "GoogleLogin auth=" + getAC2DMToken()); + byte[] responseBytes = getClient().post(C2DM_REGISTER_URL, params, headers); + return parseResponse(new String(responseBytes)); + } + + /** + * Equivalent of search(query, null, null) + */ + public SearchResponse search(String query) throws IOException { + return search(query, null, null); + } + + /** + * Fetches a search results for given query. Offset and numberOfResults + * parameters are optional and null can be passed! + * + * Warning! offset and numberOfResults do not seem to work anymore. + * The api always returns first 30 results. Fetching further results is done through + * nextPageUrl returned with the search result. + */ + public SearchResponse search(String query, Integer offset, Integer numberOfResults) throws IOException { + String url = SEARCH_URL; + Map params = new HashMap<>(); + if (this.searchNextPage.containsKey(query)) { + url = this.searchNextPage.get(query); + if (null == url) { + throw new GooglePlayException("No more results for query " + query); + } + } else { + params = getDefaultGetParams(offset, numberOfResults); + params.put("q", query); + } + + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().get(url, params, getDefaultHeaders())); + SearchResponse response = responseWrapper.getPayload().getSearchResponse(); + if (response.getDocCount() > 0 + && response.getDocList().get(0).hasContainerMetadata() + && response.getDocList().get(0).getContainerMetadata().hasNextPageUrl() + ) { + this.searchNextPage.put(query, FDFE_URL + response.getDocList().get(0).getContainerMetadata().getNextPageUrl()); + } else { + this.searchNextPage.put(query, null); + } + return responseWrapper.getPayload().getSearchResponse(); + } + + public boolean hasNextSearchPage(String query) { + return !this.searchNextPage.containsKey(query) || this.searchNextPage.get(query) != null; + } + + /** + * Fetches detailed information about passed package name. If it is needed + * to fetch information about more than one application, consider to use + * bulkDetails. + */ + public DetailsResponse details(String packageName) throws IOException { + Map params = new HashMap<>(); + params.put("doc", packageName); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().get(DETAILS_URL, params, getDefaultHeaders())); + return responseWrapper.getPayload().getDetailsResponse(); + } + + /** + * Equivalent of details but bulky one! + */ + public BulkDetailsResponse bulkDetails(List packageNames) throws IOException { + BulkDetailsRequest.Builder bulkDetailsRequestBuilder = BulkDetailsRequest.newBuilder(); + bulkDetailsRequestBuilder.addAllDocid(packageNames); + byte[] request = bulkDetailsRequestBuilder.build().toByteArray(); + byte[] content = getClient().post(BULKDETAILS_URL, new ByteArrayEntity(request), getDefaultHeaders()); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(content); + 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 { + Map params = getDefaultGetParams(null, null); + params.put("cat", categoryId); + params.put("ctr", subCategoryId); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().get(BROWSE_URL, params, getDefaultHeaders())); + return responseWrapper.getPayload().getBrowseResponse(); + } + + /** + * Equivalent of list(categoryId, null, null, null). 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 + * null 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 numberOfResults) throws IOException { + Map params = getDefaultGetParams(offset, numberOfResults); + params.put("cat", categoryId); + params.put("ctr", subCategoryId); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().get(LIST_URL, params, getDefaultHeaders())); + return responseWrapper.getPayload().getListResponse(); + } + + /** + * This function is used for fetching download url and download cookie, + * rather than actual purchasing. + */ + public BuyResponse purchase(String packageName, int versionCode, int offerType) throws IOException { + Map params = new HashMap<>(); + params.put("ot", String.valueOf(offerType)); + params.put("doc", packageName); + params.put("vc", String.valueOf(versionCode)); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().post(PURCHASE_URL, params, getDefaultHeaders())); + return responseWrapper.getPayload().getBuyResponse(); + } + + /** + * 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 numberOfResults) throws IOException { + Map params = getDefaultGetParams(offset, numberOfResults); + params.put("doc", packageName); + params.put("sort", (sort == null) ? null : String.valueOf(sort.value)); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().get(REVIEWS_URL, params, getDefaultHeaders())); + return responseWrapper.getPayload().getReviewResponse(); + } + + /** + * Uploads device configuration to google server so that can be seen from + * web as a registered device!! + */ + public UploadDeviceConfigResponse uploadDeviceConfig() throws IOException { + UploadDeviceConfigRequest request = UploadDeviceConfigRequest.newBuilder() + .setDeviceConfiguration(this.deviceInfoProvider.getDeviceConfigurationProto()) + .build(); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().post(UPLOADDEVICECONFIG_URL, new ByteArrayEntity(request.toByteArray()), getDefaultHeaders())); + 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 numberOfResults) throws IOException { + Map params = getDefaultGetParams(offset, numberOfResults); + params.put("doc", packageName); + params.put("rt", (type == null) ? null : String.valueOf(type.value)); + ResponseWrapper responseWrapper = ResponseWrapper.parseFrom(getClient().get(RECOMMENDATIONS_URL, params, getDefaultHeaders())); + return responseWrapper.getPayload().getListResponse(); + } + + /** + * login methods use this + * Most likely not all of these are required, but the Market app sends them, so we will too + * + */ + private Map getDefaultLoginParams() { + Map params = new HashMap<>(); + params.put("Email", this.email); + params.put("Passwd", this.password); + params.put("accountType", ACCOUNT_TYPE_HOSTED_OR_GOOGLE); + params.put("has_permission", "1"); + params.put("source", "android"); + params.put("device_country", this.locale.getCountry().toLowerCase()); + params.put("lang", this.locale.getLanguage().toLowerCase()); + params.put("sdk_version", String.valueOf(this.deviceInfoProvider.getSdkVersion())); + params.put("client_sig", "38918a453d07199354f8b19af05ec6562ced5788"); + return params; + } + + /** + * Using Accept-Language you can fetch localized informations such as reviews and descriptions. + * Note that changing this value has no affect on localized application list that + * server provides. It depends on only your IP location. + * + */ + private Map getDefaultHeaders() { + Map headers = new HashMap<>(); + if (this.token != null && !this.token.isEmpty()) { + headers.put("Authorization", "GoogleLogin auth=" + this.token); + } + headers.put("User-Agent", this.deviceInfoProvider.getUserAgentString()); + if (this.gsfId != null && !this.gsfId.isEmpty()) { + headers.put("X-DFE-Device-Id", this.gsfId); + } + headers.put("Accept-Language", this.locale.toString().replace("_", "-")); + return headers; + } + + /** + * Most list requests (apps, categories,..) take these params + * + * @param offset + * @param numberOfResults + */ + private Map getDefaultGetParams(Integer offset, Integer numberOfResults) { + Map params = new HashMap<>(); + params.put("c", "3"); + if (offset != null) { + params.put("o", String.valueOf(offset)); + } + if (numberOfResults != null) { + params.put("n", String.valueOf(numberOfResults)); + } + return params; + } + + private static Map parseResponse(String response) { + Map keyValueMap = new HashMap<>(); + StringTokenizer st = new StringTokenizer(response, "\n\r"); + while (st.hasMoreTokens()) { + String[] keyValue = st.nextToken().split("="); + keyValueMap.put(keyValue[0], keyValue[1]); + } + return keyValueMap; + } +} diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/GooglePlayException.java b/app/src/main/java/com/github/yeriomin/playstoreapi/GooglePlayException.java new file mode 100644 index 00000000..e45b7783 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/GooglePlayException.java @@ -0,0 +1,24 @@ +package com.github.yeriomin.playstoreapi; + +import java.io.IOException; + +public class GooglePlayException extends IOException { + + private int code; + + public GooglePlayException(String message) { + super(message); + } + public GooglePlayException(String message, int code) { + super(message); + this.code = code; + } + + public GooglePlayException(String message, Throwable cause) { + super(message, cause); + } + + public int getCode() { + return this.code; + } +} diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/NativeDeviceInfoProvider.java b/app/src/main/java/com/github/yeriomin/playstoreapi/NativeDeviceInfoProvider.java new file mode 100644 index 00000000..de08d1ac --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/NativeDeviceInfoProvider.java @@ -0,0 +1,246 @@ +package com.github.yeriomin.playstoreapi; + +import android.content.Context; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.DisplayMetrics; + +import com.github.yeriomin.playstoreapi.AndroidBuildProto; +import com.github.yeriomin.playstoreapi.AndroidCheckinProto; +import com.github.yeriomin.playstoreapi.AndroidCheckinRequest; +import com.github.yeriomin.playstoreapi.DeviceConfigurationProto; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +public class NativeDeviceInfoProvider implements DeviceInfoProvider { + + private Context context; + private String localeString; + + public void setContext(Context context) { + this.context = context; + } + + public void setLocaleString(String localeString) { + this.localeString = localeString; + } + + public int getSdkVersion() { + return Build.VERSION.SDK_INT; + } + + public String getUserAgentString() { + return "Android-Finsky/7.1.15 (" + + "api=3" + + ",versionCode=80711500" + + ",sdk=" + Build.VERSION.SDK_INT + + ",device=" + Build.DEVICE + + ",hardware=" + Build.HARDWARE + + ",product=" + Build.PRODUCT + + ")"; + } + + public AndroidCheckinRequest generateAndroidCheckinRequest() { + return AndroidCheckinRequest + .newBuilder() + .setId(0) + .setCheckin( + AndroidCheckinProto.newBuilder() + .setBuild( + AndroidBuildProto.newBuilder() + .setId(Build.FINGERPRINT) + .setProduct(Build.HARDWARE) + .setCarrier(Build.BRAND) + .setRadio(Build.RADIO) + .setBootloader(Build.BOOTLOADER) + .setDevice(Build.DEVICE) + .setSdkVersion(Build.VERSION.SDK_INT) + .setModel(Build.MODEL) + .setManufacturer(Build.MANUFACTURER) + .setBuildProduct(Build.PRODUCT) + .setClient("android-google") + .setOtaInstalled(false) + .setTimestamp(System.currentTimeMillis() / 1000) + .setGoogleServices(16) + ) + .setLastCheckinMsec(0) + .setCellOperator("310260") // Getting this and the next two requires permission + .setSimOperator("310260") + .setRoaming("mobile-notroaming") + .setUserNumber(0) + ) + .setLocale(this.localeString) + .setTimeZone(TimeZone.getDefault().getID()) + .setVersion(3) + .setDeviceConfiguration(getDeviceConfigurationProto()) + .setFragment(0) + .build(); + } + + public DeviceConfigurationProto getDeviceConfigurationProto() { + DisplayMetrics metrics = this.context.getResources().getDisplayMetrics(); + PackageManager packageManager = this.context.getPackageManager(); + FeatureInfo[] featuresList = packageManager.getSystemAvailableFeatures(); + List featureStringList = new ArrayList<>(); + for (FeatureInfo feature : featuresList) { + if (feature.name != null) { + featureStringList.add(feature.name); + } + } + List localeStringList = new ArrayList<>(); + for (Locale locale : Locale.getAvailableLocales()) { + localeStringList.add(locale.toString()); + } + + return DeviceConfigurationProto.newBuilder() + .setTouchScreen(3) + .setKeyboard(1) + .setNavigation(1) + .setScreenLayout(2) + .setHasHardKeyboard(false) + .setHasFiveWayNavigation(false) + .setScreenDensity((int) (metrics.density * 160f)) + .setScreenWidth(metrics.widthPixels) + .setScreenHeight(metrics.heightPixels) + .addAllNativePlatform(Arrays.asList(Build.CPU_ABI, Build.CPU_ABI2)) + .addAllSystemSharedLibrary(Arrays.asList( + "ConnectivityExt", + "activation.jar", + "android-support-v13.jar", + "android-support-v4.jar", + "android-support-v7-recyclerview.jar", + "cloud-common.jar", + "com.android.media.remotedisplay", + "com.android.mediadrm.signer", + "android.test.runner", + "com.android.future.usb.accessory", + "com.android.location.provider", + "com.android.nfc_extras", + "com.google.android.maps", + "com.google.android.media.effects", + "com.google.widevine.software.drm", + "javax.obex" + )) + .addAllSystemAvailableFeature(featureStringList) + .addAllSystemSupportedLocale(localeStringList) + .setGlEsVersion(196609) // Getting this and next list requires messing with ndk + .addAllGlExtension(Arrays.asList( + "GL_AMD_compressed_ATC_texture", + "GL_AMD_performance_monitor", + "GL_ANDROID_extension_pack_es31a", + "GL_APPLE_texture_2D_limited_npot", + "GL_ARB_vertex_buffer_object", + "GL_ARM_shader_framebuffer_fetch_depth_stencil", + "GL_EXT_YUV_target", + "GL_EXT_blit_framebuffer_params", + "GL_EXT_buffer_storage", + "GL_EXT_color_buffer_float", + "GL_EXT_color_buffer_half_float", + "GL_EXT_copy_image", + "GL_EXT_debug_label", + "GL_EXT_debug_marker", + "GL_EXT_discard_framebuffer", + "GL_EXT_disjoint_timer_query", + "GL_EXT_draw_buffers_indexed", + "GL_EXT_geometry_shader", + "GL_EXT_gpu_shader5", + "GL_EXT_multisampled_render_to_texture", + "GL_EXT_primitive_bounding_box", + "GL_EXT_robustness", + "GL_EXT_sRGB", + "GL_EXT_sRGB_write_control", + "GL_EXT_shader_framebuffer_fetch", + "GL_EXT_shader_io_blocks", + "GL_EXT_tessellation_shader", + "GL_EXT_texture_border_clamp", + "GL_EXT_texture_buffer", + "GL_EXT_texture_cube_map_array", + "GL_EXT_texture_filter_anisotropic", + "GL_EXT_texture_format_BGRA8888", + "GL_EXT_texture_norm16", + "GL_EXT_texture_sRGB_R8", + "GL_EXT_texture_sRGB_decode", + "GL_EXT_texture_type_2_10_10_10_REV", + "GL_KHR_blend_equation_advanced", + "GL_KHR_blend_equation_advanced_coherent", + "GL_KHR_debug", + "GL_KHR_no_error", + "GL_KHR_texture_compression_astc_hdr", + "GL_KHR_texture_compression_astc_ldr", + "GL_OES_EGL_image", + "GL_OES_EGL_image_external", + "GL_OES_EGL_sync", + "GL_OES_blend_equation_separate", + "GL_OES_blend_func_separate", + "GL_OES_blend_subtract", + "GL_OES_compressed_ETC1_RGB8_texture", + "GL_OES_compressed_paletted_texture", + "GL_OES_depth24", + "GL_OES_depth_texture", + "GL_OES_depth_texture_cube_map", + "GL_OES_draw_texture", + "GL_OES_element_index_uint", + "GL_OES_framebuffer_object", + "GL_OES_get_program_binary", + "GL_OES_matrix_palette", + "GL_OES_packed_depth_stencil", + "GL_OES_point_size_array", + "GL_OES_point_sprite", + "GL_OES_read_format", + "GL_OES_rgb8_rgba8", + "GL_OES_sample_shading", + "GL_OES_sample_variables", + "GL_OES_shader_image_atomic", + "GL_OES_shader_multisample_interpolation", + "GL_OES_standard_derivatives", + "GL_OES_stencil_wrap", + "GL_OES_surfaceless_context", + "GL_OES_texture_3D", + "GL_OES_texture_compression_astc", + "GL_OES_texture_cube_map", + "GL_OES_texture_env_crossbar", + "GL_OES_texture_float", + "GL_OES_texture_float_linear", + "GL_OES_texture_half_float", + "GL_OES_texture_half_float_linear", + "GL_OES_texture_mirrored_repeat", + "GL_OES_texture_npot", + "GL_OES_texture_stencil8", + "GL_OES_texture_storage_multisample_2d_array", + "GL_OES_vertex_array_object", + "GL_OES_vertex_half_float", + "GL_OVR_multiview", + "GL_OVR_multiview2", + "GL_OVR_multiview_multisampled_render_to_texture", + "GL_QCOM_alpha_test", + "GL_QCOM_extended_get", + "GL_QCOM_tiled_rendering", + "GL_EXT_multi_draw_arrays", + "GL_EXT_shader_texture_lod", + "GL_IMG_multisampled_render_to_texture", + "GL_IMG_program_binary", + "GL_IMG_read_format", + "GL_IMG_shader_binary", + "GL_IMG_texture_compression_pvrtc", + "GL_IMG_texture_format_BGRA8888", + "GL_IMG_texture_npot", + "GL_IMG_vertex_array_object", + "GL_OES_byte_coordinates", + "GL_OES_extended_matrix_palette", + "GL_OES_fixed_point", + "GL_OES_fragment_precision_high", + "GL_OES_mapbuffer", + "GL_OES_matrix_get", + "GL_OES_query_matrix", + "GL_OES_required_internalformat", + "GL_OES_single_precision", + "GL_OES_stencil8" + )) + .build(); + } +} diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/PropertiesDeviceInfoProvider.java b/app/src/main/java/com/github/yeriomin/playstoreapi/PropertiesDeviceInfoProvider.java new file mode 100644 index 00000000..18643c9e --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/PropertiesDeviceInfoProvider.java @@ -0,0 +1,94 @@ +package com.github.yeriomin.playstoreapi; + +import com.github.yeriomin.playstoreapi.AndroidBuildProto; +import com.github.yeriomin.playstoreapi.AndroidCheckinProto; +import com.github.yeriomin.playstoreapi.AndroidCheckinRequest; +import com.github.yeriomin.playstoreapi.DeviceConfigurationProto; + +import java.util.Arrays; +import java.util.Properties; + +public class PropertiesDeviceInfoProvider implements DeviceInfoProvider { + + private Properties properties; + private String localeString; + + public void setProperties(Properties properties) { + this.properties = properties; + } + + public void setLocaleString(String localeString) { + this.localeString = localeString; + } + + public int getSdkVersion() { + return Integer.parseInt(this.properties.getProperty("Build.VERSION.SDK_INT")); + } + + public String getUserAgentString() { + return "Android-Finsky/7.1.15 (" + + "api=3" + + ",versionCode=80711500" + + ",sdk=" + this.properties.getProperty("Build.VERSION.SDK_INT") + + ",device=" + this.properties.getProperty("Build.DEVICE") + + ",hardware=" + this.properties.getProperty("Build.HARDWARE") + + ",product=" + this.properties.getProperty("Build.PRODUCT") + + ")"; + } + + public AndroidCheckinRequest generateAndroidCheckinRequest() { + return AndroidCheckinRequest.newBuilder() + .setId(0) + .setCheckin( + AndroidCheckinProto.newBuilder() + .setBuild( + AndroidBuildProto.newBuilder() + .setId(this.properties.getProperty("Build.FINGERPRINT")) + .setProduct(this.properties.getProperty("Build.HARDWARE")) + .setCarrier(this.properties.getProperty("Build.BRAND")) + .setRadio(this.properties.getProperty("Build.RADIO")) + .setBootloader(this.properties.getProperty("Build.BOOTLOADER")) + .setDevice(this.properties.getProperty("Build.DEVICE")) + .setSdkVersion(Integer.getInteger(this.properties.getProperty("Build.VERSION.SDK_INT"))) + .setModel(this.properties.getProperty("Build.MODEL")) + .setManufacturer(this.properties.getProperty("Build.MANUFACTURER")) + .setBuildProduct(this.properties.getProperty("Build.PRODUCT")) + .setClient(this.properties.getProperty("Client")) + .setOtaInstalled(Boolean.getBoolean(this.properties.getProperty("OtaInstalled"))) + .setTimestamp(System.currentTimeMillis() / 1000) + .setGoogleServices(Integer.getInteger(this.properties.getProperty("GSF.version"))) + ) + .setLastCheckinMsec(0) + .setCellOperator(this.properties.getProperty("CellOperator")) + .setSimOperator(this.properties.getProperty("SimOperator")) + .setRoaming(this.properties.getProperty("Roaming")) + .setUserNumber(0) + ) + .setLocale(this.localeString) + .setTimeZone(this.properties.getProperty("TimeZone")) + .setVersion(3) + .setDeviceConfiguration(getDeviceConfigurationProto()) + .setFragment(0) + .build(); + } + + public DeviceConfigurationProto getDeviceConfigurationProto() { + return DeviceConfigurationProto.newBuilder() + .setTouchScreen(Integer.getInteger(this.properties.getProperty("TouchScreen"))) + .setKeyboard(Integer.getInteger(this.properties.getProperty("Keyboard"))) + .setNavigation(Integer.getInteger(this.properties.getProperty("Navigation"))) + .setScreenLayout(Integer.getInteger(this.properties.getProperty("ScreenLayout"))) + .setHasHardKeyboard(Boolean.getBoolean(this.properties.getProperty("HasHardKeyboard"))) + .setHasFiveWayNavigation(Boolean.getBoolean(this.properties.getProperty("HasFiveWayNavigation"))) + .setScreenDensity(Integer.getInteger(this.properties.getProperty("Screen.Density"))) + .setScreenWidth(Integer.getInteger(this.properties.getProperty("Screen.Width"))) + .setScreenHeight(Integer.getInteger(this.properties.getProperty("Screen.Height"))) + .addAllNativePlatform(Arrays.asList(this.properties.getProperty("Platforms").split(","))) + .addAllSystemSharedLibrary(Arrays.asList(this.properties.getProperty("SharedLibraries").split(","))) + .addAllSystemAvailableFeature(Arrays.asList(this.properties.getProperty("Features").split(","))) + .addAllSystemSupportedLocale(Arrays.asList(this.properties.getProperty("Locales").split(","))) + .setGlEsVersion(Integer.getInteger(this.properties.getProperty("GL.Version"))) + .addAllGlExtension(Arrays.asList(this.properties.getProperty("GL.Extensions").split(","))) + .build(); + } +} diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/ThrottledHttpClient.java b/app/src/main/java/com/github/yeriomin/playstoreapi/ThrottledHttpClient.java new file mode 100644 index 00000000..154362d6 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/ThrottledHttpClient.java @@ -0,0 +1,124 @@ +package com.github.yeriomin.playstoreapi; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +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.HttpRequestBase; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpParams; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class ThrottledHttpClient extends DefaultHttpClient { + + private static final long DEFAULT_REQUEST_INTERVAL = 2000; + + private long lastRequestTime; + private long requestInterval = DEFAULT_REQUEST_INTERVAL; + + public ThrottledHttpClient(HttpParams httpParams) { + super(httpParams); + } + + public void setRequestInterval(long requestInterval) { + this.requestInterval = requestInterval; + } + + public byte[] get(String url, Map params) throws IOException { + return get(url, params, null); + } + + public byte[] get(String url, Map params, Map headers) throws IOException { + if (!params.isEmpty()) { + url = url + "?" + URLEncodedUtils.format(getNameValuePairList(params), "UTF-8"); + } + HttpRequestBase request = new HttpGet(url); + return request(request, headers); + } + + public byte[] post(String url, Map params, Map headers) throws IOException { + try { + headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + return post(url, new UrlEncodedFormEntity(getNameValuePairList(params), "UTF-8"), headers); + } catch (UnsupportedEncodingException e) { + // Highly unlikely + return null; + } + } + + public byte[] post(String url, HttpEntity body, Map headers) throws IOException { + HttpPost request = new HttpPost(url); + request.setEntity(body); + if (!headers.containsKey("Content-Type")) { + headers.put("Content-Type", "application/x-protobuf"); + } + return request(request, headers); + } + + private byte[] request(HttpRequestBase request, Map headers) throws IOException { + System.out.println("Requesting: " + request.getURI().toString()); + if (this.lastRequestTime > 0) { + long msecRemaining = this.requestInterval - System.currentTimeMillis() + this.lastRequestTime; + if (msecRemaining > 0) { + try { + Thread.currentThread().sleep(msecRemaining); + } catch (InterruptedException e) { + // Unlikely + System.err.println(e.getMessage()); + } + } + } + + if (null != headers) { + for (String header: headers.keySet()) { + request.setHeader(header, headers.get(header)); + } + } + + HttpResponse response = this.execute(request); + this.lastRequestTime = System.currentTimeMillis(); + int statusCode = response.getStatusLine().getStatusCode(); + boolean isProtobuf = response.containsHeader("Content-Type") + && response.getHeaders("Content-Type")[0].getValue().contains("protobuf"); + byte[] content = readAll(response.getEntity().getContent()); + + if (!isProtobuf) { + throw new GooglePlayException(String.valueOf(statusCode) + " Thats not even protobuf: " + new String(content), statusCode); + } + + if (statusCode >= 400) { + throw new GooglePlayException(String.valueOf(statusCode) + " Probably an auth error: " + new String(content), statusCode); + } + + return content; + } + + private static List getNameValuePairList(Map map) { + List list = new ArrayList<>(); + for (String param: map.keySet()) { + list.add(new BasicNameValuePair(param, map.get(param))); + } + return list; + } + + private static byte[] readAll(InputStream inputStream) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + return outputStream.toByteArray(); + } +} diff --git a/app/src/main/java/com/github/yeriomin/playstoreapi/Utils.java b/app/src/main/java/com/github/yeriomin/playstoreapi/Utils.java new file mode 100644 index 00000000..f8270203 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/playstoreapi/Utils.java @@ -0,0 +1,281 @@ +package com.github.yeriomin.playstoreapi; + +/** + * @author akdeniz + */ +public class Utils { + + /* +* + + id: "Xiaomi/gemini/gemini:6.0/MRA58K/V7.2.8.0.MAACNDB:user/release-keys" + product: "qcom" + carrier: "Xiaomi" + radio: "TH20.c1.3-0321_1155_57c5007" + bootloader: "unknown" + client: "unknown" + timestamp: 1458643019 + googleServices: 10084448 + device: "gemini" + sdkVersion: 23 + model: "MI 5" + manufacturer: "Xiaomi" + buildProduct: "gemini" + otaInstalled: 0 + + +Build.BOARD msm8996 +Build.BOOTLOADER unknown +Build.BRAND Xiaomi +Build.DEVICE gemini +Build.DISPLAY MRA58K +Build.FINGERPRINT Xiaomi/gemini/gemini:6.0/MRA58K/V7.2.8.0.MAACNDB:user/release-keys +Build.HARDWARE qcom +Build.HOST qh-miui-ota-bd72.bj +Build.ID MRA58K +Build.MANUFACTURER Xiaomi +Build.MODEL MI 5 +Build.PRODUCT gemini +Build.SERIAL d0189fa4 +Build.TAGS release-keys +Build.TYPE user +Build.UNKNOWN unknown +Build.USER builder +Build.CPU_ABI arm64-v8a +Build.CPU_ABI2 +Build.TIME 1458643019000 +Build.VERSION.CODENAME REL +Build.VERSION.INCREMENTAL V7.2.8.0.0.MAACNDB +Build.VERSION.RELEASE 6.0 +Build.VERSION.SDK_INT 23 + */ + +// /** +// * Generates android checkin request with properties of "Galaxy S3". +// *

+// * http://www.glbenchmark.com/phonedetails.jsp?benchmark=glpro25&D=Samsung +// * +GT-I9300+Galaxy+S+III&testgroup=system +// */ +// public static AndroidCheckinRequest generateAndroidCheckinRequest() { +// +// return AndroidCheckinRequest +// .newBuilder() +// .setId(0) +// .setCheckin( +// AndroidCheckinProto.newBuilder() +// .setBuild( +// AndroidBuildProto.newBuilder() +// .setId(Build.FINGERPRINT) +// .setProduct(Build.HARDWARE) +// .setCarrier(Build.BRAND) +// .setRadio(Build.RADIO) +// .setBootloader(Build.BOOTLOADER) +// .setDevice(Build.DEVICE) +// .setSdkVersion(Build.VERSION.SDK_INT) +// .setModel(Build.MODEL) +// .setManufacturer(Build.MANUFACTURER) +// .setBuildProduct(Build.PRODUCT) +// .setClient("android-google") +// .setOtaInstalled(false) +// .setTimestamp(new Date().getTime() / 1000) +// .setGoogleServices(16) +// ) +// .setLastCheckinMsec(0) +// .setCellOperator("310260") +// .setSimOperator("310260") +// .setRoaming("mobile-notroaming") +// .setUserNumber(0) +// ) +// .setLocale(Locale.getDefault().toString()) +// .setTimeZone(TimeZone.getDefault().getID()) +// .setVersion(3) +// .setDeviceConfiguration(getDeviceConfigurationProto()) +// .setFragment(0) +// .build(); +// } +// +// public static AndroidCheckinRequest generateAndroidCheckinRequestOriginal() { +// +// return AndroidCheckinRequest +// .newBuilder() +// .setId(0) +// .setCheckin( +// AndroidCheckinProto.newBuilder() +// .setBuild( +// AndroidBuildProto.newBuilder() +// .setId("samsung/m0xx/m0:4.0.4/IMM76D/I9300XXALF2:user/release-keys") +// .setProduct("smdk4x12") +// .setCarrier("Google") +// .setRadio("I9300XXALF2") +// .setBootloader("PRIMELA03") +// .setClient("android-google") +// .setTimestamp(new Date().getTime() / 1000) +// .setGoogleServices(16) +// .setDevice("m0") +// .setSdkVersion(16) +// .setModel("GT-I9300") +// .setManufacturer("Samsung") +// .setBuildProduct("m0xx") +// .setOtaInstalled(false) +// ) +// .setLastCheckinMsec(0) +// .setCellOperator("310260") +// .setSimOperator("310260") +// .setRoaming("mobile-notroaming") +// .setUserNumber(0) +// ) +// .setLocale("en_US") +// .setTimeZone("Europe/Istanbul") +// .setVersion(3) +// .setDeviceConfiguration(getDeviceConfigurationProto()) +// .setFragment(0) +// .build(); +// } +// +// public static AndroidCheckinRequest generateAndroidCheckinRequestNviennot() { +// +// return AndroidCheckinRequest +// .newBuilder() +// .setId(0) +// .setCheckin( +// AndroidCheckinProto.newBuilder() +// .setBuild( +// AndroidBuildProto.newBuilder() +// .setId("google/yakju/maguro:4.1.1/JRO03C/398337:user/release-keys") +// .setProduct("tuna") +// .setCarrier("Google") +// .setRadio("I9250XXLA2") +// .setBootloader("PRIMELA03") +// .setClient("android-google") +// .setTimestamp(new Date().getTime()/1000) +// .setGoogleServices(16) +// .setDevice("maguro") +// .setSdkVersion(16) +// .setModel("Galaxy Nexus") +// .setManufacturer("Samsung") +// .setBuildProduct("yakju") +// .setOtaInstalled(false) +// ) +// .setLastCheckinMsec(0) +// .setCellOperator("310260") +// .setSimOperator("310260") +// .setRoaming("mobile-notroaming") +// .setUserNumber(0) +// ) +// .setLocale("en_US") +// .setTimeZone("Europe/Istanbul") +// .setVersion(3) +// .setDeviceConfiguration(getDeviceConfigurationProto()) +// .setFragment(0) +// .build(); +// } +// +// public static AndroidCheckinRequest generateAndroidCheckinRequestRacoon() { +// +// return AndroidCheckinRequest +// .newBuilder() +// .setId(0) +// .setCheckin( +// AndroidCheckinProto.newBuilder() +// .setBuild( +// AndroidBuildProto.newBuilder() +// .setId("samsung/nobleltejv/noblelte:6.0.1/MMB29K/N920CXXU2BPD6:user/release-keys") +// .setProduct("noblelte") +// .setCarrier("Google") +// .setRadio("I9300XXALF2") +// .setBootloader("PRIMELA03") +// .setClient("android-google") +// .setTimestamp(new Date().getTime() / 1000) +// .setGoogleServices(16) +// .setDevice("noblelte") +// .setSdkVersion(23) +// .setModel("SM-N920C") +// .setManufacturer("Samsung") +// .setBuildProduct("noblelte") +// .setOtaInstalled(false) +// ) +// .setLastCheckinMsec(0) +// .setCellOperator("310260") +// .setSimOperator("310260") +// .setRoaming("mobile-notroaming") +// .setUserNumber(0) +// ) +// .setLocale("en_US") +// .setTimeZone("Europe/Berlin") +// .setVersion(3) +// .setDeviceConfiguration(getDeviceConfigurationProto()) +// .setFragment(0) +// .build(); +// } +// +// public static DeviceConfigurationProto getDeviceConfigurationProtoAAAAA() { +//// DisplayMetrics metrics = new DisplayMetrics(); +//// WindowManager wm = (WindowManager) this.context.getSystemService(Context.WINDOW_SERVICE); +//// wm.getDefaultDisplay().getMetrics(metrics); +// return DeviceConfigurationProto.newBuilder() +// .setTouchScreen(3) +// .setKeyboard(1) +// .setNavigation(1) +// .setScreenLayout(2) +// .setHasHardKeyboard(false) +// .setHasFiveWayNavigation(false) +// .setScreenDensity(320) +// .setScreenWidth(720) +// .setScreenHeight(1184) +// .setGlEsVersion(131072) +// .addAllNativePlatform(Arrays.asList(Build.CPU_ABI, Build.CPU_ABI2)) +//// .addAllNativePlatform(Arrays.asList("armeabi-v7a", "armeabi")) +// .addAllSystemSharedLibrary( +// Arrays.asList("android.test.runner", "com.android.future.usb.accessory", "com.android.location.provider", +// "com.android.nfc_extras", "com.google.android.maps", "com.google.android.media.effects", +// "com.google.widevine.software.drm", "javax.obex")) +// .addAllSystemAvailableFeature( +// Arrays.asList("android.hardware.bluetooth", "android.hardware.camera", +// "android.hardware.camera.autofocus", "android.hardware.camera.flash", +// "android.hardware.camera.front", "android.hardware.faketouch", "android.hardware.location", +// "android.hardware.location.gps", "android.hardware.location.network", +// "android.hardware.microphone", "android.hardware.nfc", "android.hardware.screen.landscape", +// "android.hardware.screen.portrait", "android.hardware.sensor.accelerometer", +// "android.hardware.sensor.barometer", "android.hardware.sensor.compass", +// "android.hardware.sensor.gyroscope", "android.hardware.sensor.light", +// "android.hardware.sensor.proximity", "android.hardware.telephony", +// "android.hardware.telephony.gsm", "android.hardware.touchscreen", +// "android.hardware.touchscreen.multitouch", "android.hardware.touchscreen.multitouch.distinct", +// "android.hardware.touchscreen.multitouch.jazzhand", "android.hardware.usb.accessory", +// "android.hardware.usb.host", "android.hardware.wifi", "android.hardware.wifi.direct", +// "android.software.live_wallpaper", "android.software.sip", "android.software.sip.voip", +// "com.cyanogenmod.android", "com.cyanogenmod.nfc.enhanced", +// "com.google.android.feature.GOOGLE_BUILD", "com.nxp.mifare", "com.tmobile.software.themes")) +// .addAllSystemSupportedLocale( +// Arrays.asList("af", "af_ZA", "am", "am_ET", "ar", "ar_EG", "bg", "bg_BG", "ca", "ca_ES", "cs", "cs_CZ", +// "da", "da_DK", "de", "de_AT", "de_CH", "de_DE", "de_LI", "el", "el_GR", "en", "en_AU", "en_CA", +// "en_GB", "en_NZ", "en_SG", "en_US", "es", "es_ES", "es_US", "fa", "fa_IR", "fi", "fi_FI", "fr", +// "fr_BE", "fr_CA", "fr_CH", "fr_FR", "hi", "hi_IN", "hr", "hr_HR", "hu", "hu_HU", "in", "in_ID", +// "it", "it_CH", "it_IT", "iw", "iw_IL", "ja", "ja_JP", "ko", "ko_KR", "lt", "lt_LT", "lv", +// "lv_LV", "ms", "ms_MY", "nb", "nb_NO", "nl", "nl_BE", "nl_NL", "pl", "pl_PL", "pt", "pt_BR", +// "pt_PT", "rm", "rm_CH", "ro", "ro_RO", "ru", "ru_RU", "sk", "sk_SK", "sl", "sl_SI", "sr", +// "sr_RS", "sv", "sv_SE", "sw", "sw_TZ", "th", "th_TH", "tl", "tl_PH", "tr", "tr_TR", "ug", +// "ug_CN", "uk", "uk_UA", "vi", "vi_VN", "zh_CN", "zh_TW", "zu", "zu_ZA")) +// .addAllGlExtension( +// Arrays.asList("GL_EXT_debug_marker", "GL_EXT_discard_framebuffer", "GL_EXT_multi_draw_arrays", +// "GL_EXT_shader_texture_lod", "GL_EXT_texture_format_BGRA8888", +// "GL_IMG_multisampled_render_to_texture", "GL_IMG_program_binary", "GL_IMG_read_format", +// "GL_IMG_shader_binary", "GL_IMG_texture_compression_pvrtc", "GL_IMG_texture_format_BGRA8888", +// "GL_IMG_texture_npot", "GL_IMG_vertex_array_object", "GL_OES_EGL_image", +// "GL_OES_EGL_image_external", "GL_OES_blend_equation_separate", "GL_OES_blend_func_separate", +// "GL_OES_blend_subtract", "GL_OES_byte_coordinates", "GL_OES_compressed_ETC1_RGB8_texture", +// "GL_OES_compressed_paletted_texture", "GL_OES_depth24", "GL_OES_depth_texture", +// "GL_OES_draw_texture", "GL_OES_egl_sync", "GL_OES_element_index_uint", +// "GL_OES_extended_matrix_palette", "GL_OES_fixed_point", "GL_OES_fragment_precision_high", +// "GL_OES_framebuffer_object", "GL_OES_get_program_binary", "GL_OES_mapbuffer", +// "GL_OES_matrix_get", "GL_OES_matrix_palette", "GL_OES_packed_depth_stencil", +// "GL_OES_point_size_array", "GL_OES_point_sprite", "GL_OES_query_matrix", "GL_OES_read_format", +// "GL_OES_required_internalformat", "GL_OES_rgb8_rgba8", "GL_OES_single_precision", +// "GL_OES_standard_derivatives", "GL_OES_stencil8", "GL_OES_stencil_wrap", +// "GL_OES_texture_cube_map", "GL_OES_texture_env_crossbar", "GL_OES_texture_float", +// "GL_OES_texture_half_float", "GL_OES_texture_mirrored_repeat", "GL_OES_vertex_array_object", +// "GL_OES_vertex_half_float")).build(); +// } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/AppListActivity.java b/app/src/main/java/com/github/yeriomin/yalpstore/AppListActivity.java new file mode 100644 index 00000000..82c79ee9 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/AppListActivity.java @@ -0,0 +1,200 @@ +package com.github.yeriomin.yalpstore; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.SimpleAdapter; + +import com.github.yeriomin.yalpstore.model.App; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +abstract public class AppListActivity extends ListActivity { + + public static final String PREFERENCE_EMAIL = "PREFERENCE_EMAIL"; + public static final String PREFERENCE_PASSWORD = "PREFERENCE_PASSWORD"; + public static final String PREFERENCE_AUTH_TOKEN = "PREFERENCE_AUTH_TOKEN"; + public static final String PREFERENCE_GSF_ID = "PREFERENCE_GSF_ID"; + + protected static final String LINE1 = "LINE1"; + protected static final String LINE2 = "LINE2"; + protected static final String ICON = "ICON"; + protected static final String PACKAGE_NAME = "PACKAGE_NAME"; + + protected List> data = new ArrayList<>(); + + abstract protected void loadApps(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.applist_activity_layout); + + setListAdapter(getSimpleListAdapter()); + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, final int position, long id) { + Intent intent = new Intent(getApplicationContext(), DetailsActivity.class); + intent.putExtra(DetailsActivity.INTENT_PACKAGE_NAME, (String) data.get(position).get(PACKAGE_NAME)); + startActivity(intent); + } + }); + + loadApps(); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_logout: + new AlertDialog.Builder(this) + .setMessage(R.string.dialog_message_logout) + .setTitle(R.string.dialog_title_logout) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new PlayStoreApiWrapper(getApplicationContext()).logout(); + dialogInterface.dismiss(); + finish(); + } + }) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + + } + }) + .show(); + break; + case R.id.action_search: + onSearchRequested(); + break; + case R.id.action_updates: + startActivity(new Intent(this, UpdatableAppsActivity.class)); + break; + } + return super.onOptionsItemSelected(item); + } + + protected Map formatApp(App app) { + Map map = new HashMap<>(); + map.put(LINE1, app.getDisplayName()); + map.put(PACKAGE_NAME, app.getPackageName()); + return map; + } + + protected void addApps(List apps) { + for (App app: apps) { + data.add(this.formatApp(app)); + } + ((SimpleAdapter) getListAdapter()).notifyDataSetChanged(); + } + + protected List getInstalledApps() { + List apps = new ArrayList<>(); + + PackageManager pm = getPackageManager(); + List packages = pm.getInstalledPackages(PackageManager.GET_META_DATA); + for (PackageInfo packageInfo : packages) { + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + // This is a system app - skipping + continue; + } + App app = new App(packageInfo); + app.setDisplayName(pm.getApplicationLabel(packageInfo.applicationInfo).toString()); + app.setIcon(pm.getApplicationIcon(packageInfo.applicationInfo)); + app.setInstalled(true); + apps.add(app); + } + return apps; + } + + private SimpleAdapter getSimpleListAdapter() { + + String[] from = { LINE1, LINE2, ICON }; + int[] to = { R.id.text1, R.id.text2, R.id.icon }; + + SimpleAdapter adapter = new SimpleAdapter( + this, + data, + R.layout.two_line_list_item_with_icon, + from, + to); + + adapter.setViewBinder(new SimpleAdapter.ViewBinder() { + + @Override + public boolean setViewValue(final View view, Object drawableOrUrl, String textRepresentation) { + if (view instanceof ImageView) { + if (drawableOrUrl instanceof String) { + ImageDownloadTask task = new ImageDownloadTask(); + task.setView((ImageView) view); + task.execute((String) drawableOrUrl); + } else { + ((ImageView) view).setImageDrawable((Drawable) drawableOrUrl); + } + return true; + } + return false; + } + }); + return adapter; + } + + class ImageDownloadTask extends AsyncTask { + + private ImageView view; + private Context context; + private Drawable drawable; + + public void setView(ImageView view) { + this.view = view; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + this.context = this.view.getContext(); + this.view.setImageDrawable(this.context.getResources().getDrawable(android.R.drawable.sym_def_app_icon)); + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + this.view.setImageDrawable(this.drawable); + } + + @Override + protected Void doInBackground(String[] params) { + BitmapManager manager = new BitmapManager(this.context); + this.drawable = new BitmapDrawable(manager.getBitmap(params[0])); + return null; + } + + } + +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/BitmapManager.java b/app/src/main/java/com/github/yeriomin/yalpstore/BitmapManager.java new file mode 100644 index 00000000..aba15c0e --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/BitmapManager.java @@ -0,0 +1,95 @@ +package com.github.yeriomin.yalpstore; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +public class BitmapManager { + + private static final long VALID_MILLIS = 1000*60*60*24*7; + + private Context context; + + public BitmapManager(Context context) { + this.context = context; + } + + public Bitmap getBitmap(String url) { + File cached = getFileName(url); + Bitmap bitmap; + if (!cached.exists() || !isValid(cached)) { + bitmap = downloadBitmap(url); + cacheBitmap(bitmap, cached); + } else { + bitmap = getCachedBitmap(cached); + } + return bitmap; + } + + private File getFileName(String urlString) { + String fileName; + try { + URL url = new URL(urlString); + fileName = new File(url.getPath()).getName(); + } catch (MalformedURLException e) { + System.out.println(e.getMessage()); + fileName = String.valueOf(urlString.hashCode()) + ".png"; + } + return new File(context.getCacheDir(), fileName); + } + + static private boolean isValid(File cached) { + return cached.lastModified() + VALID_MILLIS > System.currentTimeMillis(); + } + + static private Bitmap getCachedBitmap(File cached) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = false; + options.inDither = false; + return BitmapFactory.decodeStream(new FileInputStream(cached), null, options); + } catch (FileNotFoundException e) { + // We just checked for that + return null; + } + } + + static private void cacheBitmap(Bitmap bitmap, File cached) { + try { + FileOutputStream out = new FileOutputStream(cached); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + static private Bitmap downloadBitmap(final String url) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.connect(); + connection.setConnectTimeout(3000); + InputStream input = connection.getInputStream(); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 4; + options.inJustDecodeBounds = false; + + return BitmapFactory.decodeStream(input, null, options); + } catch (IOException e) { + System.out.println("Could not get icon from " + url); + } + return null; + } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsDialogBuilder.java b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsDialogBuilder.java new file mode 100644 index 00000000..111da43a --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsDialogBuilder.java @@ -0,0 +1,135 @@ +package com.github.yeriomin.yalpstore; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +public class CredentialsDialogBuilder { + + private Context context; + + public CredentialsDialogBuilder(Context context) { + this.context = context; + } + + public Dialog show() { + final Dialog ad = new Dialog(context); + ad.setContentView(R.layout.credentials_dialog_layout); + ad.setTitle(context.getString(R.string.credentials_title)); + ad.setCancelable(false); + + final EditText editEmail = (EditText) ad.findViewById(R.id.email); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + editEmail.setText(prefs.getString(AppListActivity.PREFERENCE_EMAIL, "")); + final EditText editPassword = (EditText) ad.findViewById(R.id.password); + + Button buttonExit = (Button) ad.findViewById(R.id.button_exit); + buttonExit.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + System.exit(0); + } + }); + + Button buttonOk = (Button) ad.findViewById(R.id.button_ok); + buttonOk.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Context c = view.getContext(); + final String email = editEmail.getText().toString(); + final String password = editPassword.getText().toString(); + if (email.isEmpty() || password.isEmpty()) { + Toast.makeText(c, c.getString(R.string.error_credentials_empty), Toast.LENGTH_LONG).show(); + return; + } + + CheckCredentialsTask task = new CheckCredentialsTask(); + task.setContext(c); + try { + Throwable result = task.execute(email, password).get(); + if (null == result) { + ad.dismiss(); + } + } catch (InterruptedException | ExecutionException e) { + System.out.println(e.getClass().getName()); + } + } + }); + + ad.show(); + return ad; + } + + private class CheckCredentialsTask extends AsyncTask { + + private Context context; + private ProgressDialog progressDialog; + + public void setContext(Context context) { + this.context = context; + } + + @Override + protected Throwable doInBackground(String[] params) { + if (params.length < 2 + || params[0] == null + || params[1] == null + || params[0].isEmpty() + || params[1].isEmpty() + ) { + System.out.println("Email - password pair expected"); + } + try { + PlayStoreApiWrapper wrapper = new PlayStoreApiWrapper(context); + wrapper.login(params[0], params[1]); + } catch (Throwable e) { + return e; + } + return null; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + this.progressDialog = new ProgressDialog(this.context.getApplicationContext()); + this.progressDialog.setTitle(this.context.getString(R.string.credentials_title_logging_in)); + this.progressDialog.setMessage(this.context.getString(R.string.credentials_message_logging_in)); + this.progressDialog.show(); + } + + @Override + protected void onPostExecute(Throwable e) { + super.onPostExecute(e); + this.progressDialog.dismiss(); + if (null != e) { + if (e instanceof CredentialsRejectedException) { + Toast.makeText( + this.context.getApplicationContext(), + this.context.getString(R.string.error_incorrect_password), + Toast.LENGTH_LONG + ).show(); + } else if (e instanceof CredentialsEmptyException) { + System.out.println("Credentials empty"); + } else if (e instanceof IOException) { + Toast.makeText( + this.context.getApplicationContext(), + this.context.getString(R.string.error_network_other, e.getMessage()), + Toast.LENGTH_LONG + ).show(); + } else { + System.out.println("Unknown exception " + e.getClass().getName() + " " + e.getMessage()); + } + } + } + } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsEmptyException.java b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsEmptyException.java new file mode 100644 index 00000000..e2b32c6c --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsEmptyException.java @@ -0,0 +1,4 @@ +package com.github.yeriomin.yalpstore; + +public class CredentialsEmptyException extends CredentialsException { +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsException.java b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsException.java new file mode 100644 index 00000000..c33a53ad --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsException.java @@ -0,0 +1,21 @@ +package com.github.yeriomin.yalpstore; + +public class CredentialsException extends Exception { + + public CredentialsException() { + super(); + } + + public CredentialsException(String message) { + super(message); + } + + public CredentialsException(String message, Throwable cause) { + super(message, cause); + } + + public CredentialsException(Throwable cause) { + super(cause); + } + +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsRejectedException.java b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsRejectedException.java new file mode 100644 index 00000000..22cafea2 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/CredentialsRejectedException.java @@ -0,0 +1,20 @@ +package com.github.yeriomin.yalpstore; + +public class CredentialsRejectedException extends CredentialsException { + + public CredentialsRejectedException() { + super(); + } + + public CredentialsRejectedException(String message) { + super(message); + } + + public CredentialsRejectedException(String message, Throwable cause) { + super(message, cause); + } + + public CredentialsRejectedException(Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/DetailsActivity.java b/app/src/main/java/com/github/yeriomin/yalpstore/DetailsActivity.java new file mode 100644 index 00000000..e22906ec --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/DetailsActivity.java @@ -0,0 +1,221 @@ +package com.github.yeriomin.yalpstore; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +import android.text.TextUtils; +import android.text.format.Formatter; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.github.yeriomin.yalpstore.model.App; + +import java.util.ArrayList; +import java.util.List; + +public class DetailsActivity extends Activity { + + private static final int PERMISSIONS_REQUEST_CODE = 828; + + static final String INTENT_PACKAGE_NAME = "INTENT_PACKAGE_NAME"; + + private GoogleApiAsyncTask task; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_logout: + new AlertDialog.Builder(this) + .setMessage(R.string.dialog_message_logout) + .setTitle(R.string.dialog_title_logout) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + new PlayStoreApiWrapper(getApplicationContext()).logout(); + dialogInterface.dismiss(); + finish(); + } + }) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + + } + }) + .show(); + break; + case R.id.action_search: + onSearchRequested(); + break; + case R.id.action_updates: + startActivity(new Intent(this, UpdatableAppsActivity.class)); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final String packageName = getIntent().getStringExtra(INTENT_PACKAGE_NAME); + if (packageName == null || packageName.isEmpty()) { + Toast.makeText(this, "No package name provided", Toast.LENGTH_LONG).show(); + finishActivity(0); + return; + } + + GoogleApiAsyncTask task = new GoogleApiAsyncTask() { + + private App app; + + @Override + protected Throwable doInBackground(Void... params) { + PlayStoreApiWrapper wrapper = new PlayStoreApiWrapper(getApplicationContext()); + try { + this.app = wrapper.getDetails(packageName); + } catch (Throwable e) { + return e; + } + Drawable icon; + try { + ApplicationInfo installedApp = getPackageManager().getApplicationInfo(packageName, 0); + icon = getPackageManager().getApplicationIcon(installedApp); + this.app.setInstalled(true); + } catch (PackageManager.NameNotFoundException e) { + BitmapManager manager = new BitmapManager(getApplicationContext()); + icon = new BitmapDrawable(manager.getBitmap(app.getIconUrl())); + } + this.app.setIcon(icon); + return null; + } + + @Override + protected void onPostExecute(Throwable e) { + super.onPostExecute(e); + drawDetails(this.app); + } + }; + task.setContext(this); + task.prepareDialog( + getString(R.string.dialog_message_loading_app_details), + getString(R.string.dialog_title_loading_app_details) + ); + task.execute(); + } + + private void drawDetails(final App app) { + setTitle(app.getDisplayName()); + setContentView(R.layout.details_activity_layout); + + ((ImageView) findViewById(R.id.icon)).setImageDrawable(app.getIcon()); + + setText(R.id.displayName, app.getDisplayName()); + setText(R.id.packageName, app.getPackageName()); + setText(R.id.installs, R.string.details_installs, app.getInstalls()); + setText(R.id.rating, R.string.details_rating, app.getRating()); + setText(R.id.updated, R.string.details_updated, app.getUpdated()); + setText(R.id.size, R.string.details_size, Formatter.formatShortFileSize(this, app.getSize())); + setText(R.id.description, Html.fromHtml(app.getDescription()).toString()); + setText(R.id.developerName, R.string.details_developer, app.getDeveloper().getName()); + setText(R.id.developerEmail, app.getDeveloper().getEmail()); + setText(R.id.developerWebsite, app.getDeveloper().getWebsite()); + String changes = app.getChanges(); + if (null != changes && !changes.isEmpty()) { + setText(R.id.changes, Html.fromHtml(changes).toString()); + findViewById(R.id.changes).setVisibility(View.VISIBLE); + findViewById(R.id.changes_title).setVisibility(View.VISIBLE); + } + + PackageManager pm = getPackageManager(); + List localizedPermissions = new ArrayList<>(); + for (String permissionName: app.getPermissions()) { + try { + localizedPermissions.add(pm.getPermissionInfo(permissionName, 0).loadLabel(pm).toString()); + } catch (PackageManager.NameNotFoundException e) { + System.out.println("NameNotFoundException " + permissionName); + } + } + setText(R.id.permissions, TextUtils.join("\n", localizedPermissions)); + + Button downloadButton = (Button) findViewById(R.id.download); + downloadButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + task = new GoogleApiAsyncTask() { + @Override + protected Throwable doInBackground(Void... params) { + PlayStoreApiWrapper wrapper = new PlayStoreApiWrapper(DetailsActivity.this); + try { + wrapper.download(app); + } catch (Throwable e) { + return e; + } + return null; + } + }; + task.setContext(v.getContext()); + task.prepareDialog( + getString(R.string.dialog_message_purchasing_app), + getString(R.string.dialog_title_purchasing_app) + ); + if (checkPermission()) { + task.execute(); + } else { + requestPermission(); + } + } + }); + } + + private void setText(int viewId, String text) { + ((TextView) findViewById(viewId)).setText(text); + } + + private void setText(int viewId, int stringId, Object... text) { + setText(viewId, getString(stringId, text)); + } + + private boolean checkPermission() { + if (Build.VERSION.SDK_INT >= 23) { + return this.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + private void requestPermission() { + if (Build.VERSION.SDK_INT >= 23) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + if (requestCode == PERMISSIONS_REQUEST_CODE + && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + task.execute(); + } + } + +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/GoogleApiAsyncTask.java b/app/src/main/java/com/github/yeriomin/yalpstore/GoogleApiAsyncTask.java new file mode 100644 index 00000000..78a0c2da --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/GoogleApiAsyncTask.java @@ -0,0 +1,76 @@ +package com.github.yeriomin.yalpstore; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketException; +import java.net.UnknownHostException; + +abstract class GoogleApiAsyncTask extends AsyncTask { + + protected Context context; + protected String progressDialogTitle; + protected String progressDialogMessage; + protected ProgressDialog dialog; + protected TextView errorView; + + public void setContext(Context context) { + this.context = context; + } + + public void prepareDialog(String message, String title) { + this.progressDialogTitle = title; + this.progressDialogMessage = message; + } + + public void setErrorView(TextView errorView) { + this.errorView = errorView; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + this.dialog = ProgressDialog.show(this.context, this.progressDialogTitle, this.progressDialogMessage, true); + } + + @Override + protected void onPostExecute(Throwable e) { + super.onPostExecute(null); + this.dialog.dismiss(); + if (e instanceof CredentialsException) { + if (e instanceof CredentialsRejectedException) { + Toast.makeText( + this.context.getApplicationContext(), + this.context.getString(R.string.error_incorrect_password), + Toast.LENGTH_LONG + ).show(); + } else if (e instanceof CredentialsEmptyException) { + System.out.println("Credentials empty"); + } + CredentialsDialogBuilder dialogBuilder = new CredentialsDialogBuilder(this.context); + dialogBuilder.show(); + } else if (e instanceof IOException) { + System.out.println(e.getClass().getName() + " " + e.getMessage()); + String message; + if (e instanceof UnknownHostException + || e instanceof ConnectException + || e instanceof SocketException) { + message = this.context.getString(R.string.error_no_network); + } else { + message = this.context.getString(R.string.error_network_other, e.getClass().getName() + " " + e.getMessage()); + } + if (null != this.errorView) { + this.errorView.setText(message); + } else { + Toast.makeText(this.context.getApplicationContext(), message, Toast.LENGTH_LONG).show(); + } + } else if (e != null) { + System.out.println("Unknown exception " + e.getClass().getName() + " " + e.getMessage()); + } + } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/PlayStoreApiWrapper.java b/app/src/main/java/com/github/yeriomin/yalpstore/PlayStoreApiWrapper.java new file mode 100644 index 00000000..242fecda --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/PlayStoreApiWrapper.java @@ -0,0 +1,220 @@ +package com.github.yeriomin.yalpstore; + +import android.app.DownloadManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Environment; +import android.preference.PreferenceManager; + +import com.github.yeriomin.playstoreapi.AndroidAppDeliveryData; +import com.github.yeriomin.playstoreapi.AppDetails; +import com.github.yeriomin.playstoreapi.BulkDetailsEntry; +import com.github.yeriomin.playstoreapi.BuyResponse; +import com.github.yeriomin.playstoreapi.DocV2; +import com.github.yeriomin.playstoreapi.GooglePlayAPI; +import com.github.yeriomin.playstoreapi.GooglePlayException; +import com.github.yeriomin.playstoreapi.HttpCookie; +import com.github.yeriomin.playstoreapi.Image; +import com.github.yeriomin.playstoreapi.NativeDeviceInfoProvider; +import com.github.yeriomin.playstoreapi.SearchResponse; +import com.github.yeriomin.yalpstore.model.App; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static android.content.Context.DOWNLOAD_SERVICE; + +/** + * Akdeniz Google Play Crawler classes are supposed to be independent from android, + * so this warpper manages anything android-related and feeds it to the Akdeniz's classes + * Specifically: credentials via Preferences, downloads via DownloadManager, app details using + * android PackageInfo + */ +public class PlayStoreApiWrapper { + + private Context context; + private String email; + private String password; + + private static GooglePlayAPI api; + + private App buildApp(DocV2 details) { + App app = new App(); + app.setDisplayName(details.getTitle()); + app.setDescription(details.getDescriptionHtml()); + app.setRating(details.getAggregateRating().getStarRating()); + if (details.getOfferCount() > 0) { + app.setOfferType(details.getOffer(0).getOfferType()); + } + AppDetails appDetails = details.getDetails().getAppDetails(); + app.getPackageInfo().packageName = appDetails.getPackageName(); + app.setVersionName(appDetails.getVersionString()); + app.setVersionCode(appDetails.getVersionCode()); + app.setSize(appDetails.getInstallationSize()); + Pattern pattern = Pattern.compile("[ ,>\\.\\+\\d\\s]+"); + Matcher matcher = pattern.matcher(appDetails.getNumDownloads()); + if (matcher.find()) { + String installs = matcher.group(0) + .replaceAll("[\\s\\.,]000[\\s\\.,]000[\\s\\.,]000", context.getString(R.string.suffix_billion)) + .replaceAll("[\\s\\.,]000[\\s\\.,]000", context.getString(R.string.suffix_million)) + ; + app.setInstalls(installs); + } + app.setUpdated(appDetails.getUploadDate()); + Image iconImage = null; + for (Image image: details.getImageList()) { + if (image.getImageType() == 4) { + iconImage = image; + break; + } + } + if (iconImage != null) { + app.setIconUrl(iconImage.getImageUrl()); + } + app.setChanges(appDetails.getRecentChangesHtml()); + app.getDeveloper().setName(appDetails.getDeveloperName()); + app.getDeveloper().setEmail(appDetails.getDeveloperEmail()); + app.getDeveloper().setWebsite(appDetails.getDeveloperWebsite()); + app.setPermissions(appDetails.getPermissionList()); + return app; + } + + private GooglePlayAPI getApi() throws IOException, CredentialsEmptyException, CredentialsRejectedException { + if (api == null) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String email = this.email == null ? prefs.getString(AppListActivity.PREFERENCE_EMAIL, "") : this.email; + String password = this.password == null ? prefs.getString(AppListActivity.PREFERENCE_PASSWORD, "") : this.password; + String gsfId = prefs.getString(AppListActivity.PREFERENCE_GSF_ID, ""); + String token = prefs.getString(AppListActivity.PREFERENCE_AUTH_TOKEN, ""); + System.out.println("email " + email); + System.out.println("password " + password); + System.out.println("gsfId " + gsfId); + System.out.println("token " + token); + if (email.isEmpty() || password.isEmpty()) { + throw new CredentialsEmptyException(); + } + + NativeDeviceInfoProvider checkinRequestBuilder = new NativeDeviceInfoProvider(); + checkinRequestBuilder.setContext(context); + checkinRequestBuilder.setLocaleString(Locale.getDefault().toString()); + api = new GooglePlayAPI(email, password); + api.setDeviceInfoProvider(checkinRequestBuilder); + api.setLocale(Locale.getDefault()); + SharedPreferences.Editor prefsEditor = prefs.edit(); + try { + if (gsfId.isEmpty()) { + gsfId = api.getGsfId(); + prefsEditor.putString(AppListActivity.PREFERENCE_GSF_ID, gsfId); + prefsEditor.commit(); + } else { + api.setGsfId(gsfId); + } + System.out.println("new gsfId " + gsfId); + if (token.isEmpty()) { + token = api.getToken(); + prefsEditor.putString(AppListActivity.PREFERENCE_EMAIL, email); + prefsEditor.putString(AppListActivity.PREFERENCE_PASSWORD, password); + prefsEditor.putString(AppListActivity.PREFERENCE_AUTH_TOKEN, token); + prefsEditor.commit(); + } else { + api.setToken(token); + } + System.out.println("new token " + token); + } catch (GooglePlayException e) { + int code = e.getCode(); + // auth/checkin requests answer with 401/403 when credentials are incorrect + // but everything else answers with just 400 + if (code == 400 || code == 401 || code == 403) { + throw new CredentialsRejectedException(e.getMessage(), e); + } + } + } + return api; + } + + public PlayStoreApiWrapper(Context context) { + this.context = context; + } + + public GooglePlayAPI login(String email, String password) throws IOException, CredentialsEmptyException, CredentialsRejectedException { + this.email = email; + this.password = password; + PlayStoreApiWrapper.api = null; + return getApi(); + } + + public void logout() { + this.email = null; + this.password = null; + SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); + prefs.remove(AppListActivity.PREFERENCE_PASSWORD); + prefs.remove(AppListActivity.PREFERENCE_GSF_ID); + prefs.remove(AppListActivity.PREFERENCE_AUTH_TOKEN); + prefs.commit(); + PlayStoreApiWrapper.api = null; + } + + public App getDetails(String packageId) throws IOException, CredentialsEmptyException, CredentialsRejectedException { + return buildApp(getApi().details(packageId).getDocV2()); + } + + public List getDetails(List packageIds) throws IOException, CredentialsEmptyException, CredentialsRejectedException { + List apps = new ArrayList<>(); + for (BulkDetailsEntry details: getApi().bulkDetails(packageIds).getEntryList()) { + App app = buildApp(details.getDoc()); + if (app.getVersionCode() > 0) { + apps.add(app); + } else { + System.out.println("Suspicious package " + app.getPackageName()); + } + } + return apps; + } + + public List search(String query) throws IOException, CredentialsEmptyException, CredentialsRejectedException { + List apps = new ArrayList<>(); + GooglePlayAPI api = getApi(); + if (api.hasNextSearchPage(query)) { + SearchResponse response = getApi().search(query); + if (response.getDocCount() > 0) { + for (DocV2 details: response.getDocList().get(0).getChildList()) { + App app = buildApp(details); + if (app.getVersionCode() > 0) { + apps.add(app); + } else { + System.out.println("Suspicious package " + app.getPackageName()); + } + } + } + } + return apps; + } + + public void download(App app) throws IOException, CredentialsEmptyException, CredentialsRejectedException { + BuyResponse response = getApi().purchase(app.getPackageName(), app.getVersionCode(), app.getOfferType()); + AndroidAppDeliveryData appDeliveryData = response.getPurchaseStatusResponse().getAppDeliveryData(); + + // Download manager cannot download https on old android versions + String downloadUrl = appDeliveryData.getDownloadUrl().replace("https", "http"); + HttpCookie downloadAuthCookie = appDeliveryData.getDownloadAuthCookie(0); + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl)); + request.addRequestHeader("Cookie", downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue()); + String localFilename = app.getPackageName() + "." + String.valueOf(app.getVersionCode()) + ".apk"; + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, localFilename); + request.setTitle(app.getDisplayName()); + + DownloadManager dm = (DownloadManager) this.context.getSystemService(DOWNLOAD_SERVICE); + dm.enqueue(request); + + Intent intent = new Intent(); + intent.setAction(DownloadManager.ACTION_VIEW_DOWNLOADS); + this.context.startActivity(intent); + } + +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/SearchResultActivity.java b/app/src/main/java/com/github/yeriomin/yalpstore/SearchResultActivity.java new file mode 100644 index 00000000..e9b83a04 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/SearchResultActivity.java @@ -0,0 +1,130 @@ +package com.github.yeriomin.yalpstore; + +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.widget.AbsListView; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; + +import com.github.yeriomin.yalpstore.model.App; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class SearchResultActivity extends AppListActivity { + + private String query; + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + String newQuery = getQuery(intent); + if (newQuery != this.query) { + this.data.clear(); + this.query = newQuery; + setTitle(getString(R.string.activity_title_search, this.query)); + loadApps(); + ((SimpleAdapter) getListAdapter()).notifyDataSetChanged(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + this.query = getQuery(getIntent()); + + super.onCreate(savedInstanceState); + + ((TextView) getListView().getEmptyView()).setText(getString(R.string.list_empty_search)); + getListView().setOnScrollListener(new ListView.OnScrollListener() { + + private int lastLastitem; + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { + return; + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + int lastItem = firstVisibleItem + visibleItemCount; + boolean loadMore = lastItem >= totalItemCount; + if (totalItemCount > 0 && loadMore) { + if (lastLastitem != lastItem) { + lastLastitem = lastItem; + loadApps(); + } + } + } + }); + } + + @Override + protected Map formatApp(App app) { + Map map = super.formatApp(app); + map.put(LINE2, getString(R.string.list_line_2_search, app.getInstalls(), app.getRating(), app.getUpdated())); + map.put(ICON, app.getIconUrl()); + return map; + } + + protected void loadApps() { + GoogleApiAsyncTask task = new GoogleApiAsyncTask() { + + private List apps = new ArrayList<>(); + private Set installedPackageNames = new HashSet<>(); + + @Override + protected void onPreExecute() { + super.onPreExecute(); + List installed = getInstalledApps(); + for (App installedApp: installed) { + installedPackageNames.add(installedApp.getPackageName()); + } + } + + @Override + protected Throwable doInBackground(Void... params) { + PlayStoreApiWrapper wrapper = new PlayStoreApiWrapper(getApplicationContext()); + try { + apps.addAll(wrapper.search(query)); + for (App app: apps) { + if (installedPackageNames.contains(app.getPackageName())) { + app.setInstalled(true); + } + } + } catch (Throwable e) { + return e; + } + return null; + } + + @Override + protected void onPostExecute(Throwable e) { + super.onPostExecute(e); + addApps(apps); + } + }; + task.setContext(this); + task.setErrorView((TextView) getListView().getEmptyView()); + task.prepareDialog( + getString(R.string.dialog_message_loading_app_list_search), + getString(R.string.dialog_title_loading_app_list_search) + ); + task.execute(); + } + + private String getQuery(Intent intent) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + return intent.getStringExtra(SearchManager.QUERY); + } + return null; + } + +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/UpdatableAppsActivity.java b/app/src/main/java/com/github/yeriomin/yalpstore/UpdatableAppsActivity.java new file mode 100644 index 00000000..1f61704c --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/UpdatableAppsActivity.java @@ -0,0 +1,97 @@ +package com.github.yeriomin.yalpstore; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.TextView; + +import com.github.yeriomin.yalpstore.model.App; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UpdatableAppsActivity extends AppListActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ((TextView) getListView().getEmptyView()).setText(getString(R.string.list_empty_updates)); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + if (this.data.isEmpty()) { + loadApps(); + } + } + + @Override + protected Map formatApp(App app) { + Map map = super.formatApp(app); + map.put(LINE2, getString(R.string.list_line_2_updatable, app.getUpdated())); + map.put(ICON, app.getIcon()); + return map; + } + + protected void loadApps() { + GoogleApiAsyncTask task = new GoogleApiAsyncTask() { + + private List apps = new ArrayList<>(); + + @Override + protected Throwable doInBackground(Void... params) { + // Building local apps list + List installedAppIds = new ArrayList<>(); + List installedApps = getInstalledApps(); + Map appMap = new HashMap<>(); + for (App installedApp: installedApps) { + String packageName = installedApp.getPackageInfo().packageName; + installedAppIds.add(packageName); + appMap.put(packageName, installedApp); + } + // Requesting info from Google Play Market for installed apps + PlayStoreApiWrapper wrapper = new PlayStoreApiWrapper(this.context); + List appsFromPlayMarket = new ArrayList<>(); + try { + appsFromPlayMarket.addAll(wrapper.getDetails(installedAppIds)); + } catch (Throwable e) { + return e; + } + // Comparing versions and building updatable apps list + for (App appFromMarket: appsFromPlayMarket) { + String packageName = appFromMarket.getPackageName(); + if (null == packageName || packageName.isEmpty()) { + continue; + } + App installedApp = appMap.get(packageName); + if (installedApp.getVersionCode() < appFromMarket.getVersionCode()) { + installedApp.setUpdated(appFromMarket.getUpdated()); + installedApp.setVersionCode(appFromMarket.getVersionCode()); + installedApp.setOfferType(appFromMarket.getOfferType()); + apps.add(installedApp); + } + } + return null; + } + + @Override + protected void onPostExecute(Throwable e) { + super.onPostExecute(e); + addApps(apps); + } + }; + task.setErrorView((TextView) getListView().getEmptyView()); + task.setContext(this); + task.prepareDialog( + getString(R.string.dialog_message_loading_app_list_update), + getString(R.string.dialog_title_loading_app_list_update) + ); + task.execute(); + } + +} + diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/model/App.java b/app/src/main/java/com/github/yeriomin/yalpstore/model/App.java new file mode 100644 index 00000000..cbf56dfb --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/model/App.java @@ -0,0 +1,178 @@ +package com.github.yeriomin.yalpstore.model; + +import android.content.pm.PackageInfo; +import android.graphics.drawable.Drawable; + +import java.util.List; + +public class App { + + private PackageInfo packageInfo; + + private String displayName; + private String versionName; + private int versionCode; + private Version version; + private int offerType; + private String updated; + private long size; + private String installs; + private double rating; + private Drawable icon; + private String iconUrl; + private String changes; + private Developer developer; + private String description; + private List permissions; + private boolean isInstalled; + + public App() { + this.packageInfo = new PackageInfo(); + } + + public App(PackageInfo packageInfo) { + this.setPackageInfo(packageInfo); + } + + public PackageInfo getPackageInfo() { + return packageInfo; + } + + public String getPackageName() { + return packageInfo.packageName; + } + + public void setPackageInfo(PackageInfo packageInfo) { + this.packageInfo = packageInfo; + this.setVersionName(packageInfo.versionName); + this.setVersionCode(packageInfo.versionCode); + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getVersionName() { + return versionName; + } + + public void setVersionName(String versionName) { + this.versionName = versionName; + this.version = new Version(versionName); + } + + public int getVersionCode() { + return versionCode; + } + + public void setVersionCode(int versionCode) { + this.versionCode = versionCode; + } + + public Version getVersion() { + return version; + } + + public int getOfferType() { + return offerType; + } + + public void setOfferType(int offerType) { + this.offerType = offerType; + } + + public String getUpdated() { + return updated; + } + + public void setUpdated(String updated) { + this.updated = updated; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getInstalls() { + return installs; + } + + public void setInstalls(String installs) { + this.installs = installs; + } + + public double getRating() { + return rating; + } + + public void setRating(double rating) { + this.rating = rating; + } + + public Drawable getIcon() { + return this.icon; + } + + public void setIcon(Drawable icon) { + this.icon = icon; + } + + public void setIconUrl(String iconUrl) { + this.iconUrl = iconUrl; + } + + public String getIconUrl() { + return this.iconUrl; + } + + public String getChanges() { + return changes; + } + + public void setChanges(String changes) { + this.changes = changes; + } + + public Developer getDeveloper() { + if (null == developer) { + developer = new Developer(); + } + return developer; + } + + public void setDeveloper(Developer developer) { + this.developer = developer; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + public boolean isInstalled() { + return isInstalled; + } + + public void setInstalled(boolean installed) { + isInstalled = installed; + } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/model/Developer.java b/app/src/main/java/com/github/yeriomin/yalpstore/model/Developer.java new file mode 100644 index 00000000..d57b17bb --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/model/Developer.java @@ -0,0 +1,32 @@ +package com.github.yeriomin.yalpstore.model; + +public class Developer { + + private String name; + private String email; + private String website; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getWebsite() { + return website; + } + + public void setWebsite(String website) { + this.website = website; + } +} diff --git a/app/src/main/java/com/github/yeriomin/yalpstore/model/Version.java b/app/src/main/java/com/github/yeriomin/yalpstore/model/Version.java new file mode 100644 index 00000000..b511f8f1 --- /dev/null +++ b/app/src/main/java/com/github/yeriomin/yalpstore/model/Version.java @@ -0,0 +1,49 @@ +package com.github.yeriomin.yalpstore.model; + +public class Version implements Comparable { + + private String version; + + public Version(String version) { + this.version = normalize(version); + } + + private String normalize(String version) { + return version.replaceAll("[^\\d.]", ""); + } + + @Override + public String toString() { + return this.version; + } + + @Override + public int compareTo(Version other) { + String[] thisParts = this.version.split("\\."); + String[] otherParts = other.toString().split("\\."); + for (int i = 0; i < Math.max(thisParts.length, otherParts.length); i++) { + if (i >= thisParts.length) { + return -1; + } else if (i >= otherParts.length) { + return 1; + } else { + Integer thisPart, otherPart; + try { + thisPart = Integer.valueOf(thisParts[i]); + } catch (NumberFormatException e) { + return -1; + } + try { + otherPart = Integer.valueOf(otherParts[i]); + } catch (NumberFormatException e) { + return 1; + } + int comparison = thisPart.compareTo(otherPart); + if (comparison != 0) { + return comparison; + } + } + } + return 0; + } +} diff --git a/app/src/main/proto/GooglePlay.proto b/app/src/main/proto/GooglePlay.proto new file mode 100644 index 00000000..6b077419 --- /dev/null +++ b/app/src/main/proto/GooglePlay.proto @@ -0,0 +1,994 @@ +option java_package = "com.github.yeriomin.playstoreapi"; +option java_multiple_files = true; + +import "GoogleServicesFramework.proto"; + +message AndroidAppDeliveryData { + optional int64 downloadSize = 1; + optional string signature = 2; + optional string downloadUrl = 3; + repeated AppFileMetadata additionalFile = 4; + repeated HttpCookie downloadAuthCookie = 5; + optional bool forwardLocked = 6; + optional int64 refundTimeout = 7; + optional bool serverInitiated = 8; + optional int64 postInstallRefundWindowMillis = 9; + optional bool immediateStartNeeded = 10; + optional AndroidAppPatchData patchData = 11; + optional EncryptionParams encryptionParams = 12; +} +message AndroidAppPatchData { + optional int32 baseVersionCode = 1; + optional string baseSignature = 2; + optional string downloadUrl = 3; + optional int32 patchFormat = 4; + optional int64 maxPatchSize = 5; +} +message AppFileMetadata { + optional int32 fileType = 1; + optional int32 versionCode = 2; + optional int64 size = 3; + optional string downloadUrl = 4; +} +message EncryptionParams { + optional int32 version = 1; + optional string encryptionKey = 2; + optional string hmacKey = 3; +} +message HttpCookie { + optional string name = 1; + optional string value = 2; +} +message Address { + optional string name = 1; + optional string addressLine1 = 2; + optional string addressLine2 = 3; + optional string city = 4; + optional string state = 5; + optional string postalCode = 6; + optional string postalCountry = 7; + optional string dependentLocality = 8; + optional string sortingCode = 9; + optional string languageCode = 10; + optional string phoneNumber = 11; + optional bool isReduced = 12; + optional string firstName = 13; + optional string lastName = 14; + optional string email = 15; +} +message BookAuthor { + optional string name = 1; + optional string deprecatedQuery = 2; + optional Docid docid = 3; +} +message BookDetails { + repeated BookSubject subject = 3; + optional string publisher = 4; + optional string publicationDate = 5; + optional string isbn = 6; + optional int32 numberOfPages = 7; + optional string subtitle = 8; + repeated BookAuthor author = 9; + optional string readerUrl = 10; + optional string downloadEpubUrl = 11; + optional string downloadPdfUrl = 12; + optional string acsEpubTokenUrl = 13; + optional string acsPdfTokenUrl = 14; + optional bool epubAvailable = 15; + optional bool pdfAvailable = 16; + optional string aboutTheAuthor = 17; + repeated group Identifier = 18 { + optional int32 type = 19; + optional string identifier = 20; + } +} +message BookSubject { + optional string name = 1; + optional string query = 2; + optional string subjectId = 3; +} +message BrowseLink { + optional string name = 1; + optional string dataUrl = 3; +} +message BrowseResponse { + optional string contentsUrl = 1; + optional string promoUrl = 2; + repeated BrowseLink category = 3; + repeated BrowseLink breadcrumb = 4; +} +message AddressChallenge { + optional string responseAddressParam = 1; + optional string responseCheckboxesParam = 2; + optional string title = 3; + optional string descriptionHtml = 4; + repeated FormCheckbox checkbox = 5; + optional Address address = 6; + repeated InputValidationError errorInputField = 7; + optional string errorHtml = 8; + repeated int32 requiredField = 9; +} +message AuthenticationChallenge { + optional int32 authenticationType = 1; + optional string responseAuthenticationTypeParam = 2; + optional string responseRetryCountParam = 3; + optional string pinHeaderText = 4; + optional string pinDescriptionTextHtml = 5; + optional string gaiaHeaderText = 6; + optional string gaiaDescriptionTextHtml = 7; +} +message BuyResponse { + optional PurchaseNotificationResponse purchaseResponse = 1; + optional group CheckoutInfo = 2 { + optional LineItem item = 3; + repeated LineItem subItem = 4; + repeated group CheckoutOption = 5 { + optional string formOfPayment = 6; + optional string encodedAdjustedCart = 7; + optional string instrumentId = 15; + repeated LineItem item = 16; + repeated LineItem subItem = 17; + optional LineItem total = 18; + repeated string footerHtml = 19; + optional int32 instrumentFamily = 29; + repeated int32 deprecatedInstrumentInapplicableReason = 30; + optional bool selectedInstrument = 32; + optional LineItem summary = 33; + repeated string footnoteHtml = 35; + optional Instrument instrument = 43; + optional string purchaseCookie = 45; + repeated string disabledReason = 48; + } + optional string deprecatedCheckoutUrl = 10; + optional string addInstrumentUrl = 11; + repeated string footerHtml = 20; + repeated int32 eligibleInstrumentFamily = 31; + repeated string footnoteHtml = 36; + repeated Instrument eligibleInstrument = 44; + } + optional string continueViaUrl = 8; + optional string purchaseStatusUrl = 9; + optional string checkoutServiceId = 12; + optional bool checkoutTokenRequired = 13; + optional string baseCheckoutUrl = 14; + repeated string tosCheckboxHtml = 37; + optional int32 iabPermissionError = 38; + optional PurchaseStatusResponse purchaseStatusResponse = 39; + optional string purchaseCookie = 46; + optional Challenge challenge = 49; +} +message Challenge { + optional AddressChallenge addressChallenge = 1; + optional AuthenticationChallenge authenticationChallenge = 2; +} +message FormCheckbox { + optional string description = 1; + optional bool checked = 2; + optional bool required = 3; +} +message LineItem { + optional string name = 1; + optional string description = 2; + optional Offer offer = 3; + optional Money amount = 4; +} +message Money { + optional int64 micros = 1; + optional string currencyCode = 2; + optional string formattedAmount = 3; +} +message PurchaseNotificationResponse { + optional int32 status = 1; + optional DebugInfo debugInfo = 2; + optional string localizedErrorMessage = 3; + optional string purchaseId = 4; +} +message PurchaseStatusResponse { + optional int32 status = 1; + optional string statusMsg = 2; + optional string statusTitle = 3; + optional string briefMessage = 4; + optional string infoUrl = 5; + optional LibraryUpdate libraryUpdate = 6; + optional Instrument rejectedInstrument = 7; + optional AndroidAppDeliveryData appDeliveryData = 8; +} + +message Docid { + optional string backendDocid = 1; + optional int32 type = 2; + optional int32 backend = 3; +} +message Install { + optional fixed64 androidId = 1; + optional int32 version = 2; + optional bool bundled = 3; +} +message Offer { + optional int64 micros = 1; + optional string currencyCode = 2; + optional string formattedAmount = 3; + repeated Offer convertedPrice = 4; + optional bool checkoutFlowRequired = 5; + optional int64 fullPriceMicros = 6; + optional string formattedFullAmount = 7; + optional int32 offerType = 8; + optional RentalTerms rentalTerms = 9; + optional int64 onSaleDate = 10; + repeated string promotionLabel = 11; + optional SubscriptionTerms subscriptionTerms = 12; + optional string formattedName = 13; + optional string formattedDescription = 14; +} +message OwnershipInfo { + optional int64 initiationTimestampMsec = 1; + optional int64 validUntilTimestampMsec = 2; + optional bool autoRenewing = 3; + optional int64 refundTimeoutTimestampMsec = 4; + optional int64 postDeliveryRefundWindowMsec = 5; +} +message RentalTerms { + optional int32 grantPeriodSeconds = 1; + optional int32 activatePeriodSeconds = 2; +} +message SubscriptionTerms { + optional TimePeriod recurringPeriod = 1; + optional TimePeriod trialPeriod = 2; +} +message TimePeriod { + optional int32 unit = 1; + optional int32 count = 2; +} +message BillingAddressSpec { + optional int32 billingAddressType = 1; + repeated int32 requiredField = 2; +} +message CarrierBillingCredentials { + optional string value = 1; + optional int64 expiration = 2; +} +message CarrierBillingInstrument { + optional string instrumentKey = 1; + optional string accountType = 2; + optional string currencyCode = 3; + optional int64 transactionLimit = 4; + optional string subscriberIdentifier = 5; + optional EncryptedSubscriberInfo encryptedSubscriberInfo = 6; + optional CarrierBillingCredentials credentials = 7; + optional CarrierTos acceptedCarrierTos = 8; +} +message CarrierBillingInstrumentStatus { + optional CarrierTos carrierTos = 1; + optional bool associationRequired = 2; + optional bool passwordRequired = 3; + optional PasswordPrompt carrierPasswordPrompt = 4; + optional int32 apiVersion = 5; + optional string name = 6; +} +message CarrierTos { + optional CarrierTosEntry dcbTos = 1; + optional CarrierTosEntry piiTos = 2; + optional bool needsDcbTosAcceptance = 3; + optional bool needsPiiTosAcceptance = 4; +} +message CarrierTosEntry { + optional string url = 1; + optional string version = 2; +} +message CreditCardInstrument { + optional int32 type = 1; + optional string escrowHandle = 2; + optional string lastDigits = 3; + optional int32 expirationMonth = 4; + optional int32 expirationYear = 5; + repeated EfeParam escrowEfeParam = 6; +} +message EfeParam { + optional int32 key = 1; + optional string value = 2; +} +message InputValidationError { + optional int32 inputField = 1; + optional string errorMessage = 2; +} +message Instrument { + optional string instrumentId = 1; + optional Address billingAddress = 2; + optional CreditCardInstrument creditCard = 3; + optional CarrierBillingInstrument carrierBilling = 4; + optional BillingAddressSpec billingAddressSpec = 5; + optional int32 instrumentFamily = 6; + optional CarrierBillingInstrumentStatus carrierBillingStatus = 7; + optional string displayTitle = 8; +} +message PasswordPrompt { + optional string prompt = 1; + optional string forgotPasswordUrl = 2; +} +message ContainerMetadata { + optional string browseUrl = 1; + optional string nextPageUrl = 2; + optional double relevance = 3; + optional int64 estimatedResults = 4; + optional string analyticsCookie = 5; + optional bool ordered = 6; +} + +message DebugInfo { + repeated string message = 1; + repeated group Timing = 2 { + optional string name = 3; + optional double timeInMs = 4; + } +} + +message BulkDetailsEntry { + optional DocV2 doc = 1; +} +message BulkDetailsRequest { + repeated string docid = 1; + optional bool includeChildDocs = 2; +} +message BulkDetailsResponse { + repeated BulkDetailsEntry entry = 1; +} +message DetailsResponse { + optional DocV1 docV1 = 1; + optional string analyticsCookie = 2; + optional Review userReview = 3; + optional DocV2 docV2 = 4; + optional string footerHtml = 5; +} +message DeviceConfigurationProto { + optional int32 touchScreen = 1; + optional int32 keyboard = 2; + optional int32 navigation = 3; + optional int32 screenLayout = 4; + optional bool hasHardKeyboard = 5; + optional bool hasFiveWayNavigation = 6; + optional int32 screenDensity = 7; + optional int32 glEsVersion = 8; + repeated string systemSharedLibrary = 9; + repeated string systemAvailableFeature = 10; + repeated string nativePlatform = 11; + optional int32 screenWidth = 12; + optional int32 screenHeight = 13; + repeated string systemSupportedLocale = 14; + repeated string glExtension = 15; + optional int32 deviceClass = 16; + optional int32 maxApkDownloadSizeMb = 17; +} +message Document { + optional Docid docid = 1; + optional Docid fetchDocid = 2; + optional Docid sampleDocid = 3; + optional string title = 4; + optional string url = 5; + repeated string snippet = 6; + optional Offer priceDeprecated = 7; + optional Availability availability = 9; + repeated Image image = 10; + repeated Document child = 11; + optional AggregateRating aggregateRating = 13; + repeated Offer offer = 14; + repeated TranslatedText translatedSnippet = 15; + repeated DocumentVariant documentVariant = 16; + repeated string categoryId = 17; + repeated Document decoration = 18; + repeated Document parent = 19; + optional string privacyPolicyUrl = 20; +} +message DocumentVariant { + optional int32 variationType = 1; + optional Rule rule = 2; + optional string title = 3; + repeated string snippet = 4; + optional string recentChanges = 5; + repeated TranslatedText autoTranslation = 6; + repeated Offer offer = 7; + optional int64 channelId = 9; + repeated Document child = 10; + repeated Document decoration = 11; +} +message Image { + optional int32 imageType = 1; + optional group Dimension = 2 { + optional int32 width = 3; + optional int32 height = 4; + } + optional string imageUrl = 5; + optional string altTextLocalized = 6; + optional string secureUrl = 7; + optional int32 positionInSequence = 8; + optional bool supportsFifeUrlOptions = 9; + optional group Citation = 10 { + optional string titleLocalized = 11; + optional string url = 12; + } +} +message TranslatedText { + optional string text = 1; + optional string sourceLocale = 2; + optional string targetLocale = 3; +} + +message PlusOneData { + optional bool setByUser = 1; + optional int64 total = 2; + optional int64 circlesTotal = 3; + repeated PlusPerson circlesPeople = 4; +} +message PlusPerson { + optional string displayName = 2; + optional string profileImageUrl = 4; +} + +message AlbumDetails { + optional string name = 1; + optional MusicDetails details = 2; + optional ArtistDetails displayArtist = 3; +} +message AppDetails { + optional string developerName = 1; + optional int32 majorVersionNumber = 2; + optional int32 versionCode = 3; + optional string versionString = 4; + optional string title = 5; + repeated string appCategory = 7; + optional int32 contentRating = 8; + optional int64 installationSize = 9; + repeated string permission = 10; + optional string developerEmail = 11; + optional string developerWebsite = 12; + optional string numDownloads = 13; + optional string packageName = 14; + optional string recentChangesHtml = 15; + optional string uploadDate = 16; + repeated FileMetadata file = 17; + optional string appType = 18; +} +message ArtistDetails { + optional string detailsUrl = 1; + optional string name = 2; + optional ArtistExternalLinks externalLinks = 3; +} +message ArtistExternalLinks { + repeated string websiteUrl = 1; + optional string googlePlusProfileUrl = 2; + optional string youtubeChannelUrl = 3; +} +message DocumentDetails { + optional AppDetails appDetails = 1; + optional AlbumDetails albumDetails = 2; + optional ArtistDetails artistDetails = 3; + optional SongDetails songDetails = 4; + optional BookDetails bookDetails = 5; + optional VideoDetails videoDetails = 6; + optional SubscriptionDetails subscriptionDetails = 7; + optional MagazineDetails magazineDetails = 8; + optional TvShowDetails tvShowDetails = 9; + optional TvSeasonDetails tvSeasonDetails = 10; + optional TvEpisodeDetails tvEpisodeDetails = 11; +} +message FileMetadata { + optional int32 fileType = 1; + optional int32 versionCode = 2; + optional int64 size = 3; +} +message MagazineDetails { + optional string parentDetailsUrl = 1; + optional string deviceAvailabilityDescriptionHtml = 2; + optional string psvDescription = 3; + optional string deliveryFrequencyDescription = 4; +} +message MusicDetails { + optional int32 censoring = 1; + optional int32 durationSec = 2; + optional string originalReleaseDate = 3; + optional string label = 4; + repeated ArtistDetails artist = 5; + repeated string genre = 6; + optional string releaseDate = 7; + repeated int32 releaseType = 8; +} +message SongDetails { + optional string name = 1; + optional MusicDetails details = 2; + optional string albumName = 3; + optional int32 trackNumber = 4; + optional string previewUrl = 5; + optional ArtistDetails displayArtist = 6; +} +message SubscriptionDetails { + optional int32 subscriptionPeriod = 1; +} +message Trailer { + optional string trailerId = 1; + optional string title = 2; + optional string thumbnailUrl = 3; + optional string watchUrl = 4; + optional string duration = 5; +} +message TvEpisodeDetails { + optional string parentDetailsUrl = 1; + optional int32 episodeIndex = 2; + optional string releaseDate = 3; +} +message TvSeasonDetails { + optional string parentDetailsUrl = 1; + optional int32 seasonIndex = 2; + optional string releaseDate = 3; + optional string broadcaster = 4; +} +message TvShowDetails { + optional int32 seasonCount = 1; + optional int32 startYear = 2; + optional int32 endYear = 3; + optional string broadcaster = 4; +} +message VideoCredit { + optional int32 creditType = 1; + optional string credit = 2; + repeated string name = 3; +} +message VideoDetails { + repeated VideoCredit credit = 1; + optional string duration = 2; + optional string releaseDate = 3; + optional string contentRating = 4; + optional int64 likes = 5; + optional int64 dislikes = 6; + repeated string genre = 7; + repeated Trailer trailer = 8; + repeated VideoRentalTerm rentalTerm = 9; +} +message VideoRentalTerm { + optional int32 offerType = 1; + optional string offerAbbreviation = 2; + optional string rentalHeader = 3; + repeated group Term = 4 { + optional string header = 5; + optional string body = 6; + } +} +message Bucket { + repeated DocV1 document = 1; + optional bool multiCorpus = 2; + optional string title = 3; + optional string iconUrl = 4; + optional string fullContentsUrl = 5; + optional double relevance = 6; + optional int64 estimatedResults = 7; + optional string analyticsCookie = 8; + optional string fullContentsListUrl = 9; + optional string nextPageUrl = 10; + optional bool ordered = 11; +} +message ListResponse { + repeated Bucket bucket = 1; + repeated DocV2 doc = 2; +} +message DocV1 { + optional Document finskyDoc = 1; + optional string docid = 2; + optional string detailsUrl = 3; + optional string reviewsUrl = 4; + optional string relatedListUrl = 5; + optional string moreByListUrl = 6; + optional string shareUrl = 7; + optional string creator = 8; + optional DocumentDetails details = 9; + optional string descriptionHtml = 10; + optional string relatedBrowseUrl = 11; + optional string moreByBrowseUrl = 12; + optional string relatedHeader = 13; + optional string moreByHeader = 14; + optional string title = 15; + optional PlusOneData plusOneData = 16; + optional string warningMessage = 17; +} + +message DocV2 { + optional string docid = 1; + optional string backendDocid = 2; + optional int32 docType = 3; + optional int32 backendId = 4; + optional string title = 5; + optional string creator = 6; + optional string descriptionHtml = 7; + repeated Offer offer = 8; + optional Availability availability = 9; + repeated Image image = 10; + repeated DocV2 child = 11; + optional ContainerMetadata containerMetadata = 12; + optional DocumentDetails details = 13; + optional AggregateRating aggregateRating = 14; + + optional string detailsUrl = 16; + optional string shareUrl = 17; + optional string reviewsUrl = 18; + optional string backendUrl = 19; + optional string purchaseDetailsUrl = 20; + optional bool detailsReusable = 21; + optional string subtitle = 22; +} +message EncryptedSubscriberInfo { + optional string data = 1; + optional string encryptedKey = 2; + optional string signature = 3; + optional string initVector = 4; + optional int32 googleKeyVersion = 5; + optional int32 carrierKeyVersion = 6; +} +message Availability { + optional int32 restriction = 5; + optional int32 offerType = 6; + optional Rule rule = 7; + repeated group PerDeviceAvailabilityRestriction = 9 { + optional fixed64 androidId = 10; + optional int32 deviceRestriction = 11; + optional int64 channelId = 12; + optional FilterEvaluationInfo filterInfo = 15; + } + optional bool availableIfOwned = 13; + repeated Install install = 14; + optional FilterEvaluationInfo filterInfo = 16; + optional OwnershipInfo ownershipInfo = 17; +} +message FilterEvaluationInfo { + repeated RuleEvaluation ruleEvaluation = 1; +} +message Rule { + optional bool negate = 1; + optional int32 operator = 2; + optional int32 key = 3; + repeated string stringArg = 4; + repeated int64 longArg = 5; + repeated double doubleArg = 6; + repeated Rule subrule = 7; + optional int32 responseCode = 8; + optional string comment = 9; + repeated fixed64 stringArgHash = 10; + repeated int32 constArg = 11; +} +message RuleEvaluation { + optional Rule rule = 1; + repeated string actualStringValue = 2; + repeated int64 actualLongValue = 3; + repeated bool actualBoolValue = 4; + repeated double actualDoubleValue = 5; +} +message LibraryAppDetails { + optional string certificateHash = 2; + optional int64 refundTimeoutTimestampMsec = 3; + optional int64 postDeliveryRefundWindowMsec = 4; +} +message LibraryInAppDetails { + optional string signedPurchaseData = 1; + optional string signature = 2; +} +message LibraryMutation { + optional Docid docid = 1; + optional int32 offerType = 2; + optional int64 documentHash = 3; + optional bool deleted = 4; + optional LibraryAppDetails appDetails = 5; + optional LibrarySubscriptionDetails subscriptionDetails = 6; + optional LibraryInAppDetails inAppDetails = 7; +} +message LibrarySubscriptionDetails { + optional int64 initiationTimestampMsec = 1; + optional int64 validUntilTimestampMsec = 2; + optional bool autoRenewing = 3; + optional int64 trialUntilTimestampMsec = 4; +} +message LibraryUpdate { + optional int32 status = 1; + optional int32 corpus = 2; + optional bytes serverToken = 3; + repeated LibraryMutation mutation = 4; + optional bool hasMore = 5; + optional string libraryId = 6; +} + +message AndroidAppNotificationData { + optional int32 versionCode = 1; + optional string assetId = 2; +} +message InAppNotificationData { + optional string checkoutOrderId = 1; + optional string inAppNotificationId = 2; +} +message LibraryDirtyData { + optional int32 backend = 1; +} +message Notification { + optional int32 notificationType = 1; + optional int64 timestamp = 3; + optional Docid docid = 4; + optional string docTitle = 5; + optional string userEmail = 6; + optional AndroidAppNotificationData appData = 7; + optional AndroidAppDeliveryData appDeliveryData = 8; + optional PurchaseRemovalData purchaseRemovalData = 9; + optional UserNotificationData userNotificationData = 10; + optional InAppNotificationData inAppNotificationData = 11; + optional PurchaseDeclinedData purchaseDeclinedData = 12; + optional string notificationId = 13; + optional LibraryUpdate libraryUpdate = 14; + optional LibraryDirtyData libraryDirtyData = 15; +} +message PurchaseDeclinedData { + optional int32 reason = 1; + optional bool showNotification = 2; +} +message PurchaseRemovalData { + optional bool malicious = 1; +} +message UserNotificationData { + optional string notificationTitle = 1; + optional string notificationText = 2; + optional string tickerText = 3; + optional string dialogTitle = 4; + optional string dialogText = 5; +} + +message AggregateRating { + optional int32 type = 1; + optional float starRating = 2; + optional uint64 ratingsCount = 3; + optional uint64 oneStarRatings = 4; + optional uint64 twoStarRatings = 5; + optional uint64 threeStarRatings = 6; + optional uint64 fourStarRatings = 7; + optional uint64 fiveStarRatings = 8; + optional uint64 thumbsUpCount = 9; + optional uint64 thumbsDownCount = 10; + optional uint64 commentCount = 11; + optional double bayesianMeanRating = 12; +} +message Payload { + optional ListResponse listResponse = 1; + optional DetailsResponse detailsResponse = 2; + optional ReviewResponse reviewResponse = 3; + optional BuyResponse buyResponse = 4; + optional SearchResponse searchResponse = 5; + optional BrowseResponse browseResponse = 7; + optional PurchaseStatusResponse purchaseStatusResponse = 8; + optional BulkDetailsResponse bulkDetailsResponse = 19; + optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 25; + optional AndroidCheckinResponse androidCheckinResponse = 26; +} +message PreFetch { + optional string url = 1; + optional bytes response = 2; + optional string etag = 3; + optional int64 ttl = 4; + optional int64 softTtl = 5; +} +message ResponseWrapper { + optional Payload payload = 1; + optional ServerCommands commands = 2; + repeated PreFetch preFetch = 3; + repeated Notification notification = 4; +} +message ServerCommands { + optional bool clearCache = 1; + optional string displayErrorMessage = 2; + optional string logErrorStacktrace = 3; +} +message GetReviewsResponse { + repeated Review review = 1; + optional int64 matchingCount = 2; +} +message Review { + optional string authorName = 1; + optional string url = 2; + optional string source = 3; + optional string documentVersion = 4; + optional int64 timestampMsec = 5; + optional int32 starRating = 6; + optional string title = 7; + optional string comment = 8; + optional string commentId = 9; + optional string deviceName = 19; + optional string replyText = 29; + optional int64 replyTimestampMsec = 30; + optional Author author = 31; +} +message Author { + optional string name = 2; + optional Avatar urls = 5; +} +message Avatar { + optional bool unknown1 = 1; + optional string url = 5; + optional string secureUrl = 7; + optional bool unknown2 = 9; +} +message ReviewResponse { + optional GetReviewsResponse getResponse = 1; + optional string nextPageUrl = 2; +} + +message RelatedSearch { + optional string searchUrl = 1; + optional string header = 2; + optional int32 backendId = 3; + optional int32 docType = 4; + optional bool current = 5; +} +message SearchResponse { + optional string originalQuery = 1; + optional string suggestedQuery = 2; + optional bool aggregateQuery = 3; + repeated Bucket bucket = 4; + repeated DocV2 doc = 5; + repeated RelatedSearch relatedSearch = 6; +} + + +message UploadDeviceConfigRequest { + optional DeviceConfigurationProto deviceConfiguration = 1; + optional string manufacturer = 2; + optional string gcmRegistrationId = 3; +} +message UploadDeviceConfigResponse { + optional string uploadDeviceConfigToken = 1; +} +message AndroidCheckinRequest { + optional string imei = 1; + optional int64 id = 2; + optional string digest = 3; + optional AndroidCheckinProto checkin = 4; + optional string desiredBuild = 5; + optional string locale = 6; + optional int64 loggingId = 7; + optional string marketCheckin = 8; + repeated string macAddr = 9; + optional string meid = 10; + repeated string accountCookie = 11; + optional string timeZone = 12; + optional fixed64 securityToken = 13; + optional int32 version = 14; + repeated string otaCert = 15; + optional string serialNumber = 16; + optional string esn = 17; + optional DeviceConfigurationProto deviceConfiguration = 18; + repeated string macAddrType = 19; + optional int32 fragment = 20; + optional string userName = 21; + optional int32 userSerialNumber = 22; +} +message AndroidCheckinResponse { + optional bool statsOk = 1; + repeated AndroidIntentProto intent = 2; + optional int64 timeMsec = 3; + optional string digest = 4; + repeated GservicesSetting setting = 5; + optional bool marketOk = 6; + optional fixed64 androidId = 7; + optional fixed64 securityToken = 8; + optional bool settingsDiff = 9; + repeated string deleteSetting = 10; +} +message GservicesSetting { + optional bytes name = 1; + optional bytes value = 2; +} +message AndroidBuildProto { + optional string id = 1; + optional string product = 2; + optional string carrier = 3; + optional string radio = 4; + optional string bootloader = 5; + optional string client = 6; + optional int64 timestamp = 7; + optional int32 googleServices = 8; + optional string device = 9; + optional int32 sdkVersion = 10; + optional string model = 11; + optional string manufacturer = 12; + optional string buildProduct = 13; + optional bool otaInstalled = 14; +} +message AndroidCheckinProto { + optional AndroidBuildProto build = 1; + optional int64 lastCheckinMsec = 2; + repeated AndroidEventProto event = 3; + repeated AndroidStatisticProto stat = 4; + repeated string requestedGroup = 5; + optional string cellOperator = 6; + optional string simOperator = 7; + optional string roaming = 8; + optional int32 userNumber = 9; +} +message AndroidEventProto { + optional string tag = 1; + optional string value = 2; + optional int64 timeMsec = 3; +} +message AndroidIntentProto { + optional string action = 1; + optional string dataUri = 2; + optional string mimeType = 3; + optional string javaClass = 4; + repeated group Extra = 5 { + optional string name = 6; + optional string value = 7; + } +} +message AndroidStatisticProto { + optional string tag = 1; + optional int32 count = 2; + optional float sum = 3; +} +message ClientLibraryState { + optional int32 corpus = 1; + optional bytes serverToken = 2; + optional int64 hashCodeSum = 3; + optional int32 librarySize = 4; + optional string libraryId = 5; +} +message AndroidDataUsageProto { + optional int32 version = 1; + optional int64 currentReportMsec = 2; + repeated KeyToPackageNameMapping keyToPackageNameMapping = 3; + repeated PayloadLevelAppStat payloadLevelAppStat = 4; + repeated IpLayerNetworkStat ipLayerNetworkStat = 5; +} +message AndroidUsageStatsReport { + optional int64 androidId = 1; + optional int64 loggingId = 2; + optional UsageStatsExtensionProto usageStats = 3; +} +message AppBucket { + optional int64 bucketStartMsec = 1; + optional int64 bucketDurationMsec = 2; + repeated StatCounters statCounters = 3; + optional int64 operationCount = 4; +} +message CounterData { + optional int64 bytes = 1; + optional int64 packets = 2; +} +message IpLayerAppStat { + optional int32 packageKey = 1; + optional int32 applicationTag = 2; + repeated AppBucket ipLayerAppBucket = 3; +} +message IpLayerNetworkBucket { + optional int64 bucketStartMsec = 1; + optional int64 bucketDurationMsec = 2; + repeated StatCounters statCounters = 3; + optional int64 networkActiveDuration = 4; +} +message IpLayerNetworkStat { + optional string networkDetails = 1; + optional int32 type = 2; + repeated IpLayerNetworkBucket ipLayerNetworkBucket = 3; + repeated IpLayerAppStat ipLayerAppStat = 4; +} +message KeyToPackageNameMapping { + optional int32 packageKey = 1; + optional string uidName = 2; + repeated PackageInfo sharedPackageList = 3; +} +message PackageInfo { + optional string pkgName = 1; + optional int32 versionCode = 2; +} +message PayloadLevelAppStat { + optional int32 packageKey = 1; + optional int32 applicationTag = 2; + repeated AppBucket payloadLevelAppBucket = 3; +} +message StatCounters { + optional int32 networkProto = 1; + optional int32 direction = 2; + optional CounterData counterData = 3; + optional int32 fgBg = 4; +} +message UsageStatsExtensionProto { + optional AndroidDataUsageProto dataUsage = 1; +} diff --git a/app/src/main/proto/GoogleServicesFramework.proto b/app/src/main/proto/GoogleServicesFramework.proto new file mode 100644 index 00000000..5a25b945 --- /dev/null +++ b/app/src/main/proto/GoogleServicesFramework.proto @@ -0,0 +1,137 @@ +option java_package = "com.github.yeriomin.playstoreapi.gsf"; +option java_multiple_files = true; + +message LoginRequest { + optional string packetid = 1; + optional string domain = 2; + optional string user = 3; + optional string resource = 4; + optional string token = 5; + optional string deviceid = 6; + optional int64 lastrmqid = 7; + repeated Setting settings = 8; + optional int32 compress = 9; + repeated string persistentids = 10; + optional bool includestreamidinprotobuf = 11; + optional bool adaptiveheartbeat = 12; + optional HeartBeatStat heartbeatstat = 13; + optional bool usermq2 = 14; + optional int64 accountid = 15; + optional int64 unknown1 = 16; + optional int32 networktype = 17; +} +message HeartBeatStat { + optional string ip = 1; + optional bool timeout = 2; + optional int64 interval = 3; +} +message Setting { + optional string key = 1; + optional string value = 2; +} + +message HeartBeatConfig { + optional int64 interval = 3; + optional string ip = 2; + optional bool uploadstat = 1; +} + +message LoginResponse { + optional string packetid = 1; + optional string jid = 2; + optional int64 servertimestamp = 8; + optional HeartBeatConfig heartbeatconfig = 7; + repeated Setting settings = 4; + optional int32 laststreamid = 6; + optional int32 streamid = 5; + optional XMPPError error = 3; +} + +message XMPPError { + optional int32 code = 1; + optional string message = 2; + optional string type = 3; + repeated Extension extension = 4; +} + +message Extension { + optional int32 code = 1; + optional string message = 2; +} + +message BindAccountRequest { + optional string packetid = 1; + optional string domain = 2; + optional string user = 3; + optional string resource = 4; + optional int64 accountid = 9; + optional string token = 5; +} + +message BindAccountResponse { + optional string packetid = 1; + optional string jid = 2; + optional int32 laststreamid = 5; + optional int32 streamid = 4; + optional XMPPError error = 3; +} + +message Close{ +} + +message HeartbeatAck{ +} + +message DataMessageStanza{ + optional int64 rmqid = 1; + optional string packetid = 2; + optional string from = 3; + optional string to = 4; + optional string category = 5; + optional string token = 6; + repeated AppData appdata = 7; + optional bool fromtrustedserver = 8; + optional string rmq2id = 9; + optional int32 streamid =10; + optional int32 laststreamid = 11; + optional string permission = 12; + optional string regid = 13; + optional string pkgsignature = 14; + +} + +message AppData { + optional string key = 1; + optional string value = 2; +} + +message IQStanza { + optional int64 rmqid = 1; + optional int32 type = 2; + optional string packetid = 3; + optional string from = 4; + optional string to = 5; + optional XMPPError error = 6; + optional Extension extension = 7; + optional string rmq2id = 8; + optional int32 streamid = 9; + optional int32 laststreamid =10; + optional int64 accountid=11; +} + +message State { + optional bool state1 = 1; + optional bool state2 = 2; +} + +message PostAuthBatchQuery { + optional bool online = 1; + optional bool deviceidle = 2; + optional bool showmobileindicator = 3; + optional int32 sharedstatusversion = 4; + optional string rosteretag = 5; + optional string otretag = 6; + optional string avatarhash = 7; + optional string vcardquerystanzaid = 8; + optional int32 capabilities = 9; +} diff --git a/app/src/main/res/drawable-hdpi/ic_logout.png b/app/src/main/res/drawable-hdpi/ic_logout.png new file mode 100644 index 00000000..a2a2aae5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_refresh.png b/app/src/main/res/drawable-hdpi/ic_refresh.png new file mode 100644 index 00000000..80ce86ff Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_refresh.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_search.png b/app/src/main/res/drawable-hdpi/ic_search.png new file mode 100644 index 00000000..0cfc65c4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_logout.png b/app/src/main/res/drawable-mdpi/ic_logout.png new file mode 100644 index 00000000..ed69ff9b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_refresh.png b/app/src/main/res/drawable-mdpi/ic_refresh.png new file mode 100644 index 00000000..cad76b0f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_refresh.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_search.png b/app/src/main/res/drawable-mdpi/ic_search.png new file mode 100644 index 00000000..c5b33f51 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_logout.png b/app/src/main/res/drawable-xhdpi/ic_logout.png new file mode 100644 index 00000000..d937cf19 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_refresh.png b/app/src/main/res/drawable-xhdpi/ic_refresh.png new file mode 100644 index 00000000..3309e91f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_refresh.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search.png b/app/src/main/res/drawable-xhdpi/ic_search.png new file mode 100644 index 00000000..abc4036c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_logout.png b/app/src/main/res/drawable-xxhdpi/ic_logout.png new file mode 100644 index 00000000..b99d9ade Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_refresh.png new file mode 100644 index 00000000..59a92c1a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_refresh.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search.png b/app/src/main/res/drawable-xxhdpi/ic_search.png new file mode 100644 index 00000000..d3dd5af9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_logout.png b/app/src/main/res/drawable-xxxhdpi/ic_logout.png new file mode 100644 index 00000000..22d04376 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_refresh.png b/app/src/main/res/drawable-xxxhdpi/ic_refresh.png new file mode 100644 index 00000000..9569cbf4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_refresh.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_search.png b/app/src/main/res/drawable-xxxhdpi/ic_search.png new file mode 100644 index 00000000..1f03c4a5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_search.png differ diff --git a/app/src/main/res/layout/applist_activity_layout.xml b/app/src/main/res/layout/applist_activity_layout.xml new file mode 100644 index 00000000..88fff80d --- /dev/null +++ b/app/src/main/res/layout/applist_activity_layout.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/credentials_dialog_layout.xml b/app/src/main/res/layout/credentials_dialog_layout.xml new file mode 100644 index 00000000..953f8a9a --- /dev/null +++ b/app/src/main/res/layout/credentials_dialog_layout.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + +