├── .github
└── workflows
│ └── gradle.yml
├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── jarRepositories.xml
├── libraries-with-intellij-classes.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── LICENSE
├── README.md
├── build.gradle
├── cover.jpeg
├── gpm.json
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
├── java
│ └── com
│ │ └── akdeniz
│ │ └── googleplaycrawler
│ │ ├── DownloadData.java
│ │ ├── GooglePlay.java
│ │ ├── GooglePlayAPI.java
│ │ ├── GooglePlayException.java
│ │ ├── Identity.java
│ │ ├── Utils.java
│ │ ├── gsf
│ │ └── GoogleServicesFramework.java
│ │ └── misc
│ │ ├── Base64.java
│ │ ├── DummyX509TrustManager.java
│ │ └── HexDumpEncoder.java
├── kotlin
│ └── com
│ │ └── github
│ │ └── theapache64
│ │ └── gpa
│ │ ├── api
│ │ ├── Play.kt
│ │ └── PlayUtils.kt
│ │ ├── core
│ │ ├── SearchEngineResultPage.kt
│ │ ├── Unwrap.kt
│ │ └── net
│ │ │ ├── DefaultTlsAuthentication.kt
│ │ │ ├── DroidConnectionSocketFactory.kt
│ │ │ ├── DroidSocket.kt
│ │ │ ├── JellyBeanTlsClient.kt
│ │ │ └── OrderedHashtable.kt
│ │ └── model
│ │ └── Account.kt
└── resources
│ └── com
│ └── akdeniz
│ └── googleplaycrawler
│ └── crypt.properties
└── test
└── kotlin
└── com
└── github
└── theapache64
└── gpa
├── api
└── PlayTest.kt
└── utils
└── CoroutinesTestUtil.kt
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Gradle
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
3 |
4 | name: Java CI with Gradle
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up JDK 1.8
20 | uses: actions/setup-java@v1
21 | with:
22 | java-version: 1.8
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x gradlew
25 | - name: Build with Gradle
26 | env:
27 | PLAY_API_GOOGLE_USERNAME: ${{ secrets.PLAY_API_GOOGLE_USERNAME }}
28 | PLAY_API_GOOGLE_PASSWORD: ${{ secrets.PLAY_API_GOOGLE_PASSWORD }}
29 | run: ./gradlew build
30 |
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,intellij,gradle
4 |
5 | ### Intellij ###
6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8 |
9 | # User-specific stuff
10 | .idea/**/workspace.xml
11 | .idea/**/tasks.xml
12 | .idea/**/usage.statistics.xml
13 | .idea/**/dictionaries
14 | .idea/**/shelf
15 |
16 | # Generated files
17 | .idea/**/contentModel.xml
18 |
19 | # Sensitive or high-churn files
20 | .idea/**/dataSources/
21 | .idea/**/dataSources.ids
22 | .idea/**/dataSources.local.xml
23 | .idea/**/sqlDataSources.xml
24 | .idea/**/dynamic.xml
25 | .idea/**/uiDesigner.xml
26 | .idea/**/dbnavigator.xml
27 |
28 | # Gradle
29 | .idea/**/gradle.xml
30 | .idea/**/libraries
31 |
32 | # Gradle and Maven with auto-import
33 | # When using Gradle or Maven with auto-import, you should exclude module files,
34 | # since they will be recreated, and may cause churn. Uncomment if using
35 | # auto-import.
36 | # .idea/artifacts
37 | # .idea/compiler.xml
38 | # .idea/jarRepositories.xml
39 | # .idea/modules.xml
40 | # .idea/*.iml
41 | # .idea/modules
42 | # *.iml
43 | # *.ipr
44 |
45 | # CMake
46 | cmake-build-*/
47 |
48 | # Mongo Explorer plugin
49 | .idea/**/mongoSettings.xml
50 |
51 | # File-based project format
52 | *.iws
53 |
54 | # IntelliJ
55 | out/
56 |
57 | # mpeltonen/sbt-idea plugin
58 | .idea_modules/
59 |
60 | # JIRA plugin
61 | atlassian-ide-plugin.xml
62 |
63 | # Cursive Clojure plugin
64 | .idea/replstate.xml
65 |
66 | # Crashlytics plugin (for Android Studio and IntelliJ)
67 | com_crashlytics_export_strings.xml
68 | crashlytics.properties
69 | crashlytics-build.properties
70 | fabric.properties
71 |
72 | # Editor-based Rest Client
73 | .idea/httpRequests
74 |
75 | # Android studio 3.1+ serialized cache file
76 | .idea/caches/build_file_checksums.ser
77 |
78 | ### Intellij Patch ###
79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
80 |
81 | # *.iml
82 | # modules.xml
83 | # .idea/misc.xml
84 | # *.ipr
85 |
86 | # Sonarlint plugin
87 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
88 | .idea/**/sonarlint/
89 |
90 | # SonarQube Plugin
91 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
92 | .idea/**/sonarIssues.xml
93 |
94 | # Markdown Navigator plugin
95 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
96 | .idea/**/markdown-navigator.xml
97 | .idea/**/markdown-navigator-enh.xml
98 | .idea/**/markdown-navigator/
99 |
100 | # Cache file creation bug
101 | # See https://youtrack.jetbrains.com/issue/JBR-2257
102 | .idea/$CACHE_FILE$
103 |
104 | # CodeStream plugin
105 | # https://plugins.jetbrains.com/plugin/12206-codestream
106 | .idea/codestream.xml
107 |
108 | ### Kotlin ###
109 | # Compiled class file
110 | *.class
111 |
112 | # Log file
113 | *.log
114 |
115 | # BlueJ files
116 | *.ctxt
117 |
118 | # Mobile Tools for Java (J2ME)
119 | .mtj.tmp/
120 |
121 | # Package Files #
122 | *.jar
123 | *.war
124 | *.nar
125 | *.ear
126 | *.zip
127 | *.tar.gz
128 | *.rar
129 |
130 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
131 | hs_err_pid*
132 |
133 | ### Gradle ###
134 | .gradle
135 | build/
136 |
137 | # Ignore Gradle GUI config
138 | gradle-app.setting
139 |
140 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
141 | !gradle-wrapper.jar
142 |
143 | # Cache of project
144 | .gradletasknamecache
145 |
146 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
147 | # gradle/wrapper/gradle-wrapper.properties
148 |
149 | ### Gradle Patch ###
150 | **/build/
151 |
152 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle
153 | build
154 | TestAccount.kt
155 | *.apk
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | > A coroutines based Kotlin library to access play store
12 |
13 | ## 🛠 Installation
14 |
15 | ```groovy
16 | repositories {
17 | maven { url = uri("https://jitpack.io") }
18 | }
19 |
20 | dependencies {
21 | implementation("com.google.protobuf:protobuf-java:3.14.0")
22 | implementation("com.github.theapache64:google-play-api:latest.version")
23 | }
24 | ```
25 |
26 |
27 | ## ⌨️ Usage
28 |
29 | ```kotlin
30 | val username = "example@gmail.com"
31 | val password = "pass1234"
32 |
33 | // Logging in
34 | val account = Play.login(username, password)
35 |
36 | // Creating API using logged in account
37 | val api = Play.getApi(account)
38 |
39 | // Accessing API
40 | val appDetails = api.details(packageName) // to get all app details
41 | val downloadData = api.download("com.whatsapp") // to download APK
42 |
43 | // and much more...
44 | ```
45 |
46 | ## 🥼 Run tests
47 |
48 | ```shell script
49 | ./gradlew test
50 | ```
51 |
52 | ## ✍️ Author
53 |
54 | 👤 **theapache64**
55 |
56 | * Twitter: @theapache64
57 | * Email: theapache64@gmail.com
58 |
59 | This library is a combination of APIs collected from `raccoon4` and `playcrawler`.
60 | All credit goes to them.
61 |
62 | ## 🤝 Contributing
63 |
64 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any
65 | contributions you make are **greatly appreciated**.
66 |
67 | 1. Open an issue first to discuss what you would like to change.
68 | 1. Fork the Project
69 | 1. Create your feature branch (`git checkout -b feature/amazing-feature`)
70 | 1. Commit your changes (`git commit -m 'Add some amazing feature'`)
71 | 1. Push to the branch (`git push origin feature/amazing-feature`)
72 | 1. Open a pull request
73 |
74 | Please make sure to update tests as appropriate.
75 |
76 | ## ❤ Show your support
77 |
78 | Give a ⭐️ if this project helped you!
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | ## 📝 License
93 |
94 | ```
95 | Copyright © 2021 - theapache64
96 |
97 | Licensed under the Apache License, Version 2.0 (the "License");
98 | you may not use this file except in compliance with the License.
99 | You may obtain a copy of the License at
100 |
101 | http://www.apache.org/licenses/LICENSE-2.0
102 |
103 | Unless required by applicable law or agreed to in writing, software
104 | distributed under the License is distributed on an "AS IS" BASIS,
105 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
106 | See the License for the specific language governing permissions and
107 | limitations under the License.
108 | ```
109 |
110 | _This README was generated by [readgen](https://github.com/theapache64/readgen)_ ❤
111 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'maven'
4 | id 'maven-publish'
5 | id 'org.jetbrains.kotlin.jvm' version '1.4.30'
6 | }
7 |
8 | group 'com.github.theapache64'
9 | version '0.0.9'
10 |
11 | repositories {
12 | jcenter()
13 | mavenCentral()
14 | }
15 |
16 | test {
17 | useJUnitPlatform()
18 | environment("PLAY_API_GOOGLE_USERNAME", System.getenv("PLAY_API_GOOGLE_USERNAME"))
19 | environment("PLAY_API_GOOGLE_PASSWORD", System.getenv("PLAY_API_GOOGLE_PASSWORD"))
20 | }
21 |
22 |
23 | dependencies {
24 | implementation "org.jetbrains.kotlin:kotlin-stdlib"
25 |
26 | // Akdeniz Deps
27 | implementation 'com.google.protobuf:protobuf-java:3.14.0'
28 | implementation 'org.apache.httpcomponents:httpclient:4.5.13'
29 | implementation 'org.apache.httpcomponents:httpcore:4.4.14'
30 |
31 | // Bouncy Castle Provider : The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms.
32 | // This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography
33 | // APIs for JDK 1.5 and up.
34 | implementation 'org.bouncycastle:bctls-jdk15on:1.57'
35 |
36 | // Kotlinx Coroutines Core : Coroutines support libraries for Kotlin
37 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
38 | implementation 'org.junit.jupiter:junit-jupiter:5.4.2'
39 |
40 | // Expekt : An assertion library for Kotlin
41 | testImplementation 'com.theapache64:expekt:0.0.1'
42 | }
43 |
44 | task sourcesJar(type: Jar, dependsOn: classes) {
45 | archiveClassifier.set('sources')
46 | from sourceSets.main.allSource
47 | }
48 |
49 | javadoc.failOnError = false
50 | task javadocJar(type: Jar, dependsOn: javadoc) {
51 | archiveClassifier.set('javadoc')
52 | from javadoc.destinationDir
53 | }
54 |
55 | artifacts {
56 | archives sourcesJar
57 | archives javadocJar
58 | }
59 |
60 | def pomConfig = {
61 | licenses {
62 | license {
63 | name "The Apache Software License, Version 2.0"
64 | url "http://www.apache.org/licenses/LICENSE-2.0.txt"
65 | distribution "repo"
66 | }
67 | }
68 | developers {
69 | developer {
70 | id "shifarshifz"
71 | name "shifarshifz"
72 | email "theapache64@gmail.com"
73 | }
74 | }
75 |
76 | scm {
77 | url "https://github.com/theapache64/google-play-api"
78 | }
79 | }
80 |
81 | publishing {
82 | publications {
83 | mavenPublication(MavenPublication) {
84 | from components.java
85 | artifact sourcesJar {
86 | classifier "sources"
87 | }
88 | artifact javadocJar {
89 | classifier "javadoc"
90 | }
91 | groupId 'com.github.theapache64'
92 | artifactId 'google-play-api'
93 | version "$version"
94 | pom.withXml {
95 | def root = asNode()
96 | root.appendNode('description', 'To access play store')
97 | root.appendNode('name', 'google-play-api')
98 | root.appendNode('url', 'https://github.com/theapache64/google-play-api')
99 | root.children().last() + pomConfig
100 | }
101 | }
102 | }
103 | }
104 |
105 |
106 |
--------------------------------------------------------------------------------
/cover.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/google-play-api/cbac21a07efd9a17ea10e6ff293b22e27067af3a/cover.jpeg
--------------------------------------------------------------------------------
/gpm.json:
--------------------------------------------------------------------------------
1 | {
2 | "added": [
3 | {
4 | "id": 1,
5 | "type": "implementation",
6 | "installed_name": "bouncycastle",
7 | "gpm_dep": {
8 | "artifact_id": "bcprov-jdk15on",
9 | "default_type": "implementation",
10 | "docs": "https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on",
11 | "get_from": "Central",
12 | "group_id": "org.bouncycastle",
13 | "name": "Bouncy Castle Provider",
14 | "description": "The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.5 and up."
15 | }
16 | },
17 | {
18 | "id": 2,
19 | "type": "implementation",
20 | "installed_name": "junit",
21 | "gpm_dep": {
22 | "artifact_id": "junit",
23 | "default_type": "implementation",
24 | "docs": "https://mvnrepository.com/artifact/junit/junit",
25 | "get_from": "Central",
26 | "group_id": "junit",
27 | "name": "JUnit",
28 | "description": "JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck."
29 | }
30 | },
31 | {
32 | "id": 3,
33 | "type": "implementation",
34 | "installed_name": "coroutines-core",
35 | "gpm_dep": {
36 | "artifact_id": "kotlinx-coroutines-core",
37 | "default_type": "implementation",
38 | "docs": "https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core",
39 | "get_from": "Central",
40 | "group_id": "org.jetbrains.kotlinx",
41 | "name": "Kotlinx Coroutines Core",
42 | "description": "Coroutines support libraries for Kotlin"
43 | }
44 | },
45 | {
46 | "id": 4,
47 | "type": "testImplementation",
48 | "installed_name": "expekt",
49 | "gpm_dep": {
50 | "artifact_id": "expekt",
51 | "default_type": "implementation",
52 | "docs": "https://mvnrepository.com/artifact/com.theapache64/expekt",
53 | "get_from": "JCenter",
54 | "group_id": "com.theapache64",
55 | "name": "Expekt",
56 | "description": "An assertion library for Kotlin"
57 | }
58 | }
59 | ]
60 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theapache64/google-play-api/cbac21a07efd9a17ea10e6ff293b22e27067af3a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'google-play-api'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/DownloadData.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.security.InvalidAlgorithmParameterException;
6 | import java.security.InvalidKeyException;
7 | import java.security.NoSuchAlgorithmException;
8 | import java.security.NoSuchProviderException;
9 | import java.util.zip.GZIPInputStream;
10 |
11 | import javax.crypto.Cipher;
12 | import javax.crypto.CipherInputStream;
13 | import javax.crypto.NoSuchPaddingException;
14 | import javax.crypto.spec.IvParameterSpec;
15 | import javax.crypto.spec.SecretKeySpec;
16 |
17 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidAppDeliveryData;
18 | import com.akdeniz.googleplaycrawler.GooglePlay.HttpCookie;
19 | import com.akdeniz.googleplaycrawler.misc.Base64;
20 |
21 | public class DownloadData {
22 |
23 | private AndroidAppDeliveryData appDeliveryData;
24 | private String downloadUrl;
25 | private HttpCookie downloadAuthCookie;
26 | private GooglePlayAPI api;
27 | private long totalUncompressedSize;
28 | private long totalCompressedSize;
29 | private boolean compress;
30 |
31 | public DownloadData(GooglePlayAPI api,
32 | AndroidAppDeliveryData appDeliveryData) {
33 | this.appDeliveryData = appDeliveryData;
34 | this.api = api;
35 | this.downloadUrl = appDeliveryData.getDownloadUrl();
36 | for (HttpCookie cookie : appDeliveryData.getDownloadAuthCookieList()) {
37 | this.downloadAuthCookie = cookie;
38 | }
39 | /*
40 | * this.totalSize = appDeliveryData.getDownloadSize(); for (int i = 0; i <
41 | * appDeliveryData.getAdditionalFileCount(); i++) { totalSize +=
42 | * appDeliveryData.getAdditionalFile(i).getSize(); }
43 | */
44 | setCompress(false);
45 | }
46 |
47 | public void setCompress(boolean c) {
48 | compress = c;
49 | this.totalUncompressedSize = appDeliveryData.getDownloadSize();
50 | this.totalCompressedSize = appDeliveryData.getGzippedDownloadSize();
51 | for (int i = 0; i < appDeliveryData.getAdditionalFileCount(); i++) {
52 | if (!appDeliveryData.getAdditionalFile(i).hasCompressedDownloadUrl()) {
53 | compress = false;
54 | }
55 | this.totalUncompressedSize += appDeliveryData.getAdditionalFile(i)
56 | .getSize();
57 | this.totalCompressedSize += appDeliveryData.getAdditionalFile(i)
58 | .getCompressedSize();
59 | }
60 | for (int i = 0; i < appDeliveryData.getSplitDeliveryDataCount(); i++) {
61 | if (!appDeliveryData.getSplitDeliveryData(i).hasGzippedDownloadUrl()) {
62 | break;
63 | }
64 | this.totalUncompressedSize += appDeliveryData.getSplitDeliveryData(i)
65 | .getDownloadSize();
66 | this.totalCompressedSize += appDeliveryData.getSplitDeliveryData(i)
67 | .getGzippedDownloadSize();
68 | }
69 | if (!appDeliveryData.hasGzippedDownloadUrl()) {
70 | compress = false;
71 | }
72 | }
73 |
74 | /**
75 | * Access the APK file
76 | *
77 | * @return an inputstream from which the app can be read (already processed
78 | * through crypto).
79 | * @throws NoSuchPaddingException
80 | * @throws NoSuchProviderException
81 | * @throws NoSuchAlgorithmException
82 | * @throws InvalidAlgorithmParameterException
83 | * @throws InvalidKeyException
84 | */
85 | public InputStream openApp() throws IOException, NoSuchAlgorithmException,
86 | NoSuchProviderException, NoSuchPaddingException, InvalidKeyException,
87 | InvalidAlgorithmParameterException {
88 | InputStream ret = null;
89 | String tmp = null;
90 | if (downloadAuthCookie != null) {
91 | tmp = downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue();
92 | }
93 | if (compress) {
94 | ret = new GZIPInputStream(
95 | api.executeDownload(appDeliveryData.getGzippedDownloadUrl(), tmp));
96 | }
97 | else {
98 | ret = api.executeDownload(downloadUrl, tmp);
99 | }
100 | if (appDeliveryData.hasEncryptionParams()) {
101 | int version = ret.read();
102 | if (version != 0) {
103 | throw new IOException("Unknown crypto container!");
104 | }
105 | ret.skip(4); // Meta data
106 | byte[] iv = new byte[16];
107 | ret.read(iv);
108 | byte[] encoded = appDeliveryData.getEncryptionParams().getEncryptionKey()
109 | .getBytes("UTF-8");
110 | byte[] decoded = Base64.decode(encoded, Base64.DEFAULT);
111 | Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE");
112 | SecretKeySpec key = new SecretKeySpec(decoded, "AES");
113 | cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
114 | return new CipherInputStream(ret, cipher);
115 | }
116 | else {
117 | return ret;
118 | }
119 | }
120 |
121 | public long getAppSize() {
122 | return appDeliveryData.getDownloadSize();
123 | }
124 |
125 | /**
126 | * Query the total downloadsize
127 | *
128 | * @return number of bytes to transfer.
129 | */
130 | public long getTotalSize() {
131 | if (compress) {
132 | return totalUncompressedSize;
133 | }
134 | else {
135 | return totalUncompressedSize;
136 | }
137 | }
138 |
139 | /**
140 | * Access the first expansion
141 | *
142 | * @return a stream or null if there is no expansion.
143 | */
144 | public InputStream openMainExpansion() throws IOException {
145 | if (appDeliveryData.getAdditionalFileCount() < 1) {
146 | return null;
147 | }
148 | if (compress) {
149 | String url = appDeliveryData.getAdditionalFile(0)
150 | .getCompressedDownloadUrl();
151 | return new GZIPInputStream(api.executeDownload(url,
152 | downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue()));
153 | }
154 | else {
155 | String url = appDeliveryData.getAdditionalFile(0).getDownloadUrl();
156 | return api.executeDownload(url,
157 | downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue());
158 | }
159 | }
160 |
161 | public boolean hasMainExpansion() {
162 | return appDeliveryData.getAdditionalFileCount() > 0;
163 | }
164 |
165 | public int getMainFileVersion() {
166 | if (appDeliveryData.getAdditionalFileCount() > 0) {
167 | return appDeliveryData.getAdditionalFile(0).getVersionCode();
168 | }
169 | return -1;
170 | }
171 |
172 | public long getMainSize() {
173 | return appDeliveryData.getAdditionalFile(0).getSize();
174 | }
175 |
176 | /**
177 | * Access the second expansion
178 | *
179 | * @return a stream or null if there is no expansion.
180 | */
181 | public InputStream openPatchExpansion() throws IOException {
182 | if (appDeliveryData.getAdditionalFileCount() < 2) {
183 | return null;
184 | }
185 | if (compress) {
186 | String url = appDeliveryData.getAdditionalFile(1)
187 | .getCompressedDownloadUrl();
188 | return new GZIPInputStream(api.executeDownload(url,
189 | downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue()));
190 | }
191 | else {
192 | String url = appDeliveryData.getAdditionalFile(1).getDownloadUrl();
193 | return api.executeDownload(url,
194 | downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue());
195 | }
196 | }
197 |
198 | public boolean hasPatchExpansion() {
199 | return appDeliveryData.getAdditionalFileCount() > 1;
200 | }
201 |
202 | public long getPatchSize() {
203 | return appDeliveryData.getAdditionalFile(1).getSize();
204 | }
205 |
206 | public int getPatchFileVersion() {
207 | if (appDeliveryData.getAdditionalFileCount() > 1) {
208 | return appDeliveryData.getAdditionalFile(1).getVersionCode();
209 | }
210 | return -1;
211 | }
212 |
213 | public int getSplitCount() {
214 | return appDeliveryData.getSplitDeliveryDataCount();
215 | }
216 |
217 | public InputStream openSplitDelivery(int n) throws IOException {
218 | if (getSplitCount() < 1) {
219 | return null;
220 | }
221 | if (compress) {
222 | String url = appDeliveryData.getSplitDeliveryData(n)
223 | .getGzippedDownloadUrl();
224 | return new GZIPInputStream(api.executeDownload(url,
225 | downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue()));
226 | }
227 | else {
228 | String url = appDeliveryData.getSplitDeliveryData(n).getDownloadUrl();
229 | return api.executeDownload(url,
230 | downloadAuthCookie.getName() + "=" + downloadAuthCookie.getValue());
231 | }
232 | }
233 |
234 | public String toString() {
235 | return appDeliveryData.toString();
236 | }
237 |
238 | public String getSplitId(int n) {
239 | if (getSplitCount() > 0) {
240 | return appDeliveryData.getSplitDeliveryData(n).getId();
241 | }
242 | return null;
243 | }
244 |
245 | public long getSplitSize(int n) {
246 | if (getSplitCount() > 0) {
247 | return appDeliveryData.getSplitDeliveryData(n).getDownloadSize();
248 | }
249 | return -1;
250 | }
251 |
252 | }
253 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/GooglePlayAPI.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.math.BigInteger;
6 | import java.security.Key;
7 | import java.security.KeyFactory;
8 | import java.security.MessageDigest;
9 | import java.security.PublicKey;
10 | import java.security.spec.RSAPublicKeySpec;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.Map;
14 | import java.util.PropertyResourceBundle;
15 | import java.util.ResourceBundle;
16 |
17 | import javax.crypto.Cipher;
18 |
19 | import org.apache.http.HttpEntity;
20 | import org.apache.http.HttpResponse;
21 | import org.apache.http.NameValuePair;
22 | import org.apache.http.client.ClientProtocolException;
23 | import org.apache.http.client.HttpClient;
24 | import org.apache.http.client.entity.UrlEncodedFormEntity;
25 | import org.apache.http.client.methods.HttpGet;
26 | import org.apache.http.client.methods.HttpPost;
27 | import org.apache.http.client.methods.HttpUriRequest;
28 | import org.apache.http.client.utils.URLEncodedUtils;
29 | import org.apache.http.conn.ClientConnectionManager;
30 | import org.apache.http.entity.ByteArrayEntity;
31 | import org.apache.http.impl.client.DefaultHttpClient;
32 | import org.apache.http.impl.conn.PoolingClientConnectionManager;
33 | import org.apache.http.impl.conn.SchemeRegistryFactory;
34 | import org.apache.http.message.BasicNameValuePair;
35 |
36 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidAppDeliveryData;
37 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinRequest;
38 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinResponse;
39 | //import com.akdeniz.googleplaycrawler.GooglePlay.BrowseResponse;
40 | import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsRequest;
41 | import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsRequest.Builder;
42 | import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsResponse;
43 | import com.akdeniz.googleplaycrawler.GooglePlay.BuyResponse;
44 | import com.akdeniz.googleplaycrawler.GooglePlay.DetailsResponse;
45 | import com.akdeniz.googleplaycrawler.GooglePlay.ListResponse;
46 | import com.akdeniz.googleplaycrawler.GooglePlay.ResponseWrapper;
47 | import com.akdeniz.googleplaycrawler.GooglePlay.ReviewResponse;
48 | import com.akdeniz.googleplaycrawler.GooglePlay.SearchResponse;
49 | import com.akdeniz.googleplaycrawler.GooglePlay.UploadDeviceConfigRequest;
50 | import com.akdeniz.googleplaycrawler.GooglePlay.UploadDeviceConfigResponse;
51 | import com.akdeniz.googleplaycrawler.misc.Base64;
52 |
53 | /**
54 | * This class provides
55 | *
checkin, search, details, bulkDetails, browse, list and download
56 | * capabilities. It uses Apache Commons HttpClient
for POST and GET
57 | * requests.
58 | *
59 | *
60 | * XXX : DO NOT call checkin, login and download consecutively. To allow 61 | * server to catch up, sleep for a while before download! (5 sec will do!) Also 62 | * it is recommended to call checkin once and use generated android-id for 63 | * further operations. 64 | *
65 | * 66 | * @author akdeniz 67 | * 68 | */ 69 | public class GooglePlayAPI { 70 | 71 | private static final String CHECKIN_URL = "https://android.clients.google.com/checkin"; 72 | private static final String URL_LOGIN = "https://android.clients.google.com/auth"; 73 | private static final String C2DM_REGISTER_URL = "https://android.clients.google.com/c2dm/register2"; 74 | private static final String FDFE_URL = "https://android.clients.google.com/fdfe/"; 75 | private static final String LIST_URL = FDFE_URL + "list"; 76 | private static final String BROWSE_URL = FDFE_URL + "browse"; 77 | private static final String DETAILS_URL = FDFE_URL + "details"; 78 | private static final String SEARCH_URL = FDFE_URL + "search"; 79 | private static final String BULKDETAILS_URL = FDFE_URL + "bulkDetails"; 80 | private static final String PURCHASE_URL = FDFE_URL + "purchase"; 81 | private static final String REVIEWS_URL = FDFE_URL + "rev"; 82 | private static final String UPLOADDEVICECONFIG_URL = FDFE_URL 83 | + "uploadDeviceConfig"; 84 | private static final String RECOMMENDATIONS_URL = FDFE_URL + "rec"; 85 | private static final String DELIVERY_URL = FDFE_URL + "delivery"; 86 | 87 | private static final String ACCOUNT_TYPE_HOSTED_OR_GOOGLE = "HOSTED_OR_GOOGLE"; 88 | 89 | public static enum REVIEW_SORT { 90 | NEWEST(0), HIGHRATING(1), HELPFUL(2); 91 | 92 | public int value; 93 | 94 | private REVIEW_SORT(int value) { 95 | this.value = value; 96 | } 97 | } 98 | 99 | public static enum RECOMMENDATION_TYPE { 100 | ALSO_VIEWED(1), ALSO_INSTALLED(2); 101 | 102 | public int value; 103 | 104 | private RECOMMENDATION_TYPE(int value) { 105 | this.value = value; 106 | } 107 | } 108 | 109 | private String token; 110 | private String androidID; 111 | private String email; 112 | private String password; 113 | private HttpClient client; 114 | private String securityToken; 115 | private String localization; 116 | private String useragent; 117 | 118 | /** 119 | * Default constructor. ANDROID ID and Authentication token must be supplied 120 | * before any other operation. 121 | */ 122 | public GooglePlayAPI() { 123 | } 124 | 125 | /** 126 | * Constructs a ready to login {@link GooglePlayAPI}. 127 | */ 128 | public GooglePlayAPI(String email, String password, String androidID) { 129 | this(email, password); 130 | this.setAndroidID(androidID); 131 | } 132 | 133 | /** 134 | * If this constructor is used, Android ID must be generated by calling 135 | *checkin()
or set by using setAndroidID
before
136 | * using other abilities.
137 | */
138 | public GooglePlayAPI(String email, String password) {
139 | this.setEmail(email);
140 | this.password = password;
141 | setClient(new DefaultHttpClient(getConnectionManager()));
142 | // setUseragent("Android-Finsky/3.10.14 (api=3,versionCode=8016014,sdk=15,device=GT-I9300,hardware=aries,product=GT-I9300)");
143 | // setUseragent("Android-Finsky/6.5.08.D-all (versionCode=80650800,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)");
144 | setUseragent("Android-Finsky/13.1.32-all (versionCode=81313200,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)");
145 | }
146 |
147 | /**
148 | * Connection manager to allow concurrent connections.
149 | *
150 | * @return {@link ClientConnectionManager} instance
151 | */
152 | public static ClientConnectionManager getConnectionManager() {
153 | PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(
154 | SchemeRegistryFactory.createDefault());
155 | connManager.setMaxTotal(100);
156 | connManager.setDefaultMaxPerRoute(30);
157 | return connManager;
158 | }
159 |
160 | /**
161 | * Performs authentication on "ac2dm" service and match up android id,
162 | * security token and email by checking them in on this server.
163 | *
164 | * This function sets check-inded android ID and that can be taken either by
165 | * using getToken()
or from returned
166 | * {@link AndroidCheckinResponse} instance.
167 | *
168 | */
169 | public AndroidCheckinResponse checkin() throws Exception {
170 |
171 | // this first checkin is for generating android-id
172 | AndroidCheckinResponse checkinResponse = postCheckin(Utils
173 | .generateAndroidCheckinRequest().toByteArray());
174 | this.setAndroidID(BigInteger.valueOf(checkinResponse.getGsfId()).toString(
175 | 16));
176 | setSecurityToken((BigInteger.valueOf(checkinResponse.getSecurityToken())
177 | .toString(16)));
178 |
179 | String c2dmAuth = loginAC2DM();
180 | // login();
181 | // String c2dmAuth= getToken();
182 |
183 | AndroidCheckinRequest.Builder checkInbuilder = AndroidCheckinRequest
184 | .newBuilder(Utils.generateAndroidCheckinRequest());
185 |
186 | AndroidCheckinRequest build = checkInbuilder
187 | .setId(new BigInteger(this.getAndroidID(), 16).longValue())
188 | .setSecurityToken(new BigInteger(getSecurityToken(), 16).longValue())
189 | .addAccountCookie("[" + getEmail() + "]").addAccountCookie(c2dmAuth)
190 | .build();
191 | // this is the second checkin to match credentials with android-id
192 | return postCheckin(build.toByteArray());
193 | }
194 |
195 | private static int readInt(byte[] bArr, int i) {
196 | return (((((bArr[i] & 255) << 24) | 0) | ((bArr[i + 1] & 255) << 16)) | ((bArr[i + 2] & 255) << 8))
197 | | (bArr[i + 3] & 255);
198 | }
199 |
200 | public static PublicKey createKeyFromString(String str, byte[] bArr) {
201 | try {
202 | byte[] decode = Base64.decode(str, 0);
203 | int readInt = readInt(decode, 0);
204 | byte[] obj = new byte[readInt];
205 | System.arraycopy(decode, 4, obj, 0, readInt);
206 | BigInteger bigInteger = new BigInteger(1, obj);
207 | int readInt2 = readInt(decode, readInt + 4);
208 | byte[] obj2 = new byte[readInt2];
209 | System.arraycopy(decode, readInt + 8, obj2, 0, readInt2);
210 | BigInteger bigInteger2 = new BigInteger(1, obj2);
211 | decode = MessageDigest.getInstance("SHA-1").digest(decode);
212 | bArr[0] = (byte) 0;
213 | System.arraycopy(decode, 0, bArr, 1, 4);
214 | return KeyFactory.getInstance("RSA").generatePublic(
215 | new RSAPublicKeySpec(bigInteger, bigInteger2));
216 | }
217 | catch (Throwable e) {
218 | throw new RuntimeException(e);
219 | }
220 | }
221 |
222 | private static String encryptString(String str) {
223 | int i = 0;
224 | ResourceBundle bundle = PropertyResourceBundle
225 | .getBundle("com.akdeniz.googleplaycrawler.crypt");
226 | String string = bundle.getString("key");
227 |
228 | byte[] obj = new byte[5];
229 | Key createKeyFromString = createKeyFromString(string, obj);
230 | if (createKeyFromString == null) {
231 | return null;
232 | }
233 | try {
234 | Cipher instance = Cipher
235 | .getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
236 | byte[] bytes = str.getBytes("UTF-8");
237 | int length = ((bytes.length - 1) / 86) + 1;
238 | byte[] obj2 = new byte[(length * 133)];
239 | while (i < length) {
240 | instance.init(1, createKeyFromString);
241 | byte[] doFinal = instance.doFinal(bytes, i * 86,
242 | i == length + -1 ? bytes.length - (i * 86) : 86);
243 | System.arraycopy(obj, 0, obj2, i * 133, obj.length);
244 | System.arraycopy(doFinal, 0, obj2, (i * 133) + obj.length,
245 | doFinal.length);
246 | i++;
247 | }
248 | return Base64.encodeToString(obj2, 10);
249 | }
250 | catch (Throwable e) {
251 | throw new RuntimeException(e);
252 | }
253 | }
254 |
255 | /**
256 | * Logins AC2DM server and returns authentication string.
257 | */
258 | public String loginAC2DM() throws IOException {
259 | HttpEntity c2dmResponseEntity = executePost(URL_LOGIN,
260 | new String[][] {
261 | { "Email", this.getEmail() },
262 | { "EncryptedPasswd",
263 | encryptString(this.getEmail() + "\u0000" + this.password) },
264 | { "add_account", "1" }, { "service", "ac2dm" },
265 | { "accountType", ACCOUNT_TYPE_HOSTED_OR_GOOGLE },
266 | { "has_permission", "1" }, { "source", "android" },
267 | { "app", "com.google.android.gsf" }, { "device_country", "us" },
268 | { "device_country", "us" }, { "lang", "en" },
269 | { "sdk_version", "16" }, }, null);
270 |
271 | MapsetToken
. This function does not performs
292 | * authentication, it simply sets authentication token.
293 | */
294 | public void login(String token) throws Exception {
295 | setToken(token);
296 | }
297 |
298 | /**
299 | * Authenticates on server with given email and password and sets
300 | * authentication token. This token can be used to login instead of using
301 | * email and password every time.
302 | */
303 | public void login() throws Exception {
304 | /*
305 | * HttpEntity responseEntity = executePost(URL_LOGIN, new String[][] { {
306 | * "Email", this.getEmail() }, { "EncryptedPasswd",
307 | * encryptString(this.getEmail()+"\u0000"+this.password) }, { "service",
308 | * "androidmarket" }, { "add_account", "1"}, { "accountType",
309 | * ACCOUNT_TYPE_HOSTED_OR_GOOGLE }, { "has_permission", "1" }, { "source",
310 | * "android" }, { "androidId", this.getAndroidID() }, { "app",
311 | * "com.android.vending" }, { "device_country", "en" }, { "lang", "en" }, {
312 | * "sdk_version", "17" }, }, null);
313 | *
314 | * Mapsearch(query, null, null)
325 | */
326 | public SearchResponse search(String query) throws IOException {
327 | return search(query, null, null);
328 | }
329 |
330 | /**
331 | * Fetches a search results for given query. Offset and numberOfResult
332 | * parameters are optional and null
can be passed!
333 | */
334 | public SearchResponse search(String query, Integer offset,
335 | Integer numberOfResult) throws IOException {
336 |
337 | ResponseWrapper responseWrapper = executeGETRequest(
338 | SEARCH_URL,
339 | new String[][] {
340 | { "c", "3" },
341 | { "q", query },
342 | { "o", (offset == null) ? null : String.valueOf(offset) },
343 | {
344 | "n",
345 | (numberOfResult == null) ? null : String
346 | .valueOf(numberOfResult) }, });
347 |
348 | return responseWrapper.getPayload().getSearchResponse();
349 | }
350 |
351 | public ResponseWrapper searchApp(String query) throws IOException {
352 | ResponseWrapper responseWrapper = executeGETRequest(SEARCH_URL,
353 | new String[][] { { "c", "3" }, { "q", query },
354 |
355 | });
356 |
357 | return responseWrapper;
358 | }
359 |
360 | public ResponseWrapper getList(String url) throws IOException {
361 | return executeGETRequest(FDFE_URL+url, null);
362 | }
363 |
364 | /**
365 | * Fetches detailed information about passed package name. If it is needed to
366 | * fetch information about more than one application, consider to use
367 | * bulkDetails
.
368 | */
369 | public DetailsResponse details(String packageName) throws IOException {
370 | ResponseWrapper responseWrapper = executeGETRequest(DETAILS_URL,
371 | new String[][] { { "doc", packageName }, });
372 |
373 | return responseWrapper.getPayload().getDetailsResponse();
374 | }
375 |
376 | /** Equivalent of details but bulky one! */
377 | public BulkDetailsResponse bulkDetails(Listlist(categoryId, null, null, null)
. It
406 | * fetches sub-categories of given category!
407 | */
408 | public ListResponse list(String categoryId) throws IOException {
409 | return list(categoryId, null, null, null);
410 | }
411 |
412 | /**
413 | * Fetches applications within supplied category and sub-category. If
414 | * null
is given for sub-category, it fetches sub-categories of
415 | * passed category.
416 | *
417 | * Default values for offset and numberOfResult are "0" and "20" respectively.
418 | * These values are determined by Google Play Store.
419 | */
420 | public ListResponse list(String categoryId, String subCategoryId,
421 | Integer offset, Integer numberOfResult) throws IOException {
422 | ResponseWrapper responseWrapper = executeGETRequest(
423 | LIST_URL,
424 | new String[][] {
425 | { "c", "3" },
426 | { "cat", categoryId },
427 | { "ctr", subCategoryId },
428 | { "o", (offset == null) ? null : String.valueOf(offset) },
429 | {
430 | "n",
431 | (numberOfResult == null) ? null : String
432 | .valueOf(numberOfResult) }, });
433 |
434 | return responseWrapper.getPayload().getListResponse();
435 | }
436 |
437 | public ListResponse nextPage(String url) throws IOException {
438 | ResponseWrapper responseWrapper = executeGETRequest(FDFE_URL + url, null);
439 | return responseWrapper.getPayload().getListResponse();
440 | }
441 |
442 | /**
443 | * Downloads given application package name, version and offer type. Version
444 | * code and offer type can be fetch by details
interface.
445 | **/
446 | public DownloadData download(String packageName, int versionCode,
447 | int offerType) throws IOException {
448 |
449 | BuyResponse buyResponse = purchase(packageName, versionCode, offerType);
450 |
451 | return new DownloadData(this, buyResponse.getPurchaseStatusResponse()
452 | .getAppDeliveryData());
453 |
454 | }
455 |
456 | public DownloadData delivery(String packageName, int versionCode,
457 | int offerType) throws IOException {
458 | ResponseWrapper responseWrapper = executeGETRequest(DELIVERY_URL,
459 | new String[][] { { "ot", String.valueOf(offerType) },
460 | { "doc", packageName }, { "vc", String.valueOf(versionCode) }, });
461 |
462 | AndroidAppDeliveryData appDeliveryData = responseWrapper.getPayload()
463 | .getDeliveryResponse().getAppDeliveryData();
464 | return new DownloadData(this, appDeliveryData);
465 | }
466 |
467 | public DownloadData purchaseAndDeliver(String packageName, int versionCode,
468 | int offerType) throws IOException {
469 | BuyResponse buyResponse = purchase(packageName, versionCode, offerType);
470 | AndroidAppDeliveryData ada = buyResponse.getPurchaseStatusResponse()
471 | .getAppDeliveryData();
472 | if (ada.hasDownloadUrl() && ada.getDownloadAuthCookieCount() > 0) {
473 | // This is for backwards compatibility.
474 | return new DownloadData(this, ada);
475 | }
476 | return delivery(packageName, versionCode, offerType);
477 | }
478 |
479 | /**
480 | * Posts given check-in request content and returns
481 | * {@link AndroidCheckinResponse}.
482 | */
483 | private AndroidCheckinResponse postCheckin(byte[] request) throws IOException {
484 |
485 | HttpEntity httpEntity = executePost(CHECKIN_URL, new ByteArrayEntity(
486 | request), new String[][] {
487 | { "User-Agent", "Android-Checkin/2.0 (generic JRO03E); gzip" },
488 | { "Host", "android.clients.google.com" },
489 | { "Content-Type", "application/x-protobuffer" } });
490 | return AndroidCheckinResponse.parseFrom(httpEntity.getContent());
491 | }
492 |
493 | /**
494 | * This function is used for fetching download url and donwload cookie, rather
495 | * than actual purchasing.
496 | */
497 | private BuyResponse purchase(String packageName, int versionCode,
498 | int offerType) throws IOException {
499 |
500 | ResponseWrapper responseWrapper = executePOSTRequest(PURCHASE_URL,
501 | new String[][] { { "ot", String.valueOf(offerType) },
502 | { "doc", packageName }, { "vc", String.valueOf(versionCode) }, });
503 |
504 | return responseWrapper.getPayload().getBuyResponse();
505 | }
506 |
507 | /**
508 | * Fetches url content by executing GET request with provided cookie string.
509 | */
510 | public InputStream executeDownload(String url, String cookie)
511 | throws IOException {
512 |
513 | if (cookie!= null) {
514 | String[][] headerParams = new String[][] {
515 | { "Cookie", cookie },
516 | { "User-Agent",
517 | "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)" }, };
518 |
519 | HttpEntity httpEntity = executeGet(url, null, headerParams);
520 | return httpEntity.getContent();
521 | }
522 | else {
523 | String[][] headerParams = new String[][] {
524 | { "User-Agent",
525 | "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)" }, };
526 |
527 | HttpEntity httpEntity = executeGet(url, null, headerParams);
528 | return httpEntity.getContent();
529 | }
530 | }
531 |
532 | /**
533 | * Fetches the reviews of given package name by sorting passed choice.
534 | *
535 | * Default values for offset and numberOfResult are "0" and "20" respectively.
536 | * These values are determined by Google Play Store.
537 | */
538 | public ReviewResponse reviews(String packageName, REVIEW_SORT sort,
539 | Integer offset, Integer numberOfResult) throws IOException {
540 | ResponseWrapper responseWrapper = executeGETRequest(
541 | REVIEWS_URL,
542 | new String[][] {
543 | { "doc", packageName },
544 | { "sort", (sort == null) ? null : String.valueOf(sort.value) },
545 | { "o", (offset == null) ? null : String.valueOf(offset) },
546 | {
547 | "n",
548 | (numberOfResult == null) ? null : String
549 | .valueOf(numberOfResult) } });
550 |
551 | return responseWrapper.getPayload().getReviewResponse();
552 | }
553 |
554 | /**
555 | * Uploads device configuration to google server so that can be seen from web
556 | * as a registered device!!
557 | *
558 | * @see https://play.google.com/store/account
559 | */
560 | public UploadDeviceConfigResponse uploadDeviceConfig() throws Exception {
561 |
562 | UploadDeviceConfigRequest request = UploadDeviceConfigRequest.newBuilder()
563 | .setDeviceConfiguration(Utils.getDeviceConfigurationProto())
564 | .setManufacturer("Samsung").build();
565 | ResponseWrapper responseWrapper = executePOSTRequest(
566 | UPLOADDEVICECONFIG_URL, request.toByteArray(), "application/x-protobuf");
567 | return responseWrapper.getPayload().getUploadDeviceConfigResponse();
568 | }
569 |
570 | /**
571 | * Fetches the recommendations of given package name.
572 | *
573 | * Default values for offset and numberOfResult are "0" and "20" respectively.
574 | * These values are determined by Google Play Store.
575 | */
576 | public ListResponse recommendations(String packageName,
577 | RECOMMENDATION_TYPE type, Integer offset, Integer numberOfResult)
578 | throws IOException {
579 | ResponseWrapper responseWrapper = executeGETRequest(
580 | RECOMMENDATIONS_URL,
581 | new String[][] {
582 | { "c", "3" },
583 | { "doc", packageName },
584 | { "rt", (type == null) ? null : String.valueOf(type.value) },
585 | { "o", (offset == null) ? null : String.valueOf(offset) },
586 | {
587 | "n",
588 | (numberOfResult == null) ? null : String
589 | .valueOf(numberOfResult) } });
590 |
591 | return responseWrapper.getPayload().getListResponse();
592 | }
593 |
594 | /* =======================Helper Functions====================== */
595 |
596 | /**
597 | * Executes GET request and returns result as {@link ResponseWrapper}.
598 | * Standard header parameters will be used for request.
599 | *
600 | * @see getHeaderParameters
601 | * */
602 | private ResponseWrapper executeGETRequest(String path, String[][] datapost)
603 | throws IOException {
604 |
605 | HttpEntity httpEntity = executeGet(path, datapost,
606 | getHeaderParameters(this.getToken(), null));
607 | return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent());
608 |
609 | }
610 |
611 | /**
612 | * Executes POST request and returns result as {@link ResponseWrapper}.
613 | * Standard header parameters will be used for request.
614 | *
615 | * @see getHeaderParameters
616 | * */
617 | private ResponseWrapper executePOSTRequest(String path, String[][] datapost)
618 | throws IOException {
619 |
620 | HttpEntity httpEntity = executePost(path, datapost,
621 | getHeaderParameters(this.getToken(), null));
622 | return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent());
623 |
624 | }
625 |
626 | /**
627 | * Executes POST request and returns result as {@link ResponseWrapper}.
628 | * Content type can be specified for given byte array.
629 | */
630 | private ResponseWrapper executePOSTRequest(String url, byte[] datapost,
631 | String contentType) throws IOException {
632 |
633 | HttpEntity httpEntity = executePost(url, new ByteArrayEntity(datapost),
634 | getHeaderParameters(this.getToken(), contentType));
635 | return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent());
636 |
637 | }
638 |
639 | /**
640 | * Executes POST request on given URL with POST parameters and header
641 | * parameters.
642 | */
643 | private HttpEntity executePost(String url, String[][] postParams,
644 | String[][] headerParams) throws IOException {
645 |
646 | List809 | * Note that changing this value has no affect on localized application list 810 | * that server provides. It depends on only your IP location. 811 | *
812 | *
813 | * @param localization
814 | * can be en-EN, en-US, tr-TR, fr-FR ... (default : en-EN)
815 | */
816 | public void setLocalization(String localization) {
817 | this.localization = localization;
818 | }
819 |
820 | /**
821 | * @return the useragent
822 | */
823 | public String getUseragent() {
824 | return useragent;
825 | }
826 |
827 | /**
828 | * @param useragent
829 | * the useragent to set
830 | */
831 | public void setUseragent(String useragent) {
832 | this.useragent = useragent;
833 | }
834 |
835 | }
836 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/GooglePlayException.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.util.ArrayList;
6 | import java.util.Collections;
7 | import java.util.List;
8 |
9 | import org.apache.http.HttpResponse;
10 |
11 | import com.akdeniz.googleplaycrawler.GooglePlay.ResponseWrapper;
12 | import com.google.protobuf.CodedInputStream;
13 | import com.google.protobuf.WireFormat;
14 |
15 | public class GooglePlayException extends IOException {
16 | private static final long serialVersionUID = 1L;
17 |
18 | private final int httpStatus;
19 |
20 | public GooglePlayException(int httpStatus, String message) {
21 | super(message);
22 | this.httpStatus = httpStatus;
23 | }
24 |
25 | public int getHttpStatus() {
26 | return httpStatus;
27 | }
28 |
29 | public static GooglePlayException create(HttpResponse httpResponse) {
30 | String message = httpResponse.getStatusLine().getReasonPhrase();
31 |
32 | // If the reponse contains a Protobuf response, retrieves the message from a
33 | // ResponseWrapper object
34 | InputStream content=null;
35 | try {
36 | content = httpResponse.getEntity().getContent();
37 | if ("application/protobuf".equals(httpResponse.getEntity()
38 | .getContentType().getValue())) {
39 | ResponseWrapper rw = ResponseWrapper.parseFrom(content);
40 | if (rw.hasCommands() && rw.getCommands().hasDisplayErrorMessage()) {
41 | message = rw.getCommands().getDisplayErrorMessage();
42 | }
43 | }
44 | }
45 | catch (Exception e) {
46 | }
47 | try {
48 | content.close();
49 | }
50 | catch (IOException e) {
51 | }
52 |
53 | return new GooglePlayException(
54 | httpResponse.getStatusLine().getStatusCode(), message);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/Identity.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler;
2 |
3 | import com.akdeniz.googleplaycrawler.misc.Base64;
4 | import org.apache.http.HttpResponse;
5 | import org.apache.http.NameValuePair;
6 | import org.apache.http.client.ClientProtocolException;
7 | import org.apache.http.client.HttpClient;
8 | import org.apache.http.client.HttpResponseException;
9 | import org.apache.http.client.entity.UrlEncodedFormEntity;
10 | import org.apache.http.client.methods.HttpPost;
11 | import org.apache.http.message.BasicNameValuePair;
12 |
13 | import javax.crypto.BadPaddingException;
14 | import javax.crypto.Cipher;
15 | import javax.crypto.IllegalBlockSizeException;
16 | import javax.crypto.NoSuchPaddingException;
17 | import java.io.IOException;
18 | import java.io.InputStream;
19 | import java.io.UnsupportedEncodingException;
20 | import java.math.BigInteger;
21 | import java.security.*;
22 | import java.security.spec.InvalidKeySpecException;
23 | import java.security.spec.RSAPublicKeySpec;
24 | import java.util.*;
25 |
26 | /**
27 | * ClientLogin implementation.
28 | *
29 | * @author patrick
30 | *
31 | */
32 | class Identity {
33 |
34 | private static final String LOGIN_URL = "https://android.clients.google.com/auth";
35 | private static final String PUBKEY = "AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/6rmf5AAAAAwEAAQ==";
36 | private static final String SERVICE = "androidmarket";
37 |
38 | private String firstName;
39 | private String lastName;
40 | private String email;
41 | private String services;
42 | private String authToken;
43 |
44 | private Identity() {
45 | }
46 |
47 | /**
48 | * User's first name
49 | *
50 | * @return the first name, retrieved from Play
51 | */
52 | public String getFirstName() {
53 | return firstName;
54 | }
55 |
56 | /**
57 | * User's last name
58 | *
59 | * @return the lastname, retrieved from Play
60 | */
61 | public String getLastName() {
62 | return lastName;
63 | }
64 |
65 | /**
66 | * User's current email address
67 | *
68 | * @return the email address, retrieved from Play.
69 | */
70 | public String getEmail() {
71 | return email;
72 | }
73 |
74 | /**
75 | * List the services, the user is clear for.
76 | *
77 | * @return list of service names. Potentially empty, never null
78 | */
79 | public List The padding '=' characters at the end are considered optional, but
106 | * if any are present, there must be the correct number of them.
107 | *
108 | * @param str the input String to decode, which is converted to
109 | * bytes using the default charset
110 | * @param flags controls certain features of the decoded output.
111 | * Pass {@code DEFAULT} to decode standard Base64.
112 | *
113 | * @throws IllegalArgumentException if the input contains
114 | * incorrect padding
115 | */
116 | public static byte[] decode(String str, int flags) {
117 | return decode(str.getBytes(), flags);
118 | }
119 |
120 | /**
121 | * Decode the Base64-encoded data in input and return the data in
122 | * a new byte array.
123 | *
124 | * The padding '=' characters at the end are considered optional, but
125 | * if any are present, there must be the correct number of them.
126 | *
127 | * @param input the input array to decode
128 | * @param flags controls certain features of the decoded output.
129 | * Pass {@code DEFAULT} to decode standard Base64.
130 | *
131 | * @throws IllegalArgumentException if the input contains
132 | * incorrect padding
133 | */
134 | public static byte[] decode(byte[] input, int flags) {
135 | return decode(input, 0, input.length, flags);
136 | }
137 |
138 | /**
139 | * Decode the Base64-encoded data in input and return the data in
140 | * a new byte array.
141 | *
142 | * The padding '=' characters at the end are considered optional, but
143 | * if any are present, there must be the correct number of them.
144 | *
145 | * @param input the data to decode
146 | * @param offset the position within the input array at which to start
147 | * @param len the number of bytes of input to decode
148 | * @param flags controls certain features of the decoded output.
149 | * Pass {@code DEFAULT} to decode standard Base64.
150 | *
151 | * @throws IllegalArgumentException if the input contains
152 | * incorrect padding
153 | */
154 | public static byte[] decode(byte[] input, int offset, int len, int flags) {
155 | // Allocate space for the most data the input could represent.
156 | // (It could contain less if it contains whitespace, etc.)
157 | Decoder decoder = new Decoder(flags, new byte[len*3/4]);
158 |
159 | if (!decoder.process(input, offset, len, true)) {
160 | throw new IllegalArgumentException("bad base-64");
161 | }
162 |
163 | // Maybe we got lucky and allocated exactly enough output space.
164 | if (decoder.op == decoder.output.length) {
165 | return decoder.output;
166 | }
167 |
168 | // Need to shorten the array, so allocate a new one of the
169 | // right size and copy.
170 | byte[] temp = new byte[decoder.op];
171 | System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
172 | return temp;
173 | }
174 |
175 | /* package */ static class Decoder extends Coder {
176 | /**
177 | * Lookup table for turning bytes into their position in the
178 | * Base64 alphabet.
179 | */
180 | private static final int DECODE[] = {
181 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
182 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
183 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
184 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
185 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
186 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
187 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
188 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
189 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
190 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
191 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
192 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
193 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
194 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
195 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
196 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
197 | };
198 |
199 | /**
200 | * Decode lookup table for the "web safe" variant (RFC 3548
201 | * sec. 4) where - and _ replace + and /.
202 | */
203 | private static final int DECODE_WEBSAFE[] = {
204 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
205 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
206 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
207 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
208 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
209 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
210 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
211 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
212 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
213 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
214 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
215 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
216 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
217 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
218 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
219 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
220 | };
221 |
222 | /** Non-data values in the DECODE arrays. */
223 | private static final int SKIP = -1;
224 | private static final int EQUALS = -2;
225 |
226 | /**
227 | * States 0-3 are reading through the next input tuple.
228 | * State 4 is having read one '=' and expecting exactly
229 | * one more.
230 | * State 5 is expecting no more data or padding characters
231 | * in the input.
232 | * State 6 is the error state; an error has been detected
233 | * in the input and no future input can "fix" it.
234 | */
235 | private int state; // state number (0 to 6)
236 | private int value;
237 |
238 | final private int[] alphabet;
239 |
240 | public Decoder(int flags, byte[] output) {
241 | this.output = output;
242 |
243 | alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
244 | state = 0;
245 | value = 0;
246 | }
247 |
248 | /**
249 | * @return an overestimate for the number of bytes {@code
250 | * len} bytes could decode to.
251 | */
252 | public int maxOutputSize(int len) {
253 | return len * 3/4 + 10;
254 | }
255 |
256 | /**
257 | * Decode another block of input data.
258 | *
259 | * @return true if the state machine is still healthy. false if
260 | * bad base-64 data has been detected in the input stream.
261 | */
262 | public boolean process(byte[] input, int offset, int len, boolean finish) {
263 | if (this.state == 6) return false;
264 |
265 | int p = offset;
266 | len += offset;
267 |
268 | // Using local variables makes the decoder about 12%
269 | // faster than if we manipulate the member variables in
270 | // the loop. (Even alphabet makes a measurable
271 | // difference, which is somewhat surprising to me since
272 | // the member variable is final.)
273 | int state = this.state;
274 | int value = this.value;
275 | int op = 0;
276 | final byte[] output = this.output;
277 | final int[] alphabet = this.alphabet;
278 |
279 | while (p < len) {
280 | // Try the fast path: we're starting a new tuple and the
281 | // next four bytes of the input stream are all data
282 | // bytes. This corresponds to going through states
283 | // 0-1-2-3-0. We expect to use this method for most of
284 | // the data.
285 | //
286 | // If any of the next four bytes of input are non-data
287 | // (whitespace, etc.), value will end up negative. (All
288 | // the non-data values in decode are small negative
289 | // numbers, so shifting any of them up and or'ing them
290 | // together will result in a value with its top bit set.)
291 | //
292 | // You can remove this whole block and the output should
293 | // be the same, just slower.
294 | if (state == 0) {
295 | while (p+4 <= len &&
296 | (value = ((alphabet[input[p] & 0xff] << 18) |
297 | (alphabet[input[p+1] & 0xff] << 12) |
298 | (alphabet[input[p+2] & 0xff] << 6) |
299 | (alphabet[input[p+3] & 0xff]))) >= 0) {
300 | output[op+2] = (byte) value;
301 | output[op+1] = (byte) (value >> 8);
302 | output[op] = (byte) (value >> 16);
303 | op += 3;
304 | p += 4;
305 | }
306 | if (p >= len) break;
307 | }
308 |
309 | // The fast path isn't available -- either we've read a
310 | // partial tuple, or the next four input bytes aren't all
311 | // data, or whatever. Fall back to the slower state
312 | // machine implementation.
313 |
314 | int d = alphabet[input[p++] & 0xff];
315 |
316 | switch (state) {
317 | case 0:
318 | if (d >= 0) {
319 | value = d;
320 | ++state;
321 | } else if (d != SKIP) {
322 | this.state = 6;
323 | return false;
324 | }
325 | break;
326 |
327 | case 1:
328 | if (d >= 0) {
329 | value = (value << 6) | d;
330 | ++state;
331 | } else if (d != SKIP) {
332 | this.state = 6;
333 | return false;
334 | }
335 | break;
336 |
337 | case 2:
338 | if (d >= 0) {
339 | value = (value << 6) | d;
340 | ++state;
341 | } else if (d == EQUALS) {
342 | // Emit the last (partial) output tuple;
343 | // expect exactly one more padding character.
344 | output[op++] = (byte) (value >> 4);
345 | state = 4;
346 | } else if (d != SKIP) {
347 | this.state = 6;
348 | return false;
349 | }
350 | break;
351 |
352 | case 3:
353 | if (d >= 0) {
354 | // Emit the output triple and return to state 0.
355 | value = (value << 6) | d;
356 | output[op+2] = (byte) value;
357 | output[op+1] = (byte) (value >> 8);
358 | output[op] = (byte) (value >> 16);
359 | op += 3;
360 | state = 0;
361 | } else if (d == EQUALS) {
362 | // Emit the last (partial) output tuple;
363 | // expect no further data or padding characters.
364 | output[op+1] = (byte) (value >> 2);
365 | output[op] = (byte) (value >> 10);
366 | op += 2;
367 | state = 5;
368 | } else if (d != SKIP) {
369 | this.state = 6;
370 | return false;
371 | }
372 | break;
373 |
374 | case 4:
375 | if (d == EQUALS) {
376 | ++state;
377 | } else if (d != SKIP) {
378 | this.state = 6;
379 | return false;
380 | }
381 | break;
382 |
383 | case 5:
384 | if (d != SKIP) {
385 | this.state = 6;
386 | return false;
387 | }
388 | break;
389 | }
390 | }
391 |
392 | if (!finish) {
393 | // We're out of input, but a future call could provide
394 | // more.
395 | this.state = state;
396 | this.value = value;
397 | this.op = op;
398 | return true;
399 | }
400 |
401 | // Done reading input. Now figure out where we are left in
402 | // the state machine and finish up.
403 |
404 | switch (state) {
405 | case 0:
406 | // Output length is a multiple of three. Fine.
407 | break;
408 | case 1:
409 | // Read one extra input byte, which isn't enough to
410 | // make another output byte. Illegal.
411 | this.state = 6;
412 | return false;
413 | case 2:
414 | // Read two extra input bytes, enough to emit 1 more
415 | // output byte. Fine.
416 | output[op++] = (byte) (value >> 4);
417 | break;
418 | case 3:
419 | // Read three extra input bytes, enough to emit 2 more
420 | // output bytes. Fine.
421 | output[op++] = (byte) (value >> 10);
422 | output[op++] = (byte) (value >> 2);
423 | break;
424 | case 4:
425 | // Read one padding '=' when we expected 2. Illegal.
426 | this.state = 6;
427 | return false;
428 | case 5:
429 | // Read all the padding '='s we expected and no more.
430 | // Fine.
431 | break;
432 | }
433 |
434 | this.state = state;
435 | this.op = op;
436 | return true;
437 | }
438 | }
439 |
440 | // --------------------------------------------------------
441 | // encoding
442 | // --------------------------------------------------------
443 |
444 | /**
445 | * Base64-encode the given data and return a newly allocated
446 | * String with the result.
447 | *
448 | * @param input the data to encode
449 | * @param flags controls certain features of the encoded output.
450 | * Passing {@code DEFAULT} results in output that
451 | * adheres to RFC 2045.
452 | */
453 | public static String encodeToString(byte[] input, int flags) {
454 | try {
455 | return new String(encode(input, flags), "US-ASCII");
456 | } catch (UnsupportedEncodingException e) {
457 | // US-ASCII is guaranteed to be available.
458 | throw new AssertionError(e);
459 | }
460 | }
461 |
462 | /**
463 | * Base64-encode the given data and return a newly allocated
464 | * String with the result.
465 | *
466 | * @param input the data to encode
467 | * @param offset the position within the input array at which to
468 | * start
469 | * @param len the number of bytes of input to encode
470 | * @param flags controls certain features of the encoded output.
471 | * Passing {@code DEFAULT} results in output that
472 | * adheres to RFC 2045.
473 | */
474 | public static String encodeToString(byte[] input, int offset, int len, int flags) {
475 | try {
476 | return new String(encode(input, offset, len, flags), "US-ASCII");
477 | } catch (UnsupportedEncodingException e) {
478 | // US-ASCII is guaranteed to be available.
479 | throw new AssertionError(e);
480 | }
481 | }
482 |
483 | /**
484 | * Base64-encode the given data and return a newly allocated
485 | * byte[] with the result.
486 | *
487 | * @param input the data to encode
488 | * @param flags controls certain features of the encoded output.
489 | * Passing {@code DEFAULT} results in output that
490 | * adheres to RFC 2045.
491 | */
492 | public static byte[] encode(byte[] input, int flags) {
493 | return encode(input, 0, input.length, flags);
494 | }
495 |
496 | /**
497 | * Base64-encode the given data and return a newly allocated
498 | * byte[] with the result.
499 | *
500 | * @param input the data to encode
501 | * @param offset the position within the input array at which to
502 | * start
503 | * @param len the number of bytes of input to encode
504 | * @param flags controls certain features of the encoded output.
505 | * Passing {@code DEFAULT} results in output that
506 | * adheres to RFC 2045.
507 | */
508 | public static byte[] encode(byte[] input, int offset, int len, int flags) {
509 | Encoder encoder = new Encoder(flags, null);
510 |
511 | // Compute the exact length of the array we will produce.
512 | int output_len = len / 3 * 4;
513 |
514 | // Account for the tail of the data and the padding bytes, if any.
515 | if (encoder.do_padding) {
516 | if (len % 3 > 0) {
517 | output_len += 4;
518 | }
519 | } else {
520 | switch (len % 3) {
521 | case 0: break;
522 | case 1: output_len += 2; break;
523 | case 2: output_len += 3; break;
524 | }
525 | }
526 |
527 | // Account for the newlines, if any.
528 | if (encoder.do_newline && len > 0) {
529 | output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
530 | (encoder.do_cr ? 2 : 1);
531 | }
532 |
533 | encoder.output = new byte[output_len];
534 | encoder.process(input, offset, len, true);
535 |
536 | assert encoder.op == output_len;
537 |
538 | return encoder.output;
539 | }
540 |
541 | /* package */ static class Encoder extends Coder {
542 | /**
543 | * Emit a new line every this many output tuples. Corresponds to
544 | * a 76-character line length (the maximum allowable according to
545 | * RFC 2045).
546 | */
547 | public static final int LINE_GROUPS = 19;
548 |
549 | /**
550 | * Lookup table for turning Base64 alphabet positions (6 bits)
551 | * into output bytes.
552 | */
553 | private static final byte ENCODE[] = {
554 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
555 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
556 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
557 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
558 | };
559 |
560 | /**
561 | * Lookup table for turning Base64 alphabet positions (6 bits)
562 | * into output bytes.
563 | */
564 | private static final byte ENCODE_WEBSAFE[] = {
565 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
566 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
567 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
568 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
569 | };
570 |
571 | final private byte[] tail;
572 | /* package */ int tailLen;
573 | private int count;
574 |
575 | final public boolean do_padding;
576 | final public boolean do_newline;
577 | final public boolean do_cr;
578 | final private byte[] alphabet;
579 |
580 | public Encoder(int flags, byte[] output) {
581 | this.output = output;
582 |
583 | do_padding = (flags & NO_PADDING) == 0;
584 | do_newline = (flags & NO_WRAP) == 0;
585 | do_cr = (flags & CRLF) != 0;
586 | alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
587 |
588 | tail = new byte[2];
589 | tailLen = 0;
590 |
591 | count = do_newline ? LINE_GROUPS : -1;
592 | }
593 |
594 | /**
595 | * @return an overestimate for the number of bytes {@code
596 | * len} bytes could encode to.
597 | */
598 | public int maxOutputSize(int len) {
599 | return len * 8/5 + 10;
600 | }
601 |
602 | public boolean process(byte[] input, int offset, int len, boolean finish) {
603 | // Using local variables makes the encoder about 9% faster.
604 | final byte[] alphabet = this.alphabet;
605 | final byte[] output = this.output;
606 | int op = 0;
607 | int count = this.count;
608 |
609 | int p = offset;
610 | len += offset;
611 | int v = -1;
612 |
613 | // First we need to concatenate the tail of the previous call
614 | // with any input bytes available now and see if we can empty
615 | // the tail.
616 |
617 | switch (tailLen) {
618 | case 0:
619 | // There was no tail.
620 | break;
621 |
622 | case 1:
623 | if (p+2 <= len) {
624 | // A 1-byte tail with at least 2 bytes of
625 | // input available now.
626 | v = ((tail[0] & 0xff) << 16) |
627 | ((input[p++] & 0xff) << 8) |
628 | (input[p++] & 0xff);
629 | tailLen = 0;
630 | };
631 | break;
632 |
633 | case 2:
634 | if (p+1 <= len) {
635 | // A 2-byte tail with at least 1 byte of input.
636 | v = ((tail[0] & 0xff) << 16) |
637 | ((tail[1] & 0xff) << 8) |
638 | (input[p++] & 0xff);
639 | tailLen = 0;
640 | }
641 | break;
642 | }
643 |
644 | if (v != -1) {
645 | output[op++] = alphabet[(v >> 18) & 0x3f];
646 | output[op++] = alphabet[(v >> 12) & 0x3f];
647 | output[op++] = alphabet[(v >> 6) & 0x3f];
648 | output[op++] = alphabet[v & 0x3f];
649 | if (--count == 0) {
650 | if (do_cr) output[op++] = '\r';
651 | output[op++] = '\n';
652 | count = LINE_GROUPS;
653 | }
654 | }
655 |
656 | // At this point either there is no tail, or there are fewer
657 | // than 3 bytes of input available.
658 |
659 | // The main loop, turning 3 input bytes into 4 output bytes on
660 | // each iteration.
661 | while (p+3 <= len) {
662 | v = ((input[p] & 0xff) << 16) |
663 | ((input[p+1] & 0xff) << 8) |
664 | (input[p+2] & 0xff);
665 | output[op] = alphabet[(v >> 18) & 0x3f];
666 | output[op+1] = alphabet[(v >> 12) & 0x3f];
667 | output[op+2] = alphabet[(v >> 6) & 0x3f];
668 | output[op+3] = alphabet[v & 0x3f];
669 | p += 3;
670 | op += 4;
671 | if (--count == 0) {
672 | if (do_cr) output[op++] = '\r';
673 | output[op++] = '\n';
674 | count = LINE_GROUPS;
675 | }
676 | }
677 |
678 | if (finish) {
679 | // Finish up the tail of the input. Note that we need to
680 | // consume any bytes in tail before any bytes
681 | // remaining in input; there should be at most two bytes
682 | // total.
683 |
684 | if (p-tailLen == len-1) {
685 | int t = 0;
686 | v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
687 | tailLen -= t;
688 | output[op++] = alphabet[(v >> 6) & 0x3f];
689 | output[op++] = alphabet[v & 0x3f];
690 | if (do_padding) {
691 | output[op++] = '=';
692 | output[op++] = '=';
693 | }
694 | if (do_newline) {
695 | if (do_cr) output[op++] = '\r';
696 | output[op++] = '\n';
697 | }
698 | } else if (p-tailLen == len-2) {
699 | int t = 0;
700 | v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
701 | (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
702 | tailLen -= t;
703 | output[op++] = alphabet[(v >> 12) & 0x3f];
704 | output[op++] = alphabet[(v >> 6) & 0x3f];
705 | output[op++] = alphabet[v & 0x3f];
706 | if (do_padding) {
707 | output[op++] = '=';
708 | }
709 | if (do_newline) {
710 | if (do_cr) output[op++] = '\r';
711 | output[op++] = '\n';
712 | }
713 | } else if (do_newline && op > 0 && count != LINE_GROUPS) {
714 | if (do_cr) output[op++] = '\r';
715 | output[op++] = '\n';
716 | }
717 |
718 | assert tailLen == 0;
719 | assert p == len;
720 | } else {
721 | // Save the leftovers in tail to be consumed on the next
722 | // call to encodeInternal.
723 |
724 | if (p == len-1) {
725 | tail[tailLen++] = input[p];
726 | } else if (p == len-2) {
727 | tail[tailLen++] = input[p];
728 | tail[tailLen++] = input[p+1];
729 | }
730 | }
731 |
732 | this.op = op;
733 | this.count = count;
734 |
735 | return true;
736 | }
737 | }
738 |
739 | private Base64() { } // don't instantiate
740 | }
741 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/misc/DummyX509TrustManager.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler.misc;
2 |
3 | import java.security.cert.CertificateException;
4 | import java.security.cert.X509Certificate;
5 |
6 | import javax.net.ssl.X509TrustManager;
7 |
8 | /**
9 | * Dummy trust manager that accepts all certificates.
10 | *
11 | * @author akdeniz
12 | */
13 | public class DummyX509TrustManager implements X509TrustManager {
14 |
15 | public X509Certificate[] getAcceptedIssuers() {
16 | return null;
17 | }
18 |
19 | public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString)
20 | throws CertificateException {
21 | }
22 |
23 | public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString)
24 | throws CertificateException {
25 | }
26 | };
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/misc/HexDumpEncoder.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler.misc;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.ByteArrayOutputStream;
5 | import java.io.InputStream;
6 | import java.io.PrintStream;
7 | import java.io.OutputStream;
8 | import java.io.IOException;
9 | import java.nio.ByteBuffer;
10 |
11 | /**
12 | * This class encodes a buffer into the classic: "Hexadecimal Dump" format of
13 | * the past. It is useful for analyzing the contents of binary buffers.
14 | * The format produced is as follows:
15 | *
193 | * The ByteBuffer's position will be advanced to ByteBuffer's limit.
194 | *
195 | * To avoid an extra copy, the implementation will attempt to return the
196 | * byte array backing the ByteBuffer. If this is not possible, a
197 | * new byte array will be created.
198 | */
199 | private byte [] getBytes(ByteBuffer bb) {
200 | /*
201 | * This should never return a BufferOverflowException, as we're
202 | * careful to allocate just the right amount.
203 | */
204 | byte [] buf = null;
205 |
206 | /*
207 | * If it has a usable backing byte buffer, use it. Use only
208 | * if the array exactly represents the current ByteBuffer.
209 | */
210 | if (bb.hasArray()) {
211 | byte [] tmp = bb.array();
212 | if ((tmp.length == bb.capacity()) &&
213 | (tmp.length == bb.remaining())) {
214 | buf = tmp;
215 | bb.position(bb.limit());
216 | }
217 | }
218 |
219 | if (buf == null) {
220 | /*
221 | * This class doesn't have a concept of encode(buf, len, off),
222 | * so if we have a partial buffer, we must reallocate
223 | * space.
224 | */
225 | buf = new byte[bb.remaining()];
226 |
227 | /*
228 | * position() automatically updated
229 | */
230 | bb.get(buf);
231 | }
232 |
233 | return buf;
234 | }
235 |
236 | /**
237 | * Encode the aBuffer ByteBuffer and write the encoded
238 | * result to the OutputStream aStream.
239 | *
240 | * The ByteBuffer's position will be advanced to ByteBuffer's limit.
241 | */
242 | public void encode(ByteBuffer aBuffer, OutputStream aStream)
243 | throws IOException {
244 | byte [] buf = getBytes(aBuffer);
245 | encode(buf, aStream);
246 | }
247 |
248 | /**
249 | * A 'streamless' version of encode that simply takes a ByteBuffer
250 | * and returns a string containing the encoded buffer.
251 | *
252 | * The ByteBuffer's position will be advanced to ByteBuffer's limit.
253 | */
254 | public String encode(ByteBuffer aBuffer) {
255 | byte [] buf = getBytes(aBuffer);
256 | return encode(buf);
257 | }
258 |
259 | /**
260 | * Encode bytes from the input stream, and write them as text characters
261 | * to the output stream. This method will run until it exhausts the
262 | * input stream. It differs from encode in that it will add the
263 | * line at the end of a final line that is shorter than bytesPerLine().
264 | */
265 | public void encodeBuffer(InputStream inStream, OutputStream outStream)
266 | throws IOException {
267 | int j;
268 | int numBytes;
269 | byte tmpbuffer[] = new byte[bytesPerLine()];
270 |
271 | encodeBufferPrefix(outStream);
272 |
273 | while (true) {
274 | numBytes = readFully(inStream, tmpbuffer);
275 | if (numBytes == 0) {
276 | break;
277 | }
278 | encodeLinePrefix(outStream, numBytes);
279 | for (j = 0; j < numBytes; j += bytesPerAtom()) {
280 | if ((j + bytesPerAtom()) <= numBytes) {
281 | encodeAtom(outStream, tmpbuffer, j, bytesPerAtom());
282 | } else {
283 | encodeAtom(outStream, tmpbuffer, j, (numBytes)- j);
284 | }
285 | }
286 | encodeLineSuffix(outStream);
287 | if (numBytes < bytesPerLine()) {
288 | break;
289 | }
290 | }
291 | encodeBufferSuffix(outStream);
292 | }
293 |
294 | /**
295 | * Encode the buffer in aBuffer and write the encoded
296 | * result to the OutputStream aStream.
297 | */
298 | public void encodeBuffer(byte aBuffer[], OutputStream aStream)
299 | throws IOException {
300 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
301 | encodeBuffer(inStream, aStream);
302 | }
303 |
304 | /**
305 | * A 'streamless' version of encode that simply takes a buffer of
306 | * bytes and returns a string containing the encoded buffer.
307 | */
308 | public String encodeBuffer(byte aBuffer[]) {
309 | ByteArrayOutputStream outStream = new ByteArrayOutputStream();
310 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
311 | try {
312 | encodeBuffer(inStream, outStream);
313 | } catch (Exception IOException) {
314 | // This should never happen.
315 | throw new Error("CharacterEncoder.encodeBuffer internal error");
316 | }
317 | return (outStream.toString());
318 | }
319 |
320 | /**
321 | * Encode the aBuffer ByteBuffer and write the encoded
322 | * result to the OutputStream aStream.
323 | *
324 | * The ByteBuffer's position will be advanced to ByteBuffer's limit.
325 | */
326 | public void encodeBuffer(ByteBuffer aBuffer, OutputStream aStream)
327 | throws IOException {
328 | byte [] buf = getBytes(aBuffer);
329 | encodeBuffer(buf, aStream);
330 | }
331 |
332 | /**
333 | * A 'streamless' version of encode that simply takes a ByteBuffer
334 | * and returns a string containing the encoded buffer.
335 | *
336 | * The ByteBuffer's position will be advanced to ByteBuffer's limit.
337 | */
338 | public String encodeBuffer(ByteBuffer aBuffer) {
339 | byte [] buf = getBytes(aBuffer);
340 | return encodeBuffer(buf);
341 | }
342 |
343 | }
344 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/theapache64/gpa/api/Play.kt:
--------------------------------------------------------------------------------
1 | package com.github.theapache64.gpa.api
2 |
3 | import com.akdeniz.googleplaycrawler.GooglePlayAPI
4 | import com.github.theapache64.gpa.core.SearchEngineResultPage
5 | import com.github.theapache64.gpa.model.Account
6 | import kotlinx.coroutines.CoroutineDispatcher
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.delay
9 | import kotlinx.coroutines.withContext
10 |
11 | object Play {
12 | private const val USER_AGENT =
13 | "Android-Finsky/13.1.32-all (versionCode=81313200,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)"
14 |
15 | /**
16 | * To login and get userToken and gsfId
17 | */
18 | suspend fun login(
19 | username: String,
20 | password: String,
21 | locale: String = PlayUtils.getLocalization(),
22 | dispatcher: CoroutineDispatcher = Dispatchers.IO,
23 | loginDelay: Long = 10_000,
24 | sdkVersion: Int = 17
25 | ): Account = withContext(dispatcher) {
26 |
27 | // Building GooglePlayAPI
28 | val api = GooglePlayAPI(
29 | username,
30 | password
31 | ).apply {
32 | client = PlayUtils.createLoginClient()
33 | localization = locale
34 | useragent = USER_AGENT
35 | }
36 |
37 |
38 | // Requesting for login
39 | api.login()
40 |
41 | // To get GSF id
42 | api.checkin()
43 |
44 | // Upload device config
45 | api.uploadDeviceConfig()
46 |
47 | // giving time to sync the device config in google servers.
48 | delay(loginDelay)
49 |
50 | Account(
51 | username,
52 | password,
53 | api.token,
54 | api.androidID,
55 | locale
56 | )
57 | }
58 |
59 | fun getApi(account: Account): GooglePlayAPI {
60 | return GooglePlayAPI(
61 | account.username,
62 | account.password
63 | ).apply {
64 | useragent = USER_AGENT
65 | androidID = account.gsfId
66 | token = account.token
67 | localization = account.locale
68 | }
69 | }
70 |
71 | suspend fun search(
72 | query: String,
73 | api: GooglePlayAPI,
74 | _serp: SearchEngineResultPage? = null,
75 | ): SearchEngineResultPage = withContext(Dispatchers.IO) {
76 |
77 | var serp = _serp
78 | var nextPageUrl: String? = null
79 |
80 | if (serp != null) {
81 | // second+ time
82 | nextPageUrl = serp.nextPageUrl
83 | }
84 |
85 | if (serp == null) {
86 | serp = SearchEngineResultPage(SearchEngineResultPage.SEARCH)
87 | }
88 |
89 | serp.append(api.searchApp(query))
90 |
91 | if (nextPageUrl == null) {
92 | // first time
93 | nextPageUrl = serp.nextPageUrl
94 | }
95 |
96 | if (nextPageUrl?.isNotBlank() == true) {
97 | serp.append(api.getList(nextPageUrl))
98 | }
99 |
100 | serp
101 | }
102 |
103 |
104 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/theapache64/gpa/api/PlayUtils.kt:
--------------------------------------------------------------------------------
1 | package com.github.theapache64.gpa.api
2 |
3 | import com.github.theapache64.gpa.core.net.DroidConnectionSocketFactory
4 | import org.apache.http.client.HttpClient
5 | import org.apache.http.client.config.RequestConfig
6 | import org.apache.http.config.RegistryBuilder
7 | import org.apache.http.conn.socket.ConnectionSocketFactory
8 | import org.apache.http.impl.client.HttpClientBuilder
9 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager
10 | import java.util.*
11 |
12 | internal object PlayUtils {
13 | fun createLoginClient(): HttpClient? {
14 | val rb = RegistryBuilder.create
16 | * xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................
17 | *
18 | * Where xxxx is the offset into the buffer in 16 byte chunks, followed
19 | * by ascii coded hexadecimal bytes followed by the ASCII representation of
20 | * the bytes or '.' if they are not valid bytes.
21 | *
22 | * @author Chuck McManis
23 | */
24 |
25 | public class HexDumpEncoder {
26 |
27 | protected PrintStream pStream;
28 |
29 | private int offset;
30 | private int thisLineLength;
31 | private int currentByte;
32 | private byte thisLine[] = new byte[16];
33 |
34 | static void hexDigit(PrintStream p, byte x) {
35 | char c;
36 |
37 | c = (char) ((x >> 4) & 0xf);
38 | if (c > 9)
39 | c = (char) ((c-10) + 'A');
40 | else
41 | c = (char)(c + '0');
42 | p.write(c);
43 | c = (char) (x & 0xf);
44 | if (c > 9)
45 | c = (char)((c-10) + 'A');
46 | else
47 | c = (char)(c + '0');
48 | p.write(c);
49 | }
50 |
51 | protected int bytesPerAtom() {
52 | return (1);
53 | }
54 |
55 | protected int bytesPerLine() {
56 | return (16);
57 | }
58 |
59 | /**
60 | * Encode the prefix for the entire buffer. By default is simply
61 | * opens the PrintStream for use by the other functions.
62 | */
63 | protected void encodeBufferPrefix(OutputStream aStream) throws IOException {
64 | offset = 0;
65 | pStream = new PrintStream(aStream);
66 | }
67 |
68 | protected void encodeLinePrefix(OutputStream o, int len) throws IOException {
69 | hexDigit(pStream, (byte)((offset >>> 8) & 0xff));
70 | hexDigit(pStream, (byte)(offset & 0xff));
71 | pStream.print(": ");
72 | currentByte = 0;
73 | thisLineLength = len;
74 | }
75 |
76 | protected void encodeAtom(OutputStream o, byte buf[], int off, int len) throws IOException {
77 | thisLine[currentByte] = buf[off];
78 | hexDigit(pStream, buf[off]);
79 | pStream.print(" ");
80 | currentByte++;
81 | if (currentByte == 8)
82 | pStream.print(" ");
83 | }
84 |
85 | protected void encodeLineSuffix(OutputStream o) throws IOException {
86 | if (thisLineLength < 16) {
87 | for (int i = thisLineLength; i < 16; i++) {
88 | pStream.print(" ");
89 | if (i == 7)
90 | pStream.print(" ");
91 | }
92 | }
93 | pStream.print(" ");
94 | for (int i = 0; i < thisLineLength; i++) {
95 | if ((thisLine[i] < ' ') || (thisLine[i] > 'z')) {
96 | pStream.print(".");
97 | } else {
98 | pStream.write(thisLine[i]);
99 | }
100 | }
101 | pStream.println();
102 | offset += thisLineLength;
103 | }
104 |
105 |
106 | /**
107 | * Encode bytes from the input stream, and write them as text characters
108 | * to the output stream. This method will run until it exhausts the
109 | * input stream, but does not print the line suffix for a final
110 | * line that is shorter than bytesPerLine().
111 | */
112 | public void encode(InputStream inStream, OutputStream outStream)
113 | throws IOException {
114 | int j;
115 | int numBytes;
116 | byte tmpbuffer[] = new byte[bytesPerLine()];
117 |
118 | encodeBufferPrefix(outStream);
119 |
120 | while (true) {
121 | numBytes = readFully(inStream, tmpbuffer);
122 | if (numBytes == 0) {
123 | break;
124 | }
125 | encodeLinePrefix(outStream, numBytes);
126 | for (j = 0; j < numBytes; j += bytesPerAtom()) {
127 |
128 | if ((j + bytesPerAtom()) <= numBytes) {
129 | encodeAtom(outStream, tmpbuffer, j, bytesPerAtom());
130 | } else {
131 | encodeAtom(outStream, tmpbuffer, j, (numBytes)- j);
132 | }
133 | }
134 | encodeLineSuffix(outStream);
135 | }
136 | encodeBufferSuffix(outStream);
137 | }
138 |
139 | /**
140 | * Encode the buffer in aBuffer and write the encoded
141 | * result to the OutputStream aStream.
142 | */
143 | public void encode(byte aBuffer[], OutputStream aStream)
144 | throws IOException {
145 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
146 | encode(inStream, aStream);
147 | }
148 |
149 | /**
150 | * A 'streamless' version of encode that simply takes a buffer of
151 | * bytes and returns a string containing the encoded buffer.
152 | */
153 | public String encode(byte aBuffer[]) {
154 | ByteArrayOutputStream outStream = new ByteArrayOutputStream();
155 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
156 | String retVal = null;
157 | try {
158 | encode(inStream, outStream);
159 | // explicit ascii->unicode conversion
160 | retVal = outStream.toString("8859_1");
161 | } catch (Exception IOException) {
162 | // This should never happen.
163 | throw new Error("CharacterEncoder.encode internal error");
164 | }
165 | return (retVal);
166 | }
167 |
168 | /**
169 | * This method works around the bizarre semantics of BufferedInputStream's
170 | * read method.
171 | */
172 | protected int readFully(InputStream in, byte buffer[])
173 | throws java.io.IOException {
174 | for (int i = 0; i < buffer.length; i++) {
175 | int q = in.read();
176 | if (q == -1)
177 | return i;
178 | buffer[i] = (byte)q;
179 | }
180 | return buffer.length;
181 | }
182 |
183 | /**
184 | * Encode the suffix for the entire buffer.
185 | */
186 | protected void encodeBufferSuffix(OutputStream aStream) throws IOException {
187 | }
188 |
189 |
190 | /**
191 | * Return a byte array from the remaining bytes in this ByteBuffer.
192 | *