Fix lint error/warning while building android template
							
								
								
									
										47
									
								
								platform/android/java/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,47 @@ | |||
| # Third party libraries | ||||
| 
 | ||||
| 
 | ||||
| ## Google's vending library | ||||
| 
 | ||||
| - Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/src/main/java/com/google/android/vending | ||||
| - Version: git (eb57657, 2018) with modifications | ||||
| - License: Apache 2.0 | ||||
| 
 | ||||
| Overwrite all files under `com/google/android/vending` | ||||
| 
 | ||||
| ### Modify some files to avoid compile error and lint warning | ||||
| 
 | ||||
| #### com/google/android/vending/licensing/util/Base64.java | ||||
| ``` | ||||
| @@ -338,7 +338,8 @@ public class Base64 { | ||||
|                         e += 4; | ||||
|                 } | ||||
|   | ||||
| -               assert (e == outBuff.length); | ||||
| +               if (BuildConfig.DEBUG && e != outBuff.length) | ||||
| +                       throw new RuntimeException(); | ||||
|                 return outBuff; | ||||
|         } | ||||
| ``` | ||||
| 
 | ||||
| #### com/google/android/vending/licensing/LicenseChecker.java | ||||
| ``` | ||||
| @@ -29,8 +29,8 @@ import android.os.RemoteException; | ||||
|  import android.provider.Settings.Secure; | ||||
|  import android.util.Log; | ||||
|   | ||||
| -import com.android.vending.licensing.ILicenseResultListener; | ||||
| -import com.android.vending.licensing.ILicensingService; | ||||
| +import com.google.android.vending.licensing.ILicenseResultListener; | ||||
| +import com.google.android.vending.licensing.ILicensingService; | ||||
|  import com.google.android.vending.licensing.util.Base64; | ||||
|  import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| ``` | ||||
| ``` | ||||
| @@ -287,13 +287,15 @@ public class LicenseChecker implements ServiceConnection { | ||||
|      if (logResponse) { | ||||
| -        String android_id = Secure.getString(mContext.getContentResolver(), | ||||
| -                            Secure.ANDROID_ID); | ||||
| +        String android_id = Secure.ANDROID_ID; | ||||
|          Date date = new Date(); | ||||
| ``` | ||||
|  | @ -1,6 +1,5 @@ | |||
| #Sat Jul 29 16:10:03 ICT 2017 | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip | ||||
|  |  | |||
							
								
								
									
										100
									
								
								platform/android/java/gradlew
									
										
									
									
										vendored
									
									
								
							
							
						
						|  | @ -1,4 +1,4 @@ | |||
| #!/usr/bin/env bash | ||||
| #!/usr/bin/env sh | ||||
| 
 | ||||
| ############################################################################## | ||||
| ## | ||||
|  | @ -6,42 +6,6 @@ | |||
| ## | ||||
| ############################################################################## | ||||
| 
 | ||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| DEFAULT_JVM_OPTS="" | ||||
| 
 | ||||
| APP_NAME="Gradle" | ||||
| APP_BASE_NAME=`basename "$0"` | ||||
| 
 | ||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||
| MAX_FD="maximum" | ||||
| 
 | ||||
| warn ( ) { | ||||
|     echo "$*" | ||||
| } | ||||
| 
 | ||||
| die ( ) { | ||||
|     echo | ||||
|     echo "$*" | ||||
|     echo | ||||
|     exit 1 | ||||
| } | ||||
| 
 | ||||
| # OS specific support (must be 'true' or 'false'). | ||||
| cygwin=false | ||||
| msys=false | ||||
| darwin=false | ||||
| case "`uname`" in | ||||
|   CYGWIN* ) | ||||
|     cygwin=true | ||||
|     ;; | ||||
|   Darwin* ) | ||||
|     darwin=true | ||||
|     ;; | ||||
|   MINGW* ) | ||||
|     msys=true | ||||
|     ;; | ||||
| esac | ||||
| 
 | ||||
| # Attempt to set APP_HOME | ||||
| # Resolve links: $0 may be a link | ||||
| PRG="$0" | ||||
|  | @ -60,6 +24,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null | |||
| APP_HOME="`pwd -P`" | ||||
| cd "$SAVED" >/dev/null | ||||
| 
 | ||||
| APP_NAME="Gradle" | ||||
| APP_BASE_NAME=`basename "$0"` | ||||
| 
 | ||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| DEFAULT_JVM_OPTS="" | ||||
| 
 | ||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||
| MAX_FD="maximum" | ||||
| 
 | ||||
| warn () { | ||||
|     echo "$*" | ||||
| } | ||||
| 
 | ||||
| die () { | ||||
|     echo | ||||
|     echo "$*" | ||||
|     echo | ||||
|     exit 1 | ||||
| } | ||||
| 
 | ||||
| # OS specific support (must be 'true' or 'false'). | ||||
| cygwin=false | ||||
| msys=false | ||||
| darwin=false | ||||
| nonstop=false | ||||
| case "`uname`" in | ||||
|   CYGWIN* ) | ||||
|     cygwin=true | ||||
|     ;; | ||||
|   Darwin* ) | ||||
|     darwin=true | ||||
|     ;; | ||||
|   MINGW* ) | ||||
|     msys=true | ||||
|     ;; | ||||
|   NONSTOP* ) | ||||
|     nonstop=true | ||||
|     ;; | ||||
| esac | ||||
| 
 | ||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||
| 
 | ||||
| # Determine the Java command to use to start the JVM. | ||||
|  | @ -85,7 +89,7 @@ location of your Java installation." | |||
| fi | ||||
| 
 | ||||
| # Increase the maximum file descriptors if we can. | ||||
| if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then | ||||
| if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then | ||||
|     MAX_FD_LIMIT=`ulimit -H -n` | ||||
|     if [ $? -eq 0 ] ; then | ||||
|         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then | ||||
|  | @ -150,11 +154,19 @@ if $cygwin ; then | |||
|     esac | ||||
| fi | ||||
| 
 | ||||
| # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules | ||||
| function splitJvmOpts() { | ||||
|     JVM_OPTS=("$@") | ||||
| # Escape application args | ||||
| save () { | ||||
|     for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done | ||||
|     echo " " | ||||
| } | ||||
| eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS | ||||
| JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" | ||||
| APP_ARGS=$(save "$@") | ||||
| 
 | ||||
| exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" | ||||
| # Collect all arguments for the java command, following the shell quoting and substitution rules | ||||
| eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" | ||||
| 
 | ||||
| # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong | ||||
| if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then | ||||
|   cd "$(dirname "$0")" | ||||
| fi | ||||
| 
 | ||||
| exec "$JAVACMD" "$@" | ||||
|  |  | |||
							
								
								
									
										174
									
								
								platform/android/java/gradlew.bat
									
										
									
									
										vendored
									
									
								
							
							
						
						|  | @ -1,90 +1,84 @@ | |||
| @if "%DEBUG%" == "" @echo off | ||||
| @rem ########################################################################## | ||||
| @rem | ||||
| @rem  Gradle startup script for Windows | ||||
| @rem | ||||
| @rem ########################################################################## | ||||
| 
 | ||||
| @rem Set local scope for the variables with windows NT shell | ||||
| if "%OS%"=="Windows_NT" setlocal | ||||
| 
 | ||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| set DEFAULT_JVM_OPTS= | ||||
| 
 | ||||
| set DIRNAME=%~dp0 | ||||
| if "%DIRNAME%" == "" set DIRNAME=. | ||||
| set APP_BASE_NAME=%~n0 | ||||
| set APP_HOME=%DIRNAME% | ||||
| 
 | ||||
| @rem Find java.exe | ||||
| if defined JAVA_HOME goto findJavaFromJavaHome | ||||
| 
 | ||||
| set JAVA_EXE=java.exe | ||||
| %JAVA_EXE% -version >NUL 2>&1 | ||||
| if "%ERRORLEVEL%" == "0" goto init | ||||
| 
 | ||||
| echo. | ||||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||
| echo. | ||||
| echo Please set the JAVA_HOME variable in your environment to match the | ||||
| echo location of your Java installation. | ||||
| 
 | ||||
| goto fail | ||||
| 
 | ||||
| :findJavaFromJavaHome | ||||
| set JAVA_HOME=%JAVA_HOME:"=% | ||||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||||
| 
 | ||||
| if exist "%JAVA_EXE%" goto init | ||||
| 
 | ||||
| echo. | ||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||||
| echo. | ||||
| echo Please set the JAVA_HOME variable in your environment to match the | ||||
| echo location of your Java installation. | ||||
| 
 | ||||
| goto fail | ||||
| 
 | ||||
| :init | ||||
| @rem Get command-line arguments, handling Windowz variants | ||||
| 
 | ||||
| if not "%OS%" == "Windows_NT" goto win9xME_args | ||||
| if "%@eval[2+2]" == "4" goto 4NT_args | ||||
| 
 | ||||
| :win9xME_args | ||||
| @rem Slurp the command line arguments. | ||||
| set CMD_LINE_ARGS= | ||||
| set _SKIP=2 | ||||
| 
 | ||||
| :win9xME_args_slurp | ||||
| if "x%~1" == "x" goto execute | ||||
| 
 | ||||
| set CMD_LINE_ARGS=%* | ||||
| goto execute | ||||
| 
 | ||||
| :4NT_args | ||||
| @rem Get arguments from the 4NT Shell from JP Software | ||||
| set CMD_LINE_ARGS=%$ | ||||
| 
 | ||||
| :execute | ||||
| @rem Setup the command line | ||||
| 
 | ||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||
| 
 | ||||
| @rem Execute Gradle | ||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||||
| 
 | ||||
| :end | ||||
| @rem End local scope for the variables with windows NT shell | ||||
| if "%ERRORLEVEL%"=="0" goto mainEnd | ||||
| 
 | ||||
| :fail | ||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||
| rem the _cmd.exe /c_ return code! | ||||
| if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | ||||
| exit /b 1 | ||||
| 
 | ||||
| :mainEnd | ||||
| if "%OS%"=="Windows_NT" endlocal | ||||
| 
 | ||||
| :omega | ||||
| @if "%DEBUG%" == "" @echo off | ||||
| @rem ########################################################################## | ||||
| @rem | ||||
| @rem  Gradle startup script for Windows | ||||
| @rem | ||||
| @rem ########################################################################## | ||||
| 
 | ||||
| @rem Set local scope for the variables with windows NT shell | ||||
| if "%OS%"=="Windows_NT" setlocal | ||||
| 
 | ||||
| set DIRNAME=%~dp0 | ||||
| if "%DIRNAME%" == "" set DIRNAME=. | ||||
| set APP_BASE_NAME=%~n0 | ||||
| set APP_HOME=%DIRNAME% | ||||
| 
 | ||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| set DEFAULT_JVM_OPTS= | ||||
| 
 | ||||
| @rem Find java.exe | ||||
| if defined JAVA_HOME goto findJavaFromJavaHome | ||||
| 
 | ||||
| set JAVA_EXE=java.exe | ||||
| %JAVA_EXE% -version >NUL 2>&1 | ||||
| if "%ERRORLEVEL%" == "0" goto init | ||||
| 
 | ||||
| echo. | ||||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||
| echo. | ||||
| echo Please set the JAVA_HOME variable in your environment to match the | ||||
| echo location of your Java installation. | ||||
| 
 | ||||
| goto fail | ||||
| 
 | ||||
| :findJavaFromJavaHome | ||||
| set JAVA_HOME=%JAVA_HOME:"=% | ||||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||||
| 
 | ||||
| if exist "%JAVA_EXE%" goto init | ||||
| 
 | ||||
| echo. | ||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||||
| echo. | ||||
| echo Please set the JAVA_HOME variable in your environment to match the | ||||
| echo location of your Java installation. | ||||
| 
 | ||||
| goto fail | ||||
| 
 | ||||
| :init | ||||
| @rem Get command-line arguments, handling Windows variants | ||||
| 
 | ||||
| if not "%OS%" == "Windows_NT" goto win9xME_args | ||||
| 
 | ||||
| :win9xME_args | ||||
| @rem Slurp the command line arguments. | ||||
| set CMD_LINE_ARGS= | ||||
| set _SKIP=2 | ||||
| 
 | ||||
| :win9xME_args_slurp | ||||
| if "x%~1" == "x" goto execute | ||||
| 
 | ||||
| set CMD_LINE_ARGS=%* | ||||
| 
 | ||||
| :execute | ||||
| @rem Setup the command line | ||||
| 
 | ||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||
| 
 | ||||
| @rem Execute Gradle | ||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||||
| 
 | ||||
| :end | ||||
| @rem End local scope for the variables with windows NT shell | ||||
| if "%ERRORLEVEL%"=="0" goto mainEnd | ||||
| 
 | ||||
| :fail | ||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||
| rem the _cmd.exe /c_ return code! | ||||
| if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | ||||
| exit /b 1 | ||||
| 
 | ||||
| :mainEnd | ||||
| if "%OS%"=="Windows_NT" endlocal | ||||
| 
 | ||||
| :omega | ||||
|  |  | |||
| Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 127 B After Width: | Height: | Size: 718 B | 
| Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB | 
| After Width: | Height: | Size: 462 B | 
| After Width: | Height: | Size: 2.8 KiB | 
|  | @ -15,7 +15,7 @@ | |||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginBottom="10dp" | ||||
|             android:layout_marginLeft="5dp" | ||||
|             android:layout_marginStart="5dp" | ||||
|             android:layout_marginTop="10dp" | ||||
|             android:textStyle="bold" /> | ||||
| 
 | ||||
|  | @ -23,12 +23,11 @@ | |||
|             android:id="@+id/downloaderDashboard" | ||||
|             android:layout_width="fill_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_below="@id/statusText" | ||||
|             android:orientation="vertical" > | ||||
| 
 | ||||
|             <RelativeLayout | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_height="0dp" | ||||
|                 android:layout_weight="1" > | ||||
| 
 | ||||
|                 <TextView | ||||
|  | @ -36,18 +35,15 @@ | |||
|                     style="@android:style/TextAppearance.Small" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_alignParentLeft="true" | ||||
|                     android:layout_marginLeft="5dp" | ||||
|                     android:text="0MB / 0MB" > | ||||
|                 </TextView> | ||||
|                     android:layout_alignParentStart="true" | ||||
|                     android:layout_marginStart="5dp" /> | ||||
| 
 | ||||
|                 <TextView | ||||
|                     android:id="@+id/progressAsPercentage" | ||||
|                     style="@android:style/TextAppearance.Small" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_alignRight="@+id/progressBar" | ||||
|                     android:text="0%" /> | ||||
|                     android:layout_alignEnd="@+id/progressBar" /> | ||||
| 
 | ||||
|                 <ProgressBar | ||||
|                     android:id="@+id/progressBar" | ||||
|  | @ -58,24 +54,23 @@ | |||
|                     android:layout_marginBottom="10dp" | ||||
|                     android:layout_marginLeft="10dp" | ||||
|                     android:layout_marginRight="10dp" | ||||
|                     android:layout_marginTop="10dp" | ||||
|                     android:layout_weight="1" /> | ||||
|                     android:layout_marginTop="10dp" /> | ||||
| 
 | ||||
|                 <TextView | ||||
|                     android:id="@+id/progressAverageSpeed" | ||||
|                     style="@android:style/TextAppearance.Small" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_alignParentLeft="true" | ||||
|                     android:layout_alignParentStart="true" | ||||
|                     android:layout_below="@+id/progressBar" | ||||
|                     android:layout_marginLeft="5dp" /> | ||||
|                     android:layout_marginStart="5dp" /> | ||||
| 
 | ||||
|                 <TextView | ||||
|                     android:id="@+id/progressTimeRemaining" | ||||
|                     style="@android:style/TextAppearance.Small" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_alignRight="@+id/progressBar" | ||||
|                     android:layout_alignEnd="@+id/progressBar" | ||||
|                     android:layout_below="@+id/progressBar" /> | ||||
|             </RelativeLayout> | ||||
| 
 | ||||
|  | @ -85,20 +80,6 @@ | |||
|                 android:layout_height="wrap_content" | ||||
|                 android:orientation="horizontal" > | ||||
| 
 | ||||
|                 <Button | ||||
|                     android:id="@+id/pauseButton" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_gravity="center_vertical" | ||||
|                     android:layout_marginBottom="10dp" | ||||
|                     android:layout_marginLeft="10dp" | ||||
|                     android:layout_marginRight="5dp" | ||||
|                     android:layout_marginTop="10dp" | ||||
|                     android:layout_weight="0" | ||||
|                     android:minHeight="40dp" | ||||
|                     android:minWidth="94dp" | ||||
|                     android:text="@string/text_button_pause" /> | ||||
| 
 | ||||
|                 <Button | ||||
|                     android:id="@+id/cancelButton" | ||||
|                     android:layout_width="wrap_content" | ||||
|  | @ -112,7 +93,23 @@ | |||
|                     android:minHeight="40dp" | ||||
|                     android:minWidth="94dp" | ||||
|                     android:text="@string/text_button_cancel" | ||||
|                     android:visibility="gone" /> | ||||
|                     android:visibility="gone" | ||||
|                     style="?android:attr/buttonBarButtonStyle" /> | ||||
| 
 | ||||
|                 <Button | ||||
|                     android:id="@+id/pauseButton" | ||||
|                     android:layout_width="wrap_content" | ||||
|                     android:layout_height="wrap_content" | ||||
|                     android:layout_gravity="center_vertical" | ||||
|                     android:layout_marginBottom="10dp" | ||||
|                     android:layout_marginStart="10dp" | ||||
|                     android:layout_marginEnd="5dp" | ||||
|                     android:layout_marginTop="10dp" | ||||
|                     android:layout_weight="0" | ||||
|                     android:minHeight="40dp" | ||||
|                     android:minWidth="94dp" | ||||
|                     android:text="@string/text_button_pause" | ||||
|                     style="?android:attr/buttonBarButtonStyle" /> | ||||
|             </LinearLayout> | ||||
|         </LinearLayout> | ||||
|     </LinearLayout> | ||||
|  | @ -151,7 +148,8 @@ | |||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:layout_margin="10dp" | ||||
|                 android:text="@string/text_button_resume_cellular" /> | ||||
|                 android:text="@string/text_button_resume_cellular" | ||||
|                 style="?android:attr/buttonBarButtonStyle" /> | ||||
| 
 | ||||
|             <Button | ||||
|                 android:id="@+id/wifiSettingsButton" | ||||
|  | @ -159,7 +157,8 @@ | |||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:layout_margin="10dp" | ||||
|                 android:text="@string/text_button_wifi_settings" /> | ||||
|                 android:text="@string/text_button_wifi_settings" | ||||
|                 style="?android:attr/buttonBarButtonStyle" /> | ||||
|             </LinearLayout> | ||||
|     </LinearLayout> | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,7 +17,8 @@ | |||
| */ | ||||
| --> | ||||
| 
 | ||||
| <LinearLayout android:layout_width="match_parent" | ||||
| <LinearLayout xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:baselineAligned="false" | ||||
|     android:orientation="horizontal" android:id="@+id/notificationLayout" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|  | @ -33,16 +34,17 @@ | |||
|             android:layout_width="fill_parent" | ||||
|             android:layout_height="25dp" | ||||
|             android:scaleType="centerInside" | ||||
|             android:layout_alignParentLeft="true" | ||||
|             android:layout_alignParentStart="true" | ||||
|             android:layout_alignParentTop="true" | ||||
|             android:src="@android:drawable/stat_sys_download" /> | ||||
|             android:src="@android:drawable/stat_sys_download" | ||||
|             android:contentDescription="@string/godot_project_name_string" /> | ||||
| 
 | ||||
|         <TextView | ||||
|             android:id="@+id/progress_text" | ||||
|             style="@style/NotificationText" | ||||
|             android:layout_width="fill_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_alignParentLeft="true" | ||||
|             android:layout_alignParentStart="true" | ||||
|             android:layout_alignParentBottom="true" | ||||
|             android:layout_gravity="center_horizontal" | ||||
|             android:singleLine="true" | ||||
|  | @ -56,15 +58,16 @@ | |||
|         android:clickable="true" | ||||
|         android:focusable="true" | ||||
|         android:paddingTop="10dp" | ||||
|         android:paddingRight="8dp" | ||||
|         android:paddingBottom="8dp" > | ||||
|         android:paddingEnd="8dp" | ||||
|         android:paddingBottom="8dp" | ||||
|         tools:ignore="RtlSymmetry"> | ||||
| 
 | ||||
|         <TextView | ||||
|             android:id="@+id/title" | ||||
|             style="@style/NotificationTitle" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_alignParentLeft="true" | ||||
|             android:layout_alignParentStart="true" | ||||
|             android:singleLine="true"/> | ||||
| 
 | ||||
|         <TextView | ||||
|  | @ -72,8 +75,9 @@ | |||
|             style="@style/NotificationText" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_alignParentRight="true" | ||||
|             android:singleLine="true"/> | ||||
|             android:layout_alignParentEnd="true" | ||||
|             android:singleLine="true" | ||||
|             tools:ignore="RelativeOverlap" /> | ||||
|         <!-- Only one of progress_bar and paused_text will be visible. --> | ||||
| 
 | ||||
|         <FrameLayout | ||||
|  | @ -87,7 +91,7 @@ | |||
|                 style="?android:attr/progressBarStyleHorizontal" | ||||
|                 android:layout_width="fill_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:paddingRight="25dp" /> | ||||
|                 android:paddingEnd="25dp" /> | ||||
| 
 | ||||
|             <TextView | ||||
|                 android:id="@+id/description" | ||||
|  | @ -95,7 +99,7 @@ | |||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:paddingRight="25dp" | ||||
|                 android:paddingEnd="25dp" | ||||
|                 android:singleLine="true" /> | ||||
|         </FrameLayout> | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ | |||
|     <string name="notification_download_failed">다운로드 실패</string> | ||||
| 
 | ||||
| 
 | ||||
|     <string name="state_unknown">시작중...</string> | ||||
|     <string name="state_unknown">시작중…</string> | ||||
|     <string name="state_idle">다운로드 시작을 기다리는 중</string> | ||||
|     <string name="state_fetching_url">다운로드할 항목을 찾는 중</string> | ||||
|     <string name="state_connecting">다운로드 서버에 연결 중</string> | ||||
|  |  | |||
|  | @ -1,6 +0,0 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <style name="NotificationTextSecondary" parent="NotificationText"> | ||||
|         <item name="android:textSize">12sp</item> | ||||
|     </style> | ||||
| </resources> | ||||
|  | @ -1,5 +0,0 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <style name="NotificationText" parent="android:TextAppearance.StatusBar.EventContent" /> | ||||
|     <style name="NotificationTitle" parent="android:TextAppearance.StatusBar.EventContent.Title" /> | ||||
| </resources> | ||||
|  | @ -30,7 +30,7 @@ | |||
|     <string name="notification_download_failed">Download unsuccessful</string> | ||||
| 
 | ||||
| 
 | ||||
|     <string name="state_unknown">Starting...</string> | ||||
|     <string name="state_unknown">Starting…</string> | ||||
|     <string name="state_idle">Waiting for download to start</string> | ||||
|     <string name="state_fetching_url">Looking for resources to download</string> | ||||
|     <string name="state_connecting">Connecting to the download server</string> | ||||
|  |  | |||
|  | @ -1,110 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.Base64; | ||||
| import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.security.GeneralSecurityException; | ||||
| import java.security.spec.KeySpec; | ||||
| 
 | ||||
| import javax.crypto.BadPaddingException; | ||||
| import javax.crypto.Cipher; | ||||
| import javax.crypto.IllegalBlockSizeException; | ||||
| import javax.crypto.SecretKey; | ||||
| import javax.crypto.SecretKeyFactory; | ||||
| import javax.crypto.spec.IvParameterSpec; | ||||
| import javax.crypto.spec.PBEKeySpec; | ||||
| import javax.crypto.spec.SecretKeySpec; | ||||
| 
 | ||||
| /** | ||||
|  * An Obfuscator that uses AES to encrypt data. | ||||
|  */ | ||||
| public class AESObfuscator implements Obfuscator { | ||||
|     private static final String UTF8 = "UTF-8"; | ||||
|     private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; | ||||
|     private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; | ||||
|     private static final byte[] IV = | ||||
|         { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; | ||||
|     private static final String header = "com.android.vending.licensing.AESObfuscator-1|"; | ||||
| 
 | ||||
|     private Cipher mEncryptor; | ||||
|     private Cipher mDecryptor; | ||||
| 
 | ||||
|     /** | ||||
|      * @param salt an array of random bytes to use for each (un)obfuscation | ||||
|      * @param applicationId application identifier, e.g. the package name | ||||
|      * @param deviceId device identifier. Use as many sources as possible to | ||||
|      *    create this unique identifier. | ||||
|      */ | ||||
|     public AESObfuscator(byte[] salt, String applicationId, String deviceId) { | ||||
|         try { | ||||
|             SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); | ||||
|             KeySpec keySpec =    | ||||
|                 new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); | ||||
|             SecretKey tmp = factory.generateSecret(keySpec); | ||||
|             SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); | ||||
|             mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); | ||||
|             mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); | ||||
|             mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); | ||||
|             mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); | ||||
|         } catch (GeneralSecurityException e) { | ||||
|             // This can't happen on a compatible Android device. | ||||
|             throw new RuntimeException("Invalid environment", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public String obfuscate(String original, String key) { | ||||
|         if (original == null) { | ||||
|             return null; | ||||
|         } | ||||
|         try { | ||||
|             // Header is appended as an integrity check | ||||
|             return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); | ||||
|         } catch (UnsupportedEncodingException e) { | ||||
|             throw new RuntimeException("Invalid environment", e); | ||||
|         } catch (GeneralSecurityException e) { | ||||
|             throw new RuntimeException("Invalid environment", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public String unobfuscate(String obfuscated, String key) throws ValidationException { | ||||
|         if (obfuscated == null) { | ||||
|             return null; | ||||
|         } | ||||
|         try { | ||||
|             String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); | ||||
|             // Check for presence of header. This serves as a final integrity check, for cases | ||||
|             // where the block size is correct during decryption. | ||||
|             int headerIndex = result.indexOf(header+key); | ||||
|             if (headerIndex != 0) { | ||||
|                 throw new ValidationException("Header not found (invalid data or key)" + ":" + | ||||
|                         obfuscated); | ||||
|             } | ||||
|             return result.substring(header.length()+key.length(), result.length()); | ||||
|         } catch (Base64DecoderException e) { | ||||
|             throw new ValidationException(e.getMessage() + ":" + obfuscated); | ||||
|         } catch (IllegalBlockSizeException e) { | ||||
|             throw new ValidationException(e.getMessage() + ":" + obfuscated); | ||||
|         } catch (BadPaddingException e) { | ||||
|             throw new ValidationException(e.getMessage() + ":" + obfuscated); | ||||
|         } catch (UnsupportedEncodingException e) { | ||||
|             throw new RuntimeException("Invalid environment", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,397 +0,0 @@ | |||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| /* | ||||
|  * Copyright (C) 2012 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import org.apache.http.NameValuePair; | ||||
| import org.apache.http.client.utils.URLEncodedUtils; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.Vector; | ||||
| 
 | ||||
| /** | ||||
|  * Default policy. All policy decisions are based off of response data received | ||||
|  * from the licensing service. Specifically, the licensing server sends the | ||||
|  * following information: response validity period, error retry period, and | ||||
|  * error retry count. | ||||
|  * <p> | ||||
|  * These values will vary based on the the way the application is configured in | ||||
|  * the Android Market publishing console, such as whether the application is | ||||
|  * marked as free or is within its refund period, as well as how often an | ||||
|  * application is checking with the licensing service. | ||||
|  * <p> | ||||
|  * Developers who need more fine grained control over their application's | ||||
|  * licensing policy should implement a custom Policy. | ||||
|  */ | ||||
| public class APKExpansionPolicy implements Policy { | ||||
| 
 | ||||
|     private static final String TAG = "APKExpansionPolicy"; | ||||
|     private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy"; | ||||
|     private static final String PREF_LAST_RESPONSE = "lastResponse"; | ||||
|     private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; | ||||
|     private static final String PREF_RETRY_UNTIL = "retryUntil"; | ||||
|     private static final String PREF_MAX_RETRIES = "maxRetries"; | ||||
|     private static final String PREF_RETRY_COUNT = "retryCount"; | ||||
|     private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; | ||||
|     private static final String DEFAULT_RETRY_UNTIL = "0"; | ||||
|     private static final String DEFAULT_MAX_RETRIES = "0"; | ||||
|     private static final String DEFAULT_RETRY_COUNT = "0"; | ||||
| 
 | ||||
|     private static final long MILLIS_PER_MINUTE = 60 * 1000; | ||||
| 
 | ||||
|     private long mValidityTimestamp; | ||||
|     private long mRetryUntil; | ||||
|     private long mMaxRetries; | ||||
|     private long mRetryCount; | ||||
|     private long mLastResponseTime = 0; | ||||
|     private int mLastResponse; | ||||
|     private PreferenceObfuscator mPreferences; | ||||
|     private Vector<String> mExpansionURLs = new Vector<String>(); | ||||
|     private Vector<String> mExpansionFileNames = new Vector<String>(); | ||||
|     private Vector<Long> mExpansionFileSizes = new Vector<Long>(); | ||||
| 
 | ||||
|     /** | ||||
|      * The design of the protocol supports n files. Currently the market can | ||||
|      * only deliver two files. To accommodate this, we have these two constants, | ||||
|      * but the order is the only relevant thing here. | ||||
|      */ | ||||
|     public static final int MAIN_FILE_URL_INDEX = 0; | ||||
|     public static final int PATCH_FILE_URL_INDEX = 1; | ||||
| 
 | ||||
|     /** | ||||
|      * @param context The context for the current application | ||||
|      * @param obfuscator An obfuscator to be used with preferences. | ||||
|      */ | ||||
|     public APKExpansionPolicy(Context context, Obfuscator obfuscator) { | ||||
|         // Import old values | ||||
|         SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); | ||||
|         mPreferences = new PreferenceObfuscator(sp, obfuscator); | ||||
|         mLastResponse = Integer.parseInt( | ||||
|                 mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); | ||||
|         mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, | ||||
|                 DEFAULT_VALIDITY_TIMESTAMP)); | ||||
|         mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); | ||||
|         mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); | ||||
|         mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * We call this to guarantee that we fetch a fresh policy from the server. | ||||
|      * This is to be used if the URL is invalid. | ||||
|      */ | ||||
|     public void resetPolicy() { | ||||
|         mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); | ||||
|         setRetryUntil(DEFAULT_RETRY_UNTIL); | ||||
|         setMaxRetries(DEFAULT_MAX_RETRIES); | ||||
|         setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); | ||||
|         setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); | ||||
|         mPreferences.commit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process a new response from the license server. | ||||
|      * <p> | ||||
|      * This data will be used for computing future policy decisions. The | ||||
|      * following parameters are processed: | ||||
|      * <ul> | ||||
|      * <li>VT: the timestamp that the client should consider the response valid | ||||
|      * until | ||||
|      * <li>GT: the timestamp that the client should ignore retry errors until | ||||
|      * <li>GR: the number of retry errors that the client should ignore | ||||
|      * </ul> | ||||
|      *  | ||||
|      * @param response the result from validating the server response | ||||
|      * @param rawData the raw server response data | ||||
|      */ | ||||
|     public void processServerResponse(int response, | ||||
|             com.google.android.vending.licensing.ResponseData rawData) { | ||||
| 
 | ||||
|         // Update retry counter | ||||
|         if (response != Policy.RETRY) { | ||||
|             setRetryCount(0); | ||||
|         } else { | ||||
|             setRetryCount(mRetryCount + 1); | ||||
|         } | ||||
| 
 | ||||
|         if (response == Policy.LICENSED) { | ||||
|             // Update server policy data | ||||
|             Map<String, String> extras = decodeExtras(rawData.extra); | ||||
|             mLastResponse = response; | ||||
|             setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); | ||||
|             Set<String> keys = extras.keySet(); | ||||
|             for (String key : keys) { | ||||
|                 if (key.equals("VT")) { | ||||
|                     setValidityTimestamp(extras.get(key)); | ||||
|                 } else if (key.equals("GT")) { | ||||
|                     setRetryUntil(extras.get(key)); | ||||
|                 } else if (key.equals("GR")) { | ||||
|                     setMaxRetries(extras.get(key)); | ||||
|                 } else if (key.startsWith("FILE_URL")) { | ||||
|                     int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; | ||||
|                     setExpansionURL(index, extras.get(key)); | ||||
|                 } else if (key.startsWith("FILE_NAME")) { | ||||
|                     int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; | ||||
|                     setExpansionFileName(index, extras.get(key)); | ||||
|                 } else if (key.startsWith("FILE_SIZE")) { | ||||
|                     int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; | ||||
|                     setExpansionFileSize(index, Long.parseLong(extras.get(key))); | ||||
|                 } | ||||
|             } | ||||
|         } else if (response == Policy.NOT_LICENSED) { | ||||
|             // Clear out stale policy data | ||||
|             setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); | ||||
|             setRetryUntil(DEFAULT_RETRY_UNTIL); | ||||
|             setMaxRetries(DEFAULT_MAX_RETRIES); | ||||
|         } | ||||
| 
 | ||||
|         setLastResponse(response); | ||||
|         mPreferences.commit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the last license response received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      *  | ||||
|      * @param l the response | ||||
|      */ | ||||
|     private void setLastResponse(int l) { | ||||
|         mLastResponseTime = System.currentTimeMillis(); | ||||
|         mLastResponse = l; | ||||
|         mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the current retry count and add to preferences. You must manually | ||||
|      * call PreferenceObfuscator.commit() to commit these changes to disk. | ||||
|      *  | ||||
|      * @param c the new retry count | ||||
|      */ | ||||
|     private void setRetryCount(long c) { | ||||
|         mRetryCount = c; | ||||
|         mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); | ||||
|     } | ||||
| 
 | ||||
|     public long getRetryCount() { | ||||
|         return mRetryCount; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the last validity timestamp (VT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      *  | ||||
|      * @param validityTimestamp the VT string received | ||||
|      */ | ||||
|     private void setValidityTimestamp(String validityTimestamp) { | ||||
|         Long lValidityTimestamp; | ||||
|         try { | ||||
|             lValidityTimestamp = Long.parseLong(validityTimestamp); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // No response or not parseable, expire in one minute. | ||||
|             Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); | ||||
|             lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; | ||||
|             validityTimestamp = Long.toString(lValidityTimestamp); | ||||
|         } | ||||
| 
 | ||||
|         mValidityTimestamp = lValidityTimestamp; | ||||
|         mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); | ||||
|     } | ||||
| 
 | ||||
|     public long getValidityTimestamp() { | ||||
|         return mValidityTimestamp; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the retry until timestamp (GT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      *  | ||||
|      * @param retryUntil the GT string received | ||||
|      */ | ||||
|     private void setRetryUntil(String retryUntil) { | ||||
|         Long lRetryUntil; | ||||
|         try { | ||||
|             lRetryUntil = Long.parseLong(retryUntil); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // No response or not parseable, expire immediately | ||||
|             Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); | ||||
|             retryUntil = "0"; | ||||
|             lRetryUntil = 0l; | ||||
|         } | ||||
| 
 | ||||
|         mRetryUntil = lRetryUntil; | ||||
|         mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); | ||||
|     } | ||||
| 
 | ||||
|     public long getRetryUntil() { | ||||
|         return mRetryUntil; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the max retries value (GR) as received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      *  | ||||
|      * @param maxRetries the GR string received | ||||
|      */ | ||||
|     private void setMaxRetries(String maxRetries) { | ||||
|         Long lMaxRetries; | ||||
|         try { | ||||
|             lMaxRetries = Long.parseLong(maxRetries); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // No response or not parseable, expire immediately | ||||
|             Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); | ||||
|             maxRetries = "0"; | ||||
|             lMaxRetries = 0l; | ||||
|         } | ||||
| 
 | ||||
|         mMaxRetries = lMaxRetries; | ||||
|         mPreferences.putString(PREF_MAX_RETRIES, maxRetries); | ||||
|     } | ||||
| 
 | ||||
|     public long getMaxRetries() { | ||||
|         return mMaxRetries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the count of expansion URLs. Since expansionURLs are not committed | ||||
|      * to preferences, this will return zero if there has been no LVL fetch | ||||
|      * in the current session. | ||||
|      *  | ||||
|      * @return the number of expansion URLs. (0,1,2) | ||||
|      */ | ||||
|     public int getExpansionURLCount() { | ||||
|         return mExpansionURLs.size(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the expansion URL. Since these URLs are not committed to | ||||
|      * preferences, this will always return null if there has not been an LVL | ||||
|      * fetch in the current session. | ||||
|      *  | ||||
|      * @param index the index of the URL to fetch. This value will be either | ||||
|      *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX | ||||
|      * @param URL the URL to set | ||||
|      */ | ||||
|     public String getExpansionURL(int index) { | ||||
|         if (index < mExpansionURLs.size()) { | ||||
|             return mExpansionURLs.elementAt(index); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the expansion URL. Expansion URL's are not committed to preferences, | ||||
|      * but are instead intended to be stored when the license response is | ||||
|      * processed by the front-end. | ||||
|      *  | ||||
|      * @param index the index of the expansion URL. This value will be either | ||||
|      *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX | ||||
|      * @param URL the URL to set | ||||
|      */ | ||||
|     public void setExpansionURL(int index, String URL) { | ||||
|         if (index >= mExpansionURLs.size()) { | ||||
|             mExpansionURLs.setSize(index + 1); | ||||
|         } | ||||
|         mExpansionURLs.set(index, URL); | ||||
|     } | ||||
| 
 | ||||
|     public String getExpansionFileName(int index) { | ||||
|         if (index < mExpansionFileNames.size()) { | ||||
|             return mExpansionFileNames.elementAt(index); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public void setExpansionFileName(int index, String name) { | ||||
|         if (index >= mExpansionFileNames.size()) { | ||||
|             mExpansionFileNames.setSize(index + 1); | ||||
|         } | ||||
|         mExpansionFileNames.set(index, name); | ||||
|     } | ||||
| 
 | ||||
|     public long getExpansionFileSize(int index) { | ||||
|         if (index < mExpansionFileSizes.size()) { | ||||
|             return mExpansionFileSizes.elementAt(index); | ||||
|         } | ||||
|         return -1; | ||||
|     } | ||||
| 
 | ||||
|     public void setExpansionFileSize(int index, long size) { | ||||
|         if (index >= mExpansionFileSizes.size()) { | ||||
|             mExpansionFileSizes.setSize(index + 1); | ||||
|         } | ||||
|         mExpansionFileSizes.set(index, size); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * {@inheritDoc} This implementation allows access if either:<br> | ||||
|      * <ol> | ||||
|      * <li>a LICENSED response was received within the validity period | ||||
|      * <li>a RETRY response was received in the last minute, and we are under | ||||
|      * the RETRY count or in the RETRY period. | ||||
|      * </ol> | ||||
|      */ | ||||
|     public boolean allowAccess() { | ||||
|         long ts = System.currentTimeMillis(); | ||||
|         if (mLastResponse == Policy.LICENSED) { | ||||
|             // Check if the LICENSED response occurred within the validity | ||||
|             // timeout. | ||||
|             if (ts <= mValidityTimestamp) { | ||||
|                 // Cached LICENSED response is still valid. | ||||
|                 return true; | ||||
|             } | ||||
|         } else if (mLastResponse == Policy.RETRY && | ||||
|                 ts < mLastResponseTime + MILLIS_PER_MINUTE) { | ||||
|             // Only allow access if we are within the retry period or we haven't | ||||
|             // used up our | ||||
|             // max retries. | ||||
|             return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     private Map<String, String> decodeExtras(String extras) { | ||||
|         Map<String, String> results = new HashMap<String, String>(); | ||||
|         try { | ||||
|             URI rawExtras = new URI("?" + extras); | ||||
|             List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); | ||||
|             for (NameValuePair item : extraList) { | ||||
|                 String name = item.getName(); | ||||
|                 int i = 0; | ||||
|                 while (results.containsKey(name)) { | ||||
|                     name = item.getName() + ++i; | ||||
|                 } | ||||
|                 results.put(name, item.getValue()); | ||||
|             } | ||||
|         } catch (URISyntaxException e) { | ||||
|             Log.w(TAG, "Invalid syntax error while decoding extras data from server."); | ||||
|         } | ||||
|         return results; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,115 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
|  * This file is auto-generated.  DO NOT MODIFY. | ||||
|  * Original file: aidl/ILicenseResultListener.aidl | ||||
|  */ | ||||
| package com.google.android.vending.licensing; | ||||
| import java.lang.String; | ||||
| import android.os.RemoteException; | ||||
| import android.os.IBinder; | ||||
| import android.os.IInterface; | ||||
| import android.os.Binder; | ||||
| import android.os.Parcel; | ||||
| public interface ILicenseResultListener extends android.os.IInterface | ||||
| { | ||||
| /** Local-side IPC implementation stub class. */ | ||||
| public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener | ||||
| { | ||||
| private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; | ||||
| /** Construct the stub at attach it to the interface. */ | ||||
| public Stub() | ||||
| { | ||||
| this.attachInterface(this, DESCRIPTOR); | ||||
| } | ||||
| /** | ||||
|  * Cast an IBinder object into an ILicenseResultListener interface, | ||||
|  * generating a proxy if needed. | ||||
|  */ | ||||
| public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) | ||||
| { | ||||
| if ((obj==null)) { | ||||
| return null; | ||||
| } | ||||
| android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); | ||||
| if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { | ||||
| return ((com.google.android.vending.licensing.ILicenseResultListener)iin); | ||||
| } | ||||
| return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); | ||||
| } | ||||
| public android.os.IBinder asBinder() | ||||
| { | ||||
| return this; | ||||
| } | ||||
| public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException | ||||
| { | ||||
| switch (code) | ||||
| { | ||||
| case INTERFACE_TRANSACTION: | ||||
| { | ||||
| reply.writeString(DESCRIPTOR); | ||||
| return true; | ||||
| } | ||||
| case TRANSACTION_verifyLicense: | ||||
| { | ||||
| data.enforceInterface(DESCRIPTOR); | ||||
| int _arg0; | ||||
| _arg0 = data.readInt(); | ||||
| java.lang.String _arg1; | ||||
| _arg1 = data.readString(); | ||||
| java.lang.String _arg2; | ||||
| _arg2 = data.readString(); | ||||
| this.verifyLicense(_arg0, _arg1, _arg2); | ||||
| return true; | ||||
| } | ||||
| } | ||||
| return super.onTransact(code, data, reply, flags); | ||||
| } | ||||
| private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener | ||||
| { | ||||
| private android.os.IBinder mRemote; | ||||
| Proxy(android.os.IBinder remote) | ||||
| { | ||||
| mRemote = remote; | ||||
| } | ||||
| public android.os.IBinder asBinder() | ||||
| { | ||||
| return mRemote; | ||||
| } | ||||
| public java.lang.String getInterfaceDescriptor() | ||||
| { | ||||
| return DESCRIPTOR; | ||||
| } | ||||
| public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException | ||||
| { | ||||
| android.os.Parcel _data = android.os.Parcel.obtain(); | ||||
| try { | ||||
| _data.writeInterfaceToken(DESCRIPTOR); | ||||
| _data.writeInt(responseCode); | ||||
| _data.writeString(signedData); | ||||
| _data.writeString(signature); | ||||
| mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY); | ||||
| } | ||||
| finally { | ||||
| _data.recycle(); | ||||
| } | ||||
| } | ||||
| } | ||||
| static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); | ||||
| } | ||||
| public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; | ||||
| } | ||||
|  | @ -1,115 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
|  * This file is auto-generated.  DO NOT MODIFY. | ||||
|  * Original file: aidl/ILicensingService.aidl | ||||
|  */ | ||||
| package com.google.android.vending.licensing; | ||||
| import java.lang.String; | ||||
| import android.os.RemoteException; | ||||
| import android.os.IBinder; | ||||
| import android.os.IInterface; | ||||
| import android.os.Binder; | ||||
| import android.os.Parcel; | ||||
| public interface ILicensingService extends android.os.IInterface | ||||
| { | ||||
| /** Local-side IPC implementation stub class. */ | ||||
| public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService | ||||
| { | ||||
| private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; | ||||
| /** Construct the stub at attach it to the interface. */ | ||||
| public Stub() | ||||
| { | ||||
| this.attachInterface(this, DESCRIPTOR); | ||||
| } | ||||
| /** | ||||
|  * Cast an IBinder object into an ILicensingService interface, | ||||
|  * generating a proxy if needed. | ||||
|  */ | ||||
| public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) | ||||
| { | ||||
| if ((obj==null)) { | ||||
| return null; | ||||
| } | ||||
| android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); | ||||
| if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) { | ||||
| return ((com.google.android.vending.licensing.ILicensingService)iin); | ||||
| } | ||||
| return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); | ||||
| } | ||||
| public android.os.IBinder asBinder() | ||||
| { | ||||
| return this; | ||||
| } | ||||
| public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException | ||||
| { | ||||
| switch (code) | ||||
| { | ||||
| case INTERFACE_TRANSACTION: | ||||
| { | ||||
| reply.writeString(DESCRIPTOR); | ||||
| return true; | ||||
| } | ||||
| case TRANSACTION_checkLicense: | ||||
| { | ||||
| data.enforceInterface(DESCRIPTOR); | ||||
| long _arg0; | ||||
| _arg0 = data.readLong(); | ||||
| java.lang.String _arg1; | ||||
| _arg1 = data.readString(); | ||||
| com.google.android.vending.licensing.ILicenseResultListener _arg2; | ||||
| _arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); | ||||
| this.checkLicense(_arg0, _arg1, _arg2); | ||||
| return true; | ||||
| } | ||||
| } | ||||
| return super.onTransact(code, data, reply, flags); | ||||
| } | ||||
| private static class Proxy implements com.google.android.vending.licensing.ILicensingService | ||||
| { | ||||
| private android.os.IBinder mRemote; | ||||
| Proxy(android.os.IBinder remote) | ||||
| { | ||||
| mRemote = remote; | ||||
| } | ||||
| public android.os.IBinder asBinder() | ||||
| { | ||||
| return mRemote; | ||||
| } | ||||
| public java.lang.String getInterfaceDescriptor() | ||||
| { | ||||
| return DESCRIPTOR; | ||||
| } | ||||
| public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException | ||||
| { | ||||
| android.os.Parcel _data = android.os.Parcel.obtain(); | ||||
| try { | ||||
| _data.writeInterfaceToken(DESCRIPTOR); | ||||
| _data.writeLong(nonce); | ||||
| _data.writeString(packageName); | ||||
| _data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null))); | ||||
| mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY); | ||||
| } | ||||
| finally { | ||||
| _data.recycle(); | ||||
| } | ||||
| } | ||||
| } | ||||
| static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); | ||||
| } | ||||
| public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; | ||||
| } | ||||
|  | @ -1,351 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.Base64; | ||||
| import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| 
 | ||||
| import android.content.ComponentName; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.ServiceConnection; | ||||
| import android.content.pm.PackageManager.NameNotFoundException; | ||||
| import android.os.Handler; | ||||
| import android.os.HandlerThread; | ||||
| import android.os.IBinder; | ||||
| import android.os.RemoteException; | ||||
| import android.provider.Settings.Secure; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.security.KeyFactory; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.PublicKey; | ||||
| import java.security.SecureRandom; | ||||
| import java.security.spec.InvalidKeySpecException; | ||||
| import java.security.spec.X509EncodedKeySpec; | ||||
| import java.util.Date; | ||||
| import java.util.HashSet; | ||||
| import java.util.LinkedList; | ||||
| import java.util.Queue; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| /** | ||||
|  * Client library for Android Market license verifications. | ||||
|  * <p> | ||||
|  * The LicenseChecker is configured via a {@link Policy} which contains the | ||||
|  * logic to determine whether a user should have access to the application. For | ||||
|  * example, the Policy can define a threshold for allowable number of server or | ||||
|  * client failures before the library reports the user as not having access. | ||||
|  * <p> | ||||
|  * Must also provide the Base64-encoded RSA public key associated with your | ||||
|  * developer account. The public key is obtainable from the publisher site. | ||||
|  */ | ||||
| public class LicenseChecker implements ServiceConnection { | ||||
|     private static final String TAG = "LicenseChecker"; | ||||
| 
 | ||||
|     private static final String KEY_FACTORY_ALGORITHM = "RSA"; | ||||
| 
 | ||||
|     // Timeout value (in milliseconds) for calls to service. | ||||
|     private static final int TIMEOUT_MS = 10 * 1000; | ||||
| 
 | ||||
|     private static final SecureRandom RANDOM = new SecureRandom(); | ||||
|     private static final boolean DEBUG_LICENSE_ERROR = false; | ||||
| 
 | ||||
|     private ILicensingService mService; | ||||
| 
 | ||||
|     private PublicKey mPublicKey; | ||||
|     private final Context mContext; | ||||
|     private final Policy mPolicy; | ||||
|     /** | ||||
|      * A handler for running tasks on a background thread. We don't want license | ||||
|      * processing to block the UI thread. | ||||
|      */ | ||||
|     private Handler mHandler; | ||||
|     private final String mPackageName; | ||||
|     private final String mVersionCode; | ||||
|     private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>(); | ||||
|     private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>(); | ||||
| 
 | ||||
|     /** | ||||
|      * @param context a Context | ||||
|      * @param policy implementation of Policy | ||||
|      * @param encodedPublicKey Base64-encoded RSA public key | ||||
|      * @throws IllegalArgumentException if encodedPublicKey is invalid | ||||
|      */ | ||||
|     public LicenseChecker(Context context, Policy policy, String encodedPublicKey) { | ||||
|         mContext = context; | ||||
|         mPolicy = policy; | ||||
|         mPublicKey = generatePublicKey(encodedPublicKey); | ||||
|         mPackageName = mContext.getPackageName(); | ||||
|         mVersionCode = getVersionCode(context, mPackageName); | ||||
|         HandlerThread handlerThread = new HandlerThread("background thread"); | ||||
|         handlerThread.start(); | ||||
|         mHandler = new Handler(handlerThread.getLooper()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Generates a PublicKey instance from a string containing the | ||||
|      * Base64-encoded public key. | ||||
|      *  | ||||
|      * @param encodedPublicKey Base64-encoded public key | ||||
|      * @throws IllegalArgumentException if encodedPublicKey is invalid | ||||
|      */ | ||||
|     private static PublicKey generatePublicKey(String encodedPublicKey) { | ||||
|         try { | ||||
|             byte[] decodedKey = Base64.decode(encodedPublicKey); | ||||
|             KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); | ||||
| 
 | ||||
|             return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); | ||||
|         } catch (NoSuchAlgorithmException e) { | ||||
|             // This won't happen in an Android-compatible environment. | ||||
|             throw new RuntimeException(e); | ||||
|         } catch (Base64DecoderException e) { | ||||
|             Log.e(TAG, "Could not decode from Base64."); | ||||
|             throw new IllegalArgumentException(e); | ||||
|         } catch (InvalidKeySpecException e) { | ||||
|             Log.e(TAG, "Invalid key specification."); | ||||
|             throw new IllegalArgumentException(e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if the user should have access to the app.  Binds the service if necessary. | ||||
|      * <p> | ||||
|      * NOTE: This call uses a trivially obfuscated string (base64-encoded).  For best security, | ||||
|      * we recommend obfuscating the string that is passed into bindService using another method | ||||
|      * of your own devising. | ||||
|      * <p> | ||||
|      * source string: "com.android.vending.licensing.ILicensingService" | ||||
|      * <p> | ||||
|      * @param callback | ||||
|      */ | ||||
|     public synchronized void checkAccess(LicenseCheckerCallback callback) { | ||||
|         // If we have a valid recent LICENSED response, we can skip asking | ||||
|         // Market. | ||||
|         if (mPolicy.allowAccess()) { | ||||
|             Log.i(TAG, "Using cached license response"); | ||||
|             callback.allow(Policy.LICENSED); | ||||
|         } else { | ||||
|             LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(), | ||||
|                     callback, generateNonce(), mPackageName, mVersionCode); | ||||
| 
 | ||||
|             if (mService == null) { | ||||
|                 Log.i(TAG, "Binding to licensing service."); | ||||
|                 try { | ||||
|                     Intent serviceIntent = new Intent(new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))); | ||||
| 					serviceIntent.setPackage("com.android.vending"); | ||||
|                     boolean bindResult = mContext | ||||
|                             .bindService( | ||||
|                                     serviceIntent, | ||||
|                                     this, // ServiceConnection. | ||||
|                                     Context.BIND_AUTO_CREATE); | ||||
| 
 | ||||
|                     if (bindResult) { | ||||
|                         mPendingChecks.offer(validator); | ||||
|                     } else { | ||||
|                         Log.e(TAG, "Could not bind to service."); | ||||
|                         handleServiceConnectionError(validator); | ||||
|                     } | ||||
|                 } catch (SecurityException e) { | ||||
|                     callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION); | ||||
|                 } catch (Base64DecoderException e) { | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } else { | ||||
|                 mPendingChecks.offer(validator); | ||||
|                 runChecks(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void runChecks() { | ||||
|         LicenseValidator validator; | ||||
|         while ((validator = mPendingChecks.poll()) != null) { | ||||
|             try { | ||||
|                 Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName()); | ||||
|                 mService.checkLicense( | ||||
|                         validator.getNonce(), validator.getPackageName(), | ||||
|                         new ResultListener(validator)); | ||||
|                 mChecksInProgress.add(validator); | ||||
|             } catch (RemoteException e) { | ||||
|                 Log.w(TAG, "RemoteException in checkLicense call.", e); | ||||
|                 handleServiceConnectionError(validator); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private synchronized void finishCheck(LicenseValidator validator) { | ||||
|         mChecksInProgress.remove(validator); | ||||
|         if (mChecksInProgress.isEmpty()) { | ||||
|             cleanupService(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private class ResultListener extends ILicenseResultListener.Stub { | ||||
|         private final LicenseValidator mValidator; | ||||
|         private Runnable mOnTimeout; | ||||
| 
 | ||||
|         public ResultListener(LicenseValidator validator) { | ||||
|             mValidator = validator; | ||||
|             mOnTimeout = new Runnable() { | ||||
|                 public void run() { | ||||
|                     Log.i(TAG, "Check timed out."); | ||||
|                     handleServiceConnectionError(mValidator); | ||||
|                     finishCheck(mValidator); | ||||
|                 } | ||||
|             }; | ||||
|             startTimeout(); | ||||
|         } | ||||
| 
 | ||||
|         private static final int ERROR_CONTACTING_SERVER = 0x101; | ||||
|         private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; | ||||
|         private static final int ERROR_NON_MATCHING_UID = 0x103; | ||||
| 
 | ||||
|         // Runs in IPC thread pool. Post it to the Handler, so we can guarantee | ||||
|         // either this or the timeout runs. | ||||
|         public void verifyLicense(final int responseCode, final String signedData, | ||||
|                 final String signature) { | ||||
|             mHandler.post(new Runnable() { | ||||
|                 public void run() { | ||||
|                     Log.i(TAG, "Received response."); | ||||
|                     // Make sure it hasn't already timed out. | ||||
|                     if (mChecksInProgress.contains(mValidator)) { | ||||
|                         clearTimeout(); | ||||
|                         mValidator.verify(mPublicKey, responseCode, signedData, signature); | ||||
|                         finishCheck(mValidator); | ||||
|                     } | ||||
|                     if (DEBUG_LICENSE_ERROR) { | ||||
|                         boolean logResponse; | ||||
|                         String stringError = null; | ||||
|                         switch (responseCode) { | ||||
|                             case ERROR_CONTACTING_SERVER: | ||||
|                                 logResponse = true; | ||||
|                                 stringError = "ERROR_CONTACTING_SERVER"; | ||||
|                                 break; | ||||
|                             case ERROR_INVALID_PACKAGE_NAME: | ||||
|                                 logResponse = true; | ||||
|                                 stringError = "ERROR_INVALID_PACKAGE_NAME"; | ||||
|                                 break; | ||||
|                             case ERROR_NON_MATCHING_UID: | ||||
|                                 logResponse = true; | ||||
|                                 stringError = "ERROR_NON_MATCHING_UID"; | ||||
|                                 break; | ||||
|                             default: | ||||
|                                 logResponse = false; | ||||
|                         } | ||||
| 
 | ||||
|                         if (logResponse) { | ||||
|                             String android_id = Secure.getString(mContext.getContentResolver(), | ||||
|                                     Secure.ANDROID_ID); | ||||
|                             Date date = new Date(); | ||||
|                             Log.d(TAG, "Server Failure: " + stringError); | ||||
|                             Log.d(TAG, "Android ID: " + android_id); | ||||
|                             Log.d(TAG, "Time: " + date.toGMTString()); | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         private void startTimeout() { | ||||
|             Log.i(TAG, "Start monitoring timeout."); | ||||
|             mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); | ||||
|         } | ||||
| 
 | ||||
|         private void clearTimeout() { | ||||
|             Log.i(TAG, "Clearing timeout."); | ||||
|             mHandler.removeCallbacks(mOnTimeout); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void onServiceConnected(ComponentName name, IBinder service) { | ||||
|         mService = ILicensingService.Stub.asInterface(service); | ||||
|         runChecks(); | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void onServiceDisconnected(ComponentName name) { | ||||
|         // Called when the connection with the service has been | ||||
|         // unexpectedly disconnected. That is, Market crashed. | ||||
|         // If there are any checks in progress, the timeouts will handle them. | ||||
|         Log.w(TAG, "Service unexpectedly disconnected."); | ||||
|         mService = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Generates policy response for service connection errors, as a result of | ||||
|      * disconnections or timeouts. | ||||
|      */ | ||||
|     private synchronized void handleServiceConnectionError(LicenseValidator validator) { | ||||
|         mPolicy.processServerResponse(Policy.RETRY, null); | ||||
| 
 | ||||
|         if (mPolicy.allowAccess()) { | ||||
|             validator.getCallback().allow(Policy.RETRY); | ||||
|         } else { | ||||
|             validator.getCallback().dontAllow(Policy.RETRY); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** Unbinds service if necessary and removes reference to it. */ | ||||
|     private void cleanupService() { | ||||
|         if (mService != null) { | ||||
|             try { | ||||
|                 mContext.unbindService(this); | ||||
|             } catch (IllegalArgumentException e) { | ||||
|                 // Somehow we've already been unbound. This is a non-fatal | ||||
|                 // error. | ||||
|                 Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); | ||||
|             } | ||||
|             mService = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Inform the library that the context is about to be destroyed, so that any | ||||
|      * open connections can be cleaned up. | ||||
|      * <p> | ||||
|      * Failure to call this method can result in a crash under certain | ||||
|      * circumstances, such as during screen rotation if an Activity requests the | ||||
|      * license check or when the user exits the application. | ||||
|      */ | ||||
|     public synchronized void onDestroy() { | ||||
|         cleanupService(); | ||||
|         mHandler.getLooper().quit(); | ||||
|     } | ||||
| 
 | ||||
|     /** Generates a nonce (number used once). */ | ||||
|     private int generateNonce() { | ||||
|         return RANDOM.nextInt(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get version code for the application package name. | ||||
|      *  | ||||
|      * @param context | ||||
|      * @param packageName application package name | ||||
|      * @return the version code or empty string if package not found | ||||
|      */ | ||||
|     private static String getVersionCode(Context context, String packageName) { | ||||
|         try { | ||||
|             return String.valueOf(context.getPackageManager().getPackageInfo(packageName, 0). | ||||
|                     versionCode); | ||||
|         } catch (NameNotFoundException e) { | ||||
|             Log.e(TAG, "Package not found. could not get version code."); | ||||
|             return ""; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,224 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.Base64; | ||||
| import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| 
 | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.security.InvalidKeyException; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.PublicKey; | ||||
| import java.security.Signature; | ||||
| import java.security.SignatureException; | ||||
| 
 | ||||
| /** | ||||
|  * Contains data related to a licensing request and methods to verify | ||||
|  * and process the response. | ||||
|  */ | ||||
| class LicenseValidator { | ||||
|     private static final String TAG = "LicenseValidator"; | ||||
| 
 | ||||
|     // Server response codes. | ||||
|     private static final int LICENSED = 0x0; | ||||
|     private static final int NOT_LICENSED = 0x1; | ||||
|     private static final int LICENSED_OLD_KEY = 0x2; | ||||
|     private static final int ERROR_NOT_MARKET_MANAGED = 0x3; | ||||
|     private static final int ERROR_SERVER_FAILURE = 0x4; | ||||
|     private static final int ERROR_OVER_QUOTA = 0x5; | ||||
| 
 | ||||
|     private static final int ERROR_CONTACTING_SERVER = 0x101; | ||||
|     private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; | ||||
|     private static final int ERROR_NON_MATCHING_UID = 0x103; | ||||
| 
 | ||||
|     private final Policy mPolicy; | ||||
|     private final LicenseCheckerCallback mCallback; | ||||
|     private final int mNonce; | ||||
|     private final String mPackageName; | ||||
|     private final String mVersionCode; | ||||
|     private final DeviceLimiter mDeviceLimiter; | ||||
| 
 | ||||
|     LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback, | ||||
|              int nonce, String packageName, String versionCode) { | ||||
|         mPolicy = policy; | ||||
|         mDeviceLimiter = deviceLimiter; | ||||
|         mCallback = callback; | ||||
|         mNonce = nonce; | ||||
|         mPackageName = packageName; | ||||
|         mVersionCode = versionCode; | ||||
|     } | ||||
| 
 | ||||
|     public LicenseCheckerCallback getCallback() { | ||||
|         return mCallback; | ||||
|     } | ||||
| 
 | ||||
|     public int getNonce() { | ||||
|         return mNonce; | ||||
|     } | ||||
| 
 | ||||
|     public String getPackageName() { | ||||
|         return mPackageName; | ||||
|     } | ||||
| 
 | ||||
|     private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; | ||||
| 
 | ||||
|     /** | ||||
|      * Verifies the response from server and calls appropriate callback method. | ||||
|      * | ||||
|      * @param publicKey public key associated with the developer account | ||||
|      * @param responseCode server response code | ||||
|      * @param signedData signed data from server | ||||
|      * @param signature server signature | ||||
|      */ | ||||
|     public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) { | ||||
|         String userId = null; | ||||
|         // Skip signature check for unsuccessful requests | ||||
|         ResponseData data = null; | ||||
|         if (responseCode == LICENSED || responseCode == NOT_LICENSED || | ||||
|                 responseCode == LICENSED_OLD_KEY) { | ||||
|             // Verify signature. | ||||
|             try { | ||||
|                 Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); | ||||
|                 sig.initVerify(publicKey); | ||||
|                 sig.update(signedData.getBytes()); | ||||
| 
 | ||||
|                 if (!sig.verify(Base64.decode(signature))) { | ||||
|                     Log.e(TAG, "Signature verification failed."); | ||||
|                     handleInvalidResponse(); | ||||
|                     return; | ||||
|                 } | ||||
|             } catch (NoSuchAlgorithmException e) { | ||||
|                 // This can't happen on an Android compatible device. | ||||
|                 throw new RuntimeException(e); | ||||
|             } catch (InvalidKeyException e) { | ||||
|                 handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); | ||||
|                 return; | ||||
|             } catch (SignatureException e) { | ||||
|                 throw new RuntimeException(e); | ||||
|             } catch (Base64DecoderException e) { | ||||
|                 Log.e(TAG, "Could not Base64-decode signature."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Parse and validate response. | ||||
|             try { | ||||
|                 data = ResponseData.parse(signedData); | ||||
|             } catch (IllegalArgumentException e) { | ||||
|                 Log.e(TAG, "Could not parse response."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (data.responseCode != responseCode) { | ||||
|                 Log.e(TAG, "Response codes don't match."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (data.nonce != mNonce) { | ||||
|                 Log.e(TAG, "Nonce doesn't match."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!data.packageName.equals(mPackageName)) { | ||||
|                 Log.e(TAG, "Package name doesn't match."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!data.versionCode.equals(mVersionCode)) { | ||||
|                 Log.e(TAG, "Version codes don't match."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Application-specific user identifier. | ||||
|             userId = data.userId; | ||||
|             if (TextUtils.isEmpty(userId)) { | ||||
|                 Log.e(TAG, "User identifier is empty."); | ||||
|                 handleInvalidResponse(); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         switch (responseCode) { | ||||
|             case LICENSED: | ||||
|             case LICENSED_OLD_KEY: | ||||
|                 int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); | ||||
|                 handleResponse(limiterResponse, data); | ||||
|                 break; | ||||
|             case NOT_LICENSED: | ||||
|                 handleResponse(Policy.NOT_LICENSED, data); | ||||
|                 break; | ||||
|             case ERROR_CONTACTING_SERVER: | ||||
|                 Log.w(TAG, "Error contacting licensing server."); | ||||
|                 handleResponse(Policy.RETRY, data); | ||||
|                 break; | ||||
|             case ERROR_SERVER_FAILURE: | ||||
|                 Log.w(TAG, "An error has occurred on the licensing server."); | ||||
|                 handleResponse(Policy.RETRY, data); | ||||
|                 break; | ||||
|             case ERROR_OVER_QUOTA: | ||||
|                 Log.w(TAG, "Licensing server is refusing to talk to this device, over quota."); | ||||
|                 handleResponse(Policy.RETRY, data); | ||||
|                 break; | ||||
|             case ERROR_INVALID_PACKAGE_NAME: | ||||
|                 handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME); | ||||
|                 break; | ||||
|             case ERROR_NON_MATCHING_UID: | ||||
|                 handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID); | ||||
|                 break; | ||||
|             case ERROR_NOT_MARKET_MANAGED: | ||||
|                 handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED); | ||||
|                 break; | ||||
|             default: | ||||
|                 Log.e(TAG, "Unknown response code for license check."); | ||||
|                 handleInvalidResponse(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Confers with policy and calls appropriate callback method. | ||||
|      * | ||||
|      * @param response | ||||
|      * @param rawData | ||||
|      */ | ||||
|     private void handleResponse(int response, ResponseData rawData) { | ||||
|         // Update policy data and increment retry counter (if needed) | ||||
|         mPolicy.processServerResponse(response, rawData); | ||||
| 
 | ||||
|         // Given everything we know, including cached data, ask the policy if we should grant | ||||
|         // access. | ||||
|         if (mPolicy.allowAccess()) { | ||||
|             mCallback.allow(response); | ||||
|         } else { | ||||
|             mCallback.dontAllow(response); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void handleApplicationError(int code) { | ||||
|         mCallback.applicationError(code); | ||||
|     } | ||||
| 
 | ||||
|     private void handleInvalidResponse() { | ||||
|         mCallback.dontAllow(Policy.NOT_LICENSED); | ||||
|     } | ||||
| } | ||||
|  | @ -1,77 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * An wrapper for SharedPreferences that transparently performs data obfuscation. | ||||
|  */ | ||||
| public class PreferenceObfuscator { | ||||
| 
 | ||||
|     private static final String TAG = "PreferenceObfuscator"; | ||||
| 
 | ||||
|     private final SharedPreferences mPreferences; | ||||
|     private final Obfuscator mObfuscator; | ||||
|     private SharedPreferences.Editor mEditor; | ||||
| 
 | ||||
|     /** | ||||
|      * Constructor. | ||||
|      * | ||||
|      * @param sp A SharedPreferences instance provided by the system. | ||||
|      * @param o The Obfuscator to use when reading or writing data. | ||||
|      */ | ||||
|     public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) { | ||||
|         mPreferences = sp; | ||||
|         mObfuscator = o; | ||||
|         mEditor = null; | ||||
|     } | ||||
| 
 | ||||
|     public void putString(String key, String value) { | ||||
|         if (mEditor == null) { | ||||
|             mEditor = mPreferences.edit(); | ||||
|         } | ||||
|         String obfuscatedValue = mObfuscator.obfuscate(value, key); | ||||
|         mEditor.putString(key, obfuscatedValue); | ||||
|     } | ||||
| 
 | ||||
|     public String getString(String key, String defValue) { | ||||
|         String result; | ||||
|         String value = mPreferences.getString(key, null); | ||||
|         if (value != null) { | ||||
|             try { | ||||
|                 result = mObfuscator.unobfuscate(value, key); | ||||
|             } catch (ValidationException e) { | ||||
|                 // Unable to unobfuscate, data corrupt or tampered | ||||
|                 Log.w(TAG, "Validation error while reading preference: " + key); | ||||
|                 result = defValue; | ||||
|             } | ||||
|         } else { | ||||
|             // Preference not found | ||||
|             result = defValue; | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     public void commit() { | ||||
|         if (mEditor != null) { | ||||
|             mEditor.commit(); | ||||
|             mEditor = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,79 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| /** | ||||
|  * ResponseData from licensing server. | ||||
|  */ | ||||
| public class ResponseData { | ||||
| 
 | ||||
|     public int responseCode; | ||||
|     public int nonce; | ||||
|     public String packageName; | ||||
|     public String versionCode; | ||||
|     public String userId; | ||||
|     public long timestamp; | ||||
|     /** Response-specific data. */ | ||||
|     public String extra; | ||||
| 
 | ||||
|     /** | ||||
|      * Parses response string into ResponseData. | ||||
|      * | ||||
|      * @param responseData response data string | ||||
|      * @throws IllegalArgumentException upon parsing error | ||||
|      * @return ResponseData object | ||||
|      */ | ||||
|     public static ResponseData parse(String responseData) { | ||||
|         // Must parse out main response data and response-specific data. | ||||
|     	int index = responseData.indexOf(':'); | ||||
|     	String mainData, extraData; | ||||
|     	if ( -1 == index ) { | ||||
|     		mainData = responseData; | ||||
|     		extraData = ""; | ||||
|     	} else { | ||||
|     		mainData = responseData.substring(0, index); | ||||
|     		extraData = index >= responseData.length() ? "" : responseData.substring(index+1); | ||||
|     	} | ||||
| 
 | ||||
|         String [] fields = TextUtils.split(mainData, Pattern.quote("|")); | ||||
|         if (fields.length < 6) { | ||||
|             throw new IllegalArgumentException("Wrong number of fields."); | ||||
|         } | ||||
| 
 | ||||
|         ResponseData data = new ResponseData(); | ||||
|         data.extra = extraData; | ||||
|         data.responseCode = Integer.parseInt(fields[0]); | ||||
|         data.nonce = Integer.parseInt(fields[1]); | ||||
|         data.packageName = fields[2]; | ||||
|         data.versionCode = fields[3]; | ||||
|         // Application-specific user identifier. | ||||
|         data.userId = fields[4]; | ||||
|         data.timestamp = Long.parseLong(fields[5]); | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode, | ||||
|             userId, timestamp }); | ||||
|     } | ||||
| } | ||||
|  | @ -1,276 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import org.apache.http.NameValuePair; | ||||
| import org.apache.http.client.utils.URLEncodedUtils; | ||||
| 
 | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * Default policy. All policy decisions are based off of response data received | ||||
|  * from the licensing service. Specifically, the licensing server sends the | ||||
|  * following information: response validity period, error retry period, and | ||||
|  * error retry count. | ||||
|  * <p> | ||||
|  * These values will vary based on the the way the application is configured in | ||||
|  * the Android Market publishing console, such as whether the application is | ||||
|  * marked as free or is within its refund period, as well as how often an | ||||
|  * application is checking with the licensing service. | ||||
|  * <p> | ||||
|  * Developers who need more fine grained control over their application's | ||||
|  * licensing policy should implement a custom Policy. | ||||
|  */ | ||||
| public class ServerManagedPolicy implements Policy { | ||||
| 
 | ||||
|     private static final String TAG = "ServerManagedPolicy"; | ||||
|     private static final String PREFS_FILE = "com.android.vending.licensing.ServerManagedPolicy"; | ||||
|     private static final String PREF_LAST_RESPONSE = "lastResponse"; | ||||
|     private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; | ||||
|     private static final String PREF_RETRY_UNTIL = "retryUntil"; | ||||
|     private static final String PREF_MAX_RETRIES = "maxRetries"; | ||||
|     private static final String PREF_RETRY_COUNT = "retryCount"; | ||||
|     private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; | ||||
|     private static final String DEFAULT_RETRY_UNTIL = "0"; | ||||
|     private static final String DEFAULT_MAX_RETRIES = "0"; | ||||
|     private static final String DEFAULT_RETRY_COUNT = "0"; | ||||
| 
 | ||||
|     private static final long MILLIS_PER_MINUTE = 60 * 1000; | ||||
| 
 | ||||
|     private long mValidityTimestamp; | ||||
|     private long mRetryUntil; | ||||
|     private long mMaxRetries; | ||||
|     private long mRetryCount; | ||||
|     private long mLastResponseTime = 0; | ||||
|     private int mLastResponse; | ||||
|     private PreferenceObfuscator mPreferences; | ||||
| 
 | ||||
|     /** | ||||
|      * @param context The context for the current application | ||||
|      * @param obfuscator An obfuscator to be used with preferences. | ||||
|      */ | ||||
|     public ServerManagedPolicy(Context context, Obfuscator obfuscator) { | ||||
|         // Import old values | ||||
|         SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); | ||||
|         mPreferences = new PreferenceObfuscator(sp, obfuscator); | ||||
|         mLastResponse = Integer.parseInt( | ||||
|             mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); | ||||
|         mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, | ||||
|                 DEFAULT_VALIDITY_TIMESTAMP)); | ||||
|         mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); | ||||
|         mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); | ||||
|         mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Process a new response from the license server. | ||||
|      * <p> | ||||
|      * This data will be used for computing future policy decisions. The | ||||
|      * following parameters are processed: | ||||
|      * <ul> | ||||
|      * <li>VT: the timestamp that the client should consider the response | ||||
|      *   valid until | ||||
|      * <li>GT: the timestamp that the client should ignore retry errors until | ||||
|      * <li>GR: the number of retry errors that the client should ignore | ||||
|      * </ul> | ||||
|      * | ||||
|      * @param response the result from validating the server response | ||||
|      * @param rawData the raw server response data | ||||
|      */ | ||||
|     public void processServerResponse(int response, ResponseData rawData) { | ||||
| 
 | ||||
|         // Update retry counter | ||||
|         if (response != Policy.RETRY) { | ||||
|             setRetryCount(0); | ||||
|         } else { | ||||
|             setRetryCount(mRetryCount + 1); | ||||
|         } | ||||
| 
 | ||||
|         if (response == Policy.LICENSED) { | ||||
|             // Update server policy data | ||||
|             Map<String, String> extras = decodeExtras(rawData.extra); | ||||
|             mLastResponse = response; | ||||
|             setValidityTimestamp(extras.get("VT")); | ||||
|             setRetryUntil(extras.get("GT")); | ||||
|             setMaxRetries(extras.get("GR")); | ||||
|         } else if (response == Policy.NOT_LICENSED) { | ||||
|             // Clear out stale policy data | ||||
|             setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); | ||||
|             setRetryUntil(DEFAULT_RETRY_UNTIL); | ||||
|             setMaxRetries(DEFAULT_MAX_RETRIES); | ||||
|         } | ||||
| 
 | ||||
|         setLastResponse(response); | ||||
|         mPreferences.commit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the last license response received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param l the response | ||||
|      */ | ||||
|     private void setLastResponse(int l) { | ||||
|         mLastResponseTime = System.currentTimeMillis(); | ||||
|         mLastResponse = l; | ||||
|         mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the current retry count and add to preferences. You must manually | ||||
|      * call PreferenceObfuscator.commit() to commit these changes to disk. | ||||
|      * | ||||
|      * @param c the new retry count | ||||
|      */ | ||||
|     private void setRetryCount(long c) { | ||||
|         mRetryCount = c; | ||||
|         mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); | ||||
|     } | ||||
| 
 | ||||
|     public long getRetryCount() { | ||||
|         return mRetryCount; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the last validity timestamp (VT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param validityTimestamp the VT string received | ||||
|      */ | ||||
|     private void setValidityTimestamp(String validityTimestamp) { | ||||
|         Long lValidityTimestamp; | ||||
|         try { | ||||
|             lValidityTimestamp = Long.parseLong(validityTimestamp); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // No response or not parsable, expire in one minute. | ||||
|             Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); | ||||
|             lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; | ||||
|             validityTimestamp = Long.toString(lValidityTimestamp); | ||||
|         } | ||||
| 
 | ||||
|         mValidityTimestamp = lValidityTimestamp; | ||||
|         mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); | ||||
|     } | ||||
| 
 | ||||
|     public long getValidityTimestamp() { | ||||
|         return mValidityTimestamp; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the retry until timestamp (GT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param retryUntil the GT string received | ||||
|      */ | ||||
|     private void setRetryUntil(String retryUntil) { | ||||
|         Long lRetryUntil; | ||||
|         try { | ||||
|             lRetryUntil = Long.parseLong(retryUntil); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // No response or not parsable, expire immediately | ||||
|             Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); | ||||
|             retryUntil = "0"; | ||||
|             lRetryUntil = 0l; | ||||
|         } | ||||
| 
 | ||||
|         mRetryUntil = lRetryUntil; | ||||
|         mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); | ||||
|     } | ||||
| 
 | ||||
|     public long getRetryUntil() { | ||||
|       return mRetryUntil; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the max retries value (GR) as received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param maxRetries the GR string received | ||||
|      */ | ||||
|     private void setMaxRetries(String maxRetries) { | ||||
|         Long lMaxRetries; | ||||
|         try { | ||||
|             lMaxRetries = Long.parseLong(maxRetries); | ||||
|         } catch (NumberFormatException e) { | ||||
|             // No response or not parsable, expire immediately | ||||
|             Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); | ||||
|             maxRetries = "0"; | ||||
|             lMaxRetries = 0l; | ||||
|         } | ||||
| 
 | ||||
|         mMaxRetries = lMaxRetries; | ||||
|         mPreferences.putString(PREF_MAX_RETRIES, maxRetries); | ||||
|     } | ||||
| 
 | ||||
|     public long getMaxRetries() { | ||||
|         return mMaxRetries; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * {@inheritDoc} | ||||
|      * | ||||
|      * This implementation allows access if either:<br> | ||||
|      * <ol> | ||||
|      * <li>a LICENSED response was received within the validity period | ||||
|      * <li>a RETRY response was received in the last minute, and we are under | ||||
|      * the RETRY count or in the RETRY period. | ||||
|      * </ol> | ||||
|      */ | ||||
|     public boolean allowAccess() { | ||||
|         long ts = System.currentTimeMillis(); | ||||
|         if (mLastResponse == Policy.LICENSED) { | ||||
|             // Check if the LICENSED response occurred within the validity timeout. | ||||
|             if (ts <= mValidityTimestamp) { | ||||
|                 // Cached LICENSED response is still valid. | ||||
|                 return true; | ||||
|             } | ||||
|         } else if (mLastResponse == Policy.RETRY && | ||||
|                    ts < mLastResponseTime + MILLIS_PER_MINUTE) { | ||||
|             // Only allow access if we are within the retry period or we haven't used up our | ||||
|             // max retries. | ||||
|             return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     private Map<String, String> decodeExtras(String extras) { | ||||
|         Map<String, String> results = new HashMap<String, String>(); | ||||
|         try { | ||||
|             URI rawExtras = new URI("?" + extras); | ||||
|             List<NameValuePair> extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); | ||||
|             for (NameValuePair item : extraList) { | ||||
|                 results.put(item.getName(), item.getValue()); | ||||
|             } | ||||
|         } catch (URISyntaxException e) { | ||||
|           Log.w(TAG, "Invalid syntax error while decoding extras data from server."); | ||||
|         } | ||||
|         return results; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,570 +0,0 @@ | |||
| // Portions copyright 2002, Google, Inc. | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
| 
 | ||||
| package com.google.android.vending.licensing.util; | ||||
| 
 | ||||
| // This code was converted from code at http://iharder.sourceforge.net/base64/ | ||||
| // Lots of extraneous features were removed. | ||||
| /* The original code said: | ||||
|  * <p> | ||||
|  * I am placing this code in the Public Domain. Do with it as you will. | ||||
|  * This software comes with no guarantees or warranties but with | ||||
|  * plenty of well-wishing instead! | ||||
|  * Please visit | ||||
|  * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> | ||||
|  * periodically to check for updates or to contribute improvements. | ||||
|  * </p> | ||||
|  * | ||||
|  * @author Robert Harder | ||||
|  * @author rharder@usa.net | ||||
|  * @version 1.3 | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * Base64 converter class. This code is not a full-blown MIME encoder; | ||||
|  * it simply converts binary data to base64 data and back. | ||||
|  * | ||||
|  * <p>Note {@link CharBase64} is a GWT-compatible implementation of this | ||||
|  * class. | ||||
|  */ | ||||
| public class Base64 { | ||||
|   /** Specify encoding (value is {@code true}). */ | ||||
|   public final static boolean ENCODE = true; | ||||
| 
 | ||||
|   /** Specify decoding (value is {@code false}). */ | ||||
|   public final static boolean DECODE = false; | ||||
| 
 | ||||
|   /** The equals sign (=) as a byte. */ | ||||
|   private final static byte EQUALS_SIGN = (byte) '='; | ||||
| 
 | ||||
|   /** The new line character (\n) as a byte. */ | ||||
|   private final static byte NEW_LINE = (byte) '\n'; | ||||
| 
 | ||||
|   /** | ||||
|    * The 64 valid Base64 values. | ||||
|    */ | ||||
|   private final static byte[] ALPHABET = | ||||
|       {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', | ||||
|           (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', | ||||
|           (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', | ||||
|           (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', | ||||
|           (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', | ||||
|           (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', | ||||
|           (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', | ||||
|           (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', | ||||
|           (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', | ||||
|           (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', | ||||
|           (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', | ||||
|           (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', | ||||
|           (byte) '9', (byte) '+', (byte) '/'}; | ||||
| 
 | ||||
|   /** | ||||
|    * The 64 valid web safe Base64 values. | ||||
|    */ | ||||
|   private final static byte[] WEBSAFE_ALPHABET = | ||||
|       {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', | ||||
|           (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', | ||||
|           (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', | ||||
|           (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', | ||||
|           (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', | ||||
|           (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', | ||||
|           (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', | ||||
|           (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', | ||||
|           (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', | ||||
|           (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', | ||||
|           (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', | ||||
|           (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', | ||||
|           (byte) '9', (byte) '-', (byte) '_'}; | ||||
| 
 | ||||
|   /** | ||||
|    * Translates a Base64 value to either its 6-bit reconstruction value | ||||
|    * or a negative number indicating some other meaning. | ||||
|    **/ | ||||
|   private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 | ||||
|       -5, -5, // Whitespace: Tab and Linefeed | ||||
|       -9, -9, // Decimal 11 - 12 | ||||
|       -5, // Whitespace: Carriage Return | ||||
|       -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 | ||||
|       -9, -9, -9, -9, -9, // Decimal 27 - 31 | ||||
|       -5, // Whitespace: Space | ||||
|       -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 | ||||
|       62, // Plus sign at decimal 43 | ||||
|       -9, -9, -9, // Decimal 44 - 46 | ||||
|       63, // Slash at decimal 47 | ||||
|       52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine | ||||
|       -9, -9, -9, // Decimal 58 - 60 | ||||
|       -1, // Equals sign at decimal 61 | ||||
|       -9, -9, -9, // Decimal 62 - 64 | ||||
|       0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' | ||||
|       14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' | ||||
|       -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 | ||||
|       26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' | ||||
|       39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' | ||||
|       -9, -9, -9, -9, -9 // Decimal 123 - 127 | ||||
|       /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ | ||||
|       }; | ||||
| 
 | ||||
|   /** The web safe decodabet */ | ||||
|   private final static byte[] WEBSAFE_DECODABET = | ||||
|       {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 | ||||
|           -5, -5, // Whitespace: Tab and Linefeed | ||||
|           -9, -9, // Decimal 11 - 12 | ||||
|           -5, // Whitespace: Carriage Return | ||||
|           -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 | ||||
|           -9, -9, -9, -9, -9, // Decimal 27 - 31 | ||||
|           -5, // Whitespace: Space | ||||
|           -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 | ||||
|           62, // Dash '-' sign at decimal 45 | ||||
|           -9, -9, // Decimal 46-47 | ||||
|           52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine | ||||
|           -9, -9, -9, // Decimal 58 - 60 | ||||
|           -1, // Equals sign at decimal 61 | ||||
|           -9, -9, -9, // Decimal 62 - 64 | ||||
|           0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' | ||||
|           14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' | ||||
|           -9, -9, -9, -9, // Decimal 91-94 | ||||
|           63, // Underscore '_' at decimal 95 | ||||
|           -9, // Decimal 96 | ||||
|           26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' | ||||
|           39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' | ||||
|           -9, -9, -9, -9, -9 // Decimal 123 - 127 | ||||
|       /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ | ||||
|       }; | ||||
| 
 | ||||
|   // Indicates white space in encoding | ||||
|   private final static byte WHITE_SPACE_ENC = -5; | ||||
|   // Indicates equals sign in encoding | ||||
|   private final static byte EQUALS_SIGN_ENC = -1; | ||||
| 
 | ||||
|   /** Defeats instantiation. */ | ||||
|   private Base64() { | ||||
|   } | ||||
| 
 | ||||
|   /* ********  E N C O D I N G   M E T H O D S  ******** */ | ||||
| 
 | ||||
|   /** | ||||
|    * Encodes up to three bytes of the array <var>source</var> | ||||
|    * and writes the resulting four Base64 bytes to <var>destination</var>. | ||||
|    * The source and destination arrays can be manipulated | ||||
|    * anywhere along their length by specifying | ||||
|    * <var>srcOffset</var> and <var>destOffset</var>. | ||||
|    * This method does not check to make sure your arrays | ||||
|    * are large enough to accommodate <var>srcOffset</var> + 3 for | ||||
|    * the <var>source</var> array or <var>destOffset</var> + 4 for | ||||
|    * the <var>destination</var> array. | ||||
|    * The actual number of significant bytes in your array is | ||||
|    * given by <var>numSigBytes</var>. | ||||
|    * | ||||
|    * @param source the array to convert | ||||
|    * @param srcOffset the index where conversion begins | ||||
|    * @param numSigBytes the number of significant bytes in your array | ||||
|    * @param destination the array to hold the conversion | ||||
|    * @param destOffset the index where output will be put | ||||
|    * @param alphabet is the encoding alphabet | ||||
|    * @return the <var>destination</var> array | ||||
|    * @since 1.3 | ||||
|    */ | ||||
|   private static byte[] encode3to4(byte[] source, int srcOffset, | ||||
|       int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { | ||||
|     //           1         2         3 | ||||
|     // 01234567890123456789012345678901 Bit position | ||||
|     // --------000000001111111122222222 Array position from threeBytes | ||||
|     // --------|    ||    ||    ||    | Six bit groups to index alphabet | ||||
|     //          >>18  >>12  >> 6  >> 0  Right shift necessary | ||||
|     //                0x3f  0x3f  0x3f  Additional AND | ||||
| 
 | ||||
|     // Create buffer with zero-padding if there are only one or two | ||||
|     // significant bytes passed in the array. | ||||
|     // We have to shift left 24 in order to flush out the 1's that appear | ||||
|     // when Java treats a value as negative that is cast from a byte to an int. | ||||
|     int inBuff = | ||||
|         (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | ||||
|             | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | ||||
|             | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); | ||||
| 
 | ||||
|     switch (numSigBytes) { | ||||
|       case 3: | ||||
|         destination[destOffset] = alphabet[(inBuff >>> 18)]; | ||||
|         destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
|         destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; | ||||
|         destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; | ||||
|         return destination; | ||||
|       case 2: | ||||
|         destination[destOffset] = alphabet[(inBuff >>> 18)]; | ||||
|         destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
|         destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; | ||||
|         destination[destOffset + 3] = EQUALS_SIGN; | ||||
|         return destination; | ||||
|       case 1: | ||||
|         destination[destOffset] = alphabet[(inBuff >>> 18)]; | ||||
|         destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
|         destination[destOffset + 2] = EQUALS_SIGN; | ||||
|         destination[destOffset + 3] = EQUALS_SIGN; | ||||
|         return destination; | ||||
|       default: | ||||
|         return destination; | ||||
|     } // end switch | ||||
|   } // end encode3to4 | ||||
| 
 | ||||
|   /** | ||||
|    * Encodes a byte array into Base64 notation. | ||||
|    * Equivalent to calling | ||||
|    * {@code encodeBytes(source, 0, source.length)} | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @since 1.4 | ||||
|    */ | ||||
|   public static String encode(byte[] source) { | ||||
|     return encode(source, 0, source.length, ALPHABET, true); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Encodes a byte array into web safe Base64 notation. | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @param doPadding is {@code true} to pad result with '=' chars | ||||
|    *        if it does not fall on 3 byte boundaries | ||||
|    */ | ||||
|   public static String encodeWebSafe(byte[] source, boolean doPadding) { | ||||
|     return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Encodes a byte array into Base64 notation. | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @param off Offset in array where conversion should begin | ||||
|    * @param len Length of data to convert | ||||
|    * @param alphabet is the encoding alphabet | ||||
|    * @param doPadding is {@code true} to pad result with '=' chars | ||||
|    *        if it does not fall on 3 byte boundaries | ||||
|    * @since 1.4 | ||||
|    */ | ||||
|   public static String encode(byte[] source, int off, int len, byte[] alphabet, | ||||
|       boolean doPadding) { | ||||
|     byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); | ||||
|     int outLen = outBuff.length; | ||||
| 
 | ||||
|     // If doPadding is false, set length to truncate '=' | ||||
|     // padding characters | ||||
|     while (doPadding == false && outLen > 0) { | ||||
|       if (outBuff[outLen - 1] != '=') { | ||||
|         break; | ||||
|       } | ||||
|       outLen -= 1; | ||||
|     } | ||||
| 
 | ||||
|     return new String(outBuff, 0, outLen); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Encodes a byte array into Base64 notation. | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @param off Offset in array where conversion should begin | ||||
|    * @param len Length of data to convert | ||||
|    * @param alphabet is the encoding alphabet | ||||
|    * @param maxLineLength maximum length of one line. | ||||
|    * @return the BASE64-encoded byte array | ||||
|    */ | ||||
|   public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, | ||||
|       int maxLineLength) { | ||||
|     int lenDiv3 = (len + 2) / 3; // ceil(len / 3) | ||||
|     int len43 = lenDiv3 * 4; | ||||
|     byte[] outBuff = new byte[len43 // Main 4:3 | ||||
|         + (len43 / maxLineLength)]; // New lines | ||||
| 
 | ||||
|     int d = 0; | ||||
|     int e = 0; | ||||
|     int len2 = len - 2; | ||||
|     int lineLength = 0; | ||||
|     for (; d < len2; d += 3, e += 4) { | ||||
| 
 | ||||
|       // The following block of code is the same as | ||||
|       // encode3to4( source, d + off, 3, outBuff, e, alphabet ); | ||||
|       // but inlined for faster encoding (~20% improvement) | ||||
|       int inBuff = | ||||
|           ((source[d + off] << 24) >>> 8) | ||||
|               | ((source[d + 1 + off] << 24) >>> 16) | ||||
|               | ((source[d + 2 + off] << 24) >>> 24); | ||||
|       outBuff[e] = alphabet[(inBuff >>> 18)]; | ||||
|       outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
|       outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; | ||||
|       outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; | ||||
| 
 | ||||
|       lineLength += 4; | ||||
|       if (lineLength == maxLineLength) { | ||||
|         outBuff[e + 4] = NEW_LINE; | ||||
|         e++; | ||||
|         lineLength = 0; | ||||
|       } // end if: end of line | ||||
|     } // end for: each piece of array | ||||
| 
 | ||||
|     if (d < len) { | ||||
|       encode3to4(source, d + off, len - d, outBuff, e, alphabet); | ||||
| 
 | ||||
|       lineLength += 4; | ||||
|       if (lineLength == maxLineLength) { | ||||
|         // Add a last newline | ||||
|         outBuff[e + 4] = NEW_LINE; | ||||
|         e++; | ||||
|       } | ||||
|       e += 4; | ||||
|     } | ||||
| 
 | ||||
|     assert (e == outBuff.length); | ||||
|     return outBuff; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   /* ********  D E C O D I N G   M E T H O D S  ******** */ | ||||
| 
 | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes four bytes from array <var>source</var> | ||||
|    * and writes the resulting bytes (up to three of them) | ||||
|    * to <var>destination</var>. | ||||
|    * The source and destination arrays can be manipulated | ||||
|    * anywhere along their length by specifying | ||||
|    * <var>srcOffset</var> and <var>destOffset</var>. | ||||
|    * This method does not check to make sure your arrays | ||||
|    * are large enough to accommodate <var>srcOffset</var> + 4 for | ||||
|    * the <var>source</var> array or <var>destOffset</var> + 3 for | ||||
|    * the <var>destination</var> array. | ||||
|    * This method returns the actual number of bytes that | ||||
|    * were converted from the Base64 encoding. | ||||
|    * | ||||
|    * | ||||
|    * @param source the array to convert | ||||
|    * @param srcOffset the index where conversion begins | ||||
|    * @param destination the array to hold the conversion | ||||
|    * @param destOffset the index where output will be put | ||||
|    * @param decodabet the decodabet for decoding Base64 content | ||||
|    * @return the number of decoded bytes converted | ||||
|    * @since 1.3 | ||||
|    */ | ||||
|   private static int decode4to3(byte[] source, int srcOffset, | ||||
|       byte[] destination, int destOffset, byte[] decodabet) { | ||||
|     // Example: Dk== | ||||
|     if (source[srcOffset + 2] == EQUALS_SIGN) { | ||||
|       int outBuff = | ||||
|           ((decodabet[source[srcOffset]] << 24) >>> 6) | ||||
|               | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); | ||||
| 
 | ||||
|       destination[destOffset] = (byte) (outBuff >>> 16); | ||||
|       return 1; | ||||
|     } else if (source[srcOffset + 3] == EQUALS_SIGN) { | ||||
|       // Example: DkL= | ||||
|       int outBuff = | ||||
|           ((decodabet[source[srcOffset]] << 24) >>> 6) | ||||
|               | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ||||
|               | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); | ||||
| 
 | ||||
|       destination[destOffset] = (byte) (outBuff >>> 16); | ||||
|       destination[destOffset + 1] = (byte) (outBuff >>> 8); | ||||
|       return 2; | ||||
|     } else { | ||||
|       // Example: DkLE | ||||
|       int outBuff = | ||||
|           ((decodabet[source[srcOffset]] << 24) >>> 6) | ||||
|               | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ||||
|               | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ||||
|               | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); | ||||
| 
 | ||||
|       destination[destOffset] = (byte) (outBuff >> 16); | ||||
|       destination[destOffset + 1] = (byte) (outBuff >> 8); | ||||
|       destination[destOffset + 2] = (byte) (outBuff); | ||||
|       return 3; | ||||
|     } | ||||
|   } // end decodeToBytes | ||||
| 
 | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes data from Base64 notation. | ||||
|    * | ||||
|    * @param s the string to decode (decoded in default encoding) | ||||
|    * @return the decoded data | ||||
|    * @since 1.4 | ||||
|    */ | ||||
|   public static byte[] decode(String s) throws Base64DecoderException { | ||||
|     byte[] bytes = s.getBytes(); | ||||
|     return decode(bytes, 0, bytes.length); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes data from web safe Base64 notation. | ||||
|    * Web safe encoding uses '-' instead of '+', '_' instead of '/' | ||||
|    * | ||||
|    * @param s the string to decode (decoded in default encoding) | ||||
|    * @return the decoded data | ||||
|    */ | ||||
|   public static byte[] decodeWebSafe(String s) throws Base64DecoderException { | ||||
|     byte[] bytes = s.getBytes(); | ||||
|     return decodeWebSafe(bytes, 0, bytes.length); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes Base64 content in byte array format and returns | ||||
|    * the decoded byte array. | ||||
|    * | ||||
|    * @param source The Base64 encoded data | ||||
|    * @return decoded data | ||||
|    * @since 1.3 | ||||
|    * @throws Base64DecoderException | ||||
|    */ | ||||
|   public static byte[] decode(byte[] source) throws Base64DecoderException { | ||||
|     return decode(source, 0, source.length); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes web safe Base64 content in byte array format and returns | ||||
|    * the decoded data. | ||||
|    * Web safe encoding uses '-' instead of '+', '_' instead of '/' | ||||
|    * | ||||
|    * @param source the string to decode (decoded in default encoding) | ||||
|    * @return the decoded data | ||||
|    */ | ||||
|   public static byte[] decodeWebSafe(byte[] source) | ||||
|       throws Base64DecoderException { | ||||
|     return decodeWebSafe(source, 0, source.length); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes Base64 content in byte array format and returns | ||||
|    * the decoded byte array. | ||||
|    * | ||||
|    * @param source The Base64 encoded data | ||||
|    * @param off    The offset of where to begin decoding | ||||
|    * @param len    The length of characters to decode | ||||
|    * @return decoded data | ||||
|    * @since 1.3 | ||||
|    * @throws Base64DecoderException | ||||
|    */ | ||||
|   public static byte[] decode(byte[] source, int off, int len) | ||||
|       throws Base64DecoderException { | ||||
|     return decode(source, off, len, DECODABET); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes web safe Base64 content in byte array format and returns | ||||
|    * the decoded byte array. | ||||
|    * Web safe encoding uses '-' instead of '+', '_' instead of '/' | ||||
|    * | ||||
|    * @param source The Base64 encoded data | ||||
|    * @param off    The offset of where to begin decoding | ||||
|    * @param len    The length of characters to decode | ||||
|    * @return decoded data | ||||
|    */ | ||||
|   public static byte[] decodeWebSafe(byte[] source, int off, int len) | ||||
|       throws Base64DecoderException { | ||||
|     return decode(source, off, len, WEBSAFE_DECODABET); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Decodes Base64 content using the supplied decodabet and returns | ||||
|    * the decoded byte array. | ||||
|    * | ||||
|    * @param source    The Base64 encoded data | ||||
|    * @param off       The offset of where to begin decoding | ||||
|    * @param len       The length of characters to decode | ||||
|    * @param decodabet the decodabet for decoding Base64 content | ||||
|    * @return decoded data | ||||
|    */ | ||||
|   public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) | ||||
|       throws Base64DecoderException { | ||||
|     int len34 = len * 3 / 4; | ||||
|     byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output | ||||
|     int outBuffPosn = 0; | ||||
| 
 | ||||
|     byte[] b4 = new byte[4]; | ||||
|     int b4Posn = 0; | ||||
|     int i = 0; | ||||
|     byte sbiCrop = 0; | ||||
|     byte sbiDecode = 0; | ||||
|     for (i = 0; i < len; i++) { | ||||
|       sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits | ||||
|       sbiDecode = decodabet[sbiCrop]; | ||||
| 
 | ||||
|       if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better | ||||
|         if (sbiDecode >= EQUALS_SIGN_ENC) { | ||||
|           // An equals sign (for padding) must not occur at position 0 or 1 | ||||
|           // and must be the last byte[s] in the encoded value | ||||
|           if (sbiCrop == EQUALS_SIGN) { | ||||
|             int bytesLeft = len - i; | ||||
|             byte lastByte = (byte) (source[len - 1 + off] & 0x7f); | ||||
|             if (b4Posn == 0 || b4Posn == 1) { | ||||
|               throw new Base64DecoderException( | ||||
|                   "invalid padding byte '=' at byte offset " + i); | ||||
|             } else if ((b4Posn == 3 && bytesLeft > 2) | ||||
|                 || (b4Posn == 4 && bytesLeft > 1)) { | ||||
|               throw new Base64DecoderException( | ||||
|                   "padding byte '=' falsely signals end of encoded value " | ||||
|                       + "at offset " + i); | ||||
|             } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { | ||||
|               throw new Base64DecoderException( | ||||
|                   "encoded value has invalid trailing byte"); | ||||
|             } | ||||
|             break; | ||||
|           } | ||||
| 
 | ||||
|           b4[b4Posn++] = sbiCrop; | ||||
|           if (b4Posn == 4) { | ||||
|             outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); | ||||
|             b4Posn = 0; | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         throw new Base64DecoderException("Bad Base64 input character at " + i | ||||
|             + ": " + source[i + off] + "(decimal)"); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Because web safe encoding allows non padding base64 encodes, we | ||||
|     // need to pad the rest of the b4 buffer with equal signs when | ||||
|     // b4Posn != 0.  There can be at most 2 equal signs at the end of | ||||
|     // four characters, so the b4 buffer must have two or three | ||||
|     // characters.  This also catches the case where the input is | ||||
|     // padded with EQUALS_SIGN | ||||
|     if (b4Posn != 0) { | ||||
|       if (b4Posn == 1) { | ||||
|         throw new Base64DecoderException("single trailing character at offset " | ||||
|             + (len - 1)); | ||||
|       } | ||||
|       b4[b4Posn++] = EQUALS_SIGN; | ||||
|       outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); | ||||
|     } | ||||
| 
 | ||||
|     byte[] out = new byte[outBuffPosn]; | ||||
|     System.arraycopy(outBuff, 0, out, 0, outBuffPosn); | ||||
|     return out; | ||||
|   } | ||||
| } | ||||
|  | @ -18,119 +18,113 @@ package com.google.android.vending.expansion.downloader; | |||
| 
 | ||||
| import java.io.File; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Contains the internal constants that are used in the download manager. | ||||
|  * As a general rule, modifying these constants should be done with care. | ||||
|  */ | ||||
| public class Constants {     | ||||
|     /** Tag used for debugging/logging */ | ||||
|     public static final String TAG = "LVLDL"; | ||||
| public class Constants { | ||||
| 	/** Tag used for debugging/logging */ | ||||
| 	public static final String TAG = "LVLDL"; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Expansion path where we store obb files | ||||
|      */ | ||||
|     public static final String EXP_PATH = File.separator + "Android" | ||||
|             + File.separator + "obb" + File.separator; | ||||
|      | ||||
|     // save to private app's data on Android 6.0 to skip requesting permission. | ||||
|     public static final String EXP_PATH_API23 = File.separator + "Android" | ||||
|             + File.separator + "data" + File.separator; | ||||
|      | ||||
|     /** The intent that gets sent when the service must wake up for a retry */ | ||||
|     public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; | ||||
| 	public static final String EXP_PATH = File.separator + "Android" + File.separator + "obb" + File.separator; | ||||
| 
 | ||||
|     /** the intent that gets sent when clicking a successful download */ | ||||
|     public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; | ||||
| 	/** The intent that gets sent when the service must wake up for a retry */ | ||||
| 	public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP"; | ||||
| 
 | ||||
|     /** the intent that gets sent when clicking an incomplete/failed download  */ | ||||
|     public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST"; | ||||
| 	/** the intent that gets sent when clicking a successful download */ | ||||
| 	public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN"; | ||||
| 
 | ||||
|     /** the intent that gets sent when deleting the notification of a completed download */ | ||||
|     public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE"; | ||||
| 	/** the intent that gets sent when clicking an incomplete/failed download  */ | ||||
| 	public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST"; | ||||
| 
 | ||||
|     /** | ||||
| 	/** the intent that gets sent when deleting the notification of a completed download */ | ||||
| 	public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE"; | ||||
| 
 | ||||
| 	/** | ||||
|      * When a number has to be appended to the filename, this string is used to separate the | ||||
|      * base filename from the sequence number | ||||
|      */ | ||||
|     public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; | ||||
| 	public static final String FILENAME_SEQUENCE_SEPARATOR = "-"; | ||||
| 
 | ||||
|     /** The default user agent used for downloads */ | ||||
|     public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; | ||||
| 	/** The default user agent used for downloads */ | ||||
| 	public static final String DEFAULT_USER_AGENT = "Android.LVLDM"; | ||||
| 
 | ||||
|     /** The buffer size used to stream the data */ | ||||
|     public static final int BUFFER_SIZE = 4096; | ||||
| 	/** The buffer size used to stream the data */ | ||||
| 	public static final int BUFFER_SIZE = 4096; | ||||
| 
 | ||||
|     /** The minimum amount of progress that has to be done before the progress bar gets updated */ | ||||
|     public static final int MIN_PROGRESS_STEP = 4096; | ||||
| 	/** The minimum amount of progress that has to be done before the progress bar gets updated */ | ||||
| 	public static final int MIN_PROGRESS_STEP = 4096; | ||||
| 
 | ||||
|     /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ | ||||
|     public static final long MIN_PROGRESS_TIME = 1000; | ||||
| 	/** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */ | ||||
| 	public static final long MIN_PROGRESS_TIME = 1000; | ||||
| 
 | ||||
|     /** The maximum number of rows in the database (FIFO) */ | ||||
|     public static final int MAX_DOWNLOADS = 1000; | ||||
| 	/** The maximum number of rows in the database (FIFO) */ | ||||
| 	public static final int MAX_DOWNLOADS = 1000; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The number of times that the download manager will retry its network | ||||
|      * operations when no progress is happening before it gives up. | ||||
|      */ | ||||
|     public static final int MAX_RETRIES = 10; | ||||
| 	public static final int MAX_RETRIES = 5; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The minimum amount of time that the download manager accepts for | ||||
|      * a Retry-After response header with a parameter in delta-seconds. | ||||
|      */ | ||||
|     public static final int MIN_RETRY_AFTER = 30; // 30s | ||||
| 	public static final int MIN_RETRY_AFTER = 30; // 30s | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The maximum amount of time that the download manager accepts for | ||||
|      * a Retry-After response header with a parameter in delta-seconds. | ||||
|      */ | ||||
|     public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h | ||||
| 	public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The maximum number of redirects. | ||||
|      */ | ||||
|     public static final int MAX_REDIRECTS = 5; // can't be more than 7. | ||||
| 	public static final int MAX_REDIRECTS = 5; // can't be more than 7. | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The time between a failure and the first retry after an IOException. | ||||
|      * Each subsequent retry grows exponentially, doubling each time. | ||||
|      * The time is in seconds. | ||||
|      */ | ||||
|     public static final int RETRY_FIRST_DELAY = 30; | ||||
| 	public static final int RETRY_FIRST_DELAY = 30; | ||||
| 
 | ||||
|     /** Enable separate connectivity logging */ | ||||
|     public static final boolean LOGX = true; | ||||
| 	/** Enable separate connectivity logging */ | ||||
| 	public static final boolean LOGX = true; | ||||
| 
 | ||||
|     /** Enable verbose logging */ | ||||
|     public static final boolean LOGV = false; | ||||
|      | ||||
|     /** Enable super-verbose logging */ | ||||
|     private static final boolean LOCAL_LOGVV = false; | ||||
|     public static final boolean LOGVV = LOCAL_LOGVV && LOGV; | ||||
|      | ||||
|     /** | ||||
| 	/** Enable verbose logging */ | ||||
| 	public static final boolean LOGV = false; | ||||
| 
 | ||||
| 	/** Enable super-verbose logging */ | ||||
| 	private static final boolean LOCAL_LOGVV = false; | ||||
| 	public static final boolean LOGVV = LOCAL_LOGVV && LOGV; | ||||
| 
 | ||||
| 	/** | ||||
|      * This download has successfully completed. | ||||
|      * Warning: there might be other status values that indicate success | ||||
|      * in the future. | ||||
|      * Use isSucccess() to capture the entire category. | ||||
|      */ | ||||
|     public static final int STATUS_SUCCESS = 200; | ||||
| 	public static final int STATUS_SUCCESS = 200; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This request couldn't be parsed. This is also used when processing | ||||
|      * requests with unknown/unsupported URI schemes. | ||||
|      */ | ||||
|     public static final int STATUS_BAD_REQUEST = 400; | ||||
| 	public static final int STATUS_BAD_REQUEST = 400; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download can't be performed because the content type cannot be | ||||
|      * handled. | ||||
|      */ | ||||
|     public static final int STATUS_NOT_ACCEPTABLE = 406; | ||||
| 	public static final int STATUS_NOT_ACCEPTABLE = 406; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download cannot be performed because the length cannot be | ||||
|      * determined accurately. This is the code for the HTTP error "Length | ||||
|      * Required", which is typically used when making requests that require | ||||
|  | @ -139,102 +133,101 @@ public class Constants { | |||
|      * accurately (therefore making it impossible to know when a download | ||||
|      * completes). | ||||
|      */ | ||||
|     public static final int STATUS_LENGTH_REQUIRED = 411; | ||||
| 	public static final int STATUS_LENGTH_REQUIRED = 411; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download was interrupted and cannot be resumed. | ||||
|      * This is the code for the HTTP error "Precondition Failed", and it is | ||||
|      * also used in situations where the client doesn't have an ETag at all. | ||||
|      */ | ||||
|     public static final int STATUS_PRECONDITION_FAILED = 412; | ||||
| 	public static final int STATUS_PRECONDITION_FAILED = 412; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The lowest-valued error status that is not an actual HTTP status code. | ||||
|      */ | ||||
|     public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; | ||||
| 	public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The requested destination file already exists. | ||||
|      */ | ||||
|     public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; | ||||
| 	public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Some possibly transient error occurred, but we can't resume the download. | ||||
|      */ | ||||
|     public static final int STATUS_CANNOT_RESUME = 489; | ||||
| 	public static final int STATUS_CANNOT_RESUME = 489; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download was canceled | ||||
|      */ | ||||
|     public static final int STATUS_CANCELED = 490; | ||||
| 	public static final int STATUS_CANCELED = 490; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download has completed with an error. | ||||
|      * Warning: there will be other status values that indicate errors in | ||||
|      * the future. Use isStatusError() to capture the entire category. | ||||
|      */ | ||||
|     public static final int STATUS_UNKNOWN_ERROR = 491; | ||||
| 	public static final int STATUS_UNKNOWN_ERROR = 491; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because of a storage issue. | ||||
|      * Typically, that's because the filesystem is missing or full. | ||||
|      * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} | ||||
|      * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate. | ||||
|      */ | ||||
|     public static final int STATUS_FILE_ERROR = 492; | ||||
| 	public static final int STATUS_FILE_ERROR = 492; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because of an HTTP | ||||
|      * redirect response that the download manager couldn't | ||||
|      * handle. | ||||
|      */ | ||||
|     public static final int STATUS_UNHANDLED_REDIRECT = 493; | ||||
| 	public static final int STATUS_UNHANDLED_REDIRECT = 493; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because of an | ||||
|      * unspecified unhandled HTTP code. | ||||
|      */ | ||||
|     public static final int STATUS_UNHANDLED_HTTP_CODE = 494; | ||||
| 	public static final int STATUS_UNHANDLED_HTTP_CODE = 494; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because of an | ||||
|      * error receiving or processing data at the HTTP level. | ||||
|      */ | ||||
|     public static final int STATUS_HTTP_DATA_ERROR = 495; | ||||
| 	public static final int STATUS_HTTP_DATA_ERROR = 495; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because of an | ||||
|      * HttpException while setting up the request. | ||||
|      */ | ||||
|     public static final int STATUS_HTTP_EXCEPTION = 496; | ||||
| 	public static final int STATUS_HTTP_EXCEPTION = 496; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because there were | ||||
|      * too many redirects. | ||||
|      */ | ||||
|     public static final int STATUS_TOO_MANY_REDIRECTS = 497; | ||||
| 	public static final int STATUS_TOO_MANY_REDIRECTS = 497; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed due to insufficient storage | ||||
|      * space.  Typically, this is because the SD card is full. | ||||
|      */ | ||||
|     public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; | ||||
| 	public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This download couldn't be completed because no external storage | ||||
|      * device was found.  Typically, this is because the SD card is not | ||||
|      * mounted. | ||||
|      */ | ||||
|     public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; | ||||
| 	public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The wake duration to check to see if a download is possible. | ||||
|      */ | ||||
|     public static final long WATCHDOG_WAKE_TIMER = 60*1000;     | ||||
| 	public static final long WATCHDOG_WAKE_TIMER = 60 * 1000; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The wake duration to check to see if the process was killed. | ||||
|      */ | ||||
|     public static final long ACTIVE_THREAD_WATCHDOG = 5*1000;     | ||||
| 
 | ||||
| 	public static final long ACTIVE_THREAD_WATCHDOG = 5 * 1000; | ||||
| } | ||||
|  | @ -19,7 +19,6 @@ package com.google.android.vending.expansion.downloader; | |||
| import android.os.Parcel; | ||||
| import android.os.Parcelable; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * This class contains progress information about the active download(s). | ||||
|  * | ||||
|  | @ -31,50 +30,49 @@ import android.os.Parcelable; | |||
|  * as the progress so far, time remaining and current speed. | ||||
|  */ | ||||
| public class DownloadProgressInfo implements Parcelable { | ||||
|     public long mOverallTotal; | ||||
|     public long mOverallProgress; | ||||
|     public long mTimeRemaining; // time remaining | ||||
|     public float mCurrentSpeed; // speed in KB/S | ||||
| 	public long mOverallTotal; | ||||
| 	public long mOverallProgress; | ||||
| 	public long mTimeRemaining; // time remaining | ||||
| 	public float mCurrentSpeed; // speed in KB/S | ||||
| 
 | ||||
|     @Override | ||||
|     public int describeContents() { | ||||
|         return 0; | ||||
|     } | ||||
| 	@Override | ||||
| 	public int describeContents() { | ||||
| 		return 0; | ||||
| 	} | ||||
| 
 | ||||
|     @Override | ||||
|     public void writeToParcel(Parcel p, int i) { | ||||
|         p.writeLong(mOverallTotal); | ||||
|         p.writeLong(mOverallProgress); | ||||
|         p.writeLong(mTimeRemaining); | ||||
|         p.writeFloat(mCurrentSpeed); | ||||
|     } | ||||
| 	@Override | ||||
| 	public void writeToParcel(Parcel p, int i) { | ||||
| 		p.writeLong(mOverallTotal); | ||||
| 		p.writeLong(mOverallProgress); | ||||
| 		p.writeLong(mTimeRemaining); | ||||
| 		p.writeFloat(mCurrentSpeed); | ||||
| 	} | ||||
| 
 | ||||
|     public DownloadProgressInfo(Parcel p) { | ||||
|         mOverallTotal = p.readLong(); | ||||
|         mOverallProgress = p.readLong(); | ||||
|         mTimeRemaining = p.readLong(); | ||||
|         mCurrentSpeed = p.readFloat(); | ||||
|     } | ||||
| 	public DownloadProgressInfo(Parcel p) { | ||||
| 		mOverallTotal = p.readLong(); | ||||
| 		mOverallProgress = p.readLong(); | ||||
| 		mTimeRemaining = p.readLong(); | ||||
| 		mCurrentSpeed = p.readFloat(); | ||||
| 	} | ||||
| 
 | ||||
|     public DownloadProgressInfo(long overallTotal, long overallProgress, | ||||
|             long timeRemaining, | ||||
|             float currentSpeed) { | ||||
|         this.mOverallTotal = overallTotal; | ||||
|         this.mOverallProgress = overallProgress; | ||||
|         this.mTimeRemaining = timeRemaining; | ||||
|         this.mCurrentSpeed = currentSpeed; | ||||
|     } | ||||
| 	public DownloadProgressInfo(long overallTotal, long overallProgress, | ||||
| 			long timeRemaining, | ||||
| 			float currentSpeed) { | ||||
| 		this.mOverallTotal = overallTotal; | ||||
| 		this.mOverallProgress = overallProgress; | ||||
| 		this.mTimeRemaining = timeRemaining; | ||||
| 		this.mCurrentSpeed = currentSpeed; | ||||
| 	} | ||||
| 
 | ||||
|     public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { | ||||
|         @Override | ||||
|         public DownloadProgressInfo createFromParcel(Parcel parcel) { | ||||
|             return new DownloadProgressInfo(parcel); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public DownloadProgressInfo[] newArray(int i) { | ||||
|             return new DownloadProgressInfo[i]; | ||||
|         } | ||||
|     }; | ||||
| 	public static final Creator<DownloadProgressInfo> CREATOR = new Creator<DownloadProgressInfo>() { | ||||
| 		@Override | ||||
| 		public DownloadProgressInfo createFromParcel(Parcel parcel) { | ||||
| 			return new DownloadProgressInfo(parcel); | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public DownloadProgressInfo[] newArray(int i) { | ||||
| 			return new DownloadProgressInfo[i]; | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
|  |  | |||
|  | @ -32,13 +32,13 @@ import android.os.Messenger; | |||
| import android.os.RemoteException; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| 
 | ||||
| /** | ||||
|  * This class binds the service API to your application client.  It contains the IDownloaderClient proxy, | ||||
|  * which is used to call functions in your client as well as the Stub, which is used to call functions | ||||
|  * in the client implementation of IDownloaderClient. | ||||
|  *  | ||||
|  * | ||||
|  * <p>The IPC is implemented using an Android Messenger and a service Binder.  The connect method | ||||
|  * should be called whenever the client wants to bind to the service.  It opens up a service connection | ||||
|  * that ends up calling the onServiceConnected client API that passes the service messenger | ||||
|  | @ -58,162 +58,176 @@ import android.util.Log; | |||
|  * interface. | ||||
|  */ | ||||
| public class DownloaderClientMarshaller { | ||||
|     public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; | ||||
|     public static final int MSG_ONDOWNLOADPROGRESS = 11; | ||||
|     public static final int MSG_ONSERVICECONNECTED = 12; | ||||
| 	public static final int MSG_ONDOWNLOADSTATE_CHANGED = 10; | ||||
| 	public static final int MSG_ONDOWNLOADPROGRESS = 11; | ||||
| 	public static final int MSG_ONSERVICECONNECTED = 12; | ||||
| 
 | ||||
|     public static final String PARAM_NEW_STATE = "newState"; | ||||
|     public static final String PARAM_PROGRESS = "progress"; | ||||
|     public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; | ||||
| 	public static final String PARAM_NEW_STATE = "newState"; | ||||
| 	public static final String PARAM_PROGRESS = "progress"; | ||||
| 	public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; | ||||
| 
 | ||||
|     public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; | ||||
|     public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; | ||||
|     public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; | ||||
| 	public static final int NO_DOWNLOAD_REQUIRED = DownloaderService.NO_DOWNLOAD_REQUIRED; | ||||
| 	public static final int LVL_CHECK_REQUIRED = DownloaderService.LVL_CHECK_REQUIRED; | ||||
| 	public static final int DOWNLOAD_REQUIRED = DownloaderService.DOWNLOAD_REQUIRED; | ||||
| 
 | ||||
|     private static class Proxy implements IDownloaderClient { | ||||
|         private Messenger mServiceMessenger; | ||||
| 	private static class Proxy implements IDownloaderClient { | ||||
| 		private Messenger mServiceMessenger; | ||||
| 
 | ||||
|         @Override | ||||
|         public void onDownloadStateChanged(int newState) { | ||||
|             Bundle params = new Bundle(1); | ||||
|             params.putInt(PARAM_NEW_STATE, newState); | ||||
|             send(MSG_ONDOWNLOADSTATE_CHANGED, params); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void onDownloadStateChanged(int newState) { | ||||
| 			Bundle params = new Bundle(1); | ||||
| 			params.putInt(PARAM_NEW_STATE, newState); | ||||
| 			send(MSG_ONDOWNLOADSTATE_CHANGED, params); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void onDownloadProgress(DownloadProgressInfo progress) { | ||||
|             Bundle params = new Bundle(1); | ||||
|             params.putParcelable(PARAM_PROGRESS, progress); | ||||
|             send(MSG_ONDOWNLOADPROGRESS, params); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void onDownloadProgress(DownloadProgressInfo progress) { | ||||
| 			Bundle params = new Bundle(1); | ||||
| 			params.putParcelable(PARAM_PROGRESS, progress); | ||||
| 			send(MSG_ONDOWNLOADPROGRESS, params); | ||||
| 		} | ||||
| 
 | ||||
|         private void send(int method, Bundle params) { | ||||
|             Message m = Message.obtain(null, method); | ||||
|             m.setData(params); | ||||
|             try { | ||||
|                 mServiceMessenger.send(m); | ||||
|             } catch (RemoteException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         public Proxy(Messenger msg) { | ||||
|             mServiceMessenger = msg; | ||||
|         } | ||||
| 		private void send(int method, Bundle params) { | ||||
| 			Message m = Message.obtain(null, method); | ||||
| 			m.setData(params); | ||||
| 			try { | ||||
| 				mServiceMessenger.send(m); | ||||
| 			} catch (RemoteException e) { | ||||
| 				e.printStackTrace(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void onServiceConnected(Messenger m) { | ||||
|             /** | ||||
| 		public Proxy(Messenger msg) { | ||||
| 			mServiceMessenger = msg; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void onServiceConnected(Messenger m) { | ||||
| 			/** | ||||
|              * This is never called through the proxy. | ||||
|              */ | ||||
|         } | ||||
|     } | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     private static class Stub implements IStub { | ||||
|         private IDownloaderClient mItf = null; | ||||
|         private Class<?> mDownloaderServiceClass; | ||||
|         private boolean mBound; | ||||
|         private Messenger mServiceMessenger; | ||||
|         private Context mContext; | ||||
|         /** | ||||
| 	private static class Stub implements IStub { | ||||
| 		private IDownloaderClient mItf = null; | ||||
| 		private Class<?> mDownloaderServiceClass; | ||||
| 		private boolean mBound; | ||||
| 		private Messenger mServiceMessenger; | ||||
| 		private Context mContext; | ||||
| 		/** | ||||
|          * Target we publish for clients to send messages to IncomingHandler. | ||||
|          */ | ||||
|         final Messenger mMessenger = new Messenger(new Handler() { | ||||
|             @Override | ||||
|             public void handleMessage(Message msg) { | ||||
|                 switch (msg.what) { | ||||
|                     case MSG_ONDOWNLOADPROGRESS:                         | ||||
|                         Bundle bun = msg.getData(); | ||||
|                         if ( null != mContext ) { | ||||
|                             bun.setClassLoader(mContext.getClassLoader()); | ||||
|                             DownloadProgressInfo dpi = (DownloadProgressInfo) msg.getData() | ||||
|                                     .getParcelable(PARAM_PROGRESS); | ||||
|                             mItf.onDownloadProgress(dpi); | ||||
|                         } | ||||
|                         break; | ||||
|                     case MSG_ONDOWNLOADSTATE_CHANGED: | ||||
|                         mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); | ||||
|                         break; | ||||
|                     case MSG_ONSERVICECONNECTED: | ||||
|                         mItf.onServiceConnected( | ||||
|                                 (Messenger) msg.getData().getParcelable(PARAM_MESSENGER)); | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 		private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this); | ||||
| 		final Messenger mMessenger = new Messenger(mMsgHandler); | ||||
| 
 | ||||
|         public Stub(IDownloaderClient itf, Class<?> downloaderService) { | ||||
|             mItf = itf; | ||||
|             mDownloaderServiceClass = downloaderService; | ||||
|         } | ||||
| 		private static class MessengerHandlerClient extends Handler { | ||||
| 			private final WeakReference<Stub> mDownloader; | ||||
| 			public MessengerHandlerClient(Stub downloader) { | ||||
| 				mDownloader = new WeakReference<>(downloader); | ||||
| 			} | ||||
| 
 | ||||
|         /** | ||||
| 			@Override | ||||
| 			public void handleMessage(Message msg) { | ||||
| 				Stub downloader = mDownloader.get(); | ||||
| 				if (downloader != null) { | ||||
| 					downloader.handleMessage(msg); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		private void handleMessage(Message msg) { | ||||
| 			switch (msg.what) { | ||||
| 				case MSG_ONDOWNLOADPROGRESS: | ||||
| 					Bundle bun = msg.getData(); | ||||
| 					if (null != mContext) { | ||||
| 						bun.setClassLoader(mContext.getClassLoader()); | ||||
| 						DownloadProgressInfo dpi = (DownloadProgressInfo)msg.getData() | ||||
| 														   .getParcelable(PARAM_PROGRESS); | ||||
| 						mItf.onDownloadProgress(dpi); | ||||
| 					} | ||||
| 					break; | ||||
| 				case MSG_ONDOWNLOADSTATE_CHANGED: | ||||
| 					mItf.onDownloadStateChanged(msg.getData().getInt(PARAM_NEW_STATE)); | ||||
| 					break; | ||||
| 				case MSG_ONSERVICECONNECTED: | ||||
| 					mItf.onServiceConnected( | ||||
| 							(Messenger)msg.getData().getParcelable(PARAM_MESSENGER)); | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		public Stub(IDownloaderClient itf, Class<?> downloaderService) { | ||||
| 			mItf = itf; | ||||
| 			mDownloaderServiceClass = downloaderService; | ||||
| 		} | ||||
| 
 | ||||
| 		/** | ||||
|          * Class for interacting with the main interface of the service. | ||||
|          */ | ||||
|         private ServiceConnection mConnection = new ServiceConnection() { | ||||
|             public void onServiceConnected(ComponentName className, IBinder service) { | ||||
|                 // This is called when the connection with the service has been | ||||
|                 // established, giving us the object we can use to | ||||
|                 // interact with the service. We are communicating with the | ||||
|                 // service using a Messenger, so here we get a client-side | ||||
|                 // representation of that from the raw IBinder object. | ||||
|                 mServiceMessenger = new Messenger(service); | ||||
|                 mItf.onServiceConnected( | ||||
|                         mServiceMessenger); | ||||
|             } | ||||
| 		private ServiceConnection mConnection = new ServiceConnection() { | ||||
| 			public void onServiceConnected(ComponentName className, IBinder service) { | ||||
| 				// This is called when the connection with the service has been | ||||
| 				// established, giving us the object we can use to | ||||
| 				// interact with the service. We are communicating with the | ||||
| 				// service using a Messenger, so here we get a client-side | ||||
| 				// representation of that from the raw IBinder object. | ||||
| 				mServiceMessenger = new Messenger(service); | ||||
| 				mItf.onServiceConnected( | ||||
| 						mServiceMessenger); | ||||
| 			} | ||||
| 
 | ||||
|             public void onServiceDisconnected(ComponentName className) { | ||||
|                 // This is called when the connection with the service has been | ||||
|                 // unexpectedly disconnected -- that is, its process crashed. | ||||
|                 mServiceMessenger = null; | ||||
|             } | ||||
|         }; | ||||
| 			public void onServiceDisconnected(ComponentName className) { | ||||
| 				// This is called when the connection with the service has been | ||||
| 				// unexpectedly disconnected -- that is, its process crashed. | ||||
| 				mServiceMessenger = null; | ||||
| 			} | ||||
| 		}; | ||||
| 
 | ||||
|         @Override | ||||
|         public void connect(Context c) { | ||||
|             mContext = c; | ||||
|             Intent bindIntent = new Intent(c, mDownloaderServiceClass); | ||||
|             bindIntent.putExtra(PARAM_MESSENGER, mMessenger); | ||||
|             if ( !c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND) ) { | ||||
|                 if ( Constants.LOGVV ) { | ||||
|                     Log.d(Constants.TAG, "Service Unbound"); | ||||
|                 } | ||||
|             } else { | ||||
|                 mBound = true; | ||||
|             } | ||||
|                  | ||||
|         } | ||||
| 		@Override | ||||
| 		public void connect(Context c) { | ||||
| 			mContext = c; | ||||
| 			Intent bindIntent = new Intent(c, mDownloaderServiceClass); | ||||
| 			bindIntent.putExtra(PARAM_MESSENGER, mMessenger); | ||||
| 			if (!c.bindService(bindIntent, mConnection, Context.BIND_DEBUG_UNBIND)) { | ||||
| 				if (Constants.LOGVV) { | ||||
| 					Log.d(Constants.TAG, "Service Unbound"); | ||||
| 				} | ||||
| 			} else { | ||||
| 				mBound = true; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void disconnect(Context c) { | ||||
|             if (mBound) { | ||||
|                 c.unbindService(mConnection); | ||||
|                 mBound = false; | ||||
|             } | ||||
|             mContext = null; | ||||
|         } | ||||
| 		@Override | ||||
| 		public void disconnect(Context c) { | ||||
| 			if (mBound) { | ||||
| 				c.unbindService(mConnection); | ||||
| 				mBound = false; | ||||
| 			} | ||||
| 			mContext = null; | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public Messenger getMessenger() { | ||||
|             return mMessenger; | ||||
|         } | ||||
|     } | ||||
| 		@Override | ||||
| 		public Messenger getMessenger() { | ||||
| 			return mMessenger; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Returns a proxy that will marshal calls to IDownloaderClient methods | ||||
|      *  | ||||
|      * | ||||
|      * @param msg | ||||
|      * @return | ||||
|      */ | ||||
|     public static IDownloaderClient CreateProxy(Messenger msg) { | ||||
|         return new Proxy(msg); | ||||
|     } | ||||
| 	public static IDownloaderClient CreateProxy(Messenger msg) { | ||||
| 		return new Proxy(msg); | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Returns a stub object that, when connected, will listen for marshaled | ||||
|      * {@link IDownloaderClient} methods and translate them into calls to the supplied | ||||
|      * interface. | ||||
|      *  | ||||
|      * | ||||
|      * @param itf An implementation of IDownloaderClient that will be called | ||||
|      *            when remote method calls are unmarshaled. | ||||
|      * @param downloaderService The class for your implementation of {@link | ||||
|  | @ -221,11 +235,11 @@ public class DownloaderClientMarshaller { | |||
|      * @return The {@link IStub} that allows you to connect to the service such that | ||||
|      * your {@link IDownloaderClient} receives status updates. | ||||
|      */ | ||||
|     public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) { | ||||
|         return new Stub(itf, downloaderService); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
| 	public static IStub CreateStub(IDownloaderClient itf, Class<?> downloaderService) { | ||||
| 		return new Stub(itf, downloaderService); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Starts the download if necessary. This function starts a flow that does ` | ||||
|      * many things. 1) Checks to see if the APK version has been checked and | ||||
|      * the metadata database updated 2) If the APK version does not match, | ||||
|  | @ -237,7 +251,7 @@ public class DownloaderClientMarshaller { | |||
|      * to wait to hear about any updated APK expansion files. Note that this does | ||||
|      * mean that the application MUST be run for the first time with a network | ||||
|      * connection, even if Market delivers all of the files. | ||||
|      *  | ||||
|      * | ||||
|      * @param context Your application Context. | ||||
|      * @param notificationClient A PendingIntent to start the Activity in your application | ||||
|      * that shows the download progress and which will also start the application when download | ||||
|  | @ -248,30 +262,29 @@ public class DownloaderClientMarshaller { | |||
|      * #DOWNLOAD_REQUIRED}. | ||||
|      * @throws NameNotFoundException | ||||
|      */ | ||||
|     public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient,  | ||||
|             Class<?> serviceClass) | ||||
|             throws NameNotFoundException { | ||||
|         return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, | ||||
|                 serviceClass); | ||||
|     } | ||||
|      | ||||
|     /** | ||||
| 	public static int startDownloadServiceIfRequired(Context context, PendingIntent notificationClient, | ||||
| 			Class<?> serviceClass) | ||||
| 			throws NameNotFoundException { | ||||
| 		return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, | ||||
| 				serviceClass); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * This version assumes that the intent contains the pending intent as a parameter. This | ||||
|      * is used for responding to alarms. | ||||
|      * <p>The pending intent must be in an extra with the key {@link  | ||||
|      * <p>The pending intent must be in an extra with the key {@link | ||||
|      * impl.DownloaderService#EXTRA_PENDING_INTENT}. | ||||
|      *  | ||||
|      * | ||||
|      * @param context | ||||
|      * @param notificationClient | ||||
|      * @param serviceClass the class of the service to start | ||||
|      * @return | ||||
|      * @throws NameNotFoundException | ||||
|      */ | ||||
|     public static int startDownloadServiceIfRequired(Context context, Intent notificationClient,  | ||||
|             Class<?> serviceClass) | ||||
|             throws NameNotFoundException { | ||||
|         return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, | ||||
|                 serviceClass); | ||||
|     }     | ||||
| 
 | ||||
| 	public static int startDownloadServiceIfRequired(Context context, Intent notificationClient, | ||||
| 			Class<?> serviceClass) | ||||
| 			throws NameNotFoundException { | ||||
| 		return DownloaderService.startDownloadServiceIfRequired(context, notificationClient, | ||||
| 				serviceClass); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ import android.os.Message; | |||
| import android.os.Messenger; | ||||
| import android.os.RemoteException; | ||||
| 
 | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| 
 | ||||
| /** | ||||
|  * This class is used by the client activity to proxy requests to the Downloader | ||||
|  | @ -38,144 +38,156 @@ import android.os.RemoteException; | |||
|  */ | ||||
| public class DownloaderServiceMarshaller { | ||||
| 
 | ||||
|     public static final int MSG_REQUEST_ABORT_DOWNLOAD = | ||||
|             1; | ||||
|     public static final int MSG_REQUEST_PAUSE_DOWNLOAD = | ||||
|             2; | ||||
|     public static final int MSG_SET_DOWNLOAD_FLAGS = | ||||
|             3; | ||||
|     public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = | ||||
|             4; | ||||
|     public static final int MSG_REQUEST_DOWNLOAD_STATE = | ||||
|             5; | ||||
|     public static final int MSG_REQUEST_CLIENT_UPDATE = | ||||
|             6; | ||||
| 	public static final int MSG_REQUEST_ABORT_DOWNLOAD = | ||||
| 			1; | ||||
| 	public static final int MSG_REQUEST_PAUSE_DOWNLOAD = | ||||
| 			2; | ||||
| 	public static final int MSG_SET_DOWNLOAD_FLAGS = | ||||
| 			3; | ||||
| 	public static final int MSG_REQUEST_CONTINUE_DOWNLOAD = | ||||
| 			4; | ||||
| 	public static final int MSG_REQUEST_DOWNLOAD_STATE = | ||||
| 			5; | ||||
| 	public static final int MSG_REQUEST_CLIENT_UPDATE = | ||||
| 			6; | ||||
| 
 | ||||
|     public static final String PARAMS_FLAGS = "flags"; | ||||
|     public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; | ||||
| 	public static final String PARAMS_FLAGS = "flags"; | ||||
| 	public static final String PARAM_MESSENGER = DownloaderService.EXTRA_MESSAGE_HANDLER; | ||||
| 
 | ||||
|     private static class Proxy implements IDownloaderService { | ||||
|         private Messenger mMsg; | ||||
| 	private static class Proxy implements IDownloaderService { | ||||
| 		private Messenger mMsg; | ||||
| 
 | ||||
|         private void send(int method, Bundle params) { | ||||
|             Message m = Message.obtain(null, method); | ||||
|             m.setData(params); | ||||
|             try { | ||||
|                 mMsg.send(m); | ||||
|             } catch (RemoteException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
| 		private void send(int method, Bundle params) { | ||||
| 			Message m = Message.obtain(null, method); | ||||
| 			m.setData(params); | ||||
| 			try { | ||||
| 				mMsg.send(m); | ||||
| 			} catch (RemoteException e) { | ||||
| 				e.printStackTrace(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         public Proxy(Messenger msg) { | ||||
|             mMsg = msg; | ||||
|         } | ||||
| 		public Proxy(Messenger msg) { | ||||
| 			mMsg = msg; | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void requestAbortDownload() { | ||||
|             send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void requestAbortDownload() { | ||||
| 			send(MSG_REQUEST_ABORT_DOWNLOAD, new Bundle()); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void requestPauseDownload() { | ||||
|             send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void requestPauseDownload() { | ||||
| 			send(MSG_REQUEST_PAUSE_DOWNLOAD, new Bundle()); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void setDownloadFlags(int flags) { | ||||
|             Bundle params = new Bundle(); | ||||
|             params.putInt(PARAMS_FLAGS, flags); | ||||
|             send(MSG_SET_DOWNLOAD_FLAGS, params); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void setDownloadFlags(int flags) { | ||||
| 			Bundle params = new Bundle(); | ||||
| 			params.putInt(PARAMS_FLAGS, flags); | ||||
| 			send(MSG_SET_DOWNLOAD_FLAGS, params); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void requestContinueDownload() { | ||||
|             send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void requestContinueDownload() { | ||||
| 			send(MSG_REQUEST_CONTINUE_DOWNLOAD, new Bundle()); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void requestDownloadStatus() { | ||||
|             send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); | ||||
|         } | ||||
| 		@Override | ||||
| 		public void requestDownloadStatus() { | ||||
| 			send(MSG_REQUEST_DOWNLOAD_STATE, new Bundle()); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void onClientUpdated(Messenger clientMessenger) { | ||||
|             Bundle bundle = new Bundle(1); | ||||
|             bundle.putParcelable(PARAM_MESSENGER, clientMessenger); | ||||
|             send(MSG_REQUEST_CLIENT_UPDATE, bundle); | ||||
|         } | ||||
|     } | ||||
| 		@Override | ||||
| 		public void onClientUpdated(Messenger clientMessenger) { | ||||
| 			Bundle bundle = new Bundle(1); | ||||
| 			bundle.putParcelable(PARAM_MESSENGER, clientMessenger); | ||||
| 			send(MSG_REQUEST_CLIENT_UPDATE, bundle); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     private static class Stub implements IStub { | ||||
|         private IDownloaderService mItf = null; | ||||
|         final Messenger mMessenger = new Messenger(new Handler() { | ||||
|             @Override | ||||
|             public void handleMessage(Message msg) { | ||||
|                 switch (msg.what) { | ||||
|                     case MSG_REQUEST_ABORT_DOWNLOAD: | ||||
|                         mItf.requestAbortDownload(); | ||||
|                         break; | ||||
|                     case MSG_REQUEST_CONTINUE_DOWNLOAD: | ||||
|                         mItf.requestContinueDownload(); | ||||
|                         break; | ||||
|                     case MSG_REQUEST_PAUSE_DOWNLOAD: | ||||
|                         mItf.requestPauseDownload(); | ||||
|                         break; | ||||
|                     case MSG_SET_DOWNLOAD_FLAGS: | ||||
|                         mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); | ||||
|                         break; | ||||
|                     case MSG_REQUEST_DOWNLOAD_STATE: | ||||
|                         mItf.requestDownloadStatus(); | ||||
|                         break; | ||||
|                     case MSG_REQUEST_CLIENT_UPDATE: | ||||
|                         mItf.onClientUpdated((Messenger) msg.getData().getParcelable( | ||||
|                                 PARAM_MESSENGER)); | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 	private static class Stub implements IStub { | ||||
| 		private IDownloaderService mItf = null; | ||||
| 		private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this); | ||||
| 		final Messenger mMessenger = new Messenger(mMsgHandler); | ||||
| 
 | ||||
|         public Stub(IDownloaderService itf) { | ||||
|             mItf = itf; | ||||
|         } | ||||
| 		private static class MessengerHandlerServer extends Handler { | ||||
| 			private final WeakReference<Stub> mDownloader; | ||||
| 			public MessengerHandlerServer(Stub downloader) { | ||||
| 				mDownloader = new WeakReference<>(downloader); | ||||
| 			} | ||||
| 
 | ||||
|         @Override | ||||
|         public Messenger getMessenger() { | ||||
|             return mMessenger; | ||||
|         } | ||||
| 			@Override | ||||
| 			public void handleMessage(Message msg) { | ||||
| 				Stub downloader = mDownloader.get(); | ||||
| 				if (downloader != null) { | ||||
| 					downloader.handleMessage(msg); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void connect(Context c) { | ||||
| 		private void handleMessage(Message msg) { | ||||
| 			switch (msg.what) { | ||||
| 				case MSG_REQUEST_ABORT_DOWNLOAD: | ||||
| 					mItf.requestAbortDownload(); | ||||
| 					break; | ||||
| 				case MSG_REQUEST_CONTINUE_DOWNLOAD: | ||||
| 					mItf.requestContinueDownload(); | ||||
| 					break; | ||||
| 				case MSG_REQUEST_PAUSE_DOWNLOAD: | ||||
| 					mItf.requestPauseDownload(); | ||||
| 					break; | ||||
| 				case MSG_SET_DOWNLOAD_FLAGS: | ||||
| 					mItf.setDownloadFlags(msg.getData().getInt(PARAMS_FLAGS)); | ||||
| 					break; | ||||
| 				case MSG_REQUEST_DOWNLOAD_STATE: | ||||
| 					mItf.requestDownloadStatus(); | ||||
| 					break; | ||||
| 				case MSG_REQUEST_CLIENT_UPDATE: | ||||
| 					mItf.onClientUpdated((Messenger)msg.getData().getParcelable( | ||||
| 							PARAM_MESSENGER)); | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         } | ||||
| 		public Stub(IDownloaderService itf) { | ||||
| 			mItf = itf; | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void disconnect(Context c) { | ||||
| 		@Override | ||||
| 		public Messenger getMessenger() { | ||||
| 			return mMessenger; | ||||
| 		} | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 		@Override | ||||
| 		public void connect(Context c) { | ||||
| 		} | ||||
| 
 | ||||
|     /** | ||||
| 		@Override | ||||
| 		public void disconnect(Context c) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Returns a proxy that will marshall calls to IDownloaderService methods | ||||
|      *  | ||||
|      * | ||||
|      * @param ctx | ||||
|      * @return | ||||
|      */ | ||||
|     public static IDownloaderService CreateProxy(Messenger msg) { | ||||
|         return new Proxy(msg); | ||||
|     } | ||||
| 	public static IDownloaderService CreateProxy(Messenger msg) { | ||||
| 		return new Proxy(msg); | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Returns a stub object that, when connected, will listen for marshalled | ||||
|      * IDownloaderService methods and translate them into calls to the supplied | ||||
|      * interface. | ||||
|      *  | ||||
|      * | ||||
|      * @param itf An implementation of IDownloaderService that will be called | ||||
|      *            when remote method calls are unmarshalled. | ||||
|      * @return | ||||
|      */ | ||||
|     public static IStub CreateStub(IDownloaderService itf) { | ||||
|         return new Stub(itf); | ||||
|     } | ||||
| 
 | ||||
| 	public static IStub CreateStub(IDownloaderService itf) { | ||||
| 		return new Stub(itf); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -16,8 +16,7 @@ | |||
| 
 | ||||
| package com.google.android.vending.expansion.downloader; | ||||
| 
 | ||||
| import com.godot.game.R; | ||||
| 
 | ||||
| import android.annotation.TargetApi; | ||||
| import android.content.Context; | ||||
| import android.os.Build; | ||||
| import android.os.Environment; | ||||
|  | @ -25,6 +24,8 @@ import android.os.StatFs; | |||
| import android.os.SystemClock; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import com.godot.game.R; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.Date; | ||||
|  | @ -39,273 +40,316 @@ import java.util.regex.Pattern; | |||
|  */ | ||||
| public class Helpers { | ||||
| 
 | ||||
|     public static Random sRandom = new Random(SystemClock.uptimeMillis()); | ||||
| 	public static Random sRandom = new Random(SystemClock.uptimeMillis()); | ||||
| 
 | ||||
|     /** Regex used to parse content-disposition headers */ | ||||
|     private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern | ||||
|             .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); | ||||
| 	/** Regex used to parse content-disposition headers */ | ||||
| 	private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern | ||||
| 																	   .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); | ||||
| 
 | ||||
|     private Helpers() { | ||||
|     } | ||||
| 	private Helpers() { | ||||
| 	} | ||||
| 
 | ||||
|     /* | ||||
|      * Parse the Content-Disposition HTTP Header. The format of the header is | ||||
|      * defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This | ||||
|      * header provides a filename for content that is going to be downloaded to | ||||
|      * the file system. We only support the attachment type. | ||||
| 	/* | ||||
|      * Parse the Content-Disposition HTTP Header. The format of the header is defined here: | ||||
|      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for | ||||
|      * content that is going to be downloaded to the file system. We only support the attachment | ||||
|      * type. | ||||
|      */ | ||||
|     static String parseContentDisposition(String contentDisposition) { | ||||
|         try { | ||||
|             Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); | ||||
|             if (m.find()) { | ||||
|                 return m.group(1); | ||||
|             } | ||||
|         } catch (IllegalStateException ex) { | ||||
|             // This function is defined as returning null when it can't parse | ||||
|             // the header | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 	static String parseContentDisposition(String contentDisposition) { | ||||
| 		try { | ||||
| 			Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); | ||||
| 			if (m.find()) { | ||||
| 				return m.group(1); | ||||
| 			} | ||||
| 		} catch (IllegalStateException ex) { | ||||
| 			// This function is defined as returning null when it can't parse | ||||
| 			// the header | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * @return the root of the filesystem containing the given path | ||||
|      */ | ||||
|     public static File getFilesystemRoot(String path) { | ||||
|         File cache = Environment.getDownloadCacheDirectory(); | ||||
|         if (path.startsWith(cache.getPath())) { | ||||
|             return cache; | ||||
|         } | ||||
|         File external = Environment.getExternalStorageDirectory(); | ||||
|         if (path.startsWith(external.getPath())) { | ||||
|             return external; | ||||
|         } | ||||
|         throw new IllegalArgumentException( | ||||
|                 "Cannot determine filesystem root for " + path); | ||||
|     } | ||||
| 	public static File getFilesystemRoot(String path) { | ||||
| 		File cache = Environment.getDownloadCacheDirectory(); | ||||
| 		if (path.startsWith(cache.getPath())) { | ||||
| 			return cache; | ||||
| 		} | ||||
| 		File external = Environment.getExternalStorageDirectory(); | ||||
| 		if (path.startsWith(external.getPath())) { | ||||
| 			return external; | ||||
| 		} | ||||
| 		throw new IllegalArgumentException( | ||||
| 				"Cannot determine filesystem root for " + path); | ||||
| 	} | ||||
| 
 | ||||
|     public static boolean isExternalMediaMounted() { | ||||
|         if (!Environment.getExternalStorageState().equals( | ||||
|                 Environment.MEDIA_MOUNTED)) { | ||||
|             // No SD card found. | ||||
|             if ( Constants.LOGVV ) { | ||||
|                 Log.d(Constants.TAG, "no external storage"); | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 	public static boolean isExternalMediaMounted() { | ||||
| 		if (!Environment.getExternalStorageState().equals( | ||||
| 					Environment.MEDIA_MOUNTED)) { | ||||
| 			// No SD card found. | ||||
| 			if (Constants.LOGVV) { | ||||
| 				Log.d(Constants.TAG, "no external storage"); | ||||
| 			} | ||||
| 			return false; | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
|      * @return the number of bytes available on the filesystem rooted at the | ||||
|      *         given File | ||||
| 	/** | ||||
|      * @return the number of bytes available on the filesystem rooted at the given File | ||||
|      */ | ||||
|     public static long getAvailableBytes(File root) { | ||||
|         StatFs stat = new StatFs(root.getPath()); | ||||
|         // put a bit of margin (in case creating the file grows the system by a | ||||
|         // few blocks) | ||||
|         long availableBlocks = (long) stat.getAvailableBlocks() - 4; | ||||
|         return stat.getBlockSize() * availableBlocks; | ||||
|     } | ||||
| 	public static long getAvailableBytes(File root) { | ||||
| 		StatFs stat = new StatFs(root.getPath()); | ||||
| 		// put a bit of margin (in case creating the file grows the system by a | ||||
| 		// few blocks) | ||||
| 		long availableBlocks = (long)stat.getAvailableBlocks() - 4; | ||||
| 		return stat.getBlockSize() * availableBlocks; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Checks whether the filename looks legitimate | ||||
|      */ | ||||
|     public static boolean isFilenameValid(String filename) { | ||||
|         filename = filename.replaceFirst("/+", "/"); // normalize leading | ||||
|                                                      // slashes | ||||
|         return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) | ||||
|                 || filename.startsWith(Environment.getExternalStorageDirectory().toString()); | ||||
|     } | ||||
| 	public static boolean isFilenameValid(String filename) { | ||||
| 		filename = filename.replaceFirst("/+", "/"); // normalize leading | ||||
| 				// slashes | ||||
| 		return filename.startsWith(Environment.getDownloadCacheDirectory().toString()) || filename.startsWith(Environment.getExternalStorageDirectory().toString()); | ||||
| 	} | ||||
| 
 | ||||
|     /* | ||||
| 	/* | ||||
|      * Delete the given file from device | ||||
|      */ | ||||
|     /* package */static void deleteFile(String path) { | ||||
|         try { | ||||
|             File file = new File(path); | ||||
|             file.delete(); | ||||
|         } catch (Exception e) { | ||||
|             Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); | ||||
|         } | ||||
|     } | ||||
| 	/* package */ static void deleteFile(String path) { | ||||
| 		try { | ||||
| 			File file = new File(path); | ||||
| 			file.delete(); | ||||
| 		} catch (Exception e) { | ||||
| 			Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
|      * Showing progress in MB here. It would be nice to choose the unit (KB, MB, | ||||
|      * GB) based on total file size, but given what we know about the expected | ||||
|      * ranges of file sizes for APK expansion files, it's probably not necessary. | ||||
|      *  | ||||
| 	/** | ||||
|      * Showing progress in MB here. It would be nice to choose the unit (KB, MB, GB) based on total | ||||
|      * file size, but given what we know about the expected ranges of file sizes for APK expansion | ||||
|      * files, it's probably not necessary. | ||||
|      * | ||||
|      * @param overallProgress | ||||
|      * @param overallTotal | ||||
|      * @return | ||||
|      */ | ||||
| 
 | ||||
|     static public String getDownloadProgressString(long overallProgress, long overallTotal) { | ||||
|         if (overallTotal == 0) { | ||||
|             if ( Constants.LOGVV ) { | ||||
|                 Log.e(Constants.TAG, "Notification called when total is zero"); | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
|         return String.format("%.2f", | ||||
|                 (float) overallProgress / (1024.0f * 1024.0f)) | ||||
|                 + "MB /" + | ||||
|                 String.format("%.2f", (float) overallTotal / | ||||
|                         (1024.0f * 1024.0f)) + "MB"; | ||||
|     } | ||||
| 	static public String getDownloadProgressString(long overallProgress, long overallTotal) { | ||||
| 		if (overallTotal == 0) { | ||||
| 			if (Constants.LOGVV) { | ||||
| 				Log.e(Constants.TAG, "Notification called when total is zero"); | ||||
| 			} | ||||
| 			return ""; | ||||
| 		} | ||||
| 		return String.format(Locale.ENGLISH, "%.2f", | ||||
| 					   (float)overallProgress / (1024.0f * 1024.0f)) + | ||||
| 				"MB /" + | ||||
| 				String.format(Locale.ENGLISH, "%.2f", (float)overallTotal / (1024.0f * 1024.0f)) + "MB"; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Adds a percentile to getDownloadProgressString. | ||||
|      *  | ||||
|      * | ||||
|      * @param overallProgress | ||||
|      * @param overallTotal | ||||
|      * @return | ||||
|      */ | ||||
|     static public String getDownloadProgressStringNotification(long overallProgress, | ||||
|             long overallTotal) { | ||||
|         if (overallTotal == 0) { | ||||
|             if ( Constants.LOGVV ) { | ||||
|                 Log.e(Constants.TAG, "Notification called when total is zero"); | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
|         return getDownloadProgressString(overallProgress, overallTotal) + " (" + | ||||
|                 getDownloadProgressPercent(overallProgress, overallTotal) + ")"; | ||||
|     } | ||||
| 	static public String getDownloadProgressStringNotification(long overallProgress, | ||||
| 			long overallTotal) { | ||||
| 		if (overallTotal == 0) { | ||||
| 			if (Constants.LOGVV) { | ||||
| 				Log.e(Constants.TAG, "Notification called when total is zero"); | ||||
| 			} | ||||
| 			return ""; | ||||
| 		} | ||||
| 		return getDownloadProgressString(overallProgress, overallTotal) + " (" + | ||||
| 				getDownloadProgressPercent(overallProgress, overallTotal) + ")"; | ||||
| 	} | ||||
| 
 | ||||
|     public static String getDownloadProgressPercent(long overallProgress, long overallTotal) { | ||||
|         if (overallTotal == 0) { | ||||
|             if ( Constants.LOGVV ) { | ||||
|                 Log.e(Constants.TAG, "Notification called when total is zero"); | ||||
|             } | ||||
|             return ""; | ||||
|         } | ||||
|         return Long.toString(overallProgress * 100 / overallTotal) + "%"; | ||||
|     } | ||||
| 	public static String getDownloadProgressPercent(long overallProgress, long overallTotal) { | ||||
| 		if (overallTotal == 0) { | ||||
| 			if (Constants.LOGVV) { | ||||
| 				Log.e(Constants.TAG, "Notification called when total is zero"); | ||||
| 			} | ||||
| 			return ""; | ||||
| 		} | ||||
| 		return Long.toString(overallProgress * 100 / overallTotal) + "%"; | ||||
| 	} | ||||
| 
 | ||||
|     public static String getSpeedString(float bytesPerMillisecond) { | ||||
|         return String.format("%.2f", bytesPerMillisecond * 1000 / 1024); | ||||
|     } | ||||
| 	public static String getSpeedString(float bytesPerMillisecond) { | ||||
| 		return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024); | ||||
| 	} | ||||
| 
 | ||||
|     public static String getTimeRemaining(long durationInMilliseconds) { | ||||
|         SimpleDateFormat sdf; | ||||
|         if (durationInMilliseconds > 1000 * 60 * 60) { | ||||
|             sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); | ||||
|         } else { | ||||
|             sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); | ||||
|         } | ||||
|         return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); | ||||
|     } | ||||
| 	public static String getTimeRemaining(long durationInMilliseconds) { | ||||
| 		SimpleDateFormat sdf; | ||||
| 		if (durationInMilliseconds > 1000 * 60 * 60) { | ||||
| 			sdf = new SimpleDateFormat("HH:mm", Locale.getDefault()); | ||||
| 		} else { | ||||
| 			sdf = new SimpleDateFormat("mm:ss", Locale.getDefault()); | ||||
| 		} | ||||
| 		return sdf.format(new Date(durationInMilliseconds - TimeZone.getDefault().getRawOffset())); | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the file name (without full path) for an Expansion APK file from | ||||
|      * the given context. | ||||
|      *  | ||||
| 	/** | ||||
|      * Returns the file name (without full path) for an Expansion APK file from the given context. | ||||
|      * | ||||
|      * @param c the context | ||||
|      * @param mainFile true for main file, false for patch file | ||||
|      * @param versionCode the version of the file | ||||
|      * @return String the file name of the expansion file | ||||
|      */ | ||||
|     public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { | ||||
|         return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; | ||||
|     } | ||||
| 	public static String getExpansionAPKFileName(Context c, boolean mainFile, int versionCode) { | ||||
| 		return (mainFile ? "main." : "patch.") + versionCode + "." + c.getPackageName() + ".obb"; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the filename (where the file should be saved) from info about a | ||||
|      * download | ||||
| 	/** | ||||
|      * Returns the filename (where the file should be saved) from info about a download | ||||
|      */ | ||||
|     static public String generateSaveFileName(Context c, String fileName) { | ||||
|         String path = getSaveFilePath(c) | ||||
|                 + File.separator + fileName; | ||||
|         return path; | ||||
|     } | ||||
| 	static public String generateSaveFileName(Context c, String fileName) { | ||||
| 		String path = getSaveFilePath(c) + File.separator + fileName; | ||||
| 		return path; | ||||
| 	} | ||||
| 
 | ||||
|     static public String getSaveFilePath(Context c) { | ||||
|         File root = Environment.getExternalStorageDirectory(); | ||||
|         // this makes several issues with Android SDK >= 23 devices. | ||||
|         // https://github.com/danikula/Google-Play-Expansion-File/commit/93a03bd34acad67c6ea34cfb6c3f02c93bdcea85 | ||||
|         // https://issuetracker.google.com/issues/37075181 | ||||
|         //String path = Build.VERSION.SDK_INT >= 23 ? Constants.EXP_PATH_API23 : Constants.EXP_PATH; | ||||
|         String path = Constants.EXP_PATH; | ||||
|         return root.toString() + path + c.getPackageName(); | ||||
|     } | ||||
| 	@TargetApi(Build.VERSION_CODES.HONEYCOMB) | ||||
| 	static public String getSaveFilePath(Context c) { | ||||
| 		// This technically existed since Honeycomb, but it is critical | ||||
| 		// on KitKat and greater versions since it will create the | ||||
| 		// directory if needed | ||||
| 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||
| 			return c.getObbDir().toString(); | ||||
| 		} else { | ||||
| 			File root = Environment.getExternalStorageDirectory(); | ||||
| 			String path = root.toString() + Constants.EXP_PATH + c.getPackageName(); | ||||
| 			return path; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function to ascertain the existence of a file and return | ||||
|      * true/false appropriately | ||||
|      *  | ||||
| 	/** | ||||
|      * Helper function to ascertain the existence of a file and return true/false appropriately | ||||
|      * | ||||
|      * @param c the app/activity/service context | ||||
|      * @param fileName the name (sans path) of the file to query | ||||
|      * @param fileSize the size that the file must match | ||||
|      * @param deleteFileOnMismatch if the file sizes do not match, delete the | ||||
|      *            file | ||||
|      * @param deleteFileOnMismatch if the file sizes do not match, delete the file | ||||
|      * @return true if it does exist, false otherwise | ||||
|      */ | ||||
|     static public boolean doesFileExist(Context c, String fileName, long fileSize, | ||||
|             boolean deleteFileOnMismatch) { | ||||
|         // the file may have been delivered by Market --- let's make sure | ||||
|         // it's the size we expect | ||||
|         File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); | ||||
|         if (fileForNewFile.exists()) { | ||||
|             if (fileForNewFile.length() == fileSize) { | ||||
|                 return true; | ||||
|             } | ||||
|             if (deleteFileOnMismatch) { | ||||
|                 // delete the file --- we won't be able to resume | ||||
|                 // because we cannot confirm the integrity of the file | ||||
|                 fileForNewFile.delete(); | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 	static public boolean doesFileExist(Context c, String fileName, long fileSize, | ||||
| 			boolean deleteFileOnMismatch) { | ||||
| 		// the file may have been delivered by Play --- let's make sure | ||||
| 		// it's the size we expect | ||||
| 		File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); | ||||
| 		if (fileForNewFile.exists()) { | ||||
| 			if (fileForNewFile.length() == fileSize) { | ||||
| 				return true; | ||||
| 			} | ||||
| 			if (deleteFileOnMismatch) { | ||||
| 				// delete the file --- we won't be able to resume | ||||
| 				// because we cannot confirm the integrity of the file | ||||
| 				fileForNewFile.delete(); | ||||
| 			} | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
|      * Converts download states that are returned by the {@link  | ||||
|      * IDownloaderClient#onDownloadStateChanged} callback into usable strings. | ||||
|      * This is useful if using the state strings built into the library to display user messages. | ||||
| 	public static final int FS_READABLE = 0; | ||||
| 	public static final int FS_DOES_NOT_EXIST = 1; | ||||
| 	public static final int FS_CANNOT_READ = 2; | ||||
| 
 | ||||
| 	/** | ||||
|      * Helper function to ascertain whether a file can be read. | ||||
|      * | ||||
|      * @param c the app/activity/service context | ||||
|      * @param fileName the name (sans path) of the file to query | ||||
|      * @return true if it does exist, false otherwise | ||||
|      */ | ||||
| 	static public int getFileStatus(Context c, String fileName) { | ||||
| 		// the file may have been delivered by Play --- let's make sure | ||||
| 		// it's the size we expect | ||||
| 		File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName)); | ||||
| 		int returnValue; | ||||
| 		if (fileForNewFile.exists()) { | ||||
| 			if (fileForNewFile.canRead()) { | ||||
| 				returnValue = FS_READABLE; | ||||
| 			} else { | ||||
| 				returnValue = FS_CANNOT_READ; | ||||
| 			} | ||||
| 		} else { | ||||
| 			returnValue = FS_DOES_NOT_EXIST; | ||||
| 		} | ||||
| 		return returnValue; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Helper function to ascertain whether the application has the correct access to the OBB | ||||
|      * directory to allow an OBB file to be written. | ||||
|      *  | ||||
|      * @param c the app/activity/service context | ||||
|      * @return true if the application can write an OBB file, false otherwise | ||||
|      */ | ||||
| 	static public boolean canWriteOBBFile(Context c) { | ||||
| 		String path = getSaveFilePath(c); | ||||
| 		File fileForNewFile = new File(path); | ||||
| 		boolean canWrite; | ||||
| 		if (fileForNewFile.exists()) { | ||||
| 			canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite(); | ||||
| 		} else { | ||||
| 			canWrite = fileForNewFile.mkdirs(); | ||||
| 		} | ||||
| 		return canWrite; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Converts download states that are returned by the | ||||
|      * {@link IDownloaderClient#onDownloadStateChanged} callback into usable strings. This is useful | ||||
|      * if using the state strings built into the library to display user messages. | ||||
|      *  | ||||
|      * @param state One of the STATE_* constants from {@link IDownloaderClient}. | ||||
|      * @return string resource ID for the corresponding string. | ||||
|      */ | ||||
|     static public int getDownloaderStringResourceIDFromState(int state) { | ||||
|         switch (state) { | ||||
|             case IDownloaderClient.STATE_IDLE: | ||||
|                 return R.string.state_idle; | ||||
|             case IDownloaderClient.STATE_FETCHING_URL: | ||||
|                 return R.string.state_fetching_url; | ||||
|             case IDownloaderClient.STATE_CONNECTING: | ||||
|                 return R.string.state_connecting; | ||||
|             case IDownloaderClient.STATE_DOWNLOADING: | ||||
|                 return R.string.state_downloading; | ||||
|             case IDownloaderClient.STATE_COMPLETED: | ||||
|                 return R.string.state_completed; | ||||
|             case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE: | ||||
|                 return R.string.state_paused_network_unavailable; | ||||
|             case IDownloaderClient.STATE_PAUSED_BY_REQUEST: | ||||
|                 return R.string.state_paused_by_request; | ||||
|             case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: | ||||
|                 return R.string.state_paused_wifi_disabled; | ||||
|             case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: | ||||
|                 return R.string.state_paused_wifi_unavailable; | ||||
|             case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED: | ||||
|                 return R.string.state_paused_wifi_disabled; | ||||
|             case IDownloaderClient.STATE_PAUSED_NEED_WIFI: | ||||
|                 return R.string.state_paused_wifi_unavailable; | ||||
|             case IDownloaderClient.STATE_PAUSED_ROAMING: | ||||
|                 return R.string.state_paused_roaming; | ||||
|             case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE: | ||||
|                 return R.string.state_paused_network_setup_failure; | ||||
|             case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: | ||||
|                 return R.string.state_paused_sdcard_unavailable; | ||||
|             case IDownloaderClient.STATE_FAILED_UNLICENSED: | ||||
|                 return R.string.state_failed_unlicensed; | ||||
|             case IDownloaderClient.STATE_FAILED_FETCHING_URL: | ||||
|                 return R.string.state_failed_fetching_url; | ||||
|             case IDownloaderClient.STATE_FAILED_SDCARD_FULL: | ||||
|                 return R.string.state_failed_sdcard_full; | ||||
|             case IDownloaderClient.STATE_FAILED_CANCELED: | ||||
|                 return R.string.state_failed_cancelled; | ||||
|             default: | ||||
|                 return R.string.state_unknown; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 	static public int getDownloaderStringResourceIDFromState(int state) { | ||||
| 		switch (state) { | ||||
| 			case IDownloaderClient.STATE_IDLE: | ||||
| 				return R.string.state_idle; | ||||
| 			case IDownloaderClient.STATE_FETCHING_URL: | ||||
| 				return R.string.state_fetching_url; | ||||
| 			case IDownloaderClient.STATE_CONNECTING: | ||||
| 				return R.string.state_connecting; | ||||
| 			case IDownloaderClient.STATE_DOWNLOADING: | ||||
| 				return R.string.state_downloading; | ||||
| 			case IDownloaderClient.STATE_COMPLETED: | ||||
| 				return R.string.state_completed; | ||||
| 			case IDownloaderClient.STATE_PAUSED_NETWORK_UNAVAILABLE: | ||||
| 				return R.string.state_paused_network_unavailable; | ||||
| 			case IDownloaderClient.STATE_PAUSED_BY_REQUEST: | ||||
| 				return R.string.state_paused_by_request; | ||||
| 			case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION: | ||||
| 				return R.string.state_paused_wifi_disabled; | ||||
| 			case IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION: | ||||
| 				return R.string.state_paused_wifi_unavailable; | ||||
| 			case IDownloaderClient.STATE_PAUSED_WIFI_DISABLED: | ||||
| 				return R.string.state_paused_wifi_disabled; | ||||
| 			case IDownloaderClient.STATE_PAUSED_NEED_WIFI: | ||||
| 				return R.string.state_paused_wifi_unavailable; | ||||
| 			case IDownloaderClient.STATE_PAUSED_ROAMING: | ||||
| 				return R.string.state_paused_roaming; | ||||
| 			case IDownloaderClient.STATE_PAUSED_NETWORK_SETUP_FAILURE: | ||||
| 				return R.string.state_paused_network_setup_failure; | ||||
| 			case IDownloaderClient.STATE_PAUSED_SDCARD_UNAVAILABLE: | ||||
| 				return R.string.state_paused_sdcard_unavailable; | ||||
| 			case IDownloaderClient.STATE_FAILED_UNLICENSED: | ||||
| 				return R.string.state_failed_unlicensed; | ||||
| 			case IDownloaderClient.STATE_FAILED_FETCHING_URL: | ||||
| 				return R.string.state_failed_fetching_url; | ||||
| 			case IDownloaderClient.STATE_FAILED_SDCARD_FULL: | ||||
| 				return R.string.state_failed_sdcard_full; | ||||
| 			case IDownloaderClient.STATE_FAILED_CANCELED: | ||||
| 				return R.string.state_failed_cancelled; | ||||
| 			default: | ||||
| 				return R.string.state_unknown; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -23,26 +23,26 @@ import android.os.Messenger; | |||
|  * downloader. It is used to pass status from the service to the client. | ||||
|  */ | ||||
| public interface IDownloaderClient { | ||||
|     static final int STATE_IDLE = 1; | ||||
|     static final int STATE_FETCHING_URL = 2; | ||||
|     static final int STATE_CONNECTING = 3; | ||||
|     static final int STATE_DOWNLOADING = 4; | ||||
|     static final int STATE_COMPLETED = 5; | ||||
| 	static final int STATE_IDLE = 1; | ||||
| 	static final int STATE_FETCHING_URL = 2; | ||||
| 	static final int STATE_CONNECTING = 3; | ||||
| 	static final int STATE_DOWNLOADING = 4; | ||||
| 	static final int STATE_COMPLETED = 5; | ||||
| 
 | ||||
|     static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; | ||||
|     static final int STATE_PAUSED_BY_REQUEST = 7; | ||||
| 	static final int STATE_PAUSED_NETWORK_UNAVAILABLE = 6; | ||||
| 	static final int STATE_PAUSED_BY_REQUEST = 7; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Both STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION and | ||||
|      * STATE_PAUSED_NEED_CELLULAR_PERMISSION imply that Wi-Fi is unavailable and | ||||
|      * cellular permission will restart the service. Wi-Fi disabled means that | ||||
|      * the Wi-Fi manager is returning that Wi-Fi is not enabled, while in the | ||||
|      * other case Wi-Fi is enabled but not available. | ||||
|      */ | ||||
|     static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; | ||||
|     static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; | ||||
| 	static final int STATE_PAUSED_WIFI_DISABLED_NEED_CELLULAR_PERMISSION = 8; | ||||
| 	static final int STATE_PAUSED_NEED_CELLULAR_PERMISSION = 9; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Both STATE_PAUSED_WIFI_DISABLED and STATE_PAUSED_NEED_WIFI imply that | ||||
|      * Wi-Fi is unavailable and cellular permission will NOT restart the | ||||
|      * service. Wi-Fi disabled means that the Wi-Fi manager is returning that | ||||
|  | @ -53,27 +53,27 @@ public interface IDownloaderClient { | |||
|      * developers with very large payloads do not allow these payloads to be | ||||
|      * downloaded over cellular connections. | ||||
|      */ | ||||
|     static final int STATE_PAUSED_WIFI_DISABLED = 10; | ||||
|     static final int STATE_PAUSED_NEED_WIFI = 11; | ||||
| 	static final int STATE_PAUSED_WIFI_DISABLED = 10; | ||||
| 	static final int STATE_PAUSED_NEED_WIFI = 11; | ||||
| 
 | ||||
|     static final int STATE_PAUSED_ROAMING = 12; | ||||
| 	static final int STATE_PAUSED_ROAMING = 12; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Scary case. We were on a network that redirected us to another website | ||||
|      * that delivered us the wrong file. | ||||
|      */ | ||||
|     static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; | ||||
| 	static final int STATE_PAUSED_NETWORK_SETUP_FAILURE = 13; | ||||
| 
 | ||||
|     static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; | ||||
| 	static final int STATE_PAUSED_SDCARD_UNAVAILABLE = 14; | ||||
| 
 | ||||
|     static final int STATE_FAILED_UNLICENSED = 15; | ||||
|     static final int STATE_FAILED_FETCHING_URL = 16; | ||||
|     static final int STATE_FAILED_SDCARD_FULL = 17; | ||||
|     static final int STATE_FAILED_CANCELED = 18; | ||||
| 	static final int STATE_FAILED_UNLICENSED = 15; | ||||
| 	static final int STATE_FAILED_FETCHING_URL = 16; | ||||
| 	static final int STATE_FAILED_SDCARD_FULL = 17; | ||||
| 	static final int STATE_FAILED_CANCELED = 18; | ||||
| 
 | ||||
|     static final int STATE_FAILED = 19; | ||||
| 	static final int STATE_FAILED = 19; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Called internally by the stub when the service is bound to the client. | ||||
|      * <p> | ||||
|      * Critical implementation detail. In onServiceConnected we create the | ||||
|  | @ -86,13 +86,13 @@ public interface IDownloaderClient { | |||
|      * instance of {@link IDownloaderService}, then call | ||||
|      * {@link IDownloaderService#onClientUpdated} with the Messenger retrieved | ||||
|      * from your {@link IStub} proxy object. | ||||
|      *  | ||||
|      * | ||||
|      * @param m the service Messenger. This Messenger is used to call the | ||||
|      *            service API from the client. | ||||
|      */ | ||||
|     void onServiceConnected(Messenger m); | ||||
| 	void onServiceConnected(Messenger m); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Called when the download state changes. Depending on the state, there may | ||||
|      * be user requests. The service is free to change the download state in the | ||||
|      * middle of a user request, so the client should be able to handle this. | ||||
|  | @ -109,18 +109,18 @@ public interface IDownloaderClient { | |||
|      * cellular connections with appropriate warnings. If the application | ||||
|      * suddenly starts downloading, the application should revert to showing the | ||||
|      * progress again, rather than leaving up the download over cellular UI up. | ||||
|      *  | ||||
|      * | ||||
|      * @param newState one of the STATE_* values defined in IDownloaderClient | ||||
|      */ | ||||
|     void onDownloadStateChanged(int newState); | ||||
| 	void onDownloadStateChanged(int newState); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Shows the download progress. This is intended to be used to fill out a | ||||
|      * client UI. This progress should only be shown in a few states such as | ||||
|      * STATE_DOWNLOADING. | ||||
|      *  | ||||
|      * | ||||
|      * @param progress the DownloadProgressInfo object containing the current | ||||
|      *            progress of all downloads. | ||||
|      */ | ||||
|     void onDownloadProgress(DownloadProgressInfo progress); | ||||
| 	void onDownloadProgress(DownloadProgressInfo progress); | ||||
| } | ||||
|  |  | |||
|  | @ -31,53 +31,53 @@ import android.os.Messenger; | |||
|  * should immediately call {@link #onClientUpdated}. | ||||
|  */ | ||||
| public interface IDownloaderService { | ||||
|     /** | ||||
| 	/** | ||||
|      * Set this flag in response to the | ||||
|      * IDownloaderClient.STATE_PAUSED_NEED_CELLULAR_PERMISSION state and then | ||||
|      * call RequestContinueDownload to resume a download | ||||
|      */ | ||||
|     public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; | ||||
| 	public static final int FLAGS_DOWNLOAD_OVER_CELLULAR = 1; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Request that the service abort the current download. The service should | ||||
|      * respond by changing the state to {@link IDownloaderClient.STATE_ABORTED}. | ||||
|      */ | ||||
|     void requestAbortDownload(); | ||||
| 	void requestAbortDownload(); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Request that the service pause the current download. The service should | ||||
|      * respond by changing the state to | ||||
|      * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. | ||||
|      */ | ||||
|     void requestPauseDownload(); | ||||
| 	void requestPauseDownload(); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Request that the service continue a paused download, when in any paused | ||||
|      * or failed state, including | ||||
|      * {@link IDownloaderClient.STATE_PAUSED_BY_REQUEST}. | ||||
|      */ | ||||
|     void requestContinueDownload(); | ||||
| 	void requestContinueDownload(); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Set the flags for this download (e.g. | ||||
|      * {@link DownloaderService.FLAGS_DOWNLOAD_OVER_CELLULAR}). | ||||
|      *  | ||||
|      * | ||||
|      * @param flags | ||||
|      */ | ||||
|     void setDownloadFlags(int flags); | ||||
| 	void setDownloadFlags(int flags); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Requests that the download status be sent to the client. | ||||
|      */ | ||||
|     void requestDownloadStatus(); | ||||
| 	void requestDownloadStatus(); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Call this when you get {@link | ||||
|      * IDownloaderClient.onServiceConnected(Messenger m)} from the | ||||
|      * DownloaderClient to register the client with the service. It will | ||||
|      * automatically send the current status to the client. | ||||
|      *  | ||||
|      * | ||||
|      * @param clientMessenger | ||||
|      */ | ||||
|     void onClientUpdated(Messenger clientMessenger); | ||||
| 	void onClientUpdated(Messenger clientMessenger); | ||||
| } | ||||
|  |  | |||
|  | @ -33,9 +33,9 @@ import android.os.Messenger; | |||
|  * {@link IDownloaderService#onClientUpdated}. | ||||
|  */ | ||||
| public interface IStub { | ||||
|     Messenger getMessenger(); | ||||
| 	Messenger getMessenger(); | ||||
| 
 | ||||
|     void connect(Context c); | ||||
| 	void connect(Context c); | ||||
| 
 | ||||
|     void disconnect(Context c); | ||||
| 	void disconnect(Context c); | ||||
| } | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
| 
 | ||||
| package com.google.android.vending.expansion.downloader; | ||||
| 
 | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Notification; | ||||
| import android.app.NotificationManager; | ||||
| import android.content.Context; | ||||
|  | @ -30,94 +31,96 @@ import android.util.Log; | |||
|  * Contains useful helper functions, typically tied to the application context. | ||||
|  */ | ||||
| class SystemFacade { | ||||
|     private Context mContext; | ||||
|     private NotificationManager mNotificationManager; | ||||
| 	private Context mContext; | ||||
| 	private NotificationManager mNotificationManager; | ||||
| 
 | ||||
|     public SystemFacade(Context context) { | ||||
|         mContext = context; | ||||
|         mNotificationManager = (NotificationManager) | ||||
|                 mContext.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|     } | ||||
| 	public SystemFacade(Context context) { | ||||
| 		mContext = context; | ||||
| 		mNotificationManager = (NotificationManager) | ||||
| 									   mContext.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
| 	} | ||||
| 
 | ||||
|     public long currentTimeMillis() { | ||||
|         return System.currentTimeMillis(); | ||||
|     } | ||||
| 	public long currentTimeMillis() { | ||||
| 		return System.currentTimeMillis(); | ||||
| 	} | ||||
| 
 | ||||
|     public Integer getActiveNetworkType() { | ||||
|         ConnectivityManager connectivity = | ||||
|                 (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); | ||||
|         if (connectivity == null) { | ||||
|             Log.w(Constants.TAG, "couldn't get connectivity manager"); | ||||
|             return null; | ||||
|         } | ||||
| 	public Integer getActiveNetworkType() { | ||||
| 		ConnectivityManager connectivity = | ||||
| 				(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); | ||||
| 		if (connectivity == null) { | ||||
| 			Log.w(Constants.TAG, "couldn't get connectivity manager"); | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
|         NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); | ||||
|         if (activeInfo == null) { | ||||
|             if (Constants.LOGVV) { | ||||
|                 Log.v(Constants.TAG, "network is not available"); | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
|         return activeInfo.getType(); | ||||
|     } | ||||
| 		@SuppressLint("MissingPermission") | ||||
| 		NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); | ||||
| 		if (activeInfo == null) { | ||||
| 			if (Constants.LOGVV) { | ||||
| 				Log.v(Constants.TAG, "network is not available"); | ||||
| 			} | ||||
| 			return null; | ||||
| 		} | ||||
| 		return activeInfo.getType(); | ||||
| 	} | ||||
| 
 | ||||
|     public boolean isNetworkRoaming() { | ||||
|         ConnectivityManager connectivity = | ||||
|                 (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); | ||||
|         if (connectivity == null) { | ||||
|             Log.w(Constants.TAG, "couldn't get connectivity manager"); | ||||
|             return false; | ||||
|         } | ||||
| 	public boolean isNetworkRoaming() { | ||||
| 		ConnectivityManager connectivity = | ||||
| 				(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE); | ||||
| 		if (connectivity == null) { | ||||
| 			Log.w(Constants.TAG, "couldn't get connectivity manager"); | ||||
| 			return false; | ||||
| 		} | ||||
| 
 | ||||
|         NetworkInfo info = connectivity.getActiveNetworkInfo(); | ||||
|         boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); | ||||
|         TelephonyManager tm = (TelephonyManager) mContext | ||||
|                 .getSystemService(Context.TELEPHONY_SERVICE); | ||||
|         if (null == tm) { | ||||
|             Log.w(Constants.TAG, "couldn't get telephony manager"); | ||||
|             return false; | ||||
|         } | ||||
|         boolean isRoaming = isMobile && tm.isNetworkRoaming(); | ||||
|         if (Constants.LOGVV && isRoaming) { | ||||
|             Log.v(Constants.TAG, "network is roaming"); | ||||
|         } | ||||
|         return isRoaming; | ||||
|     } | ||||
| 		@SuppressLint("MissingPermission") | ||||
| 		NetworkInfo info = connectivity.getActiveNetworkInfo(); | ||||
| 		boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE); | ||||
| 		TelephonyManager tm = (TelephonyManager)mContext | ||||
| 									  .getSystemService(Context.TELEPHONY_SERVICE); | ||||
| 		if (null == tm) { | ||||
| 			Log.w(Constants.TAG, "couldn't get telephony manager"); | ||||
| 			return false; | ||||
| 		} | ||||
| 		boolean isRoaming = isMobile && tm.isNetworkRoaming(); | ||||
| 		if (Constants.LOGVV && isRoaming) { | ||||
| 			Log.v(Constants.TAG, "network is roaming"); | ||||
| 		} | ||||
| 		return isRoaming; | ||||
| 	} | ||||
| 
 | ||||
|     public Long getMaxBytesOverMobile() { | ||||
|         return (long) Integer.MAX_VALUE; | ||||
|     } | ||||
| 	public Long getMaxBytesOverMobile() { | ||||
| 		return (long)Integer.MAX_VALUE; | ||||
| 	} | ||||
| 
 | ||||
|     public Long getRecommendedMaxBytesOverMobile() { | ||||
|         return 2097152L; | ||||
|     } | ||||
| 	public Long getRecommendedMaxBytesOverMobile() { | ||||
| 		return 2097152L; | ||||
| 	} | ||||
| 
 | ||||
|     public void sendBroadcast(Intent intent) { | ||||
|         mContext.sendBroadcast(intent); | ||||
|     } | ||||
| 	public void sendBroadcast(Intent intent) { | ||||
| 		mContext.sendBroadcast(intent); | ||||
| 	} | ||||
| 
 | ||||
|     public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { | ||||
|         return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; | ||||
|     } | ||||
| 	public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException { | ||||
| 		return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid; | ||||
| 	} | ||||
| 
 | ||||
|     public void postNotification(long id, Notification notification) { | ||||
|         /** | ||||
| 	public void postNotification(long id, Notification notification) { | ||||
| 		/** | ||||
|          * TODO: The system notification manager takes ints, not longs, as IDs, | ||||
|          * but the download manager uses IDs take straight from the database, | ||||
|          * which are longs. This will have to be dealt with at some point. | ||||
|          */ | ||||
|         mNotificationManager.notify((int) id, notification); | ||||
|     } | ||||
| 		mNotificationManager.notify((int)id, notification); | ||||
| 	} | ||||
| 
 | ||||
|     public void cancelNotification(long id) { | ||||
|         mNotificationManager.cancel((int) id); | ||||
|     } | ||||
| 	public void cancelNotification(long id) { | ||||
| 		mNotificationManager.cancel((int)id); | ||||
| 	} | ||||
| 
 | ||||
|     public void cancelAllNotifications() { | ||||
|         mNotificationManager.cancelAll(); | ||||
|     } | ||||
| 	public void cancelAllNotifications() { | ||||
| 		mNotificationManager.cancelAll(); | ||||
| 	} | ||||
| 
 | ||||
|     public void startThread(Thread thread) { | ||||
|         thread.start(); | ||||
|     } | ||||
| 	public void startThread(Thread thread) { | ||||
| 		thread.start(); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,536 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2012 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  * This is a port of AndroidHttpClient to pre-Froyo devices, that takes advantage of | ||||
|  * the SSLSessionCache added Froyo devices using reflection. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.expansion.downloader.impl; | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.OutputStream; | ||||
| import java.lang.reflect.Constructor; | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.lang.reflect.Method; | ||||
| import java.net.URI; | ||||
| import java.util.zip.GZIPInputStream; | ||||
| import java.util.zip.GZIPOutputStream; | ||||
| 
 | ||||
| import org.apache.http.Header; | ||||
| import org.apache.http.HttpEntity; | ||||
| import org.apache.http.HttpEntityEnclosingRequest; | ||||
| import org.apache.http.HttpException; | ||||
| import org.apache.http.HttpHost; | ||||
| import org.apache.http.HttpRequest; | ||||
| import org.apache.http.HttpRequestInterceptor; | ||||
| import org.apache.http.HttpResponse; | ||||
| import org.apache.http.client.ClientProtocolException; | ||||
| import org.apache.http.client.HttpClient; | ||||
| import org.apache.http.client.ResponseHandler; | ||||
| import org.apache.http.client.methods.HttpUriRequest; | ||||
| import org.apache.http.client.params.HttpClientParams; | ||||
| import org.apache.http.client.protocol.ClientContext; | ||||
| import org.apache.http.conn.ClientConnectionManager; | ||||
| import org.apache.http.conn.scheme.PlainSocketFactory; | ||||
| import org.apache.http.conn.scheme.Scheme; | ||||
| import org.apache.http.conn.scheme.SchemeRegistry; | ||||
| import org.apache.http.conn.scheme.SocketFactory; | ||||
| import org.apache.http.conn.ssl.SSLSocketFactory; | ||||
| import org.apache.http.entity.AbstractHttpEntity; | ||||
| import org.apache.http.entity.ByteArrayEntity; | ||||
| import org.apache.http.impl.client.DefaultHttpClient; | ||||
| import org.apache.http.impl.client.RequestWrapper; | ||||
| import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; | ||||
| import org.apache.http.params.BasicHttpParams; | ||||
| import org.apache.http.params.HttpConnectionParams; | ||||
| import org.apache.http.params.HttpParams; | ||||
| import org.apache.http.params.HttpProtocolParams; | ||||
| import org.apache.http.protocol.BasicHttpContext; | ||||
| import org.apache.http.protocol.BasicHttpProcessor; | ||||
| import org.apache.http.protocol.HttpContext; | ||||
| 
 | ||||
| import android.content.ContentResolver; | ||||
| import android.content.Context; | ||||
| import android.net.SSLCertificateSocketFactory; | ||||
| import android.os.Looper; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * Subclass of the Apache {@link DefaultHttpClient} that is configured with | ||||
|  * reasonable default settings and registered schemes for Android, and | ||||
|  * also lets the user add {@link HttpRequestInterceptor} classes. | ||||
|  * Don't create this directly, use the {@link #newInstance} factory method. | ||||
|  * | ||||
|  * <p>This client processes cookies but does not retain them by default. | ||||
|  * To retain cookies, simply add a cookie store to the HttpContext:</p> | ||||
|  * | ||||
|  * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> | ||||
|  */ | ||||
| public final class AndroidHttpClient implements HttpClient { | ||||
| 
 | ||||
| 	static Class<?> sSslSessionCacheClass; | ||||
| 	static { | ||||
| 		// if we are on Froyo+ devices, we can take advantage of the SSLSessionCache | ||||
| 		try { | ||||
| 			sSslSessionCacheClass = Class.forName("android.net.SSLSessionCache"); | ||||
| 		} catch (Exception e) { | ||||
| 			 | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|     // Gzip of data shorter than this probably won't be worthwhile | ||||
|     public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; | ||||
| 
 | ||||
|     // Default connection and socket timeout of 60 seconds.  Tweak to taste. | ||||
|     private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000; | ||||
| 
 | ||||
|     private static final String TAG = "AndroidHttpClient"; | ||||
| 
 | ||||
| 
 | ||||
|     /** Interceptor throws an exception if the executing thread is blocked */ | ||||
|     private static final HttpRequestInterceptor sThreadCheckInterceptor = | ||||
|             new HttpRequestInterceptor() { | ||||
|         public void process(HttpRequest request, HttpContext context) { | ||||
|             // Prevent the HttpRequest from being sent on the main thread | ||||
|             if (Looper.myLooper() != null && Looper.myLooper() == Looper.getMainLooper() ) { | ||||
|                 throw new RuntimeException("This thread forbids HTTP requests"); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new HttpClient with reasonable defaults (which you can update). | ||||
|      * | ||||
|      * @param userAgent to report in your HTTP requests | ||||
|      * @param context to use for caching SSL sessions (may be null for no caching) | ||||
|      * @return AndroidHttpClient for you to use for all your requests. | ||||
|      */ | ||||
|     public static AndroidHttpClient newInstance(String userAgent, Context context) { | ||||
|         HttpParams params = new BasicHttpParams(); | ||||
| 
 | ||||
|         // Turn off stale checking.  Our connections break all the time anyway, | ||||
|         // and it's not worth it to pay the penalty of checking every time. | ||||
|         HttpConnectionParams.setStaleCheckingEnabled(params, false); | ||||
| 
 | ||||
|         HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT); | ||||
|         HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT); | ||||
|         HttpConnectionParams.setSocketBufferSize(params, 8192); | ||||
| 
 | ||||
|         // Don't handle redirects -- return them to the caller.  Our code | ||||
|         // often wants to re-POST after a redirect, which we must do ourselves. | ||||
|         HttpClientParams.setRedirecting(params, false); | ||||
| 
 | ||||
|         Object sessionCache = null; | ||||
|         // Use a session cache for SSL sockets -- Froyo only | ||||
|         if ( null != context && null != sSslSessionCacheClass ) { | ||||
|              Constructor<?> ct; | ||||
| 			try { | ||||
| 				ct = sSslSessionCacheClass.getConstructor(Context.class); | ||||
| 				sessionCache = ct.newInstance(context);              | ||||
| 			} catch (SecurityException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (NoSuchMethodException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (IllegalArgumentException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (InstantiationException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (IllegalAccessException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (InvocationTargetException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} | ||||
|         } | ||||
| 
 | ||||
|         // Set the specified user agent and register standard protocols. | ||||
|         HttpProtocolParams.setUserAgent(params, userAgent); | ||||
|         SchemeRegistry schemeRegistry = new SchemeRegistry(); | ||||
|         schemeRegistry.register(new Scheme("http", | ||||
|                 PlainSocketFactory.getSocketFactory(), 80)); | ||||
|         SocketFactory sslCertificateSocketFactory = null; | ||||
|         if ( null != sessionCache ) { | ||||
|         	Method getHttpSocketFactoryMethod; | ||||
| 			try { | ||||
| 				getHttpSocketFactoryMethod = SSLCertificateSocketFactory.class.getDeclaredMethod("getHttpSocketFactory",Integer.TYPE, sSslSessionCacheClass); | ||||
| 	        	sslCertificateSocketFactory = (SocketFactory)getHttpSocketFactoryMethod.invoke(null, SOCKET_OPERATION_TIMEOUT, sessionCache); | ||||
| 			} catch (SecurityException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (NoSuchMethodException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (IllegalArgumentException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (IllegalAccessException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} catch (InvocationTargetException e) { | ||||
| 				// TODO Auto-generated catch block | ||||
| 				e.printStackTrace(); | ||||
| 			} | ||||
|         } | ||||
|         if ( null == sslCertificateSocketFactory ) { | ||||
|         	sslCertificateSocketFactory = SSLSocketFactory.getSocketFactory(); | ||||
|         } | ||||
|         schemeRegistry.register(new Scheme("https", | ||||
|                 sslCertificateSocketFactory, 443)); | ||||
| 
 | ||||
|         ClientConnectionManager manager = | ||||
|                 new ThreadSafeClientConnManager(params, schemeRegistry); | ||||
| 
 | ||||
|         // We use a factory method to modify superclass initialization | ||||
|         // parameters without the funny call-a-static-method dance. | ||||
|         return new AndroidHttpClient(manager, params); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new HttpClient with reasonable defaults (which you can update). | ||||
|      * @param userAgent to report in your HTTP requests. | ||||
|      * @return AndroidHttpClient for you to use for all your requests. | ||||
|      */ | ||||
|     public static AndroidHttpClient newInstance(String userAgent) { | ||||
|         return newInstance(userAgent, null /* session cache */); | ||||
|     } | ||||
| 
 | ||||
|     private final HttpClient delegate; | ||||
| 
 | ||||
|     private RuntimeException mLeakedException = new IllegalStateException( | ||||
|             "AndroidHttpClient created and never closed"); | ||||
| 
 | ||||
|     private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { | ||||
|         this.delegate = new DefaultHttpClient(ccm, params) { | ||||
|             @Override | ||||
|             protected BasicHttpProcessor createHttpProcessor() { | ||||
|                 // Add interceptor to prevent making requests from main thread. | ||||
|                 BasicHttpProcessor processor = super.createHttpProcessor(); | ||||
|                 processor.addRequestInterceptor(sThreadCheckInterceptor); | ||||
|                 processor.addRequestInterceptor(new CurlLogger()); | ||||
| 
 | ||||
|                 return processor; | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             protected HttpContext createHttpContext() { | ||||
|                 // Same as DefaultHttpClient.createHttpContext() minus the | ||||
|                 // cookie store. | ||||
|                 HttpContext context = new BasicHttpContext(); | ||||
|                 context.setAttribute( | ||||
|                         ClientContext.AUTHSCHEME_REGISTRY, | ||||
|                         getAuthSchemes()); | ||||
|                 context.setAttribute( | ||||
|                         ClientContext.COOKIESPEC_REGISTRY, | ||||
|                         getCookieSpecs()); | ||||
|                 context.setAttribute( | ||||
|                         ClientContext.CREDS_PROVIDER, | ||||
|                         getCredentialsProvider()); | ||||
|                 return context; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void finalize() throws Throwable { | ||||
|         super.finalize(); | ||||
|         if (mLeakedException != null) { | ||||
|             Log.e(TAG, "Leak found", mLeakedException); | ||||
|             mLeakedException = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Modifies a request to indicate to the server that we would like a | ||||
|      * gzipped response.  (Uses the "Accept-Encoding" HTTP header.) | ||||
|      * @param request the request to modify | ||||
|      * @see #getUngzippedContent | ||||
|      */ | ||||
|     public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { | ||||
|         request.addHeader("Accept-Encoding", "gzip"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the input stream from a response entity.  If the entity is gzipped | ||||
|      * then this will get a stream over the uncompressed data. | ||||
|      * | ||||
|      * @param entity the entity whose content should be read | ||||
|      * @return the input stream to read from | ||||
|      * @throws IOException | ||||
|      */ | ||||
|     public static InputStream getUngzippedContent(HttpEntity entity) | ||||
|             throws IOException { | ||||
|         InputStream responseStream = entity.getContent(); | ||||
|         if (responseStream == null) return responseStream; | ||||
|         Header header = entity.getContentEncoding(); | ||||
|         if (header == null) return responseStream; | ||||
|         String contentEncoding = header.getValue(); | ||||
|         if (contentEncoding == null) return responseStream; | ||||
|         if (contentEncoding.contains("gzip")) responseStream | ||||
|                 = new GZIPInputStream(responseStream); | ||||
|         return responseStream; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Release resources associated with this client.  You must call this, | ||||
|      * or significant resources (sockets and memory) may be leaked. | ||||
|      */ | ||||
|     public void close() { | ||||
|         if (mLeakedException != null) { | ||||
|             getConnectionManager().shutdown(); | ||||
|             mLeakedException = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public HttpParams getParams() { | ||||
|         return delegate.getParams(); | ||||
|     } | ||||
| 
 | ||||
|     public ClientConnectionManager getConnectionManager() { | ||||
|         return delegate.getConnectionManager(); | ||||
|     } | ||||
| 
 | ||||
|     public HttpResponse execute(HttpUriRequest request) throws IOException { | ||||
|         return delegate.execute(request); | ||||
|     } | ||||
| 
 | ||||
|     public HttpResponse execute(HttpUriRequest request, HttpContext context) | ||||
|             throws IOException { | ||||
|         return delegate.execute(request, context); | ||||
|     } | ||||
| 
 | ||||
|     public HttpResponse execute(HttpHost target, HttpRequest request) | ||||
|             throws IOException { | ||||
|         return delegate.execute(target, request); | ||||
|     } | ||||
| 
 | ||||
|     public HttpResponse execute(HttpHost target, HttpRequest request, | ||||
|             HttpContext context) throws IOException { | ||||
|         return delegate.execute(target, request, context); | ||||
|     } | ||||
| 
 | ||||
|     public <T> T execute(HttpUriRequest request, | ||||
|             ResponseHandler<? extends T> responseHandler) | ||||
|             throws IOException, ClientProtocolException { | ||||
|         return delegate.execute(request, responseHandler); | ||||
|     } | ||||
| 
 | ||||
|     public <T> T execute(HttpUriRequest request, | ||||
|             ResponseHandler<? extends T> responseHandler, HttpContext context) | ||||
|             throws IOException, ClientProtocolException { | ||||
|         return delegate.execute(request, responseHandler, context); | ||||
|     } | ||||
| 
 | ||||
|     public <T> T execute(HttpHost target, HttpRequest request, | ||||
|             ResponseHandler<? extends T> responseHandler) throws IOException, | ||||
|             ClientProtocolException { | ||||
|         return delegate.execute(target, request, responseHandler); | ||||
|     } | ||||
| 
 | ||||
|     public <T> T execute(HttpHost target, HttpRequest request, | ||||
|             ResponseHandler<? extends T> responseHandler, HttpContext context) | ||||
|             throws IOException, ClientProtocolException { | ||||
|         return delegate.execute(target, request, responseHandler, context); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Compress data to send to server. | ||||
|      * Creates a Http Entity holding the gzipped data. | ||||
|      * The data will not be compressed if it is too short. | ||||
|      * @param data The bytes to compress | ||||
|      * @return Entity holding the data | ||||
|      */ | ||||
|     public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver) | ||||
|             throws IOException { | ||||
|         AbstractHttpEntity entity; | ||||
|         if (data.length < getMinGzipSize(resolver)) { | ||||
|             entity = new ByteArrayEntity(data); | ||||
|         } else { | ||||
|             ByteArrayOutputStream arr = new ByteArrayOutputStream(); | ||||
|             OutputStream zipper = new GZIPOutputStream(arr); | ||||
|             zipper.write(data); | ||||
|             zipper.close(); | ||||
|             entity = new ByteArrayEntity(arr.toByteArray()); | ||||
|             entity.setContentEncoding("gzip"); | ||||
|         } | ||||
|         return entity; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieves the minimum size for compressing data. | ||||
|      * Shorter data will not be compressed. | ||||
|      */ | ||||
|     public static long getMinGzipSize(ContentResolver resolver) { | ||||
|         return DEFAULT_SYNC_MIN_GZIP_BYTES;  // For now, this is just a constant. | ||||
|     } | ||||
| 
 | ||||
|     /* cURL logging support. */ | ||||
| 
 | ||||
|     /** | ||||
|      * Logging tag and level. | ||||
|      */ | ||||
|     private static class LoggingConfiguration { | ||||
| 
 | ||||
|         private final String tag; | ||||
|         private final int level; | ||||
| 
 | ||||
|         private LoggingConfiguration(String tag, int level) { | ||||
|             this.tag = tag; | ||||
|             this.level = level; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Returns true if logging is turned on for this configuration. | ||||
|          */ | ||||
|         private boolean isLoggable() { | ||||
|             return Log.isLoggable(tag, level); | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Prints a message using this configuration. | ||||
|          */ | ||||
|         private void println(String message) { | ||||
|             Log.println(level, tag, message); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** cURL logging configuration. */ | ||||
|     private volatile LoggingConfiguration curlConfiguration; | ||||
| 
 | ||||
|     /** | ||||
|      * Enables cURL request logging for this client. | ||||
|      * | ||||
|      * @param name to log messages with | ||||
|      * @param level at which to log messages (see {@link android.util.Log}) | ||||
|      */ | ||||
|     public void enableCurlLogging(String name, int level) { | ||||
|         if (name == null) { | ||||
|             throw new NullPointerException("name"); | ||||
|         } | ||||
|         if (level < Log.VERBOSE || level > Log.ASSERT) { | ||||
|             throw new IllegalArgumentException("Level is out of range [" | ||||
|                 + Log.VERBOSE + ".." + Log.ASSERT + "]"); | ||||
|         } | ||||
| 
 | ||||
|         curlConfiguration = new LoggingConfiguration(name, level); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Disables cURL logging for this client. | ||||
|      */ | ||||
|     public void disableCurlLogging() { | ||||
|         curlConfiguration = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Logs cURL commands equivalent to requests. | ||||
|      */ | ||||
|     private class CurlLogger implements HttpRequestInterceptor { | ||||
|         public void process(HttpRequest request, HttpContext context) | ||||
|                 throws HttpException, IOException { | ||||
|             LoggingConfiguration configuration = curlConfiguration; | ||||
|             if (configuration != null | ||||
|                     && configuration.isLoggable() | ||||
|                     && request instanceof HttpUriRequest) { | ||||
|                 // Never print auth token -- we used to check ro.secure=0 to | ||||
|                 // enable that, but can't do that in unbundled code. | ||||
|                 configuration.println(toCurl((HttpUriRequest) request, false)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Generates a cURL command equivalent to the given request. | ||||
|      */ | ||||
|     private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException { | ||||
|         StringBuilder builder = new StringBuilder(); | ||||
| 
 | ||||
|         builder.append("curl "); | ||||
| 
 | ||||
|         for (Header header: request.getAllHeaders()) { | ||||
|             if (!logAuthToken | ||||
|                     && (header.getName().equals("Authorization") || | ||||
|                         header.getName().equals("Cookie"))) { | ||||
|                 continue; | ||||
|             } | ||||
|             builder.append("--header \""); | ||||
|             builder.append(header.toString().trim()); | ||||
|             builder.append("\" "); | ||||
|         } | ||||
| 
 | ||||
|         URI uri = request.getURI(); | ||||
| 
 | ||||
|         // If this is a wrapped request, use the URI from the original | ||||
|         // request instead. getURI() on the wrapper seems to return a | ||||
|         // relative URI. We want an absolute URI. | ||||
|         if (request instanceof RequestWrapper) { | ||||
|             HttpRequest original = ((RequestWrapper) request).getOriginal(); | ||||
|             if (original instanceof HttpUriRequest) { | ||||
|                 uri = ((HttpUriRequest) original).getURI(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         builder.append("\""); | ||||
|         builder.append(uri); | ||||
|         builder.append("\""); | ||||
| 
 | ||||
|         if (request instanceof HttpEntityEnclosingRequest) { | ||||
|             HttpEntityEnclosingRequest entityRequest = | ||||
|                     (HttpEntityEnclosingRequest) request; | ||||
|             HttpEntity entity = entityRequest.getEntity(); | ||||
|             if (entity != null && entity.isRepeatable()) { | ||||
|                 if (entity.getContentLength() < 1024) { | ||||
|                     ByteArrayOutputStream stream = new ByteArrayOutputStream(); | ||||
|                     entity.writeTo(stream); | ||||
|                     String entityString = stream.toString(); | ||||
| 
 | ||||
|                     // TODO: Check the content type, too. | ||||
|                     builder.append(" --data-ascii \"") | ||||
|                             .append(entityString) | ||||
|                             .append("\""); | ||||
|                 } else { | ||||
|                     builder.append(" [TOO MUCH DATA TO INCLUDE]"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the date of the given HTTP date string. This method can identify | ||||
|      * and parse the date formats emitted by common HTTP servers, such as | ||||
|      * <a href="http://www.ietf.org/rfc/rfc0822.txt">RFC 822</a>, | ||||
|      * <a href="http://www.ietf.org/rfc/rfc0850.txt">RFC 850</a>, | ||||
|      * <a href="http://www.ietf.org/rfc/rfc1036.txt">RFC 1036</a>, | ||||
|      * <a href="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</a> and | ||||
|      * <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/asctime.html">ANSI | ||||
|      * C's asctime()</a>. | ||||
|      * | ||||
|      * @return the number of milliseconds since Jan. 1, 1970, midnight GMT. | ||||
|      * @throws IllegalArgumentException if {@code dateString} is not a date or | ||||
|      *     of an unsupported format. | ||||
|      */ | ||||
|     public static long parseDate(String dateString) { | ||||
|         return HttpDateTime.parse(dateString); | ||||
|     } | ||||
| } | ||||
|  | @ -32,81 +32,80 @@ import android.util.Log; | |||
|  * intent, it does not queue up batches of intents of the same type. | ||||
|  */ | ||||
| public abstract class CustomIntentService extends Service { | ||||
|     private String mName; | ||||
|     private boolean mRedelivery; | ||||
|     private volatile ServiceHandler mServiceHandler; | ||||
|     private volatile Looper mServiceLooper; | ||||
|     private static final String LOG_TAG = "CancellableIntentService"; | ||||
|     private static final int WHAT_MESSAGE = -10; | ||||
| 	private String mName; | ||||
| 	private boolean mRedelivery; | ||||
| 	private volatile ServiceHandler mServiceHandler; | ||||
| 	private volatile Looper mServiceLooper; | ||||
| 	private static final String LOG_TAG = "CustomIntentService"; | ||||
| 	private static final int WHAT_MESSAGE = -10; | ||||
| 
 | ||||
|     public CustomIntentService(String paramString) { | ||||
|         this.mName = paramString; | ||||
|     } | ||||
| 	public CustomIntentService(String paramString) { | ||||
| 		this.mName = paramString; | ||||
| 	} | ||||
| 
 | ||||
|     @Override | ||||
|     public IBinder onBind(Intent paramIntent) { | ||||
|         return null; | ||||
|     } | ||||
| 	@Override | ||||
| 	public IBinder onBind(Intent paramIntent) { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate() { | ||||
|         super.onCreate(); | ||||
|         HandlerThread localHandlerThread = new HandlerThread("IntentService[" | ||||
|                 + this.mName + "]"); | ||||
|         localHandlerThread.start(); | ||||
|         this.mServiceLooper = localHandlerThread.getLooper(); | ||||
|         this.mServiceHandler = new ServiceHandler(this.mServiceLooper); | ||||
|     } | ||||
| 	@Override | ||||
| 	public void onCreate() { | ||||
| 		super.onCreate(); | ||||
| 		HandlerThread localHandlerThread = new HandlerThread("IntentService[" + this.mName + "]"); | ||||
| 		localHandlerThread.start(); | ||||
| 		this.mServiceLooper = localHandlerThread.getLooper(); | ||||
| 		this.mServiceHandler = new ServiceHandler(this.mServiceLooper); | ||||
| 	} | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         Thread localThread = this.mServiceLooper.getThread(); | ||||
|         if ((localThread != null) && (localThread.isAlive())) { | ||||
|             localThread.interrupt(); | ||||
|         } | ||||
|         this.mServiceLooper.quit(); | ||||
|         Log.d(LOG_TAG, "onDestroy"); | ||||
|     } | ||||
| 	@Override | ||||
| 	public void onDestroy() { | ||||
| 		Thread localThread = this.mServiceLooper.getThread(); | ||||
| 		if ((localThread != null) && (localThread.isAlive())) { | ||||
| 			localThread.interrupt(); | ||||
| 		} | ||||
| 		this.mServiceLooper.quit(); | ||||
| 		Log.d(LOG_TAG, "onDestroy"); | ||||
| 	} | ||||
| 
 | ||||
|     protected abstract void onHandleIntent(Intent paramIntent); | ||||
| 	protected abstract void onHandleIntent(Intent paramIntent); | ||||
| 
 | ||||
|     protected abstract boolean shouldStop(); | ||||
| 	protected abstract boolean shouldStop(); | ||||
| 
 | ||||
|     @Override | ||||
|     public void onStart(Intent paramIntent, int startId) { | ||||
|         if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) { | ||||
|             Message localMessage = this.mServiceHandler.obtainMessage(); | ||||
|             localMessage.arg1 = startId; | ||||
|             localMessage.obj = paramIntent; | ||||
|             localMessage.what = WHAT_MESSAGE; | ||||
|             this.mServiceHandler.sendMessage(localMessage); | ||||
|         } | ||||
|     } | ||||
| 	@Override | ||||
| 	public void onStart(Intent paramIntent, int startId) { | ||||
| 		if (!this.mServiceHandler.hasMessages(WHAT_MESSAGE)) { | ||||
| 			Message localMessage = this.mServiceHandler.obtainMessage(); | ||||
| 			localMessage.arg1 = startId; | ||||
| 			localMessage.obj = paramIntent; | ||||
| 			localMessage.what = WHAT_MESSAGE; | ||||
| 			this.mServiceHandler.sendMessage(localMessage); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     @Override | ||||
|     public int onStartCommand(Intent paramIntent, int flags, int startId) { | ||||
|         onStart(paramIntent, startId); | ||||
|         return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; | ||||
|     } | ||||
| 	@Override | ||||
| 	public int onStartCommand(Intent paramIntent, int flags, int startId) { | ||||
| 		onStart(paramIntent, startId); | ||||
| 		return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; | ||||
| 	} | ||||
| 
 | ||||
|     public void setIntentRedelivery(boolean enabled) { | ||||
|         this.mRedelivery = enabled; | ||||
|     } | ||||
| 	public void setIntentRedelivery(boolean enabled) { | ||||
| 		this.mRedelivery = enabled; | ||||
| 	} | ||||
| 
 | ||||
|     private final class ServiceHandler extends Handler { | ||||
|         public ServiceHandler(Looper looper) { | ||||
|             super(looper); | ||||
|         } | ||||
| 	private final class ServiceHandler extends Handler { | ||||
| 		public ServiceHandler(Looper looper) { | ||||
| 			super(looper); | ||||
| 		} | ||||
| 
 | ||||
|         @Override | ||||
|         public void handleMessage(Message paramMessage) { | ||||
|             CustomIntentService.this | ||||
|                     .onHandleIntent((Intent) paramMessage.obj); | ||||
|             if (shouldStop()) { | ||||
|                 Log.d(LOG_TAG, "stopSelf"); | ||||
|                 CustomIntentService.this.stopSelf(paramMessage.arg1); | ||||
|                 Log.d(LOG_TAG, "afterStopSelf"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 		@Override | ||||
| 		public void handleMessage(Message paramMessage) { | ||||
| 			CustomIntentService.this | ||||
| 					.onHandleIntent((Intent)paramMessage.obj); | ||||
| 			if (shouldStop()) { | ||||
| 				Log.d(LOG_TAG, "stopSelf"); | ||||
| 				CustomIntentService.this.stopSelf(paramMessage.arg1); | ||||
| 				Log.d(LOG_TAG, "afterStopSelf"); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,30 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2012 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.expansion.downloader.impl; | ||||
| 
 | ||||
| /** | ||||
|  * Uses the class-loader model to utilize the updated notification builders in | ||||
|  * Honeycomb while maintaining a compatible version for older devices. | ||||
|  */ | ||||
| public class CustomNotificationFactory { | ||||
|     static public DownloadNotification.ICustomNotification createCustomNotification() { | ||||
|         if (android.os.Build.VERSION.SDK_INT > 13) | ||||
|             return new V14CustomNotification(); | ||||
|         else | ||||
|             throw new RuntimeException(); | ||||
|     } | ||||
| } | ||||
|  | @ -25,68 +25,68 @@ import android.util.Log; | |||
|  * Representation of information about an individual download from the database. | ||||
|  */ | ||||
| public class DownloadInfo { | ||||
|     public String mUri; | ||||
|     public final int mIndex; | ||||
|     public final String mFileName; | ||||
|     public String mETag; | ||||
|     public long mTotalBytes; | ||||
|     public long mCurrentBytes; | ||||
|     public long mLastMod; | ||||
|     public int mStatus; | ||||
|     public int mControl; | ||||
|     public int mNumFailed; | ||||
|     public int mRetryAfter; | ||||
|     public int mRedirectCount; | ||||
| 	public String mUri; | ||||
| 	public final int mIndex; | ||||
| 	public final String mFileName; | ||||
| 	public String mETag; | ||||
| 	public long mTotalBytes; | ||||
| 	public long mCurrentBytes; | ||||
| 	public long mLastMod; | ||||
| 	public int mStatus; | ||||
| 	public int mControl; | ||||
| 	public int mNumFailed; | ||||
| 	public int mRetryAfter; | ||||
| 	public int mRedirectCount; | ||||
| 
 | ||||
|     boolean mInitialized; | ||||
| 	boolean mInitialized; | ||||
| 
 | ||||
|     public int mFuzz; | ||||
| 	public int mFuzz; | ||||
| 
 | ||||
|     public DownloadInfo(int index, String fileName, String pkg) { | ||||
|         mFuzz = Helpers.sRandom.nextInt(1001); | ||||
|         mFileName = fileName; | ||||
|         mIndex = index; | ||||
|     } | ||||
| 	public DownloadInfo(int index, String fileName, String pkg) { | ||||
| 		mFuzz = Helpers.sRandom.nextInt(1001); | ||||
| 		mFileName = fileName; | ||||
| 		mIndex = index; | ||||
| 	} | ||||
| 
 | ||||
|     public void resetDownload() { | ||||
|         mCurrentBytes = 0; | ||||
|         mETag = ""; | ||||
|         mLastMod = 0; | ||||
|         mStatus = 0; | ||||
|         mControl = 0; | ||||
|         mNumFailed = 0; | ||||
|         mRetryAfter = 0; | ||||
|         mRedirectCount = 0; | ||||
|     } | ||||
| 	public void resetDownload() { | ||||
| 		mCurrentBytes = 0; | ||||
| 		mETag = ""; | ||||
| 		mLastMod = 0; | ||||
| 		mStatus = 0; | ||||
| 		mControl = 0; | ||||
| 		mNumFailed = 0; | ||||
| 		mRetryAfter = 0; | ||||
| 		mRedirectCount = 0; | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Returns the time when a download should be restarted. | ||||
|      */ | ||||
|     public long restartTime(long now) { | ||||
|         if (mNumFailed == 0) { | ||||
|             return now; | ||||
|         } | ||||
|         if (mRetryAfter > 0) { | ||||
|             return mLastMod + mRetryAfter; | ||||
|         } | ||||
|         return mLastMod + | ||||
|                 Constants.RETRY_FIRST_DELAY * | ||||
|                 (1000 + mFuzz) * (1 << (mNumFailed - 1)); | ||||
|     } | ||||
| 	public long restartTime(long now) { | ||||
| 		if (mNumFailed == 0) { | ||||
| 			return now; | ||||
| 		} | ||||
| 		if (mRetryAfter > 0) { | ||||
| 			return mLastMod + mRetryAfter; | ||||
| 		} | ||||
| 		return mLastMod + | ||||
| 				Constants.RETRY_FIRST_DELAY * | ||||
| 						(1000 + mFuzz) * (1 << (mNumFailed - 1)); | ||||
| 	} | ||||
| 
 | ||||
|     public void logVerboseInfo() { | ||||
|         Log.v(Constants.TAG, "Service adding new entry"); | ||||
|         Log.v(Constants.TAG, "FILENAME: " + mFileName); | ||||
|         Log.v(Constants.TAG, "URI     : " + mUri); | ||||
|         Log.v(Constants.TAG, "FILENAME: " + mFileName); | ||||
|         Log.v(Constants.TAG, "CONTROL : " + mControl); | ||||
|         Log.v(Constants.TAG, "STATUS  : " + mStatus); | ||||
|         Log.v(Constants.TAG, "FAILED_C: " + mNumFailed); | ||||
|         Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter); | ||||
|         Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount); | ||||
|         Log.v(Constants.TAG, "LAST_MOD: " + mLastMod); | ||||
|         Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes); | ||||
|         Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes); | ||||
|         Log.v(Constants.TAG, "ETAG    : " + mETag); | ||||
|     } | ||||
| 	public void logVerboseInfo() { | ||||
| 		Log.v(Constants.TAG, "Service adding new entry"); | ||||
| 		Log.v(Constants.TAG, "FILENAME: " + mFileName); | ||||
| 		Log.v(Constants.TAG, "URI     : " + mUri); | ||||
| 		Log.v(Constants.TAG, "FILENAME: " + mFileName); | ||||
| 		Log.v(Constants.TAG, "CONTROL : " + mControl); | ||||
| 		Log.v(Constants.TAG, "STATUS  : " + mStatus); | ||||
| 		Log.v(Constants.TAG, "FAILED_C: " + mNumFailed); | ||||
| 		Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter); | ||||
| 		Log.v(Constants.TAG, "REDIRECT: " + mRedirectCount); | ||||
| 		Log.v(Constants.TAG, "LAST_MOD: " + mLastMod); | ||||
| 		Log.v(Constants.TAG, "TOTAL   : " + mTotalBytes); | ||||
| 		Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes); | ||||
| 		Log.v(Constants.TAG, "ETAG    : " + mETag); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -22,11 +22,12 @@ import com.google.android.vending.expansion.downloader.DownloaderClientMarshalle | |||
| import com.google.android.vending.expansion.downloader.Helpers; | ||||
| import com.google.android.vending.expansion.downloader.IDownloaderClient; | ||||
| 
 | ||||
| import android.app.Notification; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| import android.content.Context; | ||||
| import android.os.Build; | ||||
| import android.os.Messenger; | ||||
| import android.support.v4.app.NotificationCompat; | ||||
| 
 | ||||
| /** | ||||
|  * This class handles displaying the notification associated with the download | ||||
|  | @ -41,190 +42,183 @@ import android.os.Messenger; | |||
|  */ | ||||
| public class DownloadNotification implements IDownloaderClient { | ||||
| 
 | ||||
|     private int mState; | ||||
|     private final Context mContext; | ||||
|     private final NotificationManager mNotificationManager; | ||||
|     private String mCurrentTitle; | ||||
| 	private int mState; | ||||
| 	private final Context mContext; | ||||
| 	private final NotificationManager mNotificationManager; | ||||
| 	private CharSequence mCurrentTitle; | ||||
| 
 | ||||
|     private IDownloaderClient mClientProxy; | ||||
|     final ICustomNotification mCustomNotification; | ||||
|     private Notification.Builder mNotificationBuilder; | ||||
|     private Notification.Builder mCurrentNotificationBuilder; | ||||
|     private CharSequence mLabel; | ||||
|     private String mCurrentText; | ||||
|     private PendingIntent mContentIntent; | ||||
|     private DownloadProgressInfo mProgressInfo; | ||||
| 	private IDownloaderClient mClientProxy; | ||||
| 	private NotificationCompat.Builder mActiveDownloadBuilder; | ||||
| 	private NotificationCompat.Builder mBuilder; | ||||
| 	private NotificationCompat.Builder mCurrentBuilder; | ||||
| 	private CharSequence mLabel; | ||||
| 	private String mCurrentText; | ||||
| 	private DownloadProgressInfo mProgressInfo; | ||||
| 	private PendingIntent mContentIntent; | ||||
| 
 | ||||
|     static final String LOGTAG = "DownloadNotification"; | ||||
|     static final int NOTIFICATION_ID = LOGTAG.hashCode(); | ||||
| 	static final String LOGTAG = "DownloadNotification"; | ||||
| 	static final int NOTIFICATION_ID = LOGTAG.hashCode(); | ||||
| 
 | ||||
|     public PendingIntent getClientIntent() { | ||||
|         return mContentIntent; | ||||
|     } | ||||
| 	public PendingIntent getClientIntent() { | ||||
| 		return mContentIntent; | ||||
| 	} | ||||
| 
 | ||||
|     public void setClientIntent(PendingIntent mClientIntent) { | ||||
|         this.mContentIntent = mClientIntent; | ||||
|     } | ||||
| 	public void setClientIntent(PendingIntent clientIntent) { | ||||
| 		this.mBuilder.setContentIntent(clientIntent); | ||||
| 		this.mActiveDownloadBuilder.setContentIntent(clientIntent); | ||||
| 		this.mContentIntent = clientIntent; | ||||
| 	} | ||||
| 
 | ||||
|     public void resendState() { | ||||
|         if (null != mClientProxy) { | ||||
|             mClientProxy.onDownloadStateChanged(mState); | ||||
|         } | ||||
|     } | ||||
| 	public void resendState() { | ||||
| 		if (null != mClientProxy) { | ||||
| 			mClientProxy.onDownloadStateChanged(mState); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDownloadStateChanged(int newState) { | ||||
|         if (null != mClientProxy) { | ||||
|             mClientProxy.onDownloadStateChanged(newState); | ||||
|         } | ||||
|         if (newState != mState) { | ||||
|             mState = newState; | ||||
|             if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) { | ||||
|                 return; | ||||
|             } | ||||
|             int stringDownloadID; | ||||
|             int iconResource; | ||||
|             boolean ongoingEvent; | ||||
| 	@Override | ||||
| 	public void onDownloadStateChanged(int newState) { | ||||
| 		if (null != mClientProxy) { | ||||
| 			mClientProxy.onDownloadStateChanged(newState); | ||||
| 		} | ||||
| 		if (newState != mState) { | ||||
| 			mState = newState; | ||||
| 			if (newState == IDownloaderClient.STATE_IDLE || null == mContentIntent) { | ||||
| 				return; | ||||
| 			} | ||||
| 			int stringDownloadID; | ||||
| 			int iconResource; | ||||
| 			boolean ongoingEvent; | ||||
| 
 | ||||
|             // get the new title string and paused text | ||||
|             switch (newState) { | ||||
|                 case 0: | ||||
|                     iconResource = android.R.drawable.stat_sys_warning; | ||||
|                     stringDownloadID = R.string.state_unknown; | ||||
|                     ongoingEvent = false; | ||||
|                     break; | ||||
| 			// get the new title string and paused text | ||||
| 			switch (newState) { | ||||
| 				case 0: | ||||
| 					iconResource = android.R.drawable.stat_sys_warning; | ||||
| 					stringDownloadID = R.string.state_unknown; | ||||
| 					ongoingEvent = false; | ||||
| 					break; | ||||
| 
 | ||||
|                 case IDownloaderClient.STATE_DOWNLOADING: | ||||
|                     iconResource = android.R.drawable.stat_sys_download; | ||||
|                     stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
|                     ongoingEvent = true; | ||||
|                     break; | ||||
| 				case IDownloaderClient.STATE_DOWNLOADING: | ||||
| 					iconResource = android.R.drawable.stat_sys_download; | ||||
| 					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
| 					ongoingEvent = true; | ||||
| 					break; | ||||
| 
 | ||||
|                 case IDownloaderClient.STATE_FETCHING_URL: | ||||
|                 case IDownloaderClient.STATE_CONNECTING: | ||||
|                     iconResource = android.R.drawable.stat_sys_download_done; | ||||
|                     stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
|                     ongoingEvent = true; | ||||
|                     break; | ||||
| 				case IDownloaderClient.STATE_FETCHING_URL: | ||||
| 				case IDownloaderClient.STATE_CONNECTING: | ||||
| 					iconResource = android.R.drawable.stat_sys_download_done; | ||||
| 					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
| 					ongoingEvent = true; | ||||
| 					break; | ||||
| 
 | ||||
|                 case IDownloaderClient.STATE_COMPLETED: | ||||
|                 case IDownloaderClient.STATE_PAUSED_BY_REQUEST: | ||||
|                     iconResource = android.R.drawable.stat_sys_download_done; | ||||
|                     stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
|                     ongoingEvent = false; | ||||
|                     break; | ||||
| 				case IDownloaderClient.STATE_COMPLETED: | ||||
| 				case IDownloaderClient.STATE_PAUSED_BY_REQUEST: | ||||
| 					iconResource = android.R.drawable.stat_sys_download_done; | ||||
| 					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
| 					ongoingEvent = false; | ||||
| 					break; | ||||
| 
 | ||||
|                 case IDownloaderClient.STATE_FAILED: | ||||
|                 case IDownloaderClient.STATE_FAILED_CANCELED: | ||||
|                 case IDownloaderClient.STATE_FAILED_FETCHING_URL: | ||||
|                 case IDownloaderClient.STATE_FAILED_SDCARD_FULL: | ||||
|                 case IDownloaderClient.STATE_FAILED_UNLICENSED: | ||||
|                     iconResource = android.R.drawable.stat_sys_warning; | ||||
|                     stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
|                     ongoingEvent = false; | ||||
|                     break; | ||||
| 				case IDownloaderClient.STATE_FAILED: | ||||
| 				case IDownloaderClient.STATE_FAILED_CANCELED: | ||||
| 				case IDownloaderClient.STATE_FAILED_FETCHING_URL: | ||||
| 				case IDownloaderClient.STATE_FAILED_SDCARD_FULL: | ||||
| 				case IDownloaderClient.STATE_FAILED_UNLICENSED: | ||||
| 					iconResource = android.R.drawable.stat_sys_warning; | ||||
| 					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
| 					ongoingEvent = false; | ||||
| 					break; | ||||
| 
 | ||||
|                 default: | ||||
|                     iconResource = android.R.drawable.stat_sys_warning; | ||||
|                     stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
|                     ongoingEvent = true; | ||||
|                     break; | ||||
|             } | ||||
|             mCurrentText = mContext.getString(stringDownloadID); | ||||
|             mCurrentTitle = mLabel.toString(); | ||||
|             mCurrentNotificationBuilder.setTicker(mLabel + ": " + mCurrentText); | ||||
|             mCurrentNotificationBuilder.setSmallIcon(iconResource); | ||||
|             mCurrentNotificationBuilder.setContentTitle(mCurrentTitle); | ||||
|             mCurrentNotificationBuilder.setContentText(mCurrentText); | ||||
|             mCurrentNotificationBuilder.setContentIntent(mContentIntent); | ||||
|             mCurrentNotificationBuilder.setOngoing(ongoingEvent); | ||||
|             mCurrentNotificationBuilder.setAutoCancel(!ongoingEvent); | ||||
|             mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotificationBuilder.build()); | ||||
|         } | ||||
|     } | ||||
| 				default: | ||||
| 					iconResource = android.R.drawable.stat_sys_warning; | ||||
| 					stringDownloadID = Helpers.getDownloaderStringResourceIDFromState(newState); | ||||
| 					ongoingEvent = true; | ||||
| 					break; | ||||
| 			} | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDownloadProgress(DownloadProgressInfo progress) { | ||||
|         mProgressInfo = progress; | ||||
|         if (null != mClientProxy) { | ||||
|             mClientProxy.onDownloadProgress(progress); | ||||
|         } | ||||
|         if (progress.mOverallTotal <= 0) { | ||||
|             // we just show the text | ||||
|             mNotificationBuilder.setTicker(mCurrentTitle); | ||||
|             mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_download); | ||||
|             mNotificationBuilder.setContentTitle(mCurrentTitle); | ||||
|             mNotificationBuilder.setContentText(mCurrentText); | ||||
|             mNotificationBuilder.setContentIntent(mContentIntent); | ||||
|             mCurrentNotificationBuilder = mNotificationBuilder; | ||||
|         } else { | ||||
|             mCustomNotification.setCurrentBytes(progress.mOverallProgress); | ||||
|             mCustomNotification.setTotalBytes(progress.mOverallTotal); | ||||
|             mCustomNotification.setIcon(android.R.drawable.stat_sys_download); | ||||
|             mCustomNotification.setPendingIntent(mContentIntent); | ||||
|             mCustomNotification.setTicker(mLabel + ": " + mCurrentText); | ||||
|             mCustomNotification.setTitle(mLabel); | ||||
|             mCustomNotification.setTimeRemaining(progress.mTimeRemaining); | ||||
|             mCurrentNotificationBuilder = mCustomNotification.updateNotification(mContext); | ||||
|         } | ||||
|         mNotificationManager.notify(NOTIFICATION_ID, mCurrentNotificationBuilder.build()); | ||||
|     } | ||||
| 			mCurrentText = mContext.getString(stringDownloadID); | ||||
| 			mCurrentTitle = mLabel; | ||||
| 			mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText); | ||||
| 			mCurrentBuilder.setSmallIcon(iconResource); | ||||
| 			mCurrentBuilder.setContentTitle(mCurrentTitle); | ||||
| 			mCurrentBuilder.setContentText(mCurrentText); | ||||
| 			if (ongoingEvent) { | ||||
| 				mCurrentBuilder.setOngoing(true); | ||||
| 			} else { | ||||
| 				mCurrentBuilder.setOngoing(false); | ||||
| 				mCurrentBuilder.setAutoCancel(true); | ||||
| 			} | ||||
| 			mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     public interface ICustomNotification { | ||||
|         void setTitle(CharSequence title); | ||||
| 	@Override | ||||
| 	public void onDownloadProgress(DownloadProgressInfo progress) { | ||||
| 		mProgressInfo = progress; | ||||
| 		if (null != mClientProxy) { | ||||
| 			mClientProxy.onDownloadProgress(progress); | ||||
| 		} | ||||
| 		if (progress.mOverallTotal <= 0) { | ||||
| 			// we just show the text | ||||
| 			mBuilder.setTicker(mCurrentTitle); | ||||
| 			mBuilder.setSmallIcon(android.R.drawable.stat_sys_download); | ||||
| 			mBuilder.setContentTitle(mCurrentTitle); | ||||
| 			mBuilder.setContentText(mCurrentText); | ||||
| 			mCurrentBuilder = mBuilder; | ||||
| 		} else { | ||||
| 			mActiveDownloadBuilder.setProgress((int)progress.mOverallTotal, (int)progress.mOverallProgress, false); | ||||
| 			mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); | ||||
| 			mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download); | ||||
| 			mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText); | ||||
| 			mActiveDownloadBuilder.setContentTitle(mLabel); | ||||
| 			mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification, | ||||
| 					Helpers.getTimeRemaining(progress.mTimeRemaining))); | ||||
| 			mCurrentBuilder = mActiveDownloadBuilder; | ||||
| 		} | ||||
| 		mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build()); | ||||
| 	} | ||||
| 
 | ||||
|         void setTicker(CharSequence ticker); | ||||
| 
 | ||||
|         void setPendingIntent(PendingIntent mContentIntent); | ||||
| 
 | ||||
|         void setTotalBytes(long totalBytes); | ||||
| 
 | ||||
|         void setCurrentBytes(long currentBytes); | ||||
| 
 | ||||
|         void setIcon(int iconResource); | ||||
| 
 | ||||
|         void setTimeRemaining(long timeRemaining); | ||||
| 
 | ||||
|         Notification.Builder updateNotification(Context c); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Called in response to onClientUpdated. Creates a new proxy and notifies | ||||
|      * it of the current state. | ||||
|      *  | ||||
|      * | ||||
|      * @param msg the client Messenger to notify | ||||
|      */ | ||||
|     public void setMessenger(Messenger msg) { | ||||
|         mClientProxy = DownloaderClientMarshaller.CreateProxy(msg); | ||||
|         if (null != mProgressInfo) { | ||||
|             mClientProxy.onDownloadProgress(mProgressInfo); | ||||
|         } | ||||
|         if (mState != -1) { | ||||
|             mClientProxy.onDownloadStateChanged(mState); | ||||
|         } | ||||
|     } | ||||
| 	public void setMessenger(Messenger msg) { | ||||
| 		mClientProxy = DownloaderClientMarshaller.CreateProxy(msg); | ||||
| 		if (null != mProgressInfo) { | ||||
| 			mClientProxy.onDownloadProgress(mProgressInfo); | ||||
| 		} | ||||
| 		if (mState != -1) { | ||||
| 			mClientProxy.onDownloadStateChanged(mState); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Constructor | ||||
|      *  | ||||
|      * | ||||
|      * @param ctx The context to use to obtain access to the Notification | ||||
|      *            Service | ||||
|      */ | ||||
|     DownloadNotification(Context ctx, CharSequence applicationLabel) { | ||||
|         mState = -1; | ||||
|         mContext = ctx; | ||||
|         mLabel = applicationLabel; | ||||
|         mNotificationManager = (NotificationManager) | ||||
|                 mContext.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|         mCustomNotification = CustomNotificationFactory | ||||
|                 .createCustomNotification(); | ||||
|         mNotificationBuilder = new Notification.Builder(ctx); | ||||
|         mCurrentNotificationBuilder = mNotificationBuilder; | ||||
| 	DownloadNotification(Context ctx, CharSequence applicationLabel) { | ||||
| 		mState = -1; | ||||
| 		mContext = ctx; | ||||
| 		mLabel = applicationLabel; | ||||
| 		mNotificationManager = (NotificationManager) | ||||
| 									   mContext.getSystemService(Context.NOTIFICATION_SERVICE); | ||||
| 		mActiveDownloadBuilder = new NotificationCompat.Builder(ctx); | ||||
| 		mBuilder = new NotificationCompat.Builder(ctx); | ||||
| 
 | ||||
|     } | ||||
| 		// Set Notification category and priorities to something that makes sense for a long | ||||
| 		// lived background task. | ||||
| 		mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW); | ||||
| 		mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); | ||||
| 
 | ||||
|     @Override | ||||
|     public void onServiceConnected(Messenger m) { | ||||
|     } | ||||
| 		mBuilder.setPriority(NotificationCompat.PRIORITY_LOW); | ||||
| 		mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS); | ||||
| 
 | ||||
| 		mCurrentBuilder = mBuilder; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void onServiceConnected(Messenger m) { | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -27,484 +27,443 @@ import android.provider.BaseColumns; | |||
| import android.util.Log; | ||||
| 
 | ||||
| public class DownloadsDB { | ||||
|     private static final String DATABASE_NAME = "DownloadsDB"; | ||||
|     private static final int DATABASE_VERSION = 7; | ||||
|     public static final String LOG_TAG = DownloadsDB.class.getName(); | ||||
|     final SQLiteOpenHelper mHelper; | ||||
|     SQLiteStatement mGetDownloadByIndex; | ||||
|     SQLiteStatement mUpdateCurrentBytes; | ||||
|     private static DownloadsDB mDownloadsDB; | ||||
|     long mMetadataRowID = -1; | ||||
|     int mVersionCode = -1; | ||||
|     int mStatus = -1; | ||||
|     int mFlags; | ||||
| 	private static final String DATABASE_NAME = "DownloadsDB"; | ||||
| 	private static final int DATABASE_VERSION = 7; | ||||
| 	public static final String LOG_TAG = DownloadsDB.class.getName(); | ||||
| 	final SQLiteOpenHelper mHelper; | ||||
| 	SQLiteStatement mGetDownloadByIndex; | ||||
| 	SQLiteStatement mUpdateCurrentBytes; | ||||
| 	private static DownloadsDB mDownloadsDB; | ||||
| 	long mMetadataRowID = -1; | ||||
| 	int mVersionCode = -1; | ||||
| 	int mStatus = -1; | ||||
| 	int mFlags; | ||||
| 
 | ||||
|     static public synchronized DownloadsDB getDB(Context paramContext) { | ||||
|         if (null == mDownloadsDB) { | ||||
|             return new DownloadsDB(paramContext); | ||||
|         } | ||||
|         return mDownloadsDB; | ||||
|     } | ||||
| 	static public synchronized DownloadsDB getDB(Context paramContext) { | ||||
| 		if (null == mDownloadsDB) { | ||||
| 			return new DownloadsDB(paramContext); | ||||
| 		} | ||||
| 		return mDownloadsDB; | ||||
| 	} | ||||
| 
 | ||||
|     private SQLiteStatement getDownloadByIndexStatement() { | ||||
|         if (null == mGetDownloadByIndex) { | ||||
|             mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement( | ||||
|                     "SELECT " + BaseColumns._ID + " FROM " | ||||
|                             + DownloadColumns.TABLE_NAME + " WHERE " | ||||
|                             + DownloadColumns.INDEX + " = ?"); | ||||
|         } | ||||
|         return mGetDownloadByIndex; | ||||
|     } | ||||
| 	private SQLiteStatement getDownloadByIndexStatement() { | ||||
| 		if (null == mGetDownloadByIndex) { | ||||
| 			mGetDownloadByIndex = mHelper.getReadableDatabase().compileStatement( | ||||
| 					"SELECT " + BaseColumns._ID + " FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.INDEX + " = ?"); | ||||
| 		} | ||||
| 		return mGetDownloadByIndex; | ||||
| 	} | ||||
| 
 | ||||
|     private SQLiteStatement getUpdateCurrentBytesStatement() { | ||||
|         if (null == mUpdateCurrentBytes) { | ||||
|             mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement( | ||||
|                     "UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES | ||||
|                             + " = ?" + | ||||
|                             " WHERE " + DownloadColumns.INDEX + " = ?"); | ||||
|         } | ||||
|         return mUpdateCurrentBytes; | ||||
|     } | ||||
| 	private SQLiteStatement getUpdateCurrentBytesStatement() { | ||||
| 		if (null == mUpdateCurrentBytes) { | ||||
| 			mUpdateCurrentBytes = mHelper.getReadableDatabase().compileStatement( | ||||
| 					"UPDATE " + DownloadColumns.TABLE_NAME + " SET " + DownloadColumns.CURRENTBYTES + " = ?" | ||||
| 					+ | ||||
| 					" WHERE " + DownloadColumns.INDEX + " = ?"); | ||||
| 		} | ||||
| 		return mUpdateCurrentBytes; | ||||
| 	} | ||||
| 
 | ||||
|     private DownloadsDB(Context paramContext) { | ||||
|         this.mHelper = new DownloadsContentDBHelper(paramContext); | ||||
|         final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
|         // Query for the version code, the row ID of the metadata (for future | ||||
|         // updating) the status and the flags | ||||
|         Cursor cur = sqldb.rawQuery("SELECT " + | ||||
|                 MetadataColumns.APKVERSION + "," + | ||||
|                 BaseColumns._ID + "," + | ||||
|                 MetadataColumns.DOWNLOAD_STATUS + "," + | ||||
|                 MetadataColumns.FLAGS + | ||||
|                 " FROM " | ||||
|                 + MetadataColumns.TABLE_NAME + " LIMIT 1", null); | ||||
|         if (null != cur && cur.moveToFirst()) { | ||||
|             mVersionCode = cur.getInt(0); | ||||
|             mMetadataRowID = cur.getLong(1); | ||||
|             mStatus = cur.getInt(2); | ||||
|             mFlags = cur.getInt(3); | ||||
|             cur.close(); | ||||
|         } | ||||
|         mDownloadsDB = this; | ||||
|     } | ||||
| 	private DownloadsDB(Context paramContext) { | ||||
| 		this.mHelper = new DownloadsContentDBHelper(paramContext); | ||||
| 		final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
| 		// Query for the version code, the row ID of the metadata (for future | ||||
| 		// updating) the status and the flags | ||||
| 		Cursor cur = sqldb.rawQuery("SELECT " + | ||||
| 											MetadataColumns.APKVERSION + "," + | ||||
| 											BaseColumns._ID + "," + | ||||
| 											MetadataColumns.DOWNLOAD_STATUS + "," + | ||||
| 											MetadataColumns.FLAGS + | ||||
| 											" FROM " + MetadataColumns.TABLE_NAME + " LIMIT 1", | ||||
| 				null); | ||||
| 		if (null != cur && cur.moveToFirst()) { | ||||
| 			mVersionCode = cur.getInt(0); | ||||
| 			mMetadataRowID = cur.getLong(1); | ||||
| 			mStatus = cur.getInt(2); | ||||
| 			mFlags = cur.getInt(3); | ||||
| 			cur.close(); | ||||
| 		} | ||||
| 		mDownloadsDB = this; | ||||
| 	} | ||||
| 
 | ||||
|     protected DownloadInfo getDownloadInfoByFileName(String fileName) { | ||||
|         final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
|         Cursor itemcur = null; | ||||
|         try { | ||||
|             itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, | ||||
|                     DownloadColumns.FILENAME + " = ?", | ||||
|                     new String[] { | ||||
|                         fileName | ||||
|                     }, null, null, null); | ||||
|             if (null != itemcur && itemcur.moveToFirst()) { | ||||
|                 return getDownloadInfoFromCursor(itemcur); | ||||
|             } | ||||
|         } finally { | ||||
|             if (null != itemcur) | ||||
|                 itemcur.close(); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 	protected DownloadInfo getDownloadInfoByFileName(String fileName) { | ||||
| 		final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
| 		Cursor itemcur = null; | ||||
| 		try { | ||||
| 			itemcur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, | ||||
| 					DownloadColumns.FILENAME + " = ?", | ||||
| 					new String[] { | ||||
| 							fileName }, | ||||
| 					null, null, null); | ||||
| 			if (null != itemcur && itemcur.moveToFirst()) { | ||||
| 				return getDownloadInfoFromCursor(itemcur); | ||||
| 			} | ||||
| 		} finally { | ||||
| 			if (null != itemcur) | ||||
| 				itemcur.close(); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
|     public long getIDForDownloadInfo(final DownloadInfo di) { | ||||
|         return getIDByIndex(di.mIndex); | ||||
|     } | ||||
| 	public long getIDForDownloadInfo(final DownloadInfo di) { | ||||
| 		return getIDByIndex(di.mIndex); | ||||
| 	} | ||||
| 
 | ||||
|     public long getIDByIndex(int index) { | ||||
|         SQLiteStatement downloadByIndex = getDownloadByIndexStatement(); | ||||
|         downloadByIndex.clearBindings(); | ||||
|         downloadByIndex.bindLong(1, index); | ||||
|         try { | ||||
|             return downloadByIndex.simpleQueryForLong(); | ||||
|         } catch (SQLiteDoneException e) { | ||||
|             return -1; | ||||
|         } | ||||
|     } | ||||
| 	public long getIDByIndex(int index) { | ||||
| 		SQLiteStatement downloadByIndex = getDownloadByIndexStatement(); | ||||
| 		downloadByIndex.clearBindings(); | ||||
| 		downloadByIndex.bindLong(1, index); | ||||
| 		try { | ||||
| 			return downloadByIndex.simpleQueryForLong(); | ||||
| 		} catch (SQLiteDoneException e) { | ||||
| 			return -1; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     public void updateDownloadCurrentBytes(final DownloadInfo di) { | ||||
|         SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement(); | ||||
|         downloadCurrentBytes.clearBindings(); | ||||
|         downloadCurrentBytes.bindLong(1, di.mCurrentBytes); | ||||
|         downloadCurrentBytes.bindLong(2, di.mIndex); | ||||
|         downloadCurrentBytes.execute(); | ||||
|     } | ||||
| 	public void updateDownloadCurrentBytes(final DownloadInfo di) { | ||||
| 		SQLiteStatement downloadCurrentBytes = getUpdateCurrentBytesStatement(); | ||||
| 		downloadCurrentBytes.clearBindings(); | ||||
| 		downloadCurrentBytes.bindLong(1, di.mCurrentBytes); | ||||
| 		downloadCurrentBytes.bindLong(2, di.mIndex); | ||||
| 		downloadCurrentBytes.execute(); | ||||
| 	} | ||||
| 
 | ||||
|     public void close() { | ||||
|         this.mHelper.close(); | ||||
|     } | ||||
| 	public void close() { | ||||
| 		this.mHelper.close(); | ||||
| 	} | ||||
| 
 | ||||
|     protected static class DownloadsContentDBHelper extends SQLiteOpenHelper { | ||||
|         DownloadsContentDBHelper(Context paramContext) { | ||||
|             super(paramContext, DATABASE_NAME, null, DATABASE_VERSION); | ||||
|         } | ||||
| 	protected static class DownloadsContentDBHelper extends SQLiteOpenHelper { | ||||
| 		DownloadsContentDBHelper(Context paramContext) { | ||||
| 			super(paramContext, DATABASE_NAME, null, DATABASE_VERSION); | ||||
| 		} | ||||
| 
 | ||||
|         private String createTableQueryFromArray(String paramString, | ||||
|                 String[][] paramArrayOfString) { | ||||
|             StringBuilder localStringBuilder = new StringBuilder(); | ||||
|             localStringBuilder.append("CREATE TABLE "); | ||||
|             localStringBuilder.append(paramString); | ||||
|             localStringBuilder.append(" ("); | ||||
|             int i = paramArrayOfString.length; | ||||
|             for (int j = 0;; j++) { | ||||
|                 if (j >= i) { | ||||
|                     localStringBuilder | ||||
|                             .setLength(localStringBuilder.length() - 1); | ||||
|                     localStringBuilder.append(");"); | ||||
|                     return localStringBuilder.toString(); | ||||
|                 } | ||||
|                 String[] arrayOfString = paramArrayOfString[j]; | ||||
|                 localStringBuilder.append(' '); | ||||
|                 localStringBuilder.append(arrayOfString[0]); | ||||
|                 localStringBuilder.append(' '); | ||||
|                 localStringBuilder.append(arrayOfString[1]); | ||||
|                 localStringBuilder.append(','); | ||||
|             } | ||||
|         } | ||||
| 		private String createTableQueryFromArray(String paramString, | ||||
| 				String[][] paramArrayOfString) { | ||||
| 			StringBuilder localStringBuilder = new StringBuilder(); | ||||
| 			localStringBuilder.append("CREATE TABLE "); | ||||
| 			localStringBuilder.append(paramString); | ||||
| 			localStringBuilder.append(" ("); | ||||
| 			int i = paramArrayOfString.length; | ||||
| 			for (int j = 0;; j++) { | ||||
| 				if (j >= i) { | ||||
| 					localStringBuilder | ||||
| 							.setLength(localStringBuilder.length() - 1); | ||||
| 					localStringBuilder.append(");"); | ||||
| 					return localStringBuilder.toString(); | ||||
| 				} | ||||
| 				String[] arrayOfString = paramArrayOfString[j]; | ||||
| 				localStringBuilder.append(' '); | ||||
| 				localStringBuilder.append(arrayOfString[0]); | ||||
| 				localStringBuilder.append(' '); | ||||
| 				localStringBuilder.append(arrayOfString[1]); | ||||
| 				localStringBuilder.append(','); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         /** | ||||
| 		/** | ||||
|          * These two arrays must match and have the same order. For every Schema | ||||
|          * there must be a corresponding table name. | ||||
|          */ | ||||
|         static final private String[][][] sSchemas = { | ||||
|                 DownloadColumns.SCHEMA, MetadataColumns.SCHEMA | ||||
|         }; | ||||
| 		static final private String[][][] sSchemas = { | ||||
| 			DownloadColumns.SCHEMA, MetadataColumns.SCHEMA | ||||
| 		}; | ||||
| 
 | ||||
|         static final private String[] sTables = { | ||||
|                 DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME | ||||
|         }; | ||||
| 		static final private String[] sTables = { | ||||
| 			DownloadColumns.TABLE_NAME, MetadataColumns.TABLE_NAME | ||||
| 		}; | ||||
| 
 | ||||
|         /** | ||||
| 		/** | ||||
|          * Goes through all of the tables in sTables and drops each table if it | ||||
|          * exists. Altered to no longer make use of reflection. | ||||
|          */ | ||||
|         private void dropTables(SQLiteDatabase paramSQLiteDatabase) { | ||||
|             for (String table : sTables) { | ||||
|                 try { | ||||
|                     paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table); | ||||
|                 } catch (Exception localException) { | ||||
|                     localException.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 		private void dropTables(SQLiteDatabase paramSQLiteDatabase) { | ||||
| 			for (String table : sTables) { | ||||
| 				try { | ||||
| 					paramSQLiteDatabase.execSQL("DROP TABLE IF EXISTS " + table); | ||||
| 				} catch (Exception localException) { | ||||
| 					localException.printStackTrace(); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         /** | ||||
| 		/** | ||||
|          * Goes through all of the tables in sTables and creates a database with | ||||
|          * the corresponding schema described in sSchemas. Altered to no longer | ||||
|          * make use of reflection. | ||||
|          */ | ||||
|         public void onCreate(SQLiteDatabase paramSQLiteDatabase) { | ||||
|             int numSchemas = sSchemas.length; | ||||
|             for (int i = 0; i < numSchemas; i++) { | ||||
|                 try { | ||||
|                     String[][] schema = (String[][]) sSchemas[i]; | ||||
|                     paramSQLiteDatabase.execSQL(createTableQueryFromArray( | ||||
|                             sTables[i], schema)); | ||||
|                 } catch (Exception localException) { | ||||
|                     while (true) | ||||
|                         localException.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 		public void onCreate(SQLiteDatabase paramSQLiteDatabase) { | ||||
| 			int numSchemas = sSchemas.length; | ||||
| 			for (int i = 0; i < numSchemas; i++) { | ||||
| 				try { | ||||
| 					String[][] schema = (String[][])sSchemas[i]; | ||||
| 					paramSQLiteDatabase.execSQL(createTableQueryFromArray( | ||||
| 							sTables[i], schema)); | ||||
| 				} catch (Exception localException) { | ||||
| 					while (true) | ||||
| 						localException.printStackTrace(); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         public void onUpgrade(SQLiteDatabase paramSQLiteDatabase, | ||||
|                 int paramInt1, int paramInt2) { | ||||
|             Log.w(DownloadsContentDBHelper.class.getName(), | ||||
|                     "Upgrading database from version " + paramInt1 + " to " | ||||
|                             + paramInt2 + ", which will destroy all old data"); | ||||
|             dropTables(paramSQLiteDatabase); | ||||
|             onCreate(paramSQLiteDatabase); | ||||
|         } | ||||
|     } | ||||
| 		public void onUpgrade(SQLiteDatabase paramSQLiteDatabase, | ||||
| 				int paramInt1, int paramInt2) { | ||||
| 			Log.w(DownloadsContentDBHelper.class.getName(), | ||||
| 					"Upgrading database from version " + paramInt1 + " to " + paramInt2 + ", which will destroy all old data"); | ||||
| 			dropTables(paramSQLiteDatabase); | ||||
| 			onCreate(paramSQLiteDatabase); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     public static class MetadataColumns implements BaseColumns { | ||||
|         public static final String APKVERSION = "APKVERSION"; | ||||
|         public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS"; | ||||
|         public static final String FLAGS = "DOWNLOADFLAGS"; | ||||
| 	public static class MetadataColumns implements BaseColumns { | ||||
| 		public static final String APKVERSION = "APKVERSION"; | ||||
| 		public static final String DOWNLOAD_STATUS = "DOWNLOADSTATUS"; | ||||
| 		public static final String FLAGS = "DOWNLOADFLAGS"; | ||||
| 
 | ||||
|         public static final String[][] SCHEMA = { | ||||
|                 { | ||||
|                         BaseColumns._ID, "INTEGER PRIMARY KEY" | ||||
|                 }, | ||||
|                 { | ||||
|                         APKVERSION, "INTEGER" | ||||
|                 }, { | ||||
|                         DOWNLOAD_STATUS, "INTEGER" | ||||
|                 }, | ||||
|                 { | ||||
|                         FLAGS, "INTEGER" | ||||
|                 } | ||||
|         }; | ||||
|         public static final String TABLE_NAME = "MetadataColumns"; | ||||
|         public static final String _ID = "MetadataColumns._id"; | ||||
|     } | ||||
| 		public static final String[][] SCHEMA = { | ||||
| 			{ BaseColumns._ID, "INTEGER PRIMARY KEY" }, | ||||
| 			{ APKVERSION, "INTEGER" }, { DOWNLOAD_STATUS, "INTEGER" }, | ||||
| 			{ FLAGS, "INTEGER" } | ||||
| 		}; | ||||
| 		public static final String TABLE_NAME = "MetadataColumns"; | ||||
| 		public static final String _ID = "MetadataColumns._id"; | ||||
| 	} | ||||
| 
 | ||||
|     public static class DownloadColumns implements BaseColumns { | ||||
|         public static final String INDEX = "FILEIDX"; | ||||
|         public static final String URI = "URI"; | ||||
|         public static final String FILENAME = "FN"; | ||||
|         public static final String ETAG = "ETAG"; | ||||
| 	public static class DownloadColumns implements BaseColumns { | ||||
| 		public static final String INDEX = "FILEIDX"; | ||||
| 		public static final String URI = "URI"; | ||||
| 		public static final String FILENAME = "FN"; | ||||
| 		public static final String ETAG = "ETAG"; | ||||
| 
 | ||||
|         public static final String TOTALBYTES = "TOTALBYTES"; | ||||
|         public static final String CURRENTBYTES = "CURRENTBYTES"; | ||||
|         public static final String LASTMOD = "LASTMOD"; | ||||
| 		public static final String TOTALBYTES = "TOTALBYTES"; | ||||
| 		public static final String CURRENTBYTES = "CURRENTBYTES"; | ||||
| 		public static final String LASTMOD = "LASTMOD"; | ||||
| 
 | ||||
|         public static final String STATUS = "STATUS"; | ||||
|         public static final String CONTROL = "CONTROL"; | ||||
|         public static final String NUM_FAILED = "FAILCOUNT"; | ||||
|         public static final String RETRY_AFTER = "RETRYAFTER"; | ||||
|         public static final String REDIRECT_COUNT = "REDIRECTCOUNT"; | ||||
| 		public static final String STATUS = "STATUS"; | ||||
| 		public static final String CONTROL = "CONTROL"; | ||||
| 		public static final String NUM_FAILED = "FAILCOUNT"; | ||||
| 		public static final String RETRY_AFTER = "RETRYAFTER"; | ||||
| 		public static final String REDIRECT_COUNT = "REDIRECTCOUNT"; | ||||
| 
 | ||||
|         public static final String[][] SCHEMA = { | ||||
|                 { | ||||
|                         BaseColumns._ID, "INTEGER PRIMARY KEY" | ||||
|                 }, | ||||
|                 { | ||||
|                         INDEX, "INTEGER UNIQUE" | ||||
|                 }, { | ||||
|                         URI, "TEXT" | ||||
|                 }, | ||||
|                 { | ||||
|                         FILENAME, "TEXT UNIQUE" | ||||
|                 }, { | ||||
|                         ETAG, "TEXT" | ||||
|                 }, | ||||
|                 { | ||||
|                         TOTALBYTES, "INTEGER" | ||||
|                 }, { | ||||
|                         CURRENTBYTES, "INTEGER" | ||||
|                 }, | ||||
|                 { | ||||
|                         LASTMOD, "INTEGER" | ||||
|                 }, { | ||||
|                         STATUS, "INTEGER" | ||||
|                 }, | ||||
|                 { | ||||
|                         CONTROL, "INTEGER" | ||||
|                 }, { | ||||
|                         NUM_FAILED, "INTEGER" | ||||
|                 }, | ||||
|                 { | ||||
|                         RETRY_AFTER, "INTEGER" | ||||
|                 }, { | ||||
|                         REDIRECT_COUNT, "INTEGER" | ||||
|                 } | ||||
|         }; | ||||
|         public static final String TABLE_NAME = "DownloadColumns"; | ||||
|         public static final String _ID = "DownloadColumns._id"; | ||||
|     } | ||||
| 		public static final String[][] SCHEMA = { | ||||
| 			{ BaseColumns._ID, "INTEGER PRIMARY KEY" }, | ||||
| 			{ INDEX, "INTEGER UNIQUE" }, { URI, "TEXT" }, | ||||
| 			{ FILENAME, "TEXT UNIQUE" }, { ETAG, "TEXT" }, | ||||
| 			{ TOTALBYTES, "INTEGER" }, { CURRENTBYTES, "INTEGER" }, | ||||
| 			{ LASTMOD, "INTEGER" }, { STATUS, "INTEGER" }, | ||||
| 			{ CONTROL, "INTEGER" }, { NUM_FAILED, "INTEGER" }, | ||||
| 			{ RETRY_AFTER, "INTEGER" }, { REDIRECT_COUNT, "INTEGER" } | ||||
| 		}; | ||||
| 		public static final String TABLE_NAME = "DownloadColumns"; | ||||
| 		public static final String _ID = "DownloadColumns._id"; | ||||
| 	} | ||||
| 
 | ||||
|     private static final String[] DC_PROJECTION = { | ||||
|             DownloadColumns.FILENAME, | ||||
|             DownloadColumns.URI, DownloadColumns.ETAG, | ||||
|             DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES, | ||||
|             DownloadColumns.LASTMOD, DownloadColumns.STATUS, | ||||
|             DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED, | ||||
|             DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT, | ||||
|             DownloadColumns.INDEX | ||||
|     }; | ||||
| 	private static final String[] DC_PROJECTION = { | ||||
| 		DownloadColumns.FILENAME, | ||||
| 		DownloadColumns.URI, DownloadColumns.ETAG, | ||||
| 		DownloadColumns.TOTALBYTES, DownloadColumns.CURRENTBYTES, | ||||
| 		DownloadColumns.LASTMOD, DownloadColumns.STATUS, | ||||
| 		DownloadColumns.CONTROL, DownloadColumns.NUM_FAILED, | ||||
| 		DownloadColumns.RETRY_AFTER, DownloadColumns.REDIRECT_COUNT, | ||||
| 		DownloadColumns.INDEX | ||||
| 	}; | ||||
| 
 | ||||
|     private static final int FILENAME_IDX = 0; | ||||
|     private static final int URI_IDX = 1; | ||||
|     private static final int ETAG_IDX = 2; | ||||
|     private static final int TOTALBYTES_IDX = 3; | ||||
|     private static final int CURRENTBYTES_IDX = 4; | ||||
|     private static final int LASTMOD_IDX = 5; | ||||
|     private static final int STATUS_IDX = 6; | ||||
|     private static final int CONTROL_IDX = 7; | ||||
|     private static final int NUM_FAILED_IDX = 8; | ||||
|     private static final int RETRY_AFTER_IDX = 9; | ||||
|     private static final int REDIRECT_COUNT_IDX = 10; | ||||
|     private static final int INDEX_IDX = 11; | ||||
| 	private static final int FILENAME_IDX = 0; | ||||
| 	private static final int URI_IDX = 1; | ||||
| 	private static final int ETAG_IDX = 2; | ||||
| 	private static final int TOTALBYTES_IDX = 3; | ||||
| 	private static final int CURRENTBYTES_IDX = 4; | ||||
| 	private static final int LASTMOD_IDX = 5; | ||||
| 	private static final int STATUS_IDX = 6; | ||||
| 	private static final int CONTROL_IDX = 7; | ||||
| 	private static final int NUM_FAILED_IDX = 8; | ||||
| 	private static final int RETRY_AFTER_IDX = 9; | ||||
| 	private static final int REDIRECT_COUNT_IDX = 10; | ||||
| 	private static final int INDEX_IDX = 11; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * This function will add a new file to the database if it does not exist. | ||||
|      *  | ||||
|      * | ||||
|      * @param di DownloadInfo that we wish to store | ||||
|      * @return the row id of the record to be updated/inserted, or -1 | ||||
|      */ | ||||
|     public boolean updateDownload(DownloadInfo di) { | ||||
|         ContentValues cv = new ContentValues(); | ||||
|         cv.put(DownloadColumns.INDEX, di.mIndex); | ||||
|         cv.put(DownloadColumns.FILENAME, di.mFileName); | ||||
|         cv.put(DownloadColumns.URI, di.mUri); | ||||
|         cv.put(DownloadColumns.ETAG, di.mETag); | ||||
|         cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes); | ||||
|         cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes); | ||||
|         cv.put(DownloadColumns.LASTMOD, di.mLastMod); | ||||
|         cv.put(DownloadColumns.STATUS, di.mStatus); | ||||
|         cv.put(DownloadColumns.CONTROL, di.mControl); | ||||
|         cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed); | ||||
|         cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter); | ||||
|         cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount); | ||||
|         return updateDownload(di, cv); | ||||
|     } | ||||
| 	public boolean updateDownload(DownloadInfo di) { | ||||
| 		ContentValues cv = new ContentValues(); | ||||
| 		cv.put(DownloadColumns.INDEX, di.mIndex); | ||||
| 		cv.put(DownloadColumns.FILENAME, di.mFileName); | ||||
| 		cv.put(DownloadColumns.URI, di.mUri); | ||||
| 		cv.put(DownloadColumns.ETAG, di.mETag); | ||||
| 		cv.put(DownloadColumns.TOTALBYTES, di.mTotalBytes); | ||||
| 		cv.put(DownloadColumns.CURRENTBYTES, di.mCurrentBytes); | ||||
| 		cv.put(DownloadColumns.LASTMOD, di.mLastMod); | ||||
| 		cv.put(DownloadColumns.STATUS, di.mStatus); | ||||
| 		cv.put(DownloadColumns.CONTROL, di.mControl); | ||||
| 		cv.put(DownloadColumns.NUM_FAILED, di.mNumFailed); | ||||
| 		cv.put(DownloadColumns.RETRY_AFTER, di.mRetryAfter); | ||||
| 		cv.put(DownloadColumns.REDIRECT_COUNT, di.mRedirectCount); | ||||
| 		return updateDownload(di, cv); | ||||
| 	} | ||||
| 
 | ||||
|     public boolean updateDownload(DownloadInfo di, ContentValues cv) { | ||||
|         long id = di == null ? -1 : getIDForDownloadInfo(di); | ||||
|         try { | ||||
|             final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); | ||||
|             if (id != -1) { | ||||
|                 if (1 != sqldb.update(DownloadColumns.TABLE_NAME, | ||||
|                         cv, DownloadColumns._ID + " = " + id, null)) { | ||||
|                     return false; | ||||
|                 } | ||||
|             } else { | ||||
|                 return -1 != sqldb.insert(DownloadColumns.TABLE_NAME, | ||||
|                         DownloadColumns.URI, cv); | ||||
|             } | ||||
|         } catch (android.database.sqlite.SQLiteException ex) { | ||||
|             ex.printStackTrace(); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 	public boolean updateDownload(DownloadInfo di, ContentValues cv) { | ||||
| 		long id = di == null ? -1 : getIDForDownloadInfo(di); | ||||
| 		try { | ||||
| 			final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); | ||||
| 			if (id != -1) { | ||||
| 				if (1 != sqldb.update(DownloadColumns.TABLE_NAME, | ||||
| 								 cv, DownloadColumns._ID + " = " + id, null)) { | ||||
| 					return false; | ||||
| 				} | ||||
| 			} else { | ||||
| 				return -1 != sqldb.insert(DownloadColumns.TABLE_NAME, | ||||
| 									 DownloadColumns.URI, cv); | ||||
| 			} | ||||
| 		} catch (android.database.sqlite.SQLiteException ex) { | ||||
| 			ex.printStackTrace(); | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
|     public int getLastCheckedVersionCode() { | ||||
|         return mVersionCode; | ||||
|     } | ||||
| 	public int getLastCheckedVersionCode() { | ||||
| 		return mVersionCode; | ||||
| 	} | ||||
| 
 | ||||
|     public boolean isDownloadRequired() { | ||||
|         final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
|         Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " | ||||
|                 + DownloadColumns.TABLE_NAME + " WHERE " | ||||
|                 + DownloadColumns.STATUS + " <> 0", null); | ||||
|         try { | ||||
|             if (null != cur && cur.moveToFirst()) { | ||||
|                 return 0 == cur.getInt(0); | ||||
|             } | ||||
|         } finally { | ||||
|             if (null != cur) | ||||
|                 cur.close(); | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 	public boolean isDownloadRequired() { | ||||
| 		final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
| 		Cursor cur = sqldb.rawQuery("SELECT Count(*) FROM " + DownloadColumns.TABLE_NAME + " WHERE " + DownloadColumns.STATUS + " <> 0", null); | ||||
| 		try { | ||||
| 			if (null != cur && cur.moveToFirst()) { | ||||
| 				return 0 == cur.getInt(0); | ||||
| 			} | ||||
| 		} finally { | ||||
| 			if (null != cur) | ||||
| 				cur.close(); | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
|     public int getFlags() { | ||||
|         return mFlags; | ||||
|     } | ||||
| 	public int getFlags() { | ||||
| 		return mFlags; | ||||
| 	} | ||||
| 
 | ||||
|     public boolean updateFlags(int flags) { | ||||
|         if (mFlags != flags) { | ||||
|             ContentValues cv = new ContentValues(); | ||||
|             cv.put(MetadataColumns.FLAGS, flags); | ||||
|             if (updateMetadata(cv)) { | ||||
|                 mFlags = flags; | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return false; | ||||
|             } | ||||
|         } else { | ||||
|             return true; | ||||
|         } | ||||
|     }; | ||||
| 	public boolean updateFlags(int flags) { | ||||
| 		if (mFlags != flags) { | ||||
| 			ContentValues cv = new ContentValues(); | ||||
| 			cv.put(MetadataColumns.FLAGS, flags); | ||||
| 			if (updateMetadata(cv)) { | ||||
| 				mFlags = flags; | ||||
| 				return true; | ||||
| 			} else { | ||||
| 				return false; | ||||
| 			} | ||||
| 		} else { | ||||
| 			return true; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
|     public boolean updateStatus(int status) { | ||||
|         if (mStatus != status) { | ||||
|             ContentValues cv = new ContentValues(); | ||||
|             cv.put(MetadataColumns.DOWNLOAD_STATUS, status); | ||||
|             if (updateMetadata(cv)) { | ||||
|                 mStatus = status; | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return false; | ||||
|             } | ||||
|         } else { | ||||
|             return true; | ||||
|         } | ||||
|     }; | ||||
| 	public boolean updateStatus(int status) { | ||||
| 		if (mStatus != status) { | ||||
| 			ContentValues cv = new ContentValues(); | ||||
| 			cv.put(MetadataColumns.DOWNLOAD_STATUS, status); | ||||
| 			if (updateMetadata(cv)) { | ||||
| 				mStatus = status; | ||||
| 				return true; | ||||
| 			} else { | ||||
| 				return false; | ||||
| 			} | ||||
| 		} else { | ||||
| 			return true; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
|     public boolean updateMetadata(ContentValues cv) { | ||||
|         final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); | ||||
|         if (-1 == this.mMetadataRowID) { | ||||
|             long newID = sqldb.insert(MetadataColumns.TABLE_NAME, | ||||
|                     MetadataColumns.APKVERSION, cv); | ||||
|             if (-1 == newID) | ||||
|                 return false; | ||||
|             mMetadataRowID = newID; | ||||
|         } else { | ||||
|             if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv, | ||||
|                     BaseColumns._ID + " = " + mMetadataRowID, null)) | ||||
|                 return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 	public boolean updateMetadata(ContentValues cv) { | ||||
| 		final SQLiteDatabase sqldb = mHelper.getWritableDatabase(); | ||||
| 		if (-1 == this.mMetadataRowID) { | ||||
| 			long newID = sqldb.insert(MetadataColumns.TABLE_NAME, | ||||
| 					MetadataColumns.APKVERSION, cv); | ||||
| 			if (-1 == newID) | ||||
| 				return false; | ||||
| 			mMetadataRowID = newID; | ||||
| 		} else { | ||||
| 			if (0 == sqldb.update(MetadataColumns.TABLE_NAME, cv, | ||||
| 							 BaseColumns._ID + " = " + mMetadataRowID, null)) | ||||
| 				return false; | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
|     public boolean updateMetadata(int apkVersion, int downloadStatus) { | ||||
|         ContentValues cv = new ContentValues(); | ||||
|         cv.put(MetadataColumns.APKVERSION, apkVersion); | ||||
|         cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus); | ||||
|         if (updateMetadata(cv)) { | ||||
|             mVersionCode = apkVersion; | ||||
|             mStatus = downloadStatus; | ||||
|             return true; | ||||
|         } else { | ||||
|             return false; | ||||
|         } | ||||
|     }; | ||||
| 	public boolean updateMetadata(int apkVersion, int downloadStatus) { | ||||
| 		ContentValues cv = new ContentValues(); | ||||
| 		cv.put(MetadataColumns.APKVERSION, apkVersion); | ||||
| 		cv.put(MetadataColumns.DOWNLOAD_STATUS, downloadStatus); | ||||
| 		if (updateMetadata(cv)) { | ||||
| 			mVersionCode = apkVersion; | ||||
| 			mStatus = downloadStatus; | ||||
| 			return true; | ||||
| 		} else { | ||||
| 			return false; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
|     public boolean updateFromDb(DownloadInfo di) { | ||||
|         final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
|         Cursor cur = null; | ||||
|         try { | ||||
|             cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, | ||||
|                     DownloadColumns.FILENAME + "= ?", | ||||
|                     new String[] { | ||||
|                         di.mFileName | ||||
|                     }, null, null, null); | ||||
|             if (null != cur && cur.moveToFirst()) { | ||||
|                 setDownloadInfoFromCursor(di, cur); | ||||
|                 return true; | ||||
|             } | ||||
|             return false; | ||||
|         } finally { | ||||
|             if (null != cur) { | ||||
|                 cur.close(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 	public boolean updateFromDb(DownloadInfo di) { | ||||
| 		final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
| 		Cursor cur = null; | ||||
| 		try { | ||||
| 			cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, | ||||
| 					DownloadColumns.FILENAME + "= ?", | ||||
| 					new String[] { | ||||
| 							di.mFileName }, | ||||
| 					null, null, null); | ||||
| 			if (null != cur && cur.moveToFirst()) { | ||||
| 				setDownloadInfoFromCursor(di, cur); | ||||
| 				return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 		} finally { | ||||
| 			if (null != cur) { | ||||
| 				cur.close(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) { | ||||
|         di.mUri = cur.getString(URI_IDX); | ||||
|         di.mETag = cur.getString(ETAG_IDX); | ||||
|         di.mTotalBytes = cur.getLong(TOTALBYTES_IDX); | ||||
|         di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX); | ||||
|         di.mLastMod = cur.getLong(LASTMOD_IDX); | ||||
|         di.mStatus = cur.getInt(STATUS_IDX); | ||||
|         di.mControl = cur.getInt(CONTROL_IDX); | ||||
|         di.mNumFailed = cur.getInt(NUM_FAILED_IDX); | ||||
|         di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX); | ||||
|         di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX); | ||||
|     } | ||||
| 	public void setDownloadInfoFromCursor(DownloadInfo di, Cursor cur) { | ||||
| 		di.mUri = cur.getString(URI_IDX); | ||||
| 		di.mETag = cur.getString(ETAG_IDX); | ||||
| 		di.mTotalBytes = cur.getLong(TOTALBYTES_IDX); | ||||
| 		di.mCurrentBytes = cur.getLong(CURRENTBYTES_IDX); | ||||
| 		di.mLastMod = cur.getLong(LASTMOD_IDX); | ||||
| 		di.mStatus = cur.getInt(STATUS_IDX); | ||||
| 		di.mControl = cur.getInt(CONTROL_IDX); | ||||
| 		di.mNumFailed = cur.getInt(NUM_FAILED_IDX); | ||||
| 		di.mRetryAfter = cur.getInt(RETRY_AFTER_IDX); | ||||
| 		di.mRedirectCount = cur.getInt(REDIRECT_COUNT_IDX); | ||||
| 	} | ||||
| 
 | ||||
|     public DownloadInfo getDownloadInfoFromCursor(Cursor cur) { | ||||
|         DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX), | ||||
|                 cur.getString(FILENAME_IDX), this.getClass().getPackage() | ||||
|                         .getName()); | ||||
|         setDownloadInfoFromCursor(di, cur); | ||||
|         return di; | ||||
|     } | ||||
| 
 | ||||
|     public DownloadInfo[] getDownloads() { | ||||
|         final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
|         Cursor cur = null; | ||||
|         try { | ||||
|             cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null, | ||||
|                     null, null, null, null); | ||||
|             if (null != cur && cur.moveToFirst()) { | ||||
|                 DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()]; | ||||
|                 int idx = 0; | ||||
|                 do { | ||||
|                     DownloadInfo di = getDownloadInfoFromCursor(cur); | ||||
|                     retInfos[idx++] = di; | ||||
|                 } while (cur.moveToNext()); | ||||
|                 return retInfos; | ||||
|             } | ||||
|             return null; | ||||
|         } finally { | ||||
|             if (null != cur) { | ||||
|                 cur.close(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 	public DownloadInfo getDownloadInfoFromCursor(Cursor cur) { | ||||
| 		DownloadInfo di = new DownloadInfo(cur.getInt(INDEX_IDX), | ||||
| 				cur.getString(FILENAME_IDX), this.getClass().getPackage().getName()); | ||||
| 		setDownloadInfoFromCursor(di, cur); | ||||
| 		return di; | ||||
| 	} | ||||
| 
 | ||||
| 	public DownloadInfo[] getDownloads() { | ||||
| 		final SQLiteDatabase sqldb = mHelper.getReadableDatabase(); | ||||
| 		Cursor cur = null; | ||||
| 		try { | ||||
| 			cur = sqldb.query(DownloadColumns.TABLE_NAME, DC_PROJECTION, null, | ||||
| 					null, null, null, null); | ||||
| 			if (null != cur && cur.moveToFirst()) { | ||||
| 				DownloadInfo[] retInfos = new DownloadInfo[cur.getCount()]; | ||||
| 				int idx = 0; | ||||
| 				do { | ||||
| 					DownloadInfo di = getDownloadInfoFromCursor(cur); | ||||
| 					retInfos[idx++] = di; | ||||
| 				} while (cur.moveToNext()); | ||||
| 				return retInfos; | ||||
| 			} | ||||
| 			return null; | ||||
| 		} finally { | ||||
| 			if (null != cur) { | ||||
| 				cur.close(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ import java.util.regex.Pattern; | |||
|  */ | ||||
| public final class HttpDateTime { | ||||
| 
 | ||||
|     /* | ||||
| 	/* | ||||
|      * Regular expression for parsing HTTP-date. Wdy, DD Mon YYYY HH:MM:SS GMT | ||||
|      * RFC 822, updated by RFC 1123 Weekday, DD-Mon-YY HH:MM:SS GMT RFC 850, | ||||
|      * obsoleted by RFC 1036 Wdy Mon DD HH:MM:SS YYYY ANSI C's asctime() format | ||||
|  | @ -37,164 +37,155 @@ public final class HttpDateTime { | |||
|      * (SP)D HH:MM:SS YYYY Wdy Mon DD HH:MM:SS YYYY GMT HH can be H if the first | ||||
|      * digit is zero. Mon can be the full name of the month. | ||||
|      */ | ||||
|     private static final String HTTP_DATE_RFC_REGEXP = | ||||
|             "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" | ||||
|                     + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; | ||||
| 	private static final String HTTP_DATE_RFC_REGEXP = | ||||
| 			"([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" | ||||
| 			+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; | ||||
| 
 | ||||
|     private static final String HTTP_DATE_ANSIC_REGEXP = | ||||
|             "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" | ||||
|                     + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; | ||||
| 	private static final String HTTP_DATE_ANSIC_REGEXP = | ||||
| 			"[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" | ||||
| 			+ "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * The compiled version of the HTTP-date regular expressions. | ||||
|      */ | ||||
|     private static final Pattern HTTP_DATE_RFC_PATTERN = | ||||
|             Pattern.compile(HTTP_DATE_RFC_REGEXP); | ||||
|     private static final Pattern HTTP_DATE_ANSIC_PATTERN = | ||||
|             Pattern.compile(HTTP_DATE_ANSIC_REGEXP); | ||||
| 	private static final Pattern HTTP_DATE_RFC_PATTERN = | ||||
| 			Pattern.compile(HTTP_DATE_RFC_REGEXP); | ||||
| 	private static final Pattern HTTP_DATE_ANSIC_PATTERN = | ||||
| 			Pattern.compile(HTTP_DATE_ANSIC_REGEXP); | ||||
| 
 | ||||
|     private static class TimeOfDay { | ||||
|         TimeOfDay(int h, int m, int s) { | ||||
|             this.hour = h; | ||||
|             this.minute = m; | ||||
|             this.second = s; | ||||
|         } | ||||
| 	private static class TimeOfDay { | ||||
| 		TimeOfDay(int h, int m, int s) { | ||||
| 			this.hour = h; | ||||
| 			this.minute = m; | ||||
| 			this.second = s; | ||||
| 		} | ||||
| 
 | ||||
|         int hour; | ||||
|         int minute; | ||||
|         int second; | ||||
|     } | ||||
| 		int hour; | ||||
| 		int minute; | ||||
| 		int second; | ||||
| 	} | ||||
| 
 | ||||
|     public static long parse(String timeString) | ||||
|             throws IllegalArgumentException { | ||||
| 	public static long parse(String timeString) | ||||
| 			throws IllegalArgumentException { | ||||
| 
 | ||||
|         int date = 1; | ||||
|         int month = Calendar.JANUARY; | ||||
|         int year = 1970; | ||||
|         TimeOfDay timeOfDay; | ||||
| 		int date = 1; | ||||
| 		int month = Calendar.JANUARY; | ||||
| 		int year = 1970; | ||||
| 		TimeOfDay timeOfDay; | ||||
| 
 | ||||
|         Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); | ||||
|         if (rfcMatcher.find()) { | ||||
|             date = getDate(rfcMatcher.group(1)); | ||||
|             month = getMonth(rfcMatcher.group(2)); | ||||
|             year = getYear(rfcMatcher.group(3)); | ||||
|             timeOfDay = getTime(rfcMatcher.group(4)); | ||||
|         } else { | ||||
|             Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); | ||||
|             if (ansicMatcher.find()) { | ||||
|                 month = getMonth(ansicMatcher.group(1)); | ||||
|                 date = getDate(ansicMatcher.group(2)); | ||||
|                 timeOfDay = getTime(ansicMatcher.group(3)); | ||||
|                 year = getYear(ansicMatcher.group(4)); | ||||
|             } else { | ||||
|                 throw new IllegalArgumentException(); | ||||
|             } | ||||
|         } | ||||
| 		Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); | ||||
| 		if (rfcMatcher.find()) { | ||||
| 			date = getDate(rfcMatcher.group(1)); | ||||
| 			month = getMonth(rfcMatcher.group(2)); | ||||
| 			year = getYear(rfcMatcher.group(3)); | ||||
| 			timeOfDay = getTime(rfcMatcher.group(4)); | ||||
| 		} else { | ||||
| 			Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); | ||||
| 			if (ansicMatcher.find()) { | ||||
| 				month = getMonth(ansicMatcher.group(1)); | ||||
| 				date = getDate(ansicMatcher.group(2)); | ||||
| 				timeOfDay = getTime(ansicMatcher.group(3)); | ||||
| 				year = getYear(ansicMatcher.group(4)); | ||||
| 			} else { | ||||
| 				throw new IllegalArgumentException(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|         // FIXME: Y2038 BUG! | ||||
|         if (year >= 2038) { | ||||
|             year = 2038; | ||||
|             month = Calendar.JANUARY; | ||||
|             date = 1; | ||||
|         } | ||||
| 		// FIXME: Y2038 BUG! | ||||
| 		if (year >= 2038) { | ||||
| 			year = 2038; | ||||
| 			month = Calendar.JANUARY; | ||||
| 			date = 1; | ||||
| 		} | ||||
| 
 | ||||
|         Time time = new Time(Time.TIMEZONE_UTC); | ||||
|         time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, | ||||
|                 month, year); | ||||
|         return time.toMillis(false /* use isDst */); | ||||
|     } | ||||
| 		Time time = new Time(Time.TIMEZONE_UTC); | ||||
| 		time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, | ||||
| 				month, year); | ||||
| 		return time.toMillis(false /* use isDst */); | ||||
| 	} | ||||
| 
 | ||||
|     private static int getDate(String dateString) { | ||||
|         if (dateString.length() == 2) { | ||||
|             return (dateString.charAt(0) - '0') * 10 | ||||
|                     + (dateString.charAt(1) - '0'); | ||||
|         } else { | ||||
|             return (dateString.charAt(0) - '0'); | ||||
|         } | ||||
|     } | ||||
| 	private static int getDate(String dateString) { | ||||
| 		if (dateString.length() == 2) { | ||||
| 			return (dateString.charAt(0) - '0') * 10 + (dateString.charAt(1) - '0'); | ||||
| 		} else { | ||||
| 			return (dateString.charAt(0) - '0'); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     /* | ||||
| 	/* | ||||
|      * jan = 9 + 0 + 13 = 22 feb = 5 + 4 + 1 = 10 mar = 12 + 0 + 17 = 29 apr = 0 | ||||
|      * + 15 + 17 = 32 may = 12 + 0 + 24 = 36 jun = 9 + 20 + 13 = 42 jul = 9 + 20 | ||||
|      * + 11 = 40 aug = 0 + 20 + 6 = 26 sep = 18 + 4 + 15 = 37 oct = 14 + 2 + 19 | ||||
|      * = 35 nov = 13 + 14 + 21 = 48 dec = 3 + 4 + 2 = 9 | ||||
|      */ | ||||
|     private static int getMonth(String monthString) { | ||||
|         int hash = Character.toLowerCase(monthString.charAt(0)) + | ||||
|                 Character.toLowerCase(monthString.charAt(1)) + | ||||
|                 Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; | ||||
|         switch (hash) { | ||||
|             case 22: | ||||
|                 return Calendar.JANUARY; | ||||
|             case 10: | ||||
|                 return Calendar.FEBRUARY; | ||||
|             case 29: | ||||
|                 return Calendar.MARCH; | ||||
|             case 32: | ||||
|                 return Calendar.APRIL; | ||||
|             case 36: | ||||
|                 return Calendar.MAY; | ||||
|             case 42: | ||||
|                 return Calendar.JUNE; | ||||
|             case 40: | ||||
|                 return Calendar.JULY; | ||||
|             case 26: | ||||
|                 return Calendar.AUGUST; | ||||
|             case 37: | ||||
|                 return Calendar.SEPTEMBER; | ||||
|             case 35: | ||||
|                 return Calendar.OCTOBER; | ||||
|             case 48: | ||||
|                 return Calendar.NOVEMBER; | ||||
|             case 9: | ||||
|                 return Calendar.DECEMBER; | ||||
|             default: | ||||
|                 throw new IllegalArgumentException(); | ||||
|         } | ||||
|     } | ||||
| 	private static int getMonth(String monthString) { | ||||
| 		int hash = Character.toLowerCase(monthString.charAt(0)) + | ||||
| 				   Character.toLowerCase(monthString.charAt(1)) + | ||||
| 				   Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; | ||||
| 		switch (hash) { | ||||
| 			case 22: | ||||
| 				return Calendar.JANUARY; | ||||
| 			case 10: | ||||
| 				return Calendar.FEBRUARY; | ||||
| 			case 29: | ||||
| 				return Calendar.MARCH; | ||||
| 			case 32: | ||||
| 				return Calendar.APRIL; | ||||
| 			case 36: | ||||
| 				return Calendar.MAY; | ||||
| 			case 42: | ||||
| 				return Calendar.JUNE; | ||||
| 			case 40: | ||||
| 				return Calendar.JULY; | ||||
| 			case 26: | ||||
| 				return Calendar.AUGUST; | ||||
| 			case 37: | ||||
| 				return Calendar.SEPTEMBER; | ||||
| 			case 35: | ||||
| 				return Calendar.OCTOBER; | ||||
| 			case 48: | ||||
| 				return Calendar.NOVEMBER; | ||||
| 			case 9: | ||||
| 				return Calendar.DECEMBER; | ||||
| 			default: | ||||
| 				throw new IllegalArgumentException(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     private static int getYear(String yearString) { | ||||
|         if (yearString.length() == 2) { | ||||
|             int year = (yearString.charAt(0) - '0') * 10 | ||||
|                     + (yearString.charAt(1) - '0'); | ||||
|             if (year >= 70) { | ||||
|                 return year + 1900; | ||||
|             } else { | ||||
|                 return year + 2000; | ||||
|             } | ||||
|         } else if (yearString.length() == 3) { | ||||
|             // According to RFC 2822, three digit years should be added to 1900. | ||||
|             int year = (yearString.charAt(0) - '0') * 100 | ||||
|                     + (yearString.charAt(1) - '0') * 10 | ||||
|                     + (yearString.charAt(2) - '0'); | ||||
|             return year + 1900; | ||||
|         } else if (yearString.length() == 4) { | ||||
|             return (yearString.charAt(0) - '0') * 1000 | ||||
|                     + (yearString.charAt(1) - '0') * 100 | ||||
|                     + (yearString.charAt(2) - '0') * 10 | ||||
|                     + (yearString.charAt(3) - '0'); | ||||
|         } else { | ||||
|             return 1970; | ||||
|         } | ||||
|     } | ||||
| 	private static int getYear(String yearString) { | ||||
| 		if (yearString.length() == 2) { | ||||
| 			int year = (yearString.charAt(0) - '0') * 10 + (yearString.charAt(1) - '0'); | ||||
| 			if (year >= 70) { | ||||
| 				return year + 1900; | ||||
| 			} else { | ||||
| 				return year + 2000; | ||||
| 			} | ||||
| 		} else if (yearString.length() == 3) { | ||||
| 			// According to RFC 2822, three digit years should be added to 1900. | ||||
| 			int year = (yearString.charAt(0) - '0') * 100 + (yearString.charAt(1) - '0') * 10 + (yearString.charAt(2) - '0'); | ||||
| 			return year + 1900; | ||||
| 		} else if (yearString.length() == 4) { | ||||
| 			return (yearString.charAt(0) - '0') * 1000 + (yearString.charAt(1) - '0') * 100 + (yearString.charAt(2) - '0') * 10 + (yearString.charAt(3) - '0'); | ||||
| 		} else { | ||||
| 			return 1970; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|     private static TimeOfDay getTime(String timeString) { | ||||
|         // HH might be H | ||||
|         int i = 0; | ||||
|         int hour = timeString.charAt(i++) - '0'; | ||||
|         if (timeString.charAt(i) != ':') | ||||
|             hour = hour * 10 + (timeString.charAt(i++) - '0'); | ||||
|         // Skip ':' | ||||
|         i++; | ||||
| 	private static TimeOfDay getTime(String timeString) { | ||||
| 		// HH might be H | ||||
| 		int i = 0; | ||||
| 		int hour = timeString.charAt(i++) - '0'; | ||||
| 		if (timeString.charAt(i) != ':') | ||||
| 			hour = hour * 10 + (timeString.charAt(i++) - '0'); | ||||
| 		// Skip ':' | ||||
| 		i++; | ||||
| 
 | ||||
|         int minute = (timeString.charAt(i++) - '0') * 10 | ||||
|                 + (timeString.charAt(i++) - '0'); | ||||
|         // Skip ':' | ||||
|         i++; | ||||
| 		int minute = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0'); | ||||
| 		// Skip ':' | ||||
| 		i++; | ||||
| 
 | ||||
|         int second = (timeString.charAt(i++) - '0') * 10 | ||||
|                 + (timeString.charAt(i++) - '0'); | ||||
| 		int second = (timeString.charAt(i++) - '0') * 10 + (timeString.charAt(i++) - '0'); | ||||
| 
 | ||||
|         return new TimeOfDay(hour, minute, second); | ||||
|     } | ||||
| 		return new TimeOfDay(hour, minute, second); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,101 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2012 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.expansion.downloader.impl; | ||||
| 
 | ||||
| import com.godot.game.R; | ||||
| import com.google.android.vending.expansion.downloader.Helpers; | ||||
| 
 | ||||
| import android.app.Notification; | ||||
| import android.app.PendingIntent; | ||||
| import android.content.Context; | ||||
| 
 | ||||
| public class V14CustomNotification implements DownloadNotification.ICustomNotification { | ||||
| 
 | ||||
|     CharSequence mTitle; | ||||
|     CharSequence mTicker; | ||||
|     int mIcon; | ||||
|     long mTotalKB = -1; | ||||
|     long mCurrentKB = -1; | ||||
|     long mTimeRemaining; | ||||
|     PendingIntent mPendingIntent; | ||||
| 
 | ||||
|     @Override | ||||
|     public void setIcon(int icon) { | ||||
|         mIcon = icon; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setTitle(CharSequence title) { | ||||
|         mTitle = title; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setTotalBytes(long totalBytes) { | ||||
|         mTotalKB = totalBytes; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setCurrentBytes(long currentBytes) { | ||||
|         mCurrentKB = currentBytes; | ||||
|     } | ||||
| 
 | ||||
|     void setProgress(Notification.Builder builder) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Notification.Builder updateNotification(Context c) { | ||||
|         Notification.Builder builder = new Notification.Builder(c); | ||||
|         builder.setContentTitle(mTitle); | ||||
|         if (mTotalKB > 0 && -1 != mCurrentKB) { | ||||
|             builder.setProgress((int) (mTotalKB >> 8), (int) (mCurrentKB >> 8), false); | ||||
|         } else { | ||||
|             builder.setProgress(0, 0, true); | ||||
|         } | ||||
|         builder.setContentText(Helpers.getDownloadProgressString(mCurrentKB, mTotalKB)); | ||||
|         builder.setContentInfo(c.getString(R.string.time_remaining_notification, | ||||
|                 Helpers.getTimeRemaining(mTimeRemaining))); | ||||
|         if (mIcon != 0) { | ||||
|             builder.setSmallIcon(mIcon); | ||||
|         } else { | ||||
|             int iconResource = android.R.drawable.stat_sys_download; | ||||
|             builder.setSmallIcon(iconResource); | ||||
|         } | ||||
|         builder.setOngoing(true); | ||||
|         builder.setTicker(mTicker); | ||||
|         builder.setContentIntent(mPendingIntent); | ||||
|         builder.setOnlyAlertOnce(true); | ||||
| 
 | ||||
|         return builder; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setPendingIntent(PendingIntent contentIntent) { | ||||
|         mPendingIntent = contentIntent; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setTicker(CharSequence ticker) { | ||||
|         mTicker = ticker; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setTimeRemaining(long timeRemaining) { | ||||
|         mTimeRemaining = timeRemaining; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,110 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.Base64; | ||||
| import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.security.GeneralSecurityException; | ||||
| import java.security.spec.KeySpec; | ||||
| 
 | ||||
| import javax.crypto.BadPaddingException; | ||||
| import javax.crypto.Cipher; | ||||
| import javax.crypto.IllegalBlockSizeException; | ||||
| import javax.crypto.SecretKey; | ||||
| import javax.crypto.SecretKeyFactory; | ||||
| import javax.crypto.spec.IvParameterSpec; | ||||
| import javax.crypto.spec.PBEKeySpec; | ||||
| import javax.crypto.spec.SecretKeySpec; | ||||
| 
 | ||||
| /** | ||||
|  * An Obfuscator that uses AES to encrypt data. | ||||
|  */ | ||||
| public class AESObfuscator implements Obfuscator { | ||||
| 	private static final String UTF8 = "UTF-8"; | ||||
| 	private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; | ||||
| 	private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; | ||||
| 	private static final byte[] IV = { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; | ||||
| 	private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|"; | ||||
| 
 | ||||
| 	private Cipher mEncryptor; | ||||
| 	private Cipher mDecryptor; | ||||
| 
 | ||||
| 	/** | ||||
|      * @param salt an array of random bytes to use for each (un)obfuscation | ||||
|      * @param applicationId application identifier, e.g. the package name | ||||
|      * @param deviceId device identifier. Use as many sources as possible to | ||||
|      *    create this unique identifier. | ||||
|      */ | ||||
| 	public AESObfuscator(byte[] salt, String applicationId, String deviceId) { | ||||
| 		try { | ||||
| 			SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); | ||||
| 			KeySpec keySpec = | ||||
| 					new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); | ||||
| 			SecretKey tmp = factory.generateSecret(keySpec); | ||||
| 			SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); | ||||
| 			mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); | ||||
| 			mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); | ||||
| 			mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); | ||||
| 			mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); | ||||
| 		} catch (GeneralSecurityException e) { | ||||
| 			// This can't happen on a compatible Android device. | ||||
| 			throw new RuntimeException("Invalid environment", e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public String obfuscate(String original, String key) { | ||||
| 		if (original == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		try { | ||||
| 			// Header is appended as an integrity check | ||||
| 			return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); | ||||
| 		} catch (UnsupportedEncodingException e) { | ||||
| 			throw new RuntimeException("Invalid environment", e); | ||||
| 		} catch (GeneralSecurityException e) { | ||||
| 			throw new RuntimeException("Invalid environment", e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public String unobfuscate(String obfuscated, String key) throws ValidationException { | ||||
| 		if (obfuscated == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		try { | ||||
| 			String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); | ||||
| 			// Check for presence of header. This serves as a final integrity check, for cases | ||||
| 			// where the block size is correct during decryption. | ||||
| 			int headerIndex = result.indexOf(header + key); | ||||
| 			if (headerIndex != 0) { | ||||
| 				throw new ValidationException("Header not found (invalid data or key)" | ||||
| 											  + ":" + | ||||
| 											  obfuscated); | ||||
| 			} | ||||
| 			return result.substring(header.length() + key.length(), result.length()); | ||||
| 		} catch (Base64DecoderException e) { | ||||
| 			throw new ValidationException(e.getMessage() + ":" + obfuscated); | ||||
| 		} catch (IllegalBlockSizeException e) { | ||||
| 			throw new ValidationException(e.getMessage() + ":" + obfuscated); | ||||
| 		} catch (BadPaddingException e) { | ||||
| 			throw new ValidationException(e.getMessage() + ":" + obfuscated); | ||||
| 		} catch (UnsupportedEncodingException e) { | ||||
| 			throw new RuntimeException("Invalid environment", e); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,413 @@ | |||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| /* | ||||
|  * Copyright (C) 2012 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.URIQueryDecoder; | ||||
| 
 | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.Vector; | ||||
| 
 | ||||
| /** | ||||
|  * Default policy. All policy decisions are based off of response data received | ||||
|  * from the licensing service. Specifically, the licensing server sends the | ||||
|  * following information: response validity period, error retry period, | ||||
|  * error retry count and a URL for restoring app access in unlicensed cases. | ||||
|  * <p> | ||||
|  * These values will vary based on the the way the application is configured in | ||||
|  * the Google Play publishing console, such as whether the application is | ||||
|  * marked as free or is within its refund period, as well as how often an | ||||
|  * application is checking with the licensing service. | ||||
|  * <p> | ||||
|  * Developers who need more fine grained control over their application's | ||||
|  * licensing policy should implement a custom Policy. | ||||
|  */ | ||||
| public class APKExpansionPolicy implements Policy { | ||||
| 
 | ||||
| 	private static final String TAG = "APKExpansionPolicy"; | ||||
| 	private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy"; | ||||
| 	private static final String PREF_LAST_RESPONSE = "lastResponse"; | ||||
| 	private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; | ||||
| 	private static final String PREF_RETRY_UNTIL = "retryUntil"; | ||||
| 	private static final String PREF_MAX_RETRIES = "maxRetries"; | ||||
| 	private static final String PREF_RETRY_COUNT = "retryCount"; | ||||
| 	private static final String PREF_LICENSING_URL = "licensingUrl"; | ||||
| 	private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; | ||||
| 	private static final String DEFAULT_RETRY_UNTIL = "0"; | ||||
| 	private static final String DEFAULT_MAX_RETRIES = "0"; | ||||
| 	private static final String DEFAULT_RETRY_COUNT = "0"; | ||||
| 
 | ||||
| 	private static final long MILLIS_PER_MINUTE = 60 * 1000; | ||||
| 
 | ||||
| 	private long mValidityTimestamp; | ||||
| 	private long mRetryUntil; | ||||
| 	private long mMaxRetries; | ||||
| 	private long mRetryCount; | ||||
| 	private long mLastResponseTime = 0; | ||||
| 	private int mLastResponse; | ||||
| 	private String mLicensingUrl; | ||||
| 	private PreferenceObfuscator mPreferences; | ||||
| 	private Vector<String> mExpansionURLs = new Vector<String>(); | ||||
| 	private Vector<String> mExpansionFileNames = new Vector<String>(); | ||||
| 	private Vector<Long> mExpansionFileSizes = new Vector<Long>(); | ||||
| 
 | ||||
| 	/** | ||||
|      * The design of the protocol supports n files. Currently the market can | ||||
|      * only deliver two files. To accommodate this, we have these two constants, | ||||
|      * but the order is the only relevant thing here. | ||||
|      */ | ||||
| 	public static final int MAIN_FILE_URL_INDEX = 0; | ||||
| 	public static final int PATCH_FILE_URL_INDEX = 1; | ||||
| 
 | ||||
| 	/** | ||||
|      * @param context The context for the current application | ||||
|      * @param obfuscator An obfuscator to be used with preferences. | ||||
|      */ | ||||
| 	public APKExpansionPolicy(Context context, Obfuscator obfuscator) { | ||||
| 		// Import old values | ||||
| 		SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); | ||||
| 		mPreferences = new PreferenceObfuscator(sp, obfuscator); | ||||
| 		mLastResponse = Integer.parseInt( | ||||
| 				mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); | ||||
| 		mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, | ||||
| 				DEFAULT_VALIDITY_TIMESTAMP)); | ||||
| 		mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); | ||||
| 		mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); | ||||
| 		mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); | ||||
| 		mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * We call this to guarantee that we fetch a fresh policy from the server. | ||||
|      * This is to be used if the URL is invalid. | ||||
|      */ | ||||
| 	public void resetPolicy() { | ||||
| 		mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); | ||||
| 		setRetryUntil(DEFAULT_RETRY_UNTIL); | ||||
| 		setMaxRetries(DEFAULT_MAX_RETRIES); | ||||
| 		setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); | ||||
| 		setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); | ||||
| 		mPreferences.commit(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Process a new response from the license server. | ||||
|      * <p> | ||||
|      * This data will be used for computing future policy decisions. The | ||||
|      * following parameters are processed: | ||||
|      * <ul> | ||||
|      * <li>VT: the timestamp that the client should consider the response valid | ||||
|      * until | ||||
|      * <li>GT: the timestamp that the client should ignore retry errors until | ||||
|      * <li>GR: the number of retry errors that the client should ignore | ||||
|      * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. | ||||
|      * buy app on the Play Store) | ||||
|      * </ul> | ||||
|      * | ||||
|      * @param response the result from validating the server response | ||||
|      * @param rawData the raw server response data | ||||
|      */ | ||||
| 	public void processServerResponse(int response, | ||||
| 			com.google.android.vending.licensing.ResponseData rawData) { | ||||
| 
 | ||||
| 		// Update retry counter | ||||
| 		if (response != Policy.RETRY) { | ||||
| 			setRetryCount(0); | ||||
| 		} else { | ||||
| 			setRetryCount(mRetryCount + 1); | ||||
| 		} | ||||
| 
 | ||||
| 		// Update server policy data | ||||
| 		Map<String, String> extras = decodeExtras(rawData); | ||||
| 		if (response == Policy.LICENSED) { | ||||
| 			mLastResponse = response; | ||||
| 			// Reset the licensing URL since it is only applicable for NOT_LICENSED responses. | ||||
| 			setLicensingUrl(null); | ||||
| 			setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); | ||||
| 			Set<String> keys = extras.keySet(); | ||||
| 			for (String key : keys) { | ||||
| 				if (key.equals("VT")) { | ||||
| 					setValidityTimestamp(extras.get(key)); | ||||
| 				} else if (key.equals("GT")) { | ||||
| 					setRetryUntil(extras.get(key)); | ||||
| 				} else if (key.equals("GR")) { | ||||
| 					setMaxRetries(extras.get(key)); | ||||
| 				} else if (key.startsWith("FILE_URL")) { | ||||
| 					int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; | ||||
| 					setExpansionURL(index, extras.get(key)); | ||||
| 				} else if (key.startsWith("FILE_NAME")) { | ||||
| 					int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; | ||||
| 					setExpansionFileName(index, extras.get(key)); | ||||
| 				} else if (key.startsWith("FILE_SIZE")) { | ||||
| 					int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; | ||||
| 					setExpansionFileSize(index, Long.parseLong(extras.get(key))); | ||||
| 				} | ||||
| 			} | ||||
| 		} else if (response == Policy.NOT_LICENSED) { | ||||
| 			// Clear out stale retry params | ||||
| 			setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); | ||||
| 			setRetryUntil(DEFAULT_RETRY_UNTIL); | ||||
| 			setMaxRetries(DEFAULT_MAX_RETRIES); | ||||
| 			// Update the licensing URL | ||||
| 			setLicensingUrl(extras.get("LU")); | ||||
| 		} | ||||
| 
 | ||||
| 		setLastResponse(response); | ||||
| 		mPreferences.commit(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the last license response received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param l the response | ||||
|      */ | ||||
| 	private void setLastResponse(int l) { | ||||
| 		mLastResponseTime = System.currentTimeMillis(); | ||||
| 		mLastResponse = l; | ||||
| 		mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the current retry count and add to preferences. You must manually | ||||
|      * call PreferenceObfuscator.commit() to commit these changes to disk. | ||||
|      * | ||||
|      * @param c the new retry count | ||||
|      */ | ||||
| 	private void setRetryCount(long c) { | ||||
| 		mRetryCount = c; | ||||
| 		mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getRetryCount() { | ||||
| 		return mRetryCount; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the last validity timestamp (VT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param validityTimestamp the VT string received | ||||
|      */ | ||||
| 	private void setValidityTimestamp(String validityTimestamp) { | ||||
| 		Long lValidityTimestamp; | ||||
| 		try { | ||||
| 			lValidityTimestamp = Long.parseLong(validityTimestamp); | ||||
| 		} catch (NumberFormatException e) { | ||||
| 			// No response or not parseable, expire in one minute. | ||||
| 			Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); | ||||
| 			lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; | ||||
| 			validityTimestamp = Long.toString(lValidityTimestamp); | ||||
| 		} | ||||
| 
 | ||||
| 		mValidityTimestamp = lValidityTimestamp; | ||||
| 		mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getValidityTimestamp() { | ||||
| 		return mValidityTimestamp; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the retry until timestamp (GT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param retryUntil the GT string received | ||||
|      */ | ||||
| 	private void setRetryUntil(String retryUntil) { | ||||
| 		Long lRetryUntil; | ||||
| 		try { | ||||
| 			lRetryUntil = Long.parseLong(retryUntil); | ||||
| 		} catch (NumberFormatException e) { | ||||
| 			// No response or not parseable, expire immediately | ||||
| 			Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); | ||||
| 			retryUntil = "0"; | ||||
| 			lRetryUntil = 0l; | ||||
| 		} | ||||
| 
 | ||||
| 		mRetryUntil = lRetryUntil; | ||||
| 		mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getRetryUntil() { | ||||
| 		return mRetryUntil; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the max retries value (GR) as received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param maxRetries the GR string received | ||||
|      */ | ||||
| 	private void setMaxRetries(String maxRetries) { | ||||
| 		Long lMaxRetries; | ||||
| 		try { | ||||
| 			lMaxRetries = Long.parseLong(maxRetries); | ||||
| 		} catch (NumberFormatException e) { | ||||
| 			// No response or not parseable, expire immediately | ||||
| 			Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); | ||||
| 			maxRetries = "0"; | ||||
| 			lMaxRetries = 0l; | ||||
| 		} | ||||
| 
 | ||||
| 		mMaxRetries = lMaxRetries; | ||||
| 		mPreferences.putString(PREF_MAX_RETRIES, maxRetries); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getMaxRetries() { | ||||
| 		return mMaxRetries; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the licensing URL that displays a Play Store UI for the user to regain app access. | ||||
|      * | ||||
|      * @param url the LU string received | ||||
|      */ | ||||
| 	private void setLicensingUrl(String url) { | ||||
| 		mLicensingUrl = url; | ||||
| 		mPreferences.putString(PREF_LICENSING_URL, url); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getLicensingUrl() { | ||||
| 		return mLicensingUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Gets the count of expansion URLs. Since expansionURLs are not committed | ||||
|      * to preferences, this will return zero if there has been no LVL fetch | ||||
|      * in the current session. | ||||
|      * | ||||
|      * @return the number of expansion URLs. (0,1,2) | ||||
|      */ | ||||
| 	public int getExpansionURLCount() { | ||||
| 		return mExpansionURLs.size(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Gets the expansion URL. Since these URLs are not committed to | ||||
|      * preferences, this will always return null if there has not been an LVL | ||||
|      * fetch in the current session. | ||||
|      * | ||||
|      * @param index the index of the URL to fetch. This value will be either | ||||
|      *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX | ||||
|      */ | ||||
| 	public String getExpansionURL(int index) { | ||||
| 		if (index < mExpansionURLs.size()) { | ||||
| 			return mExpansionURLs.elementAt(index); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Sets the expansion URL. Expansion URL's are not committed to preferences, | ||||
|      * but are instead intended to be stored when the license response is | ||||
|      * processed by the front-end. | ||||
|      * | ||||
|      * @param index the index of the expansion URL. This value will be either | ||||
|      *            MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX | ||||
|      * @param URL the URL to set | ||||
|      */ | ||||
| 	public void setExpansionURL(int index, String URL) { | ||||
| 		if (index >= mExpansionURLs.size()) { | ||||
| 			mExpansionURLs.setSize(index + 1); | ||||
| 		} | ||||
| 		mExpansionURLs.set(index, URL); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getExpansionFileName(int index) { | ||||
| 		if (index < mExpansionFileNames.size()) { | ||||
| 			return mExpansionFileNames.elementAt(index); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setExpansionFileName(int index, String name) { | ||||
| 		if (index >= mExpansionFileNames.size()) { | ||||
| 			mExpansionFileNames.setSize(index + 1); | ||||
| 		} | ||||
| 		mExpansionFileNames.set(index, name); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getExpansionFileSize(int index) { | ||||
| 		if (index < mExpansionFileSizes.size()) { | ||||
| 			return mExpansionFileSizes.elementAt(index); | ||||
| 		} | ||||
| 		return -1; | ||||
| 	} | ||||
| 
 | ||||
| 	public void setExpansionFileSize(int index, long size) { | ||||
| 		if (index >= mExpansionFileSizes.size()) { | ||||
| 			mExpansionFileSizes.setSize(index + 1); | ||||
| 		} | ||||
| 		mExpansionFileSizes.set(index, size); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * {@inheritDoc} This implementation allows access if either:<br> | ||||
|      * <ol> | ||||
|      * <li>a LICENSED response was received within the validity period | ||||
|      * <li>a RETRY response was received in the last minute, and we are under | ||||
|      * the RETRY count or in the RETRY period. | ||||
|      * </ol> | ||||
|      */ | ||||
| 	public boolean allowAccess() { | ||||
| 		long ts = System.currentTimeMillis(); | ||||
| 		if (mLastResponse == Policy.LICENSED) { | ||||
| 			// Check if the LICENSED response occurred within the validity | ||||
| 			// timeout. | ||||
| 			if (ts <= mValidityTimestamp) { | ||||
| 				// Cached LICENSED response is still valid. | ||||
| 				return true; | ||||
| 			} | ||||
| 		} else if (mLastResponse == Policy.RETRY && | ||||
| 				   ts < mLastResponseTime + MILLIS_PER_MINUTE) { | ||||
| 			// Only allow access if we are within the retry period or we haven't | ||||
| 			// used up our | ||||
| 			// max retries. | ||||
| 			return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	private Map<String, String> decodeExtras( | ||||
| 			com.google.android.vending.licensing.ResponseData rawData) { | ||||
| 		Map<String, String> results = new HashMap<String, String>(); | ||||
| 		if (rawData == null) { | ||||
| 			return results; | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			URI rawExtras = new URI("?" + rawData.extra); | ||||
| 			URIQueryDecoder.DecodeQuery(rawExtras, results); | ||||
| 		} catch (URISyntaxException e) { | ||||
| 			Log.w(TAG, "Invalid syntax error while decoding extras data from server."); | ||||
| 		} | ||||
| 		return results; | ||||
| 	} | ||||
| } | ||||
|  | @ -37,11 +37,11 @@ package com.google.android.vending.licensing; | |||
|  */ | ||||
| public interface DeviceLimiter { | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Checks if this device is allowed to use the given user's license. | ||||
|      * | ||||
|      * @param userId the user whose license the server responded with | ||||
|      * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs | ||||
|      */ | ||||
|     int isDeviceAllowed(String userId); | ||||
| 	int isDeviceAllowed(String userId); | ||||
| } | ||||
|  | @ -0,0 +1,100 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
|  * This file is auto-generated.  DO NOT MODIFY. | ||||
|  * Original file: aidl/ILicenseResultListener.aidl | ||||
|  */ | ||||
| package com.google.android.vending.licensing; | ||||
| import java.lang.String; | ||||
| import android.os.RemoteException; | ||||
| import android.os.IBinder; | ||||
| import android.os.IInterface; | ||||
| import android.os.Binder; | ||||
| import android.os.Parcel; | ||||
| public interface ILicenseResultListener extends android.os.IInterface { | ||||
| 	/** Local-side IPC implementation stub class. */ | ||||
| 	public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener { | ||||
| 		private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; | ||||
| 		/** Construct the stub at attach it to the interface. */ | ||||
| 		public Stub() { | ||||
| 			this.attachInterface(this, DESCRIPTOR); | ||||
| 		} | ||||
| 		/** | ||||
|  * Cast an IBinder object into an ILicenseResultListener interface, | ||||
|  * generating a proxy if needed. | ||||
|  */ | ||||
| 		public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) { | ||||
| 			if ((obj == null)) { | ||||
| 				return null; | ||||
| 			} | ||||
| 			android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); | ||||
| 			if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { | ||||
| 				return ((com.google.android.vending.licensing.ILicenseResultListener)iin); | ||||
| 			} | ||||
| 			return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); | ||||
| 		} | ||||
| 		public android.os.IBinder asBinder() { | ||||
| 			return this; | ||||
| 		} | ||||
| 		public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { | ||||
| 			switch (code) { | ||||
| 				case INTERFACE_TRANSACTION: { | ||||
| 					reply.writeString(DESCRIPTOR); | ||||
| 					return true; | ||||
| 				} | ||||
| 				case TRANSACTION_verifyLicense: { | ||||
| 					data.enforceInterface(DESCRIPTOR); | ||||
| 					int _arg0; | ||||
| 					_arg0 = data.readInt(); | ||||
| 					java.lang.String _arg1; | ||||
| 					_arg1 = data.readString(); | ||||
| 					java.lang.String _arg2; | ||||
| 					_arg2 = data.readString(); | ||||
| 					this.verifyLicense(_arg0, _arg1, _arg2); | ||||
| 					return true; | ||||
| 				} | ||||
| 			} | ||||
| 			return super.onTransact(code, data, reply, flags); | ||||
| 		} | ||||
| 		private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener { | ||||
| 			private android.os.IBinder mRemote; | ||||
| 			Proxy(android.os.IBinder remote) { | ||||
| 				mRemote = remote; | ||||
| 			} | ||||
| 			public android.os.IBinder asBinder() { | ||||
| 				return mRemote; | ||||
| 			} | ||||
| 			public java.lang.String getInterfaceDescriptor() { | ||||
| 				return DESCRIPTOR; | ||||
| 			} | ||||
| 			public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException { | ||||
| 				android.os.Parcel _data = android.os.Parcel.obtain(); | ||||
| 				try { | ||||
| 					_data.writeInterfaceToken(DESCRIPTOR); | ||||
| 					_data.writeInt(responseCode); | ||||
| 					_data.writeString(signedData); | ||||
| 					_data.writeString(signature); | ||||
| 					mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY); | ||||
| 				} finally { | ||||
| 					_data.recycle(); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); | ||||
| 	} | ||||
| 	public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; | ||||
| } | ||||
|  | @ -0,0 +1,100 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
|  * This file is auto-generated.  DO NOT MODIFY. | ||||
|  * Original file: aidl/ILicensingService.aidl | ||||
|  */ | ||||
| package com.google.android.vending.licensing; | ||||
| import java.lang.String; | ||||
| import android.os.RemoteException; | ||||
| import android.os.IBinder; | ||||
| import android.os.IInterface; | ||||
| import android.os.Binder; | ||||
| import android.os.Parcel; | ||||
| public interface ILicensingService extends android.os.IInterface { | ||||
| 	/** Local-side IPC implementation stub class. */ | ||||
| 	public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService { | ||||
| 		private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; | ||||
| 		/** Construct the stub at attach it to the interface. */ | ||||
| 		public Stub() { | ||||
| 			this.attachInterface(this, DESCRIPTOR); | ||||
| 		} | ||||
| 		/** | ||||
|  * Cast an IBinder object into an ILicensingService interface, | ||||
|  * generating a proxy if needed. | ||||
|  */ | ||||
| 		public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) { | ||||
| 			if ((obj == null)) { | ||||
| 				return null; | ||||
| 			} | ||||
| 			android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); | ||||
| 			if (((iin != null) && (iin instanceof com.google.android.vending.licensing.ILicensingService))) { | ||||
| 				return ((com.google.android.vending.licensing.ILicensingService)iin); | ||||
| 			} | ||||
| 			return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); | ||||
| 		} | ||||
| 		public android.os.IBinder asBinder() { | ||||
| 			return this; | ||||
| 		} | ||||
| 		public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { | ||||
| 			switch (code) { | ||||
| 				case INTERFACE_TRANSACTION: { | ||||
| 					reply.writeString(DESCRIPTOR); | ||||
| 					return true; | ||||
| 				} | ||||
| 				case TRANSACTION_checkLicense: { | ||||
| 					data.enforceInterface(DESCRIPTOR); | ||||
| 					long _arg0; | ||||
| 					_arg0 = data.readLong(); | ||||
| 					java.lang.String _arg1; | ||||
| 					_arg1 = data.readString(); | ||||
| 					com.google.android.vending.licensing.ILicenseResultListener _arg2; | ||||
| 					_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); | ||||
| 					this.checkLicense(_arg0, _arg1, _arg2); | ||||
| 					return true; | ||||
| 				} | ||||
| 			} | ||||
| 			return super.onTransact(code, data, reply, flags); | ||||
| 		} | ||||
| 		private static class Proxy implements com.google.android.vending.licensing.ILicensingService { | ||||
| 			private android.os.IBinder mRemote; | ||||
| 			Proxy(android.os.IBinder remote) { | ||||
| 				mRemote = remote; | ||||
| 			} | ||||
| 			public android.os.IBinder asBinder() { | ||||
| 				return mRemote; | ||||
| 			} | ||||
| 			public java.lang.String getInterfaceDescriptor() { | ||||
| 				return DESCRIPTOR; | ||||
| 			} | ||||
| 			public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException { | ||||
| 				android.os.Parcel _data = android.os.Parcel.obtain(); | ||||
| 				try { | ||||
| 					_data.writeInterfaceToken(DESCRIPTOR); | ||||
| 					_data.writeLong(nonce); | ||||
| 					_data.writeString(packageName); | ||||
| 					_data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null))); | ||||
| 					mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY); | ||||
| 				} finally { | ||||
| 					_data.recycle(); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); | ||||
| 	} | ||||
| 	public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; | ||||
| } | ||||
|  | @ -0,0 +1,387 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import android.content.ComponentName; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.ServiceConnection; | ||||
| import android.content.pm.PackageManager.NameNotFoundException; | ||||
| import android.net.Uri; | ||||
| import android.os.Handler; | ||||
| import android.os.HandlerThread; | ||||
| import android.os.IBinder; | ||||
| import android.os.RemoteException; | ||||
| import android.provider.Settings.Secure; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.ILicenseResultListener; | ||||
| import com.google.android.vending.licensing.ILicensingService; | ||||
| import com.google.android.vending.licensing.util.Base64; | ||||
| import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| 
 | ||||
| import java.security.KeyFactory; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.PublicKey; | ||||
| import java.security.SecureRandom; | ||||
| import java.security.spec.InvalidKeySpecException; | ||||
| import java.security.spec.X509EncodedKeySpec; | ||||
| import java.util.Date; | ||||
| import java.util.HashSet; | ||||
| import java.util.LinkedList; | ||||
| import java.util.Queue; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| /** | ||||
|  * Client library for Google Play license verifications. | ||||
|  * <p> | ||||
|  * The LicenseChecker is configured via a {@link Policy} which contains the logic to determine | ||||
|  * whether a user should have access to the application. For example, the Policy can define a | ||||
|  * threshold for allowable number of server or client failures before the library reports the user | ||||
|  * as not having access. | ||||
|  * <p> | ||||
|  * Must also provide the Base64-encoded RSA public key associated with your developer account. The | ||||
|  * public key is obtainable from the publisher site. | ||||
|  */ | ||||
| public class LicenseChecker implements ServiceConnection { | ||||
| 	private static final String TAG = "LicenseChecker"; | ||||
| 
 | ||||
| 	private static final String KEY_FACTORY_ALGORITHM = "RSA"; | ||||
| 
 | ||||
| 	// Timeout value (in milliseconds) for calls to service. | ||||
| 	private static final int TIMEOUT_MS = 10 * 1000; | ||||
| 
 | ||||
| 	private static final SecureRandom RANDOM = new SecureRandom(); | ||||
| 	private static final boolean DEBUG_LICENSE_ERROR = false; | ||||
| 
 | ||||
| 	private ILicensingService mService; | ||||
| 
 | ||||
| 	private PublicKey mPublicKey; | ||||
| 	private final Context mContext; | ||||
| 	private final Policy mPolicy; | ||||
| 	/** | ||||
|      * A handler for running tasks on a background thread. We don't want license processing to block | ||||
|      * the UI thread. | ||||
|      */ | ||||
| 	private Handler mHandler; | ||||
| 	private final String mPackageName; | ||||
| 	private final String mVersionCode; | ||||
| 	private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>(); | ||||
| 	private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>(); | ||||
| 
 | ||||
| 	/** | ||||
|      * @param context a Context | ||||
|      * @param policy implementation of Policy | ||||
|      * @param encodedPublicKey Base64-encoded RSA public key | ||||
|      * @throws IllegalArgumentException if encodedPublicKey is invalid | ||||
|      */ | ||||
| 	public LicenseChecker(Context context, Policy policy, String encodedPublicKey) { | ||||
| 		mContext = context; | ||||
| 		mPolicy = policy; | ||||
| 		mPublicKey = generatePublicKey(encodedPublicKey); | ||||
| 		mPackageName = mContext.getPackageName(); | ||||
| 		mVersionCode = getVersionCode(context, mPackageName); | ||||
| 		HandlerThread handlerThread = new HandlerThread("background thread"); | ||||
| 		handlerThread.start(); | ||||
| 		mHandler = new Handler(handlerThread.getLooper()); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Generates a PublicKey instance from a string containing the Base64-encoded public key. | ||||
|      * | ||||
|      * @param encodedPublicKey Base64-encoded public key | ||||
|      * @throws IllegalArgumentException if encodedPublicKey is invalid | ||||
|      */ | ||||
| 	private static PublicKey generatePublicKey(String encodedPublicKey) { | ||||
| 		try { | ||||
| 			byte[] decodedKey = Base64.decode(encodedPublicKey); | ||||
| 			KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); | ||||
| 
 | ||||
| 			return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); | ||||
| 		} catch (NoSuchAlgorithmException e) { | ||||
| 			// This won't happen in an Android-compatible environment. | ||||
| 			throw new RuntimeException(e); | ||||
| 		} catch (Base64DecoderException e) { | ||||
| 			Log.e(TAG, "Could not decode from Base64."); | ||||
| 			throw new IllegalArgumentException(e); | ||||
| 		} catch (InvalidKeySpecException e) { | ||||
| 			Log.e(TAG, "Invalid key specification."); | ||||
| 			throw new IllegalArgumentException(e); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Checks if the user should have access to the app. Binds the service if necessary. | ||||
|      * <p> | ||||
|      * NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we | ||||
|      * recommend obfuscating the string that is passed into bindService using another method of your | ||||
|      * own devising. | ||||
|      * <p> | ||||
|      * source string: "com.android.vending.licensing.ILicensingService" | ||||
|      * <p> | ||||
|      *  | ||||
|      * @param callback | ||||
|      */ | ||||
| 	public synchronized void checkAccess(LicenseCheckerCallback callback) { | ||||
| 		// If we have a valid recent LICENSED response, we can skip asking | ||||
| 		// Market. | ||||
| 		if (mPolicy.allowAccess()) { | ||||
| 			Log.i(TAG, "Using cached license response"); | ||||
| 			callback.allow(Policy.LICENSED); | ||||
| 		} else { | ||||
| 			LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(), | ||||
| 					callback, generateNonce(), mPackageName, mVersionCode); | ||||
| 
 | ||||
| 			if (mService == null) { | ||||
| 				Log.i(TAG, "Binding to licensing service."); | ||||
| 				try { | ||||
| 					boolean bindResult = mContext | ||||
| 												 .bindService( | ||||
| 														 new Intent( | ||||
| 																 new String( | ||||
| 																		 // Base64 encoded - | ||||
| 																		 // com.android.vending.licensing.ILicensingService | ||||
| 																		 // Consider encoding this in another way in your | ||||
| 																		 // code to improve security | ||||
| 																		 Base64.decode( | ||||
| 																				 "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))) | ||||
| 																 // As of Android 5.0, implicit | ||||
| 																 // Service Intents are no longer | ||||
| 																 // allowed because it's not | ||||
| 																 // possible for the user to | ||||
| 																 // participate in disambiguating | ||||
| 																 // them. This does mean we break | ||||
| 																 // compatibility with Android | ||||
| 																 // Cupcake devices with this | ||||
| 																 // release, since setPackage was | ||||
| 																 // added in Donut. | ||||
| 																 .setPackage( | ||||
| 																		 new String( | ||||
| 																				 // Base64 | ||||
| 																				 // encoded - | ||||
| 																				 // com.android.vending | ||||
| 																				 Base64.decode( | ||||
| 																						 "Y29tLmFuZHJvaWQudmVuZGluZw=="))), | ||||
| 														 this, // ServiceConnection. | ||||
| 														 Context.BIND_AUTO_CREATE); | ||||
| 					if (bindResult) { | ||||
| 						mPendingChecks.offer(validator); | ||||
| 					} else { | ||||
| 						Log.e(TAG, "Could not bind to service."); | ||||
| 						handleServiceConnectionError(validator); | ||||
| 					} | ||||
| 				} catch (SecurityException e) { | ||||
| 					callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION); | ||||
| 				} catch (Base64DecoderException e) { | ||||
| 					e.printStackTrace(); | ||||
| 				} | ||||
| 			} else { | ||||
| 				mPendingChecks.offer(validator); | ||||
| 				runChecks(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Triggers the last deep link licensing URL returned from the server, which redirects users to a | ||||
|      * page which enables them to gain access to the app. If no such URL is returned by the server, it | ||||
|      * will go to the details page of the app in the Play Store. | ||||
|      */ | ||||
| 	public void followLastLicensingUrl(Context context) { | ||||
| 		String licensingUrl = mPolicy.getLicensingUrl(); | ||||
| 		if (licensingUrl == null) { | ||||
| 			licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName(); | ||||
| 		} | ||||
| 		Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl)); | ||||
| 		context.startActivity(marketIntent); | ||||
| 	} | ||||
| 
 | ||||
| 	private void runChecks() { | ||||
| 		LicenseValidator validator; | ||||
| 		while ((validator = mPendingChecks.poll()) != null) { | ||||
| 			try { | ||||
| 				Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName()); | ||||
| 				mService.checkLicense( | ||||
| 						validator.getNonce(), validator.getPackageName(), | ||||
| 						new ResultListener(validator)); | ||||
| 				mChecksInProgress.add(validator); | ||||
| 			} catch (RemoteException e) { | ||||
| 				Log.w(TAG, "RemoteException in checkLicense call.", e); | ||||
| 				handleServiceConnectionError(validator); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private synchronized void finishCheck(LicenseValidator validator) { | ||||
| 		mChecksInProgress.remove(validator); | ||||
| 		if (mChecksInProgress.isEmpty()) { | ||||
| 			cleanupService(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private class ResultListener extends ILicenseResultListener.Stub { | ||||
| 		private final LicenseValidator mValidator; | ||||
| 		private Runnable mOnTimeout; | ||||
| 
 | ||||
| 		public ResultListener(LicenseValidator validator) { | ||||
| 			mValidator = validator; | ||||
| 			mOnTimeout = new Runnable() { | ||||
| 				public void run() { | ||||
| 					Log.i(TAG, "Check timed out."); | ||||
| 					handleServiceConnectionError(mValidator); | ||||
| 					finishCheck(mValidator); | ||||
| 				} | ||||
| 			}; | ||||
| 			startTimeout(); | ||||
| 		} | ||||
| 
 | ||||
| 		private static final int ERROR_CONTACTING_SERVER = 0x101; | ||||
| 		private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; | ||||
| 		private static final int ERROR_NON_MATCHING_UID = 0x103; | ||||
| 
 | ||||
| 		// Runs in IPC thread pool. Post it to the Handler, so we can guarantee | ||||
| 		// either this or the timeout runs. | ||||
| 		public void verifyLicense(final int responseCode, final String signedData, | ||||
| 				final String signature) { | ||||
| 			mHandler.post(new Runnable() { | ||||
| 				public void run() { | ||||
| 					Log.i(TAG, "Received response."); | ||||
| 					// Make sure it hasn't already timed out. | ||||
| 					if (mChecksInProgress.contains(mValidator)) { | ||||
| 						clearTimeout(); | ||||
| 						mValidator.verify(mPublicKey, responseCode, signedData, signature); | ||||
| 						finishCheck(mValidator); | ||||
| 					} | ||||
| 					if (DEBUG_LICENSE_ERROR) { | ||||
| 						boolean logResponse; | ||||
| 						String stringError = null; | ||||
| 						switch (responseCode) { | ||||
| 							case ERROR_CONTACTING_SERVER: | ||||
| 								logResponse = true; | ||||
| 								stringError = "ERROR_CONTACTING_SERVER"; | ||||
| 								break; | ||||
| 							case ERROR_INVALID_PACKAGE_NAME: | ||||
| 								logResponse = true; | ||||
| 								stringError = "ERROR_INVALID_PACKAGE_NAME"; | ||||
| 								break; | ||||
| 							case ERROR_NON_MATCHING_UID: | ||||
| 								logResponse = true; | ||||
| 								stringError = "ERROR_NON_MATCHING_UID"; | ||||
| 								break; | ||||
| 							default: | ||||
| 								logResponse = false; | ||||
| 						} | ||||
| 
 | ||||
| 						if (logResponse) { | ||||
| 							String android_id = Secure.ANDROID_ID; | ||||
| 							Date date = new Date(); | ||||
| 							Log.d(TAG, "Server Failure: " + stringError); | ||||
| 							Log.d(TAG, "Android ID: " + android_id); | ||||
| 							Log.d(TAG, "Time: " + date.toGMTString()); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		private void startTimeout() { | ||||
| 			Log.i(TAG, "Start monitoring timeout."); | ||||
| 			mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); | ||||
| 		} | ||||
| 
 | ||||
| 		private void clearTimeout() { | ||||
| 			Log.i(TAG, "Clearing timeout."); | ||||
| 			mHandler.removeCallbacks(mOnTimeout); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public synchronized void onServiceConnected(ComponentName name, IBinder service) { | ||||
| 		mService = ILicensingService.Stub.asInterface(service); | ||||
| 		runChecks(); | ||||
| 	} | ||||
| 
 | ||||
| 	public synchronized void onServiceDisconnected(ComponentName name) { | ||||
| 		// Called when the connection with the service has been | ||||
| 		// unexpectedly disconnected. That is, Market crashed. | ||||
| 		// If there are any checks in progress, the timeouts will handle them. | ||||
| 		Log.w(TAG, "Service unexpectedly disconnected."); | ||||
| 		mService = null; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Generates policy response for service connection errors, as a result of disconnections or | ||||
|      * timeouts. | ||||
|      */ | ||||
| 	private synchronized void handleServiceConnectionError(LicenseValidator validator) { | ||||
| 		mPolicy.processServerResponse(Policy.RETRY, null); | ||||
| 
 | ||||
| 		if (mPolicy.allowAccess()) { | ||||
| 			validator.getCallback().allow(Policy.RETRY); | ||||
| 		} else { | ||||
| 			validator.getCallback().dontAllow(Policy.RETRY); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** Unbinds service if necessary and removes reference to it. */ | ||||
| 	private void cleanupService() { | ||||
| 		if (mService != null) { | ||||
| 			try { | ||||
| 				mContext.unbindService(this); | ||||
| 			} catch (IllegalArgumentException e) { | ||||
| 				// Somehow we've already been unbound. This is a non-fatal | ||||
| 				// error. | ||||
| 				Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); | ||||
| 			} | ||||
| 			mService = null; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Inform the library that the context is about to be destroyed, so that any open connections | ||||
|      * can be cleaned up. | ||||
|      * <p> | ||||
|      * Failure to call this method can result in a crash under certain circumstances, such as during | ||||
|      * screen rotation if an Activity requests the license check or when the user exits the | ||||
|      * application. | ||||
|      */ | ||||
| 	public synchronized void onDestroy() { | ||||
| 		cleanupService(); | ||||
| 		mHandler.getLooper().quit(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** Generates a nonce (number used once). */ | ||||
| 	private int generateNonce() { | ||||
| 		return RANDOM.nextInt(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Get version code for the application package name. | ||||
|      * | ||||
|      * @param context | ||||
|      * @param packageName application package name | ||||
|      * @return the version code or empty string if package not found | ||||
|      */ | ||||
| 	private static String getVersionCode(Context context, String packageName) { | ||||
| 		try { | ||||
| 			return String.valueOf( | ||||
| 					context.getPackageManager().getPackageInfo(packageName, 0).versionCode); | ||||
| 		} catch (NameNotFoundException e) { | ||||
| 			Log.e(TAG, "Package not found. could not get version code."); | ||||
| 			return ""; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -34,34 +34,34 @@ package com.google.android.vending.licensing; | |||
|  */ | ||||
| public interface LicenseCheckerCallback { | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Allow use. App should proceed as normal. | ||||
|      *  | ||||
|      * | ||||
|      * @param reason Policy.LICENSED or Policy.RETRY typically. (although in | ||||
|      *            theory the policy can return Policy.NOT_LICENSED here as well) | ||||
|      */ | ||||
|     public void allow(int reason); | ||||
| 	public void allow(int reason); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Don't allow use. App should inform user and take appropriate action. | ||||
|      *  | ||||
|      * | ||||
|      * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory | ||||
|      *            the policy can return Policy.LICENSED here as well --- | ||||
|      *            perhaps the call to the LVL took too long, for example) | ||||
|      */ | ||||
|     public void dontAllow(int reason); | ||||
| 	public void dontAllow(int reason); | ||||
| 
 | ||||
|     /** Application error codes. */ | ||||
|     public static final int ERROR_INVALID_PACKAGE_NAME = 1; | ||||
|     public static final int ERROR_NON_MATCHING_UID = 2; | ||||
|     public static final int ERROR_NOT_MARKET_MANAGED = 3; | ||||
|     public static final int ERROR_CHECK_IN_PROGRESS = 4; | ||||
|     public static final int ERROR_INVALID_PUBLIC_KEY = 5; | ||||
|     public static final int ERROR_MISSING_PERMISSION = 6; | ||||
| 	/** Application error codes. */ | ||||
| 	public static final int ERROR_INVALID_PACKAGE_NAME = 1; | ||||
| 	public static final int ERROR_NON_MATCHING_UID = 2; | ||||
| 	public static final int ERROR_NOT_MARKET_MANAGED = 3; | ||||
| 	public static final int ERROR_CHECK_IN_PROGRESS = 4; | ||||
| 	public static final int ERROR_INVALID_PUBLIC_KEY = 5; | ||||
| 	public static final int ERROR_MISSING_PERMISSION = 6; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Error in application code. Caller did not call or set up license checker | ||||
|      * correctly. Should be considered fatal. | ||||
|      */ | ||||
|     public void applicationError(int errorCode); | ||||
| 	public void applicationError(int errorCode); | ||||
| } | ||||
|  | @ -0,0 +1,232 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.Base64; | ||||
| import com.google.android.vending.licensing.util.Base64DecoderException; | ||||
| 
 | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.security.InvalidKeyException; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.PublicKey; | ||||
| import java.security.Signature; | ||||
| import java.security.SignatureException; | ||||
| 
 | ||||
| /** | ||||
|  * Contains data related to a licensing request and methods to verify | ||||
|  * and process the response. | ||||
|  */ | ||||
| class LicenseValidator { | ||||
| 	private static final String TAG = "LicenseValidator"; | ||||
| 
 | ||||
| 	// Server response codes. | ||||
| 	private static final int LICENSED = 0x0; | ||||
| 	private static final int NOT_LICENSED = 0x1; | ||||
| 	private static final int LICENSED_OLD_KEY = 0x2; | ||||
| 	private static final int ERROR_NOT_MARKET_MANAGED = 0x3; | ||||
| 	private static final int ERROR_SERVER_FAILURE = 0x4; | ||||
| 	private static final int ERROR_OVER_QUOTA = 0x5; | ||||
| 
 | ||||
| 	private static final int ERROR_CONTACTING_SERVER = 0x101; | ||||
| 	private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; | ||||
| 	private static final int ERROR_NON_MATCHING_UID = 0x103; | ||||
| 
 | ||||
| 	private final Policy mPolicy; | ||||
| 	private final LicenseCheckerCallback mCallback; | ||||
| 	private final int mNonce; | ||||
| 	private final String mPackageName; | ||||
| 	private final String mVersionCode; | ||||
| 	private final DeviceLimiter mDeviceLimiter; | ||||
| 
 | ||||
| 	LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback, | ||||
| 			int nonce, String packageName, String versionCode) { | ||||
| 		mPolicy = policy; | ||||
| 		mDeviceLimiter = deviceLimiter; | ||||
| 		mCallback = callback; | ||||
| 		mNonce = nonce; | ||||
| 		mPackageName = packageName; | ||||
| 		mVersionCode = versionCode; | ||||
| 	} | ||||
| 
 | ||||
| 	public LicenseCheckerCallback getCallback() { | ||||
| 		return mCallback; | ||||
| 	} | ||||
| 
 | ||||
| 	public int getNonce() { | ||||
| 		return mNonce; | ||||
| 	} | ||||
| 
 | ||||
| 	public String getPackageName() { | ||||
| 		return mPackageName; | ||||
| 	} | ||||
| 
 | ||||
| 	private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; | ||||
| 
 | ||||
| 	/** | ||||
|      * Verifies the response from server and calls appropriate callback method. | ||||
|      * | ||||
|      * @param publicKey public key associated with the developer account | ||||
|      * @param responseCode server response code | ||||
|      * @param signedData signed data from server | ||||
|      * @param signature server signature | ||||
|      */ | ||||
| 	public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) { | ||||
| 		String userId = null; | ||||
| 		// Skip signature check for unsuccessful requests | ||||
| 		ResponseData data = null; | ||||
| 		if (responseCode == LICENSED || responseCode == NOT_LICENSED || | ||||
| 				responseCode == LICENSED_OLD_KEY) { | ||||
| 			// Verify signature. | ||||
| 			try { | ||||
| 				if (TextUtils.isEmpty(signedData)) { | ||||
| 					Log.e(TAG, "Signature verification failed: signedData is empty. " | ||||
| 									   + | ||||
| 									   "(Device not signed-in to any Google accounts?)"); | ||||
| 					handleInvalidResponse(); | ||||
| 					return; | ||||
| 				} | ||||
| 
 | ||||
| 				Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); | ||||
| 				sig.initVerify(publicKey); | ||||
| 				sig.update(signedData.getBytes()); | ||||
| 
 | ||||
| 				if (!sig.verify(Base64.decode(signature))) { | ||||
| 					Log.e(TAG, "Signature verification failed."); | ||||
| 					handleInvalidResponse(); | ||||
| 					return; | ||||
| 				} | ||||
| 			} catch (NoSuchAlgorithmException e) { | ||||
| 				// This can't happen on an Android compatible device. | ||||
| 				throw new RuntimeException(e); | ||||
| 			} catch (InvalidKeyException e) { | ||||
| 				handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); | ||||
| 				return; | ||||
| 			} catch (SignatureException e) { | ||||
| 				throw new RuntimeException(e); | ||||
| 			} catch (Base64DecoderException e) { | ||||
| 				Log.e(TAG, "Could not Base64-decode signature."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			// Parse and validate response. | ||||
| 			try { | ||||
| 				data = ResponseData.parse(signedData); | ||||
| 			} catch (IllegalArgumentException e) { | ||||
| 				Log.e(TAG, "Could not parse response."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			if (data.responseCode != responseCode) { | ||||
| 				Log.e(TAG, "Response codes don't match."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			if (data.nonce != mNonce) { | ||||
| 				Log.e(TAG, "Nonce doesn't match."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			if (!data.packageName.equals(mPackageName)) { | ||||
| 				Log.e(TAG, "Package name doesn't match."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			if (!data.versionCode.equals(mVersionCode)) { | ||||
| 				Log.e(TAG, "Version codes don't match."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			// Application-specific user identifier. | ||||
| 			userId = data.userId; | ||||
| 			if (TextUtils.isEmpty(userId)) { | ||||
| 				Log.e(TAG, "User identifier is empty."); | ||||
| 				handleInvalidResponse(); | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		switch (responseCode) { | ||||
| 			case LICENSED: | ||||
| 			case LICENSED_OLD_KEY: | ||||
| 				int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); | ||||
| 				handleResponse(limiterResponse, data); | ||||
| 				break; | ||||
| 			case NOT_LICENSED: | ||||
| 				handleResponse(Policy.NOT_LICENSED, data); | ||||
| 				break; | ||||
| 			case ERROR_CONTACTING_SERVER: | ||||
| 				Log.w(TAG, "Error contacting licensing server."); | ||||
| 				handleResponse(Policy.RETRY, data); | ||||
| 				break; | ||||
| 			case ERROR_SERVER_FAILURE: | ||||
| 				Log.w(TAG, "An error has occurred on the licensing server."); | ||||
| 				handleResponse(Policy.RETRY, data); | ||||
| 				break; | ||||
| 			case ERROR_OVER_QUOTA: | ||||
| 				Log.w(TAG, "Licensing server is refusing to talk to this device, over quota."); | ||||
| 				handleResponse(Policy.RETRY, data); | ||||
| 				break; | ||||
| 			case ERROR_INVALID_PACKAGE_NAME: | ||||
| 				handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME); | ||||
| 				break; | ||||
| 			case ERROR_NON_MATCHING_UID: | ||||
| 				handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID); | ||||
| 				break; | ||||
| 			case ERROR_NOT_MARKET_MANAGED: | ||||
| 				handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED); | ||||
| 				break; | ||||
| 			default: | ||||
| 				Log.e(TAG, "Unknown response code for license check."); | ||||
| 				handleInvalidResponse(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Confers with policy and calls appropriate callback method. | ||||
|      * | ||||
|      * @param response | ||||
|      * @param rawData | ||||
|      */ | ||||
| 	private void handleResponse(int response, ResponseData rawData) { | ||||
| 		// Update policy data and increment retry counter (if needed) | ||||
| 		mPolicy.processServerResponse(response, rawData); | ||||
| 
 | ||||
| 		// Given everything we know, including cached data, ask the policy if we should grant | ||||
| 		// access. | ||||
| 		if (mPolicy.allowAccess()) { | ||||
| 			mCallback.allow(response); | ||||
| 		} else { | ||||
| 			mCallback.dontAllow(response); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private void handleApplicationError(int code) { | ||||
| 		mCallback.applicationError(code); | ||||
| 	} | ||||
| 
 | ||||
| 	private void handleInvalidResponse() { | ||||
| 		mCallback.dontAllow(Policy.NOT_LICENSED); | ||||
| 	} | ||||
| } | ||||
|  | @ -26,7 +26,7 @@ package com.google.android.vending.licensing; | |||
|  */ | ||||
| public class NullDeviceLimiter implements DeviceLimiter { | ||||
| 
 | ||||
|     public int isDeviceAllowed(String userId) { | ||||
|         return Policy.LICENSED; | ||||
|     } | ||||
| 	public int isDeviceAllowed(String userId) { | ||||
| 		return Policy.LICENSED; | ||||
| 	} | ||||
| } | ||||
|  | @ -20,29 +20,29 @@ package com.google.android.vending.licensing; | |||
|  * Interface used as part of a {@link Policy} to allow application authors to obfuscate | ||||
|  * licensing data that will be stored into a SharedPreferences file. | ||||
|  * <p> | ||||
|  * Any transformation scheme must be reversible. Implementing classes may optionally implement an | ||||
|  * Any transformation scheme must be reversable. Implementing classes may optionally implement an | ||||
|  * integrity check to further prevent modification to preference data. Implementing classes | ||||
|  * should use device-specific information as a key in the obfuscation algorithm to prevent | ||||
|  * obfuscated preferences from being shared among devices. | ||||
|  */ | ||||
| public interface Obfuscator { | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Obfuscate a string that is being stored into shared preferences. | ||||
|      * | ||||
|      * @param original The data that is to be obfuscated. | ||||
|      * @param key The key for the data that is to be obfuscated. | ||||
|      * @return A transformed version of the original data. | ||||
|      */ | ||||
|     String obfuscate(String original, String key); | ||||
| 	String obfuscate(String original, String key); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Undo the transformation applied to data by the obfuscate() method. | ||||
|      * | ||||
|      * @param original The data that is to be obfuscated. | ||||
|      * @param key The key for the data that is to be obfuscated. | ||||
|      * @return A transformed version of the original data. | ||||
|      * @param obfuscated The data that is to be un-obfuscated. | ||||
|      * @param key The key for the data that is to be un-obfuscated. | ||||
|      * @return The original data transformed by the obfuscate() method. | ||||
|      * @throws ValidationException Optionally thrown if a data integrity check fails. | ||||
|      */ | ||||
|     String unobfuscate(String obfuscated, String key) throws ValidationException; | ||||
| 	String unobfuscate(String obfuscated, String key) throws ValidationException; | ||||
| } | ||||
|  | @ -22,38 +22,44 @@ package com.google.android.vending.licensing; | |||
|  */ | ||||
| public interface Policy { | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Change these values to make it more difficult for tools to automatically | ||||
|      * strip LVL protection from your APK. | ||||
|      */ | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * LICENSED means that the server returned back a valid license response | ||||
|      */ | ||||
|     public static final int LICENSED = 0x0100; | ||||
|     /** | ||||
| 	public static final int LICENSED = 0x0100; | ||||
| 	/** | ||||
|      * NOT_LICENSED means that the server returned back a valid license response | ||||
|      * that indicated that the user definitively is not licensed | ||||
|      */ | ||||
|     public static final int NOT_LICENSED = 0x0231; | ||||
|     /** | ||||
| 	public static final int NOT_LICENSED = 0x0231; | ||||
| 	/** | ||||
|      * RETRY means that the license response was unable to be determined --- | ||||
|      * perhaps as a result of faulty networking | ||||
|      */ | ||||
|     public static final int RETRY = 0x0123; | ||||
| 	public static final int RETRY = 0x0123; | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Provide results from contact with the license server. Retry counts are | ||||
|      * incremented if the current value of response is RETRY. Results will be | ||||
|      * used for any future policy decisions. | ||||
|      *  | ||||
|      * | ||||
|      * @param response the result from validating the server response | ||||
|      * @param rawData the raw server response data, can be null for RETRY | ||||
|      */ | ||||
|     void processServerResponse(int response, ResponseData rawData); | ||||
| 	void processServerResponse(int response, ResponseData rawData); | ||||
| 
 | ||||
|     /** | ||||
| 	/** | ||||
|      * Check if the user should be allowed access to the application. | ||||
|      */ | ||||
|     boolean allowAccess(); | ||||
| 	boolean allowAccess(); | ||||
| 
 | ||||
| 	/** | ||||
|      * Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g. | ||||
|      * buy app on the Play Store). | ||||
|      */ | ||||
| 	String getLicensingUrl(); | ||||
| } | ||||
|  | @ -0,0 +1,78 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * An wrapper for SharedPreferences that transparently performs data obfuscation. | ||||
|  */ | ||||
| public class PreferenceObfuscator { | ||||
| 
 | ||||
| 	private static final String TAG = "PreferenceObfuscator"; | ||||
| 
 | ||||
| 	private final SharedPreferences mPreferences; | ||||
| 	private final Obfuscator mObfuscator; | ||||
| 	private SharedPreferences.Editor mEditor; | ||||
| 
 | ||||
| 	/** | ||||
|      * Constructor. | ||||
|      * | ||||
|      * @param sp A SharedPreferences instance provided by the system. | ||||
|      * @param o The Obfuscator to use when reading or writing data. | ||||
|      */ | ||||
| 	public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) { | ||||
| 		mPreferences = sp; | ||||
| 		mObfuscator = o; | ||||
| 		mEditor = null; | ||||
| 	} | ||||
| 
 | ||||
| 	public void putString(String key, String value) { | ||||
| 		if (mEditor == null) { | ||||
| 			mEditor = mPreferences.edit(); | ||||
| 			mEditor.apply(); | ||||
| 		} | ||||
| 		String obfuscatedValue = mObfuscator.obfuscate(value, key); | ||||
| 		mEditor.putString(key, obfuscatedValue); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getString(String key, String defValue) { | ||||
| 		String result; | ||||
| 		String value = mPreferences.getString(key, null); | ||||
| 		if (value != null) { | ||||
| 			try { | ||||
| 				result = mObfuscator.unobfuscate(value, key); | ||||
| 			} catch (ValidationException e) { | ||||
| 				// Unable to unobfuscate, data corrupt or tampered | ||||
| 				Log.w(TAG, "Validation error while reading preference: " + key); | ||||
| 				result = defValue; | ||||
| 			} | ||||
| 		} else { | ||||
| 			// Preference not found | ||||
| 			result = defValue; | ||||
| 		} | ||||
| 		return result; | ||||
| 	} | ||||
| 
 | ||||
| 	public void commit() { | ||||
| 		if (mEditor != null) { | ||||
| 			mEditor.commit(); | ||||
| 			mEditor = null; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,80 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import android.text.TextUtils; | ||||
| 
 | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| /** | ||||
|  * ResponseData from licensing server. | ||||
|  */ | ||||
| public class ResponseData { | ||||
| 
 | ||||
| 	public int responseCode; | ||||
| 	public int nonce; | ||||
| 	public String packageName; | ||||
| 	public String versionCode; | ||||
| 	public String userId; | ||||
| 	public long timestamp; | ||||
| 	/** Response-specific data. */ | ||||
| 	public String extra; | ||||
| 
 | ||||
| 	/** | ||||
|      * Parses response string into ResponseData. | ||||
|      * | ||||
|      * @param responseData response data string | ||||
|      * @throws IllegalArgumentException upon parsing error | ||||
|      * @return ResponseData object | ||||
|      */ | ||||
| 	public static ResponseData parse(String responseData) { | ||||
| 		// Must parse out main response data and response-specific data. | ||||
| 		int index = responseData.indexOf(':'); | ||||
| 		String mainData, extraData; | ||||
| 		if (-1 == index) { | ||||
| 			mainData = responseData; | ||||
| 			extraData = ""; | ||||
| 		} else { | ||||
| 			mainData = responseData.substring(0, index); | ||||
| 			extraData = index >= responseData.length() ? "" : responseData.substring(index + 1); | ||||
| 		} | ||||
| 
 | ||||
| 		String[] fields = TextUtils.split(mainData, Pattern.quote("|")); | ||||
| 		if (fields.length < 6) { | ||||
| 			throw new IllegalArgumentException("Wrong number of fields."); | ||||
| 		} | ||||
| 
 | ||||
| 		ResponseData data = new ResponseData(); | ||||
| 		data.extra = extraData; | ||||
| 		data.responseCode = Integer.parseInt(fields[0]); | ||||
| 		data.nonce = Integer.parseInt(fields[1]); | ||||
| 		data.packageName = fields[2]; | ||||
| 		data.versionCode = fields[3]; | ||||
| 		// Application-specific user identifier. | ||||
| 		data.userId = fields[4]; | ||||
| 		data.timestamp = Long.parseLong(fields[5]); | ||||
| 
 | ||||
| 		return data; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		return TextUtils.join("|", new Object[] { | ||||
| 										   responseCode, nonce, packageName, versionCode, | ||||
| 										   userId, timestamp }); | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,299 @@ | |||
| /* | ||||
|  * Copyright (C) 2010 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import com.google.android.vending.licensing.util.URIQueryDecoder; | ||||
| 
 | ||||
| /** | ||||
|  * Default policy. All policy decisions are based off of response data received | ||||
|  * from the licensing service. Specifically, the licensing server sends the | ||||
|  * following information: response validity period, error retry period, | ||||
|  * error retry count and a URL for restoring app access in unlicensed cases. | ||||
|  * <p> | ||||
|  * These values will vary based on the the way the application is configured in | ||||
|  * the Google Play publishing console, such as whether the application is | ||||
|  * marked as free or is within its refund period, as well as how often an | ||||
|  * application is checking with the licensing service. | ||||
|  * <p> | ||||
|  * Developers who need more fine grained control over their application's | ||||
|  * licensing policy should implement a custom Policy. | ||||
|  */ | ||||
| public class ServerManagedPolicy implements Policy { | ||||
| 
 | ||||
| 	private static final String TAG = "ServerManagedPolicy"; | ||||
| 	private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy"; | ||||
| 	private static final String PREF_LAST_RESPONSE = "lastResponse"; | ||||
| 	private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; | ||||
| 	private static final String PREF_RETRY_UNTIL = "retryUntil"; | ||||
| 	private static final String PREF_MAX_RETRIES = "maxRetries"; | ||||
| 	private static final String PREF_RETRY_COUNT = "retryCount"; | ||||
| 	private static final String PREF_LICENSING_URL = "licensingUrl"; | ||||
| 	private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; | ||||
| 	private static final String DEFAULT_RETRY_UNTIL = "0"; | ||||
| 	private static final String DEFAULT_MAX_RETRIES = "0"; | ||||
| 	private static final String DEFAULT_RETRY_COUNT = "0"; | ||||
| 
 | ||||
| 	private static final long MILLIS_PER_MINUTE = 60 * 1000; | ||||
| 
 | ||||
| 	private long mValidityTimestamp; | ||||
| 	private long mRetryUntil; | ||||
| 	private long mMaxRetries; | ||||
| 	private long mRetryCount; | ||||
| 	private long mLastResponseTime = 0; | ||||
| 	private int mLastResponse; | ||||
| 	private String mLicensingUrl; | ||||
| 	private PreferenceObfuscator mPreferences; | ||||
| 
 | ||||
| 	/** | ||||
|      * @param context The context for the current application | ||||
|      * @param obfuscator An obfuscator to be used with preferences. | ||||
|      */ | ||||
| 	public ServerManagedPolicy(Context context, Obfuscator obfuscator) { | ||||
| 		// Import old values | ||||
| 		SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); | ||||
| 		mPreferences = new PreferenceObfuscator(sp, obfuscator); | ||||
| 		mLastResponse = Integer.parseInt( | ||||
| 				mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); | ||||
| 		mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, | ||||
| 				DEFAULT_VALIDITY_TIMESTAMP)); | ||||
| 		mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); | ||||
| 		mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); | ||||
| 		mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); | ||||
| 		mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Process a new response from the license server. | ||||
|      * <p> | ||||
|      * This data will be used for computing future policy decisions. The | ||||
|      * following parameters are processed: | ||||
|      * <ul> | ||||
|      * <li>VT: the timestamp that the client should consider the response valid | ||||
|      * until | ||||
|      * <li>GT: the timestamp that the client should ignore retry errors until | ||||
|      * <li>GR: the number of retry errors that the client should ignore | ||||
|      * <li>LU: a deep link URL that can enable access for unlicensed apps (e.g. | ||||
|      * buy app on the Play Store) | ||||
|      * </ul> | ||||
|      * | ||||
|      * @param response the result from validating the server response | ||||
|      * @param rawData the raw server response data | ||||
|      */ | ||||
| 	public void processServerResponse(int response, ResponseData rawData) { | ||||
| 
 | ||||
| 		// Update retry counter | ||||
| 		if (response != Policy.RETRY) { | ||||
| 			setRetryCount(0); | ||||
| 		} else { | ||||
| 			setRetryCount(mRetryCount + 1); | ||||
| 		} | ||||
| 
 | ||||
| 		// Update server policy data | ||||
| 		Map<String, String> extras = decodeExtras(rawData); | ||||
| 		if (response == Policy.LICENSED) { | ||||
| 			mLastResponse = response; | ||||
| 			// Reset the licensing URL since it is only applicable for NOT_LICENSED responses. | ||||
| 			setLicensingUrl(null); | ||||
| 			setValidityTimestamp(extras.get("VT")); | ||||
| 			setRetryUntil(extras.get("GT")); | ||||
| 			setMaxRetries(extras.get("GR")); | ||||
| 		} else if (response == Policy.NOT_LICENSED) { | ||||
| 			// Clear out stale retry params | ||||
| 			setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); | ||||
| 			setRetryUntil(DEFAULT_RETRY_UNTIL); | ||||
| 			setMaxRetries(DEFAULT_MAX_RETRIES); | ||||
| 			// Update the licensing URL | ||||
| 			setLicensingUrl(extras.get("LU")); | ||||
| 		} | ||||
| 
 | ||||
| 		setLastResponse(response); | ||||
| 		mPreferences.commit(); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the last license response received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param l the response | ||||
|      */ | ||||
| 	private void setLastResponse(int l) { | ||||
| 		mLastResponseTime = System.currentTimeMillis(); | ||||
| 		mLastResponse = l; | ||||
| 		mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the current retry count and add to preferences. You must manually | ||||
|      * call PreferenceObfuscator.commit() to commit these changes to disk. | ||||
|      * | ||||
|      * @param c the new retry count | ||||
|      */ | ||||
| 	private void setRetryCount(long c) { | ||||
| 		mRetryCount = c; | ||||
| 		mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getRetryCount() { | ||||
| 		return mRetryCount; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the last validity timestamp (VT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param validityTimestamp the VT string received | ||||
|      */ | ||||
| 	private void setValidityTimestamp(String validityTimestamp) { | ||||
| 		Long lValidityTimestamp; | ||||
| 		try { | ||||
| 			lValidityTimestamp = Long.parseLong(validityTimestamp); | ||||
| 		} catch (NumberFormatException e) { | ||||
| 			// No response or not parsable, expire in one minute. | ||||
| 			Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); | ||||
| 			lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; | ||||
| 			validityTimestamp = Long.toString(lValidityTimestamp); | ||||
| 		} | ||||
| 
 | ||||
| 		mValidityTimestamp = lValidityTimestamp; | ||||
| 		mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getValidityTimestamp() { | ||||
| 		return mValidityTimestamp; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the retry until timestamp (GT) received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param retryUntil the GT string received | ||||
|      */ | ||||
| 	private void setRetryUntil(String retryUntil) { | ||||
| 		Long lRetryUntil; | ||||
| 		try { | ||||
| 			lRetryUntil = Long.parseLong(retryUntil); | ||||
| 		} catch (NumberFormatException e) { | ||||
| 			// No response or not parsable, expire immediately | ||||
| 			Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); | ||||
| 			retryUntil = "0"; | ||||
| 			lRetryUntil = 0l; | ||||
| 		} | ||||
| 
 | ||||
| 		mRetryUntil = lRetryUntil; | ||||
| 		mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getRetryUntil() { | ||||
| 		return mRetryUntil; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the max retries value (GR) as received from the server and add to | ||||
|      * preferences. You must manually call PreferenceObfuscator.commit() to | ||||
|      * commit these changes to disk. | ||||
|      * | ||||
|      * @param maxRetries the GR string received | ||||
|      */ | ||||
| 	private void setMaxRetries(String maxRetries) { | ||||
| 		Long lMaxRetries; | ||||
| 		try { | ||||
| 			lMaxRetries = Long.parseLong(maxRetries); | ||||
| 		} catch (NumberFormatException e) { | ||||
| 			// No response or not parsable, expire immediately | ||||
| 			Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); | ||||
| 			maxRetries = "0"; | ||||
| 			lMaxRetries = 0l; | ||||
| 		} | ||||
| 
 | ||||
| 		mMaxRetries = lMaxRetries; | ||||
| 		mPreferences.putString(PREF_MAX_RETRIES, maxRetries); | ||||
| 	} | ||||
| 
 | ||||
| 	public long getMaxRetries() { | ||||
| 		return mMaxRetries; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Set the license URL value (LU) as received from the server and add to preferences. You must | ||||
|      * manually call PreferenceObfuscator.commit() to commit these changes to disk. | ||||
|      * | ||||
|      * @param url the LU string received | ||||
|      */ | ||||
| 	private void setLicensingUrl(String url) { | ||||
| 		mLicensingUrl = url; | ||||
| 		mPreferences.putString(PREF_LICENSING_URL, url); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getLicensingUrl() { | ||||
| 		return mLicensingUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * {@inheritDoc} | ||||
|      * | ||||
|      * This implementation allows access if either:<br> | ||||
|      * <ol> | ||||
|      * <li>a LICENSED response was received within the validity period | ||||
|      * <li>a RETRY response was received in the last minute, and we are under | ||||
|      * the RETRY count or in the RETRY period. | ||||
|      * </ol> | ||||
|      */ | ||||
| 	public boolean allowAccess() { | ||||
| 		long ts = System.currentTimeMillis(); | ||||
| 		if (mLastResponse == Policy.LICENSED) { | ||||
| 			// Check if the LICENSED response occurred within the validity timeout. | ||||
| 			if (ts <= mValidityTimestamp) { | ||||
| 				// Cached LICENSED response is still valid. | ||||
| 				return true; | ||||
| 			} | ||||
| 		} else if (mLastResponse == Policy.RETRY && | ||||
| 				   ts < mLastResponseTime + MILLIS_PER_MINUTE) { | ||||
| 			// Only allow access if we are within the retry period or we haven't used up our | ||||
| 			// max retries. | ||||
| 			return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); | ||||
| 		} | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	private Map<String, String> decodeExtras( | ||||
| 			com.google.android.vending.licensing.ResponseData rawData) { | ||||
| 		Map<String, String> results = new HashMap<String, String>(); | ||||
| 		if (rawData == null) { | ||||
| 			return results; | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			URI rawExtras = new URI("?" + rawData.extra); | ||||
| 			URIQueryDecoder.DecodeQuery(rawExtras, results); | ||||
| 		} catch (URISyntaxException e) { | ||||
| 			Log.w(TAG, "Invalid syntax error while decoding extras data from server."); | ||||
| 		} | ||||
| 		return results; | ||||
| 	} | ||||
| } | ||||
|  | @ -16,6 +16,13 @@ | |||
| 
 | ||||
| package com.google.android.vending.licensing; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| import com.google.android.vending.licensing.util.URIQueryDecoder; | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * Non-caching policy. All requests will be sent to the licensing service, | ||||
|  * and no local caching is performed. | ||||
|  | @ -26,38 +33,67 @@ package com.google.android.vending.licensing; | |||
|  * weigh the risks of using this Policy over one which implements caching, | ||||
|  * such as ServerManagedPolicy. | ||||
|  * <p> | ||||
|  * Access to the application is only allowed if a LICESNED response is. | ||||
|  * Access to the application is only allowed if a LICENSED response is. | ||||
|  * received. All other responses (including RETRY) will deny access. | ||||
|  */ | ||||
| public class StrictPolicy implements Policy { | ||||
| 
 | ||||
|     private int mLastResponse; | ||||
| 	private static final String TAG = "StrictPolicy"; | ||||
| 
 | ||||
|     public StrictPolicy() { | ||||
|         // Set default policy. This will force the application to check the policy on launch. | ||||
|         mLastResponse = Policy.RETRY; | ||||
|     } | ||||
| 	private int mLastResponse; | ||||
| 	private String mLicensingUrl; | ||||
| 
 | ||||
|     /** | ||||
| 	public StrictPolicy() { | ||||
| 		// Set default policy. This will force the application to check the policy on launch. | ||||
| 		mLastResponse = Policy.RETRY; | ||||
| 		mLicensingUrl = null; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * Process a new response from the license server. Since we aren't | ||||
|      * performing any caching, this equates to reading the LicenseResponse. | ||||
|      * Any ResponseData provided is ignored. | ||||
|      * Any cache-related ResponseData is ignored, but the licensing URL | ||||
|      * extra is still extracted in cases where the app is unlicensed. | ||||
|      * | ||||
|      * @param response the result from validating the server response | ||||
|      * @param rawData the raw server response data | ||||
|      */ | ||||
|     public void processServerResponse(int response, ResponseData rawData) { | ||||
|         mLastResponse = response; | ||||
|     } | ||||
| 	public void processServerResponse(int response, ResponseData rawData) { | ||||
| 		mLastResponse = response; | ||||
| 
 | ||||
|     /** | ||||
| 		if (response == Policy.NOT_LICENSED) { | ||||
| 			Map<String, String> extras = decodeExtras(rawData); | ||||
| 			mLicensingUrl = extras.get("LU"); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|      * {@inheritDoc} | ||||
|      * | ||||
|      * This implementation allows access if and only if a LICENSED response | ||||
|      * was received the last time the server was contacted. | ||||
|      */ | ||||
|     public boolean allowAccess() { | ||||
|         return (mLastResponse == Policy.LICENSED); | ||||
|     } | ||||
| 	public boolean allowAccess() { | ||||
| 		return (mLastResponse == Policy.LICENSED); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getLicensingUrl() { | ||||
| 		return mLicensingUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	private Map<String, String> decodeExtras( | ||||
| 			com.google.android.vending.licensing.ResponseData rawData) { | ||||
| 		Map<String, String> results = new HashMap<String, String>(); | ||||
| 		if (rawData == null) { | ||||
| 			return results; | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			URI rawExtras = new URI("?" + rawData.extra); | ||||
| 			URIQueryDecoder.DecodeQuery(rawExtras, results); | ||||
| 		} catch (URISyntaxException e) { | ||||
| 			Log.w(TAG, "Invalid syntax error while decoding extras data from server."); | ||||
| 		} | ||||
| 		return results; | ||||
| 	} | ||||
| } | ||||
|  | @ -21,13 +21,13 @@ package com.google.android.vending.licensing; | |||
|  * {@link Obfuscator}.} | ||||
|  */ | ||||
| public class ValidationException extends Exception { | ||||
|     public ValidationException() { | ||||
|       super(); | ||||
|     } | ||||
| 	public ValidationException() { | ||||
| 		super(); | ||||
| 	} | ||||
| 
 | ||||
|     public ValidationException(String s) { | ||||
|       super(s); | ||||
|     } | ||||
| 	public ValidationException(String s) { | ||||
| 		super(s); | ||||
| 	} | ||||
| 
 | ||||
|     private static final long serialVersionUID = 1L; | ||||
| 	private static final long serialVersionUID = 1L; | ||||
| } | ||||
|  | @ -0,0 +1,556 @@ | |||
| // Portions copyright 2002, Google, Inc. | ||||
| // | ||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| // you may not use this file except in compliance with the License. | ||||
| // You may obtain a copy of the License at | ||||
| // | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| // | ||||
| // Unless required by applicable law or agreed to in writing, software | ||||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| // See the License for the specific language governing permissions and | ||||
| // limitations under the License. | ||||
| 
 | ||||
| package com.google.android.vending.licensing.util; | ||||
| 
 | ||||
| // This code was converted from code at http://iharder.sourceforge.net/base64/ | ||||
| // Lots of extraneous features were removed. | ||||
| /* The original code said: | ||||
|  * <p> | ||||
|  * I am placing this code in the Public Domain. Do with it as you will. | ||||
|  * This software comes with no guarantees or warranties but with | ||||
|  * plenty of well-wishing instead! | ||||
|  * Please visit | ||||
|  * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a> | ||||
|  * periodically to check for updates or to contribute improvements. | ||||
|  * </p> | ||||
|  * | ||||
|  * @author Robert Harder | ||||
|  * @author rharder@usa.net | ||||
|  * @version 1.3 | ||||
|  */ | ||||
| 
 | ||||
| import com.godot.game.BuildConfig; | ||||
| 
 | ||||
| /** | ||||
|  * Base64 converter class. This code is not a full-blown MIME encoder; | ||||
|  * it simply converts binary data to base64 data and back. | ||||
|  * | ||||
|  * <p>Note {@link CharBase64} is a GWT-compatible implementation of this | ||||
|  * class. | ||||
|  */ | ||||
| public class Base64 { | ||||
| 	/** Specify encoding (value is {@code true}). */ | ||||
| 	public final static boolean ENCODE = true; | ||||
| 
 | ||||
| 	/** Specify decoding (value is {@code false}). */ | ||||
| 	public final static boolean DECODE = false; | ||||
| 
 | ||||
| 	/** The equals sign (=) as a byte. */ | ||||
| 	private final static byte EQUALS_SIGN = (byte)'='; | ||||
| 
 | ||||
| 	/** The new line character (\n) as a byte. */ | ||||
| 	private final static byte NEW_LINE = (byte)'\n'; | ||||
| 
 | ||||
| 	/** | ||||
|    * The 64 valid Base64 values. | ||||
|    */ | ||||
| 	private final static byte[] ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', | ||||
| 		(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', | ||||
| 		(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', | ||||
| 		(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', | ||||
| 		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', | ||||
| 		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', | ||||
| 		(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', | ||||
| 		(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', | ||||
| 		(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', | ||||
| 		(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', | ||||
| 		(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', | ||||
| 		(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', | ||||
| 		(byte)'9', (byte)'+', (byte)'/' }; | ||||
| 
 | ||||
| 	/** | ||||
|    * The 64 valid web safe Base64 values. | ||||
|    */ | ||||
| 	private final static byte[] WEBSAFE_ALPHABET = { (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', | ||||
| 		(byte)'G', (byte)'H', (byte)'I', (byte)'J', (byte)'K', | ||||
| 		(byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', | ||||
| 		(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', | ||||
| 		(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', | ||||
| 		(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', | ||||
| 		(byte)'f', (byte)'g', (byte)'h', (byte)'i', (byte)'j', | ||||
| 		(byte)'k', (byte)'l', (byte)'m', (byte)'n', (byte)'o', | ||||
| 		(byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', | ||||
| 		(byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', | ||||
| 		(byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3', | ||||
| 		(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', | ||||
| 		(byte)'9', (byte)'-', (byte)'_' }; | ||||
| 
 | ||||
| 	/** | ||||
|    * Translates a Base64 value to either its 6-bit reconstruction value | ||||
|    * or a negative number indicating some other meaning. | ||||
|    **/ | ||||
| 	private final static byte[] DECODABET = { | ||||
| 		-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 | ||||
| 		-5, -5, // Whitespace: Tab and Linefeed | ||||
| 		-9, -9, // Decimal 11 - 12 | ||||
| 		-5, // Whitespace: Carriage Return | ||||
| 		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 | ||||
| 		-9, -9, -9, -9, -9, // Decimal 27 - 31 | ||||
| 		-5, // Whitespace: Space | ||||
| 		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 | ||||
| 		62, // Plus sign at decimal 43 | ||||
| 		-9, -9, -9, // Decimal 44 - 46 | ||||
| 		63, // Slash at decimal 47 | ||||
| 		52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine | ||||
| 		-9, -9, -9, // Decimal 58 - 60 | ||||
| 		-1, // Equals sign at decimal 61 | ||||
| 		-9, -9, -9, // Decimal 62 - 64 | ||||
| 		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' | ||||
| 		14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' | ||||
| 		-9, -9, -9, -9, -9, -9, // Decimal 91 - 96 | ||||
| 		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' | ||||
| 		39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' | ||||
| 		-9, -9, -9, -9, -9 // Decimal 123 - 127 | ||||
| 		/*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ | ||||
| 	}; | ||||
| 
 | ||||
| 	/** The web safe decodabet */ | ||||
| 	private final static byte[] WEBSAFE_DECODABET = { | ||||
| 		-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8 | ||||
| 		-5, -5, // Whitespace: Tab and Linefeed | ||||
| 		-9, -9, // Decimal 11 - 12 | ||||
| 		-5, // Whitespace: Carriage Return | ||||
| 		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 | ||||
| 		-9, -9, -9, -9, -9, // Decimal 27 - 31 | ||||
| 		-5, // Whitespace: Space | ||||
| 		-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 | ||||
| 		62, // Dash '-' sign at decimal 45 | ||||
| 		-9, -9, // Decimal 46-47 | ||||
| 		52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine | ||||
| 		-9, -9, -9, // Decimal 58 - 60 | ||||
| 		-1, // Equals sign at decimal 61 | ||||
| 		-9, -9, -9, // Decimal 62 - 64 | ||||
| 		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' | ||||
| 		14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' | ||||
| 		-9, -9, -9, -9, // Decimal 91-94 | ||||
| 		63, // Underscore '_' at decimal 95 | ||||
| 		-9, // Decimal 96 | ||||
| 		26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' | ||||
| 		39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' | ||||
| 		-9, -9, -9, -9, -9 // Decimal 123 - 127 | ||||
| 		/*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243 | ||||
|         -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */ | ||||
| 	}; | ||||
| 
 | ||||
| 	// Indicates white space in encoding | ||||
| 	private final static byte WHITE_SPACE_ENC = -5; | ||||
| 	// Indicates equals sign in encoding | ||||
| 	private final static byte EQUALS_SIGN_ENC = -1; | ||||
| 
 | ||||
| 	/** Defeats instantiation. */ | ||||
| 	private Base64() { | ||||
| 	} | ||||
| 
 | ||||
| 	/* ********  E N C O D I N G   M E T H O D S  ******** */ | ||||
| 
 | ||||
| 	/** | ||||
|    * Encodes up to three bytes of the array <var>source</var> | ||||
|    * and writes the resulting four Base64 bytes to <var>destination</var>. | ||||
|    * The source and destination arrays can be manipulated | ||||
|    * anywhere along their length by specifying | ||||
|    * <var>srcOffset</var> and <var>destOffset</var>. | ||||
|    * This method does not check to make sure your arrays | ||||
|    * are large enough to accommodate <var>srcOffset</var> + 3 for | ||||
|    * the <var>source</var> array or <var>destOffset</var> + 4 for | ||||
|    * the <var>destination</var> array. | ||||
|    * The actual number of significant bytes in your array is | ||||
|    * given by <var>numSigBytes</var>. | ||||
|    * | ||||
|    * @param source the array to convert | ||||
|    * @param srcOffset the index where conversion begins | ||||
|    * @param numSigBytes the number of significant bytes in your array | ||||
|    * @param destination the array to hold the conversion | ||||
|    * @param destOffset the index where output will be put | ||||
|    * @param alphabet is the encoding alphabet | ||||
|    * @return the <var>destination</var> array | ||||
|    * @since 1.3 | ||||
|    */ | ||||
| 	private static byte[] encode3to4(byte[] source, int srcOffset, | ||||
| 			int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { | ||||
| 		//           1         2         3 | ||||
| 		// 01234567890123456789012345678901 Bit position | ||||
| 		// --------000000001111111122222222 Array position from threeBytes | ||||
| 		// --------|    ||    ||    ||    | Six bit groups to index alphabet | ||||
| 		//          >>18  >>12  >> 6  >> 0  Right shift necessary | ||||
| 		//                0x3f  0x3f  0x3f  Additional AND | ||||
| 
 | ||||
| 		// Create buffer with zero-padding if there are only one or two | ||||
| 		// significant bytes passed in the array. | ||||
| 		// We have to shift left 24 in order to flush out the 1's that appear | ||||
| 		// when Java treats a value as negative that is cast from a byte to an int. | ||||
| 		int inBuff = | ||||
| 				(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); | ||||
| 
 | ||||
| 		switch (numSigBytes) { | ||||
| 			case 3: | ||||
| 				destination[destOffset] = alphabet[(inBuff >>> 18)]; | ||||
| 				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
| 				destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; | ||||
| 				destination[destOffset + 3] = alphabet[(inBuff)&0x3f]; | ||||
| 				return destination; | ||||
| 			case 2: | ||||
| 				destination[destOffset] = alphabet[(inBuff >>> 18)]; | ||||
| 				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
| 				destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; | ||||
| 				destination[destOffset + 3] = EQUALS_SIGN; | ||||
| 				return destination; | ||||
| 			case 1: | ||||
| 				destination[destOffset] = alphabet[(inBuff >>> 18)]; | ||||
| 				destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
| 				destination[destOffset + 2] = EQUALS_SIGN; | ||||
| 				destination[destOffset + 3] = EQUALS_SIGN; | ||||
| 				return destination; | ||||
| 			default: | ||||
| 				return destination; | ||||
| 		} // end switch | ||||
| 	} // end encode3to4 | ||||
| 
 | ||||
| 	/** | ||||
|    * Encodes a byte array into Base64 notation. | ||||
|    * Equivalent to calling | ||||
|    * {@code encodeBytes(source, 0, source.length)} | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @since 1.4 | ||||
|    */ | ||||
| 	public static String encode(byte[] source) { | ||||
| 		return encode(source, 0, source.length, ALPHABET, true); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Encodes a byte array into web safe Base64 notation. | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @param doPadding is {@code true} to pad result with '=' chars | ||||
|    *        if it does not fall on 3 byte boundaries | ||||
|    */ | ||||
| 	public static String encodeWebSafe(byte[] source, boolean doPadding) { | ||||
| 		return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Encodes a byte array into Base64 notation. | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @param off Offset in array where conversion should begin | ||||
|    * @param len Length of data to convert | ||||
|    * @param alphabet is the encoding alphabet | ||||
|    * @param doPadding is {@code true} to pad result with '=' chars | ||||
|    *        if it does not fall on 3 byte boundaries | ||||
|    * @since 1.4 | ||||
|    */ | ||||
| 	public static String encode(byte[] source, int off, int len, byte[] alphabet, | ||||
| 			boolean doPadding) { | ||||
| 		byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); | ||||
| 		int outLen = outBuff.length; | ||||
| 
 | ||||
| 		// If doPadding is false, set length to truncate '=' | ||||
| 		// padding characters | ||||
| 		while (doPadding == false && outLen > 0) { | ||||
| 			if (outBuff[outLen - 1] != '=') { | ||||
| 				break; | ||||
| 			} | ||||
| 			outLen -= 1; | ||||
| 		} | ||||
| 
 | ||||
| 		return new String(outBuff, 0, outLen); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Encodes a byte array into Base64 notation. | ||||
|    * | ||||
|    * @param source The data to convert | ||||
|    * @param off Offset in array where conversion should begin | ||||
|    * @param len Length of data to convert | ||||
|    * @param alphabet is the encoding alphabet | ||||
|    * @param maxLineLength maximum length of one line. | ||||
|    * @return the BASE64-encoded byte array | ||||
|    */ | ||||
| 	public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, | ||||
| 			int maxLineLength) { | ||||
| 		int lenDiv3 = (len + 2) / 3; // ceil(len / 3) | ||||
| 		int len43 = lenDiv3 * 4; | ||||
| 		byte[] outBuff = new byte[len43 // Main 4:3 | ||||
| 								  + (len43 / maxLineLength)]; // New lines | ||||
| 
 | ||||
| 		int d = 0; | ||||
| 		int e = 0; | ||||
| 		int len2 = len - 2; | ||||
| 		int lineLength = 0; | ||||
| 		for (; d < len2; d += 3, e += 4) { | ||||
| 
 | ||||
| 			// The following block of code is the same as | ||||
| 			// encode3to4( source, d + off, 3, outBuff, e, alphabet ); | ||||
| 			// but inlined for faster encoding (~20% improvement) | ||||
| 			int inBuff = | ||||
| 					((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24); | ||||
| 			outBuff[e] = alphabet[(inBuff >>> 18)]; | ||||
| 			outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; | ||||
| 			outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; | ||||
| 			outBuff[e + 3] = alphabet[(inBuff)&0x3f]; | ||||
| 
 | ||||
| 			lineLength += 4; | ||||
| 			if (lineLength == maxLineLength) { | ||||
| 				outBuff[e + 4] = NEW_LINE; | ||||
| 				e++; | ||||
| 				lineLength = 0; | ||||
| 			} // end if: end of line | ||||
| 		} // end for: each piece of array | ||||
| 
 | ||||
| 		if (d < len) { | ||||
| 			encode3to4(source, d + off, len - d, outBuff, e, alphabet); | ||||
| 
 | ||||
| 			lineLength += 4; | ||||
| 			if (lineLength == maxLineLength) { | ||||
| 				// Add a last newline | ||||
| 				outBuff[e + 4] = NEW_LINE; | ||||
| 				e++; | ||||
| 			} | ||||
| 			e += 4; | ||||
| 		} | ||||
| 
 | ||||
| 		if (BuildConfig.DEBUG && e != outBuff.length) | ||||
| 			throw new RuntimeException(); | ||||
| 		return outBuff; | ||||
| 	} | ||||
| 
 | ||||
| 	/* ********  D E C O D I N G   M E T H O D S  ******** */ | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes four bytes from array <var>source</var> | ||||
|    * and writes the resulting bytes (up to three of them) | ||||
|    * to <var>destination</var>. | ||||
|    * The source and destination arrays can be manipulated | ||||
|    * anywhere along their length by specifying | ||||
|    * <var>srcOffset</var> and <var>destOffset</var>. | ||||
|    * This method does not check to make sure your arrays | ||||
|    * are large enough to accommodate <var>srcOffset</var> + 4 for | ||||
|    * the <var>source</var> array or <var>destOffset</var> + 3 for | ||||
|    * the <var>destination</var> array. | ||||
|    * This method returns the actual number of bytes that | ||||
|    * were converted from the Base64 encoding. | ||||
|    * | ||||
|    * | ||||
|    * @param source the array to convert | ||||
|    * @param srcOffset the index where conversion begins | ||||
|    * @param destination the array to hold the conversion | ||||
|    * @param destOffset the index where output will be put | ||||
|    * @param decodabet the decodabet for decoding Base64 content | ||||
|    * @return the number of decoded bytes converted | ||||
|    * @since 1.3 | ||||
|    */ | ||||
| 	private static int decode4to3(byte[] source, int srcOffset, | ||||
| 			byte[] destination, int destOffset, byte[] decodabet) { | ||||
| 		// Example: Dk== | ||||
| 		if (source[srcOffset + 2] == EQUALS_SIGN) { | ||||
| 			int outBuff = | ||||
| 					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); | ||||
| 
 | ||||
| 			destination[destOffset] = (byte)(outBuff >>> 16); | ||||
| 			return 1; | ||||
| 		} else if (source[srcOffset + 3] == EQUALS_SIGN) { | ||||
| 			// Example: DkL= | ||||
| 			int outBuff = | ||||
| 					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); | ||||
| 
 | ||||
| 			destination[destOffset] = (byte)(outBuff >>> 16); | ||||
| 			destination[destOffset + 1] = (byte)(outBuff >>> 8); | ||||
| 			return 2; | ||||
| 		} else { | ||||
| 			// Example: DkLE | ||||
| 			int outBuff = | ||||
| 					((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); | ||||
| 
 | ||||
| 			destination[destOffset] = (byte)(outBuff >> 16); | ||||
| 			destination[destOffset + 1] = (byte)(outBuff >> 8); | ||||
| 			destination[destOffset + 2] = (byte)(outBuff); | ||||
| 			return 3; | ||||
| 		} | ||||
| 	} // end decodeToBytes | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes data from Base64 notation. | ||||
|    * | ||||
|    * @param s the string to decode (decoded in default encoding) | ||||
|    * @return the decoded data | ||||
|    * @since 1.4 | ||||
|    */ | ||||
| 	public static byte[] decode(String s) throws Base64DecoderException { | ||||
| 		byte[] bytes = s.getBytes(); | ||||
| 		return decode(bytes, 0, bytes.length); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes data from web safe Base64 notation. | ||||
|    * Web safe encoding uses '-' instead of '+', '_' instead of '/' | ||||
|    * | ||||
|    * @param s the string to decode (decoded in default encoding) | ||||
|    * @return the decoded data | ||||
|    */ | ||||
| 	public static byte[] decodeWebSafe(String s) throws Base64DecoderException { | ||||
| 		byte[] bytes = s.getBytes(); | ||||
| 		return decodeWebSafe(bytes, 0, bytes.length); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes Base64 content in byte array format and returns | ||||
|    * the decoded byte array. | ||||
|    * | ||||
|    * @param source The Base64 encoded data | ||||
|    * @return decoded data | ||||
|    * @since 1.3 | ||||
|    * @throws Base64DecoderException | ||||
|    */ | ||||
| 	public static byte[] decode(byte[] source) throws Base64DecoderException { | ||||
| 		return decode(source, 0, source.length); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes web safe Base64 content in byte array format and returns | ||||
|    * the decoded data. | ||||
|    * Web safe encoding uses '-' instead of '+', '_' instead of '/' | ||||
|    * | ||||
|    * @param source the string to decode (decoded in default encoding) | ||||
|    * @return the decoded data | ||||
|    */ | ||||
| 	public static byte[] decodeWebSafe(byte[] source) | ||||
| 			throws Base64DecoderException { | ||||
| 		return decodeWebSafe(source, 0, source.length); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes Base64 content in byte array format and returns | ||||
|    * the decoded byte array. | ||||
|    * | ||||
|    * @param source The Base64 encoded data | ||||
|    * @param off    The offset of where to begin decoding | ||||
|    * @param len    The length of characters to decode | ||||
|    * @return decoded data | ||||
|    * @since 1.3 | ||||
|    * @throws Base64DecoderException | ||||
|    */ | ||||
| 	public static byte[] decode(byte[] source, int off, int len) | ||||
| 			throws Base64DecoderException { | ||||
| 		return decode(source, off, len, DECODABET); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes web safe Base64 content in byte array format and returns | ||||
|    * the decoded byte array. | ||||
|    * Web safe encoding uses '-' instead of '+', '_' instead of '/' | ||||
|    * | ||||
|    * @param source The Base64 encoded data | ||||
|    * @param off    The offset of where to begin decoding | ||||
|    * @param len    The length of characters to decode | ||||
|    * @return decoded data | ||||
|    */ | ||||
| 	public static byte[] decodeWebSafe(byte[] source, int off, int len) | ||||
| 			throws Base64DecoderException { | ||||
| 		return decode(source, off, len, WEBSAFE_DECODABET); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|    * Decodes Base64 content using the supplied decodabet and returns | ||||
|    * the decoded byte array. | ||||
|    * | ||||
|    * @param source    The Base64 encoded data | ||||
|    * @param off       The offset of where to begin decoding | ||||
|    * @param len       The length of characters to decode | ||||
|    * @param decodabet the decodabet for decoding Base64 content | ||||
|    * @return decoded data | ||||
|    */ | ||||
| 	public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) | ||||
| 			throws Base64DecoderException { | ||||
| 		int len34 = len * 3 / 4; | ||||
| 		byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output | ||||
| 		int outBuffPosn = 0; | ||||
| 
 | ||||
| 		byte[] b4 = new byte[4]; | ||||
| 		int b4Posn = 0; | ||||
| 		int i = 0; | ||||
| 		byte sbiCrop = 0; | ||||
| 		byte sbiDecode = 0; | ||||
| 		for (i = 0; i < len; i++) { | ||||
| 			sbiCrop = (byte)(source[i + off] & 0x7f); // Only the low seven bits | ||||
| 			sbiDecode = decodabet[sbiCrop]; | ||||
| 
 | ||||
| 			if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better | ||||
| 				if (sbiDecode >= EQUALS_SIGN_ENC) { | ||||
| 					// An equals sign (for padding) must not occur at position 0 or 1 | ||||
| 					// and must be the last byte[s] in the encoded value | ||||
| 					if (sbiCrop == EQUALS_SIGN) { | ||||
| 						int bytesLeft = len - i; | ||||
| 						byte lastByte = (byte)(source[len - 1 + off] & 0x7f); | ||||
| 						if (b4Posn == 0 || b4Posn == 1) { | ||||
| 							throw new Base64DecoderException( | ||||
| 									"invalid padding byte '=' at byte offset " + i); | ||||
| 						} else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) { | ||||
| 							throw new Base64DecoderException( | ||||
| 									"padding byte '=' falsely signals end of encoded value " | ||||
| 									+ "at offset " + i); | ||||
| 						} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { | ||||
| 							throw new Base64DecoderException( | ||||
| 									"encoded value has invalid trailing byte"); | ||||
| 						} | ||||
| 						break; | ||||
| 					} | ||||
| 
 | ||||
| 					b4[b4Posn++] = sbiCrop; | ||||
| 					if (b4Posn == 4) { | ||||
| 						outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); | ||||
| 						b4Posn = 0; | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)"); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// Because web safe encoding allows non padding base64 encodes, we | ||||
| 		// need to pad the rest of the b4 buffer with equal signs when | ||||
| 		// b4Posn != 0.  There can be at most 2 equal signs at the end of | ||||
| 		// four characters, so the b4 buffer must have two or three | ||||
| 		// characters.  This also catches the case where the input is | ||||
| 		// padded with EQUALS_SIGN | ||||
| 		if (b4Posn != 0) { | ||||
| 			if (b4Posn == 1) { | ||||
| 				throw new Base64DecoderException("single trailing character at offset " + (len - 1)); | ||||
| 			} | ||||
| 			b4[b4Posn++] = EQUALS_SIGN; | ||||
| 			outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); | ||||
| 		} | ||||
| 
 | ||||
| 		byte[] out = new byte[outBuffPosn]; | ||||
| 		System.arraycopy(outBuff, 0, out, 0, outBuffPosn); | ||||
| 		return out; | ||||
| 	} | ||||
| } | ||||
|  | @ -20,13 +20,13 @@ package com.google.android.vending.licensing.util; | |||
|  * @author nelson | ||||
|  */ | ||||
| public class Base64DecoderException extends Exception { | ||||
|   public Base64DecoderException() { | ||||
|     super(); | ||||
|   } | ||||
| 	public Base64DecoderException() { | ||||
| 		super(); | ||||
| 	} | ||||
| 
 | ||||
|   public Base64DecoderException(String s) { | ||||
|     super(s); | ||||
|   } | ||||
| 	public Base64DecoderException(String s) { | ||||
| 		super(s); | ||||
| 	} | ||||
| 
 | ||||
|   private static final long serialVersionUID = 1L; | ||||
| 	private static final long serialVersionUID = 1L; | ||||
| } | ||||
|  | @ -0,0 +1,60 @@ | |||
| /* | ||||
|  * Copyright (C) 2016 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package com.google.android.vending.licensing.util; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.net.URI; | ||||
| import java.net.URLDecoder; | ||||
| import java.util.Map; | ||||
| import java.util.Scanner; | ||||
| 
 | ||||
| public class URIQueryDecoder { | ||||
| 	private static final String TAG = "URIQueryDecoder"; | ||||
| 
 | ||||
| 	/** | ||||
|      * Decodes the query portion of the passed-in URI. | ||||
|      * | ||||
|      * @param encodedURI the URI containing the query to decode | ||||
|      * @param results a map containing all query parameters. Query parameters that do not have a | ||||
|      *            value will map to a null string | ||||
|      */ | ||||
| 	static public void DecodeQuery(URI encodedURI, Map<String, String> results) { | ||||
| 		Scanner scanner = new Scanner(encodedURI.getRawQuery()); | ||||
| 		scanner.useDelimiter("&"); | ||||
| 		try { | ||||
| 			while (scanner.hasNext()) { | ||||
| 				String param = scanner.next(); | ||||
| 				String[] valuePair = param.split("="); | ||||
| 				String name, value; | ||||
| 				if (valuePair.length == 1) { | ||||
| 					value = null; | ||||
| 				} else if (valuePair.length == 2) { | ||||
| 					value = URLDecoder.decode(valuePair[1], "UTF-8"); | ||||
| 				} else { | ||||
| 					throw new IllegalArgumentException("query parameter invalid"); | ||||
| 				} | ||||
| 				name = URLDecoder.decode(valuePair[0], "UTF-8"); | ||||
| 				results.put(name, value); | ||||
| 			} | ||||
| 		} catch (UnsupportedEncodingException e) { | ||||
| 			// This should never happen. | ||||
| 			Log.e(TAG, "UTF-8 Not Recognized as a charset.  Device configuration Error."); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -30,59 +30,48 @@ | |||
| 
 | ||||
| package org.godotengine.godot; | ||||
| 
 | ||||
| import android.R; | ||||
| //import android.R; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.app.ActivityManager; | ||||
| import android.app.AlertDialog; | ||||
| import android.app.PendingIntent; | ||||
| import android.content.ClipData; | ||||
| import android.content.ClipboardManager; | ||||
| import android.content.Context; | ||||
| import android.content.DialogInterface; | ||||
| import android.content.Intent; | ||||
| import android.content.SharedPreferences; | ||||
| import android.content.SharedPreferences.Editor; | ||||
| import android.content.pm.ConfigurationInfo; | ||||
| import android.content.pm.PackageManager.NameNotFoundException; | ||||
| import android.graphics.Point; | ||||
| import android.graphics.Rect; | ||||
| import android.hardware.Sensor; | ||||
| import android.hardware.SensorEvent; | ||||
| import android.hardware.SensorEventListener; | ||||
| import android.hardware.SensorManager; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.os.Environment; | ||||
| import android.os.Messenger; | ||||
| import android.provider.Settings.Secure; | ||||
| import android.util.Log; | ||||
| import android.view.Display; | ||||
| import android.view.KeyEvent; | ||||
| import android.view.MotionEvent; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.view.ViewGroup.LayoutParams; | ||||
| import android.view.ViewTreeObserver; | ||||
| import android.view.Window; | ||||
| import android.view.WindowManager; | ||||
| import android.widget.Button; | ||||
| import android.widget.FrameLayout; | ||||
| import android.widget.ProgressBar; | ||||
| import android.widget.RelativeLayout; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.TextView; | ||||
| import android.view.ViewGroup.LayoutParams; | ||||
| import android.app.*; | ||||
| import android.content.*; | ||||
| import android.content.SharedPreferences.Editor; | ||||
| import android.view.*; | ||||
| import android.view.inputmethod.InputMethodManager; | ||||
| import android.os.*; | ||||
| import android.util.Log; | ||||
| import android.graphics.*; | ||||
| import android.text.method.*; | ||||
| import android.text.*; | ||||
| import android.media.*; | ||||
| import android.hardware.*; | ||||
| import android.content.*; | ||||
| import android.content.pm.PackageManager.NameNotFoundException; | ||||
| import android.net.Uri; | ||||
| import android.media.MediaPlayer; | ||||
| 
 | ||||
| import android.content.ClipboardManager; | ||||
| import android.content.ClipData; | ||||
| 
 | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.List; | ||||
| import java.util.ArrayList; | ||||
| 
 | ||||
| import org.godotengine.godot.payments.PaymentsManager; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| import android.provider.Settings.Secure; | ||||
| import android.widget.FrameLayout; | ||||
| 
 | ||||
| import org.godotengine.godot.input.*; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| import javax.microedition.khronos.opengles.GL10; | ||||
| import java.security.MessageDigest; | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.util.LinkedList; | ||||
| 
 | ||||
| import com.google.android.vending.expansion.downloader.Constants; | ||||
| import com.google.android.vending.expansion.downloader.DownloadProgressInfo; | ||||
| import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller; | ||||
| import com.google.android.vending.expansion.downloader.DownloaderServiceMarshaller; | ||||
|  | @ -91,9 +80,20 @@ import com.google.android.vending.expansion.downloader.IDownloaderClient; | |||
| import com.google.android.vending.expansion.downloader.IDownloaderService; | ||||
| import com.google.android.vending.expansion.downloader.IStub; | ||||
| 
 | ||||
| import android.os.Bundle; | ||||
| import android.os.Messenger; | ||||
| import android.os.SystemClock; | ||||
| import org.godotengine.godot.input.GodotEditText; | ||||
| import org.godotengine.godot.payments.PaymentsManager; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.InputStream; | ||||
| import java.lang.reflect.Method; | ||||
| import java.security.MessageDigest; | ||||
| import java.util.ArrayList; | ||||
| import java.util.LinkedList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| 
 | ||||
| import javax.microedition.khronos.opengles.GL10; | ||||
| 
 | ||||
| public class Godot extends Activity implements SensorEventListener, IDownloaderClient { | ||||
| 
 | ||||
|  | @ -222,9 +222,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 	private Sensor mGyroscope; | ||||
| 
 | ||||
| 	public FrameLayout layout; | ||||
| 	public RelativeLayout adLayout; | ||||
| 
 | ||||
| 	static public GodotIO io; | ||||
| 	public static GodotIO io; | ||||
| 
 | ||||
| 	public static void setWindowTitle(String title) { | ||||
| 		//setTitle(title); | ||||
|  | @ -263,24 +262,23 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 	}; | ||||
| 
 | ||||
| 	public void onVideoInit() { | ||||
| 
 | ||||
| 		boolean use_gl3 = getGLESVersionCode() >= 0x00030000; | ||||
| 
 | ||||
| 		//mView = new GodotView(getApplication(),io,use_gl3); | ||||
| 		//setContentView(mView); | ||||
| 
 | ||||
| 		layout = new FrameLayout(this); | ||||
| 		layout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); | ||||
| 		layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); | ||||
| 		setContentView(layout); | ||||
| 
 | ||||
| 		// GodotEditText layout | ||||
| 		GodotEditText edittext = new GodotEditText(this); | ||||
| 		edittext.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); | ||||
| 		edittext.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); | ||||
| 		// ...add to FrameLayout | ||||
| 		layout.addView(edittext); | ||||
| 
 | ||||
| 		mView = new GodotView(getApplication(), io, use_gl3, use_32_bits, use_debug_opengl, this); | ||||
| 		layout.addView(mView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); | ||||
| 		layout.addView(mView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); | ||||
| 		edittext.setView(mView); | ||||
| 		io.setEdit(edittext); | ||||
| 
 | ||||
|  | @ -298,11 +296,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		// Ad layout | ||||
| 		adLayout = new RelativeLayout(this); | ||||
| 		adLayout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); | ||||
| 		layout.addView(adLayout); | ||||
| 
 | ||||
| 		final String[] current_command_line = command_line; | ||||
| 		final GodotView view = mView; | ||||
| 		mView.queueEvent(new Runnable() { | ||||
|  | @ -332,10 +325,11 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 	} | ||||
| 
 | ||||
| 	public void alert(final String message, final String title) { | ||||
| 		final Activity activity = this; | ||||
| 		runOnUiThread(new Runnable() { | ||||
| 			@Override | ||||
| 			public void run() { | ||||
| 				AlertDialog.Builder builder = new AlertDialog.Builder(getInstance()); | ||||
| 				AlertDialog.Builder builder = new AlertDialog.Builder(activity); | ||||
| 				builder.setMessage(message).setTitle(title); | ||||
| 				builder.setPositiveButton( | ||||
| 						"OK", | ||||
|  | @ -350,14 +344,8 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	private static Godot _self; | ||||
| 
 | ||||
| 	public static Godot getInstance() { | ||||
| 		return Godot._self; | ||||
| 	} | ||||
| 
 | ||||
| 	public int getGLESVersionCode() { | ||||
| 		ActivityManager am = (ActivityManager)Godot.getInstance().getSystemService(Context.ACTIVITY_SERVICE); | ||||
| 		ActivityManager am = (ActivityManager)this.getSystemService(Context.ACTIVITY_SERVICE); | ||||
| 		ConfigurationInfo deviceInfo = am.getDeviceConfigurationInfo(); | ||||
| 		return deviceInfo.reqGlEsVersion; | ||||
| 	} | ||||
|  | @ -421,7 +409,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 		} | ||||
| 
 | ||||
| 		io = new GodotIO(this); | ||||
| 		io.unique_id = Secure.getString(getContentResolver(), Secure.ANDROID_ID); | ||||
| 		io.unique_id = Secure.ANDROID_ID; | ||||
| 		GodotLib.io = io; | ||||
| 		mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); | ||||
| 		mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); | ||||
|  | @ -452,7 +440,6 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 	protected void onCreate(Bundle icicle) { | ||||
| 
 | ||||
| 		super.onCreate(icicle); | ||||
| 		_self = this; | ||||
| 		Window window = getWindow(); | ||||
| 		//window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); | ||||
| 		window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); | ||||
|  | @ -476,7 +463,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 					use_debug_opengl = true; | ||||
| 				} else if (command_line[i].equals("--use_immersive")) { | ||||
| 					use_immersive = true; | ||||
| 					if (Build.VERSION.SDK_INT >= 19.0) { // check if the application runs on an android 4.4+ | ||||
| 					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ | ||||
| 						window.getDecorView().setSystemUiVisibility( | ||||
| 								View.SYSTEM_UI_FLAG_LAYOUT_STABLE | | ||||
| 								View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||||
|  | @ -498,7 +485,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 					Editor editor = prefs.edit(); | ||||
| 					editor.putString("store_public_key", main_pack_key); | ||||
| 
 | ||||
| 					editor.commit(); | ||||
| 					editor.apply(); | ||||
| 					i++; | ||||
| 				} else if (command_line[i].trim().length() != 0) { | ||||
| 					new_args.add(command_line[i]); | ||||
|  | @ -665,7 +652,7 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 		mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); | ||||
| 		mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); | ||||
| 
 | ||||
| 		if (use_immersive && Build.VERSION.SDK_INT >= 19.0) { // check if the application runs on an android 4.4+ | ||||
| 		if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ | ||||
| 			Window window = getWindow(); | ||||
| 			window.getDecorView().setSystemUiVisibility( | ||||
| 					View.SYSTEM_UI_FLAG_LAYOUT_STABLE | | ||||
|  | @ -688,13 +675,15 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 			@Override | ||||
| 			public void onSystemUiVisibilityChange(int visibility) { | ||||
| 				if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { | ||||
| 					decorView.setSystemUiVisibility( | ||||
| 							View.SYSTEM_UI_FLAG_LAYOUT_STABLE | | ||||
| 							View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||||
| 							View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||||
| 							View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||||
| 							View.SYSTEM_UI_FLAG_FULLSCREEN | | ||||
| 							View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); | ||||
| 					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | ||||
| 						decorView.setSystemUiVisibility( | ||||
| 								View.SYSTEM_UI_FLAG_LAYOUT_STABLE | | ||||
| 								View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||||
| 								View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||||
| 								View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||||
| 								View.SYSTEM_UI_FLAG_FULLSCREEN | | ||||
| 								View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | @ -1024,12 +1013,9 @@ public class Godot extends Activity implements SensorEventListener, IDownloaderC | |||
| 		mTimeRemaining.setText(getString(com.godot.game.R.string.time_remaining, | ||||
| 				Helpers.getTimeRemaining(progress.mTimeRemaining))); | ||||
| 
 | ||||
| 		progress.mOverallTotal = progress.mOverallTotal; | ||||
| 		mPB.setMax((int)(progress.mOverallTotal >> 8)); | ||||
| 		mPB.setProgress((int)(progress.mOverallProgress >> 8)); | ||||
| 		mProgressPercent.setText(Long.toString(progress.mOverallProgress * 100 / | ||||
| 											   progress.mOverallTotal) + | ||||
| 								 "%"); | ||||
| 		mProgressPercent.setText(String.format(Locale.ENGLISH, "%d %%", progress.mOverallProgress * 100 / progress.mOverallTotal)); | ||||
| 		mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, | ||||
| 				progress.mOverallTotal)); | ||||
| 	} | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ import java.io.InputStream; | |||
| import java.io.IOException; | ||||
| import android.app.*; | ||||
| import android.content.*; | ||||
| import android.util.SparseArray; | ||||
| import android.view.*; | ||||
| import android.view.inputmethod.InputMethodManager; | ||||
| import android.os.*; | ||||
|  | @ -61,7 +62,6 @@ public class GodotIO { | |||
| 	Godot activity; | ||||
| 	GodotEditText edit; | ||||
| 
 | ||||
| 	Context applicationContext; | ||||
| 	MediaPlayer mediaPlayer; | ||||
| 
 | ||||
| 	final int SCREEN_LANDSCAPE = 0; | ||||
|  | @ -87,7 +87,7 @@ public class GodotIO { | |||
| 		public int pos; | ||||
| 	} | ||||
| 
 | ||||
| 	HashMap<Integer, AssetData> streams; | ||||
| 	SparseArray<AssetData> streams; | ||||
| 
 | ||||
| 	public int file_open(String path, boolean write) { | ||||
| 
 | ||||
|  | @ -125,7 +125,7 @@ public class GodotIO { | |||
| 	} | ||||
| 	public int file_get_size(int id) { | ||||
| 
 | ||||
| 		if (!streams.containsKey(id)) { | ||||
| 		if (streams.get(id) == null) { | ||||
| 			System.out.printf("file_get_size: Invalid file id: %d\n", id); | ||||
| 			return -1; | ||||
| 		} | ||||
|  | @ -134,7 +134,7 @@ public class GodotIO { | |||
| 	} | ||||
| 	public void file_seek(int id, int bytes) { | ||||
| 
 | ||||
| 		if (!streams.containsKey(id)) { | ||||
| 		if (streams.get(id) == null) { | ||||
| 			System.out.printf("file_get_size: Invalid file id: %d\n", id); | ||||
| 			return; | ||||
| 		} | ||||
|  | @ -174,7 +174,7 @@ public class GodotIO { | |||
| 
 | ||||
| 	public int file_tell(int id) { | ||||
| 
 | ||||
| 		if (!streams.containsKey(id)) { | ||||
| 		if (streams.get(id) == null) { | ||||
| 			System.out.printf("file_read: Can't tell eof for invalid file id: %d\n", id); | ||||
| 			return 0; | ||||
| 		} | ||||
|  | @ -184,7 +184,7 @@ public class GodotIO { | |||
| 	} | ||||
| 	public boolean file_eof(int id) { | ||||
| 
 | ||||
| 		if (!streams.containsKey(id)) { | ||||
| 		if (streams.get(id) == null) { | ||||
| 			System.out.printf("file_read: Can't check eof for invalid file id: %d\n", id); | ||||
| 			return false; | ||||
| 		} | ||||
|  | @ -195,7 +195,7 @@ public class GodotIO { | |||
| 
 | ||||
| 	public byte[] file_read(int id, int bytes) { | ||||
| 
 | ||||
| 		if (!streams.containsKey(id)) { | ||||
| 		if (streams.get(id) == null) { | ||||
| 			System.out.printf("file_read: Can't read invalid file id: %d\n", id); | ||||
| 			return new byte[0]; | ||||
| 		} | ||||
|  | @ -243,7 +243,7 @@ public class GodotIO { | |||
| 
 | ||||
| 	public void file_close(int id) { | ||||
| 
 | ||||
| 		if (!streams.containsKey(id)) { | ||||
| 		if (streams.get(id) == null) { | ||||
| 			System.out.printf("file_close: Can't close invalid file id: %d\n", id); | ||||
| 			return; | ||||
| 		} | ||||
|  | @ -264,7 +264,7 @@ public class GodotIO { | |||
| 
 | ||||
| 	public int last_dir_id = 1; | ||||
| 
 | ||||
| 	HashMap<Integer, AssetDir> dirs; | ||||
| 	SparseArray<AssetDir> dirs; | ||||
| 
 | ||||
| 	public int dir_open(String path) { | ||||
| 
 | ||||
|  | @ -293,7 +293,7 @@ public class GodotIO { | |||
| 	} | ||||
| 
 | ||||
| 	public boolean dir_is_dir(int id) { | ||||
| 		if (!dirs.containsKey(id)) { | ||||
| 		if (dirs.get(id) == null) { | ||||
| 			System.out.printf("dir_next: invalid dir id: %d\n", id); | ||||
| 			return false; | ||||
| 		} | ||||
|  | @ -320,7 +320,7 @@ public class GodotIO { | |||
| 
 | ||||
| 	public String dir_next(int id) { | ||||
| 
 | ||||
| 		if (!dirs.containsKey(id)) { | ||||
| 		if (dirs.get(id) == null) { | ||||
| 			System.out.printf("dir_next: invalid dir id: %d\n", id); | ||||
| 			return ""; | ||||
| 		} | ||||
|  | @ -339,7 +339,7 @@ public class GodotIO { | |||
| 
 | ||||
| 	public void dir_close(int id) { | ||||
| 
 | ||||
| 		if (!dirs.containsKey(id)) { | ||||
| 		if (dirs.get(id) == null) { | ||||
| 			System.out.printf("dir_close: invalid dir id: %d\n", id); | ||||
| 			return; | ||||
| 		} | ||||
|  | @ -351,9 +351,9 @@ public class GodotIO { | |||
| 
 | ||||
| 		am = p_activity.getAssets(); | ||||
| 		activity = p_activity; | ||||
| 		streams = new HashMap<Integer, AssetData>(); | ||||
| 		dirs = new HashMap<Integer, AssetDir>(); | ||||
| 		applicationContext = activity.getApplicationContext(); | ||||
| 		//streams = new HashMap<Integer, AssetData>(); | ||||
| 		streams = new SparseArray<AssetData>(); | ||||
| 		dirs = new SparseArray<AssetDir>(); | ||||
| 	} | ||||
| 
 | ||||
| 	///////////////////////// | ||||
|  | @ -365,7 +365,7 @@ public class GodotIO { | |||
| 	private AudioTrack mAudioTrack; | ||||
| 
 | ||||
| 	public Object audioInit(int sampleRate, int desiredFrames) { | ||||
| 		int channelConfig = AudioFormat.CHANNEL_CONFIGURATION_STEREO; | ||||
| 		int channelConfig = AudioFormat.CHANNEL_OUT_STEREO; | ||||
| 		int audioFormat = AudioFormat.ENCODING_PCM_16BIT; | ||||
| 		int frameSize = 4; | ||||
| 
 | ||||
|  | @ -496,13 +496,13 @@ public class GodotIO { | |||
| 	} | ||||
| 
 | ||||
| 	public int getScreenDPI() { | ||||
| 		DisplayMetrics metrics = applicationContext.getResources().getDisplayMetrics(); | ||||
| 		DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); | ||||
| 		return (int)(metrics.density * 160f); | ||||
| 	} | ||||
| 
 | ||||
| 	public boolean needsReloadHooks() { | ||||
| 
 | ||||
| 		return android.os.Build.VERSION.SDK_INT < 11; | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	public void showKeyboard(String p_existing_text) { | ||||
|  | @ -564,7 +564,7 @@ public class GodotIO { | |||
| 
 | ||||
| 		try { | ||||
| 			mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); | ||||
| 			mediaPlayer.setDataSource(applicationContext, filePath); | ||||
| 			mediaPlayer.setDataSource(activity.getApplicationContext(), filePath); | ||||
| 			mediaPlayer.prepare(); | ||||
| 			mediaPlayer.start(); | ||||
| 		} catch (IOException e) { | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ | |||
| /*************************************************************************/ | ||||
| 
 | ||||
| package org.godotengine.godot; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.graphics.PixelFormat; | ||||
| import android.opengl.GLSurfaceView; | ||||
|  | @ -75,9 +76,9 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { | |||
| 
 | ||||
| 	private static String TAG = "GodotView"; | ||||
| 	private static final boolean DEBUG = false; | ||||
| 	private static Context ctx; | ||||
| 	private Context ctx; | ||||
| 
 | ||||
| 	private static GodotIO io; | ||||
| 	private GodotIO io; | ||||
| 	private static boolean firsttime = true; | ||||
| 	private static boolean use_gl3 = false; | ||||
| 	private static boolean use_32 = false; | ||||
|  | @ -105,20 +106,26 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { | |||
| 		init(false, 16, 0); | ||||
| 	} | ||||
| 
 | ||||
| 	public GodotView(Context context) { | ||||
| 		super(context); | ||||
| 		ctx = context; | ||||
| 	} | ||||
| 
 | ||||
| 	public GodotView(Context context, boolean translucent, int depth, int stencil) { | ||||
| 		super(context); | ||||
| 		init(translucent, depth, stencil); | ||||
| 	} | ||||
| 
 | ||||
| 	@SuppressLint("ClickableViewAccessibility") | ||||
| 	@Override | ||||
| 	public boolean onTouchEvent(MotionEvent event) { | ||||
| 		super.onTouchEvent(event); | ||||
| 		return activity.gotTouchEvent(event); | ||||
| 	}; | ||||
| 	} | ||||
| 
 | ||||
| 	public int get_godot_button(int keyCode) { | ||||
| 
 | ||||
| 		int button = 0; | ||||
| 		int button; | ||||
| 		switch (keyCode) { | ||||
| 			case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B | ||||
| 				button = 0; | ||||
|  | @ -178,7 +185,7 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { | |||
| 			default: | ||||
| 				button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20; | ||||
| 				break; | ||||
| 		}; | ||||
| 		} | ||||
| 		return button; | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -440,6 +447,10 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { | |||
| 	private static class ContextFactory implements GLSurfaceView.EGLContextFactory { | ||||
| 		private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; | ||||
| 		public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { | ||||
| 			String driver_name = GodotLib.getGlobal("rendering/quality/driver/driver_name"); | ||||
| 			if (use_gl3 && !driver_name.equals("GLES3")) { | ||||
| 				use_gl3 = false; | ||||
| 			} | ||||
| 			if (use_gl3) | ||||
| 				Log.w(TAG, "creating OpenGL ES 3.0 context :"); | ||||
| 			else | ||||
|  | @ -508,26 +519,24 @@ public class GodotView extends GLSurfaceView implements InputDeviceListener { | |||
| 		 * perform actual matching in chooseConfig() below. | ||||
| 		 */ | ||||
| 		private static int EGL_OPENGL_ES2_BIT = 4; | ||||
| 		private static int[] s_configAttribs2 = | ||||
| 				{ | ||||
| 					EGL10.EGL_RED_SIZE, 4, | ||||
| 					EGL10.EGL_GREEN_SIZE, 4, | ||||
| 					EGL10.EGL_BLUE_SIZE, 4, | ||||
| 					//  EGL10.EGL_DEPTH_SIZE,     16, | ||||
| 					// EGL10.EGL_STENCIL_SIZE,   EGL10.EGL_DONT_CARE, | ||||
| 					EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, | ||||
| 					EGL10.EGL_NONE | ||||
| 				}; | ||||
| 		private static int[] s_configAttribs3 = | ||||
| 				{ | ||||
| 					EGL10.EGL_RED_SIZE, 4, | ||||
| 					EGL10.EGL_GREEN_SIZE, 4, | ||||
| 					EGL10.EGL_BLUE_SIZE, 4, | ||||
| 					// EGL10.EGL_DEPTH_SIZE,     16, | ||||
| 					//  EGL10.EGL_STENCIL_SIZE,   EGL10.EGL_DONT_CARE, | ||||
| 					EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT | ||||
| 					EGL10.EGL_NONE | ||||
| 				}; | ||||
| 		private static int[] s_configAttribs2 = { | ||||
| 			EGL10.EGL_RED_SIZE, 4, | ||||
| 			EGL10.EGL_GREEN_SIZE, 4, | ||||
| 			EGL10.EGL_BLUE_SIZE, 4, | ||||
| 			//  EGL10.EGL_DEPTH_SIZE,     16, | ||||
| 			// EGL10.EGL_STENCIL_SIZE,   EGL10.EGL_DONT_CARE, | ||||
| 			EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, | ||||
| 			EGL10.EGL_NONE | ||||
| 		}; | ||||
| 		private static int[] s_configAttribs3 = { | ||||
| 			EGL10.EGL_RED_SIZE, 4, | ||||
| 			EGL10.EGL_GREEN_SIZE, 4, | ||||
| 			EGL10.EGL_BLUE_SIZE, 4, | ||||
| 			// EGL10.EGL_DEPTH_SIZE,     16, | ||||
| 			//  EGL10.EGL_STENCIL_SIZE,   EGL10.EGL_DONT_CARE, | ||||
| 			EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT | ||||
| 			EGL10.EGL_NONE | ||||
| 		}; | ||||
| 
 | ||||
| 		public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,6 +39,8 @@ import android.os.Message; | |||
| import android.view.inputmethod.InputMethodManager; | ||||
| import android.view.inputmethod.EditorInfo; | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| 
 | ||||
| public class GodotEditText extends EditText { | ||||
| 	// =========================================================== | ||||
| 	// Constants | ||||
|  | @ -51,9 +53,24 @@ public class GodotEditText extends EditText { | |||
| 	// =========================================================== | ||||
| 	private GodotView mView; | ||||
| 	private GodotTextInputWrapper mInputWrapper; | ||||
| 	private static Handler sHandler; | ||||
| 	private EditHandler sHandler = new EditHandler(this); | ||||
| 	private String mOriginText; | ||||
| 
 | ||||
| 	private static class EditHandler extends Handler { | ||||
| 		private final WeakReference<GodotEditText> mEdit; | ||||
| 		public EditHandler(GodotEditText edit) { | ||||
| 			mEdit = new WeakReference<>(edit); | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void handleMessage(Message msg) { | ||||
| 			GodotEditText edit = mEdit.get(); | ||||
| 			if (edit != null) { | ||||
| 				edit.handleMessage(msg); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// =========================================================== | ||||
| 	// Constructors | ||||
| 	// =========================================================== | ||||
|  | @ -75,36 +92,33 @@ public class GodotEditText extends EditText { | |||
| 	protected void initView() { | ||||
| 		this.setPadding(0, 0, 0, 0); | ||||
| 		this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); | ||||
| 	} | ||||
| 
 | ||||
| 		sHandler = new Handler() { | ||||
| 			@Override | ||||
| 			public void handleMessage(final Message msg) { | ||||
| 				switch (msg.what) { | ||||
| 					case HANDLER_OPEN_IME_KEYBOARD: { | ||||
| 						GodotEditText edit = (GodotEditText)msg.obj; | ||||
| 						String text = edit.mOriginText; | ||||
| 						if (edit.requestFocus()) { | ||||
| 							edit.removeTextChangedListener(edit.mInputWrapper); | ||||
| 							edit.setText(""); | ||||
| 							edit.append(text); | ||||
| 							edit.mInputWrapper.setOriginText(text); | ||||
| 							edit.addTextChangedListener(edit.mInputWrapper); | ||||
| 							final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
| 							imm.showSoftInput(edit, 0); | ||||
| 						} | ||||
| 					} break; | ||||
| 
 | ||||
| 					case HANDLER_CLOSE_IME_KEYBOARD: { | ||||
| 						GodotEditText edit = (GodotEditText)msg.obj; | ||||
| 
 | ||||
| 						edit.removeTextChangedListener(mInputWrapper); | ||||
| 						final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
| 						imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); | ||||
| 						edit.mView.requestFocus(); | ||||
| 					} break; | ||||
| 	private void handleMessage(final Message msg) { | ||||
| 		switch (msg.what) { | ||||
| 			case HANDLER_OPEN_IME_KEYBOARD: { | ||||
| 				GodotEditText edit = (GodotEditText)msg.obj; | ||||
| 				String text = edit.mOriginText; | ||||
| 				if (edit.requestFocus()) { | ||||
| 					edit.removeTextChangedListener(edit.mInputWrapper); | ||||
| 					edit.setText(""); | ||||
| 					edit.append(text); | ||||
| 					edit.mInputWrapper.setOriginText(text); | ||||
| 					edit.addTextChangedListener(edit.mInputWrapper); | ||||
| 					final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
| 					imm.showSoftInput(edit, 0); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 			} break; | ||||
| 
 | ||||
| 			case HANDLER_CLOSE_IME_KEYBOARD: { | ||||
| 				GodotEditText edit = (GodotEditText)msg.obj; | ||||
| 
 | ||||
| 				edit.removeTextChangedListener(mInputWrapper); | ||||
| 				final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||||
| 				imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); | ||||
| 				edit.mView.requestFocus(); | ||||
| 			} break; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// =========================================================== | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ | |||
| package org.godotengine.godot.input; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.os.Build; | ||||
| import android.os.Handler; | ||||
| import android.view.InputDevice; | ||||
| import android.view.MotionEvent; | ||||
|  | @ -130,11 +129,7 @@ public interface InputManagerCompat { | |||
| 		 * @return a compatible implementation of InputManager | ||||
| 		 */ | ||||
| 		public static InputManagerCompat getInputManager(Context context) { | ||||
| 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | ||||
| 				return new InputManagerV16(context); | ||||
| 			} else { | ||||
| 				return new InputManagerV9(); | ||||
| 			} | ||||
| 			return new InputManagerV16(context); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,209 +0,0 @@ | |||
| /* | ||||
|  * Copyright (C) 2013 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package org.godotengine.godot.input; | ||||
| 
 | ||||
| import android.os.Handler; | ||||
| import android.os.Message; | ||||
| import android.os.SystemClock; | ||||
| import android.util.Log; | ||||
| import android.util.SparseArray; | ||||
| import android.view.InputDevice; | ||||
| import android.view.MotionEvent; | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.ArrayDeque; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.Queue; | ||||
| 
 | ||||
| public class InputManagerV9 implements InputManagerCompat { | ||||
| 	private static final String LOG_TAG = "InputManagerV9"; | ||||
| 	private static final int MESSAGE_TEST_FOR_DISCONNECT = 101; | ||||
| 	private static final long CHECK_ELAPSED_TIME = 3000L; | ||||
| 
 | ||||
| 	private static final int ON_DEVICE_ADDED = 0; | ||||
| 	private static final int ON_DEVICE_CHANGED = 1; | ||||
| 	private static final int ON_DEVICE_REMOVED = 2; | ||||
| 
 | ||||
| 	private final SparseArray<long[]> mDevices; | ||||
| 	private final Map<InputDeviceListener, Handler> mListeners; | ||||
| 	private final Handler mDefaultHandler; | ||||
| 
 | ||||
| 	private static class PollingMessageHandler extends Handler { | ||||
| 		private final WeakReference<InputManagerV9> mInputManager; | ||||
| 
 | ||||
| 		PollingMessageHandler(InputManagerV9 im) { | ||||
| 			mInputManager = new WeakReference<InputManagerV9>(im); | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void handleMessage(Message msg) { | ||||
| 			super.handleMessage(msg); | ||||
| 			switch (msg.what) { | ||||
| 				case MESSAGE_TEST_FOR_DISCONNECT: | ||||
| 					InputManagerV9 imv = mInputManager.get(); | ||||
| 					if (null != imv) { | ||||
| 						long time = SystemClock.elapsedRealtime(); | ||||
| 						int size = imv.mDevices.size(); | ||||
| 						for (int i = 0; i < size; i++) { | ||||
| 							long[] lastContact = imv.mDevices.valueAt(i); | ||||
| 							if (null != lastContact) { | ||||
| 								if (time - lastContact[0] > CHECK_ELAPSED_TIME) { | ||||
| 									// check to see if the device has been | ||||
| 									// disconnected | ||||
| 									int id = imv.mDevices.keyAt(i); | ||||
| 									if (null == InputDevice.getDevice(id)) { | ||||
| 										// disconnected! | ||||
| 										imv.notifyListeners(ON_DEVICE_REMOVED, id); | ||||
| 										imv.mDevices.remove(id); | ||||
| 									} else { | ||||
| 										lastContact[0] = time; | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 						sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, | ||||
| 								CHECK_ELAPSED_TIME); | ||||
| 					} | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public InputManagerV9() { | ||||
| 		mDevices = new SparseArray<long[]>(); | ||||
| 		mListeners = new HashMap<InputDeviceListener, Handler>(); | ||||
| 		mDefaultHandler = new PollingMessageHandler(this); | ||||
| 		// as a side-effect, populates our collection of watched | ||||
| 		// input devices | ||||
| 		getInputDeviceIds(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public InputDevice getInputDevice(int id) { | ||||
| 		return InputDevice.getDevice(id); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public int[] getInputDeviceIds() { | ||||
| 		// add any hitherto unknown devices to our | ||||
| 		// collection of watched input devices | ||||
| 		int[] activeDevices = InputDevice.getDeviceIds(); | ||||
| 		long time = SystemClock.elapsedRealtime(); | ||||
| 		for (int id : activeDevices) { | ||||
| 			long[] lastContact = mDevices.get(id); | ||||
| 			if (null == lastContact) { | ||||
| 				// we have a new device | ||||
| 				mDevices.put(id, new long[] { time }); | ||||
| 			} | ||||
| 		} | ||||
| 		return activeDevices; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { | ||||
| 		mListeners.remove(listener); | ||||
| 		if (handler == null) { | ||||
| 			handler = mDefaultHandler; | ||||
| 		} | ||||
| 		mListeners.put(listener, handler); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void unregisterInputDeviceListener(InputDeviceListener listener) { | ||||
| 		mListeners.remove(listener); | ||||
| 	} | ||||
| 
 | ||||
| 	private void notifyListeners(int why, int deviceId) { | ||||
| 		// the state of some device has changed | ||||
| 		if (!mListeners.isEmpty()) { | ||||
| 			// yes... this will cause an object to get created... hopefully | ||||
| 			// it won't happen very often | ||||
| 			for (InputDeviceListener listener : mListeners.keySet()) { | ||||
| 				Handler handler = mListeners.get(listener); | ||||
| 				DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener); | ||||
| 				handler.post(odc); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private static class DeviceEvent implements Runnable { | ||||
| 		private int mMessageType; | ||||
| 		private int mId; | ||||
| 		private InputDeviceListener mListener; | ||||
| 		private static Queue<DeviceEvent> sEventQueue = new ArrayDeque<DeviceEvent>(); | ||||
| 
 | ||||
| 		private DeviceEvent() { | ||||
| 		} | ||||
| 
 | ||||
| 		static DeviceEvent getDeviceEvent(int messageType, int id, | ||||
| 				InputDeviceListener listener) { | ||||
| 			DeviceEvent curChanged = sEventQueue.poll(); | ||||
| 			if (null == curChanged) { | ||||
| 				curChanged = new DeviceEvent(); | ||||
| 			} | ||||
| 			curChanged.mMessageType = messageType; | ||||
| 			curChanged.mId = id; | ||||
| 			curChanged.mListener = listener; | ||||
| 			return curChanged; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public void run() { | ||||
| 			switch (mMessageType) { | ||||
| 				case ON_DEVICE_ADDED: | ||||
| 					mListener.onInputDeviceAdded(mId); | ||||
| 					break; | ||||
| 				case ON_DEVICE_CHANGED: | ||||
| 					mListener.onInputDeviceChanged(mId); | ||||
| 					break; | ||||
| 				case ON_DEVICE_REMOVED: | ||||
| 					mListener.onInputDeviceRemoved(mId); | ||||
| 					break; | ||||
| 				default: | ||||
| 					Log.e(LOG_TAG, "Unknown Message Type"); | ||||
| 					break; | ||||
| 			} | ||||
| 			// dump this runnable back in the queue | ||||
| 			sEventQueue.offer(this); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void onGenericMotionEvent(MotionEvent event) { | ||||
| 		// detect new devices | ||||
| 		int id = event.getDeviceId(); | ||||
| 		long[] timeArray = mDevices.get(id); | ||||
| 		if (null == timeArray) { | ||||
| 			notifyListeners(ON_DEVICE_ADDED, id); | ||||
| 			timeArray = new long[1]; | ||||
| 			mDevices.put(id, timeArray); | ||||
| 		} | ||||
| 		long time = SystemClock.elapsedRealtime(); | ||||
| 		timeArray[0] = time; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void onPause() { | ||||
| 		mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void onResume() { | ||||
| 		mDefaultHandler.sendEmptyMessage(MESSAGE_TEST_FOR_DISCONNECT); | ||||
| 	} | ||||
| } | ||||
|  | @ -37,59 +37,81 @@ import android.os.AsyncTask; | |||
| import android.os.RemoteException; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| 
 | ||||
| abstract public class ConsumeTask { | ||||
| 
 | ||||
| 	private Context context; | ||||
| 
 | ||||
| 	private IInAppBillingService mService; | ||||
| 
 | ||||
| 	private String mSku; | ||||
| 	private String mToken; | ||||
| 
 | ||||
| 	private static class ConsumeAsyncTask extends AsyncTask<String, String, String> { | ||||
| 
 | ||||
| 		private WeakReference<ConsumeTask> mTask; | ||||
| 
 | ||||
| 		ConsumeAsyncTask(ConsumeTask consume) { | ||||
| 			mTask = new WeakReference<>(consume); | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected String doInBackground(String... strings) { | ||||
| 			ConsumeTask consume = mTask.get(); | ||||
| 			if (consume != null) { | ||||
| 				return consume.doInBackground(strings); | ||||
| 			} | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected void onPostExecute(String param) { | ||||
| 			ConsumeTask consume = mTask.get(); | ||||
| 			if (consume != null) { | ||||
| 				consume.onPostExecute(param); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public ConsumeTask(IInAppBillingService mService, Context context) { | ||||
| 		this.context = context; | ||||
| 		this.mService = mService; | ||||
| 	} | ||||
| 
 | ||||
| 	public void consume(final String sku) { | ||||
| 		//Log.d("XXX", "Consuming product " + sku); | ||||
| 		mSku = sku; | ||||
| 		PaymentsCache pc = new PaymentsCache(context); | ||||
| 		Boolean isBlocked = pc.getConsumableFlag("block", sku); | ||||
| 		String _token = pc.getConsumableValue("token", sku); | ||||
| 		//Log.d("XXX", "token " + _token); | ||||
| 		if (!isBlocked && _token == null) { | ||||
| 			//_token = "inapp:"+context.getPackageName()+":android.test.purchased"; | ||||
| 			//Log.d("XXX", "Consuming product " + sku + " with token " + _token); | ||||
| 		mToken = pc.getConsumableValue("token", sku); | ||||
| 		if (!isBlocked && mToken == null) { | ||||
| 			// Consuming task is processing | ||||
| 		} else if (!isBlocked) { | ||||
| 			//Log.d("XXX", "It is not blocked ¿?"); | ||||
| 			return; | ||||
| 		} else if (_token == null) { | ||||
| 			//Log.d("XXX", "No token available"); | ||||
| 		} else if (mToken == null) { | ||||
| 			this.error("No token for sku:" + sku); | ||||
| 			return; | ||||
| 		} | ||||
| 		final String token = _token; | ||||
| 		new AsyncTask<String, String, String>() { | ||||
| 			@Override | ||||
| 			protected String doInBackground(String... params) { | ||||
| 				try { | ||||
| 					//Log.d("XXX", "Requesting to release item."); | ||||
| 					int response = mService.consumePurchase(3, context.getPackageName(), token); | ||||
| 					//Log.d("XXX", "release response code: " + response); | ||||
| 					if (response == 0 || response == 8) { | ||||
| 						return null; | ||||
| 					} | ||||
| 				} catch (RemoteException e) { | ||||
| 					return e.getMessage(); | ||||
| 				} | ||||
| 				return "Some error"; | ||||
| 			} | ||||
| 		new ConsumeAsyncTask(this).execute(); | ||||
| 	} | ||||
| 
 | ||||
| 			protected void onPostExecute(String param) { | ||||
| 				if (param == null) { | ||||
| 					success(new PaymentsCache(context).getConsumableValue("ticket", sku)); | ||||
| 				} else { | ||||
| 					error(param); | ||||
| 				} | ||||
| 	private String doInBackground(String... params) { | ||||
| 		try { | ||||
| 			int response = mService.consumePurchase(3, context.getPackageName(), mToken); | ||||
| 			if (response == 0 || response == 8) { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} catch (RemoteException e) { | ||||
| 			return e.getMessage(); | ||||
| 		} | ||||
| 		return "Some error"; | ||||
| 	} | ||||
| 
 | ||||
| 	private void onPostExecute(String param) { | ||||
| 		if (param == null) { | ||||
| 			success(new PaymentsCache(context).getConsumableValue("ticket", mSku)); | ||||
| 		} else { | ||||
| 			error(param); | ||||
| 		} | ||||
| 				.execute(); | ||||
| 	} | ||||
| 
 | ||||
| 	abstract protected void success(String ticket); | ||||
|  |  | |||
|  | @ -1,79 +0,0 @@ | |||
| /*************************************************************************/ | ||||
| /*  GenericConsumeTask.java                                              */ | ||||
| /*************************************************************************/ | ||||
| /*                       This file is part of:                           */ | ||||
| /*                           GODOT ENGINE                                */ | ||||
| /*                      https://godotengine.org                          */ | ||||
| /*************************************************************************/ | ||||
| /* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur.                 */ | ||||
| /* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md)    */ | ||||
| /*                                                                       */ | ||||
| /* Permission is hereby granted, free of charge, to any person obtaining */ | ||||
| /* a copy of this software and associated documentation files (the       */ | ||||
| /* "Software"), to deal in the Software without restriction, including   */ | ||||
| /* without limitation the rights to use, copy, modify, merge, publish,   */ | ||||
| /* distribute, sublicense, and/or sell copies of the Software, and to    */ | ||||
| /* permit persons to whom the Software is furnished to do so, subject to */ | ||||
| /* the following conditions:                                             */ | ||||
| /*                                                                       */ | ||||
| /* The above copyright notice and this permission notice shall be        */ | ||||
| /* included in all copies or substantial portions of the Software.       */ | ||||
| /*                                                                       */ | ||||
| /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ | ||||
| /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ | ||||
| /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ | ||||
| /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ | ||||
| /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ | ||||
| /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ | ||||
| /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ | ||||
| /*************************************************************************/ | ||||
| 
 | ||||
| package org.godotengine.godot.payments; | ||||
| 
 | ||||
| import com.android.vending.billing.IInAppBillingService; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.RemoteException; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| abstract public class GenericConsumeTask extends AsyncTask<String, String, String> { | ||||
| 
 | ||||
| 	private Context context; | ||||
| 	private IInAppBillingService mService; | ||||
| 
 | ||||
| 	public GenericConsumeTask(Context context, IInAppBillingService mService, String sku, String receipt, String signature, String token) { | ||||
| 		this.context = context; | ||||
| 		this.mService = mService; | ||||
| 		this.sku = sku; | ||||
| 		this.receipt = receipt; | ||||
| 		this.signature = signature; | ||||
| 		this.token = token; | ||||
| 	} | ||||
| 
 | ||||
| 	private String sku; | ||||
| 	private String receipt; | ||||
| 	private String signature; | ||||
| 	private String token; | ||||
| 
 | ||||
| 	@Override | ||||
| 	protected String doInBackground(String... params) { | ||||
| 		try { | ||||
| 			//Log.d("godot", "Requesting to consume an item with token ." + token); | ||||
| 			int response = mService.consumePurchase(3, context.getPackageName(), token); | ||||
| 			//Log.d("godot", "consumePurchase response: " + response); | ||||
| 			if (response == 0 || response == 8) { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} catch (Exception e) { | ||||
| 			Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	protected void onPostExecute(String sarasa) { | ||||
| 		onSuccess(sku, receipt, signature, token); | ||||
| 	} | ||||
| 
 | ||||
| 	abstract public void onSuccess(String sku, String receipt, String signature, String token); | ||||
| } | ||||
|  | @ -46,7 +46,7 @@ public class PaymentsCache { | |||
| 		SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE); | ||||
| 		SharedPreferences.Editor editor = sharedPref.edit(); | ||||
| 		editor.putBoolean(sku, flag); | ||||
| 		editor.commit(); | ||||
| 		editor.apply(); | ||||
| 	} | ||||
| 
 | ||||
| 	public boolean getConsumableFlag(String set, String sku) { | ||||
|  | @ -60,7 +60,7 @@ public class PaymentsCache { | |||
| 		SharedPreferences.Editor editor = sharedPref.edit(); | ||||
| 		editor.putString(sku, value); | ||||
| 		//Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku); | ||||
| 		editor.commit(); | ||||
| 		editor.apply(); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getConsumableValue(String set, String sku) { | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ public class PaymentsManager { | |||
| 	}; | ||||
| 
 | ||||
| 	public void requestPurchase(final String sku, String transactionId) { | ||||
| 		new PurchaseTask(mService, Godot.getInstance()) { | ||||
| 		new PurchaseTask(mService, activity) { | ||||
| 			@Override | ||||
| 			protected void error(String message) { | ||||
| 				godotPaymentV3.callbackFail(message); | ||||
|  | @ -159,7 +159,7 @@ public class PaymentsManager { | |||
| 
 | ||||
| 	public void requestPurchased() { | ||||
| 		try { | ||||
| 			PaymentsCache pc = new PaymentsCache(Godot.getInstance()); | ||||
| 			PaymentsCache pc = new PaymentsCache(activity); | ||||
| 
 | ||||
| 			String continueToken = null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,26 +30,59 @@ | |||
| 
 | ||||
| package org.godotengine.godot.payments; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import android.content.Context; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Bundle; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import com.android.vending.billing.IInAppBillingService; | ||||
| 
 | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| import org.godotengine.godot.Dictionary; | ||||
| import org.godotengine.godot.Godot; | ||||
| import com.android.vending.billing.IInAppBillingService; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Bundle; | ||||
| import android.os.RemoteException; | ||||
| import android.util.Log; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.ArrayList; | ||||
| 
 | ||||
| abstract public class ReleaseAllConsumablesTask { | ||||
| 
 | ||||
| 	private Context context; | ||||
| 	private IInAppBillingService mService; | ||||
| 
 | ||||
| 	private static class ReleaseAllConsumablesAsyncTask extends AsyncTask<String, String, String> { | ||||
| 
 | ||||
| 		private WeakReference<ReleaseAllConsumablesTask> mTask; | ||||
| 		private String mSku; | ||||
| 		private String mReceipt; | ||||
| 		private String mSignature; | ||||
| 		private String mToken; | ||||
| 
 | ||||
| 		ReleaseAllConsumablesAsyncTask(ReleaseAllConsumablesTask task, String sku, String receipt, String signature, String token) { | ||||
| 			mTask = new WeakReference<ReleaseAllConsumablesTask>(task); | ||||
| 
 | ||||
| 			mSku = sku; | ||||
| 			mReceipt = receipt; | ||||
| 			mSignature = signature; | ||||
| 			mToken = token; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected String doInBackground(String... params) { | ||||
| 			ReleaseAllConsumablesTask consume = mTask.get(); | ||||
| 			if (consume != null) { | ||||
| 				return consume.doInBackground(mToken); | ||||
| 			} | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected void onPostExecute(String param) { | ||||
| 			ReleaseAllConsumablesTask consume = mTask.get(); | ||||
| 			if (consume != null) { | ||||
| 				consume.success(mSku, mReceipt, mSignature, mToken); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public ReleaseAllConsumablesTask(IInAppBillingService mService, Context context) { | ||||
| 		this.context = context; | ||||
| 		this.mService = mService; | ||||
|  | @ -60,12 +93,6 @@ abstract public class ReleaseAllConsumablesTask { | |||
| 			//Log.d("godot", "consumeItall for " + context.getPackageName()); | ||||
| 			Bundle bundle = mService.getPurchases(3, context.getPackageName(), "inapp", null); | ||||
| 
 | ||||
| 			for (String key : bundle.keySet()) { | ||||
| 				Object value = bundle.get(key); | ||||
| 				//Log.d("godot", String.format("%s %s (%s)", key, | ||||
| 				//value.toString(), value.getClass().getName())); | ||||
| 			} | ||||
| 
 | ||||
| 			if (bundle.getInt("RESPONSE_CODE") == 0) { | ||||
| 
 | ||||
| 				final ArrayList<String> myPurchases = bundle.getStringArrayList("INAPP_PURCHASE_DATA_LIST"); | ||||
|  | @ -87,14 +114,7 @@ abstract public class ReleaseAllConsumablesTask { | |||
| 						String token = inappPurchaseData.getString("purchaseToken"); | ||||
| 						String signature = mySignatures.get(i); | ||||
| 						//Log.d("godot", "A punto de consumir un item con token:" + token + "\n" + receipt); | ||||
| 						new GenericConsumeTask(context, mService, sku, receipt, signature, token) { | ||||
| 							@Override | ||||
| 							public void onSuccess(String sku, String receipt, String signature, String token) { | ||||
| 								ReleaseAllConsumablesTask.this.success(sku, receipt, signature, token); | ||||
| 							} | ||||
| 						} | ||||
| 								.execute(); | ||||
| 
 | ||||
| 						new ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute(); | ||||
| 					} catch (JSONException e) { | ||||
| 					} | ||||
| 				} | ||||
|  | @ -104,6 +124,20 @@ abstract public class ReleaseAllConsumablesTask { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private String doInBackground(String token) { | ||||
| 		try { | ||||
| 			//Log.d("godot", "Requesting to consume an item with token ." + token); | ||||
| 			int response = mService.consumePurchase(3, context.getPackageName(), token); | ||||
| 			//Log.d("godot", "consumePurchase response: " + response); | ||||
| 			if (response == 0 || response == 8) { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} catch (Exception e) { | ||||
| 			Log.d("godot", "Error " + e.getClass().getName() + ":" + e.getMessage()); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	abstract protected void success(String sku, String receipt, String signature, String token); | ||||
| 	abstract protected void error(String message); | ||||
| 	abstract protected void notRequired(); | ||||
|  |  | |||
|  | @ -52,69 +52,104 @@ import android.os.Bundle; | |||
| import android.os.RemoteException; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| 
 | ||||
| abstract public class ValidateTask { | ||||
| 
 | ||||
| 	private Activity context; | ||||
| 	private GodotPaymentV3 godotPaymentsV3; | ||||
| 	private ProgressDialog dialog; | ||||
| 	private String mSku; | ||||
| 
 | ||||
| 	private static class ValidateAsyncTask extends AsyncTask<String, String, String> { | ||||
| 		private WeakReference<ValidateTask> mTask; | ||||
| 
 | ||||
| 		ValidateAsyncTask(ValidateTask task) { | ||||
| 			mTask = new WeakReference<>(task); | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected void onPreExecute() { | ||||
| 			ValidateTask task = mTask.get(); | ||||
| 			if (task != null) { | ||||
| 				task.onPreExecute(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected String doInBackground(String... params) { | ||||
| 			ValidateTask task = mTask.get(); | ||||
| 			if (task != null) { | ||||
| 				return task.doInBackground(params); | ||||
| 			} | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected void onPostExecute(String response) { | ||||
| 			ValidateTask task = mTask.get(); | ||||
| 			if (task != null) { | ||||
| 				task.onPostExecute(response); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) { | ||||
| 		this.context = context; | ||||
| 		this.godotPaymentsV3 = godotPaymentsV3; | ||||
| 	} | ||||
| 
 | ||||
| 	public void validatePurchase(final String sku) { | ||||
| 		new AsyncTask<String, String, String>() { | ||||
| 			private ProgressDialog dialog; | ||||
| 
 | ||||
| 			@Override | ||||
| 			protected void onPreExecute() { | ||||
| 				dialog = ProgressDialog.show(context, null, "Please wait..."); | ||||
| 			} | ||||
| 
 | ||||
| 			@Override | ||||
| 			protected String doInBackground(String... params) { | ||||
| 				PaymentsCache pc = new PaymentsCache(context); | ||||
| 				String url = godotPaymentsV3.getPurchaseValidationUrlPrefix(); | ||||
| 				RequestParams param = new RequestParams(); | ||||
| 				param.setUrl(url); | ||||
| 				param.put("ticket", pc.getConsumableValue("ticket", sku)); | ||||
| 				param.put("purchaseToken", pc.getConsumableValue("token", sku)); | ||||
| 				param.put("sku", sku); | ||||
| 				//Log.d("XXX", "Haciendo request a " + url); | ||||
| 				//Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); | ||||
| 				//Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); | ||||
| 				//Log.d("XXX", "sku: " + sku); | ||||
| 				param.put("package", context.getApplicationContext().getPackageName()); | ||||
| 				HttpRequester requester = new HttpRequester(); | ||||
| 				String jsonResponse = requester.post(param); | ||||
| 				//Log.d("XXX", "Validation response:\n"+jsonResponse); | ||||
| 				return jsonResponse; | ||||
| 			} | ||||
| 
 | ||||
| 			@Override | ||||
| 			protected void onPostExecute(String response) { | ||||
| 				if (dialog != null) { | ||||
| 					dialog.dismiss(); | ||||
| 				} | ||||
| 				JSONObject j; | ||||
| 				try { | ||||
| 					j = new JSONObject(response); | ||||
| 					if (j.getString("status").equals("OK")) { | ||||
| 						success(); | ||||
| 						return; | ||||
| 					} else if (j.getString("status") != null) { | ||||
| 						error(j.getString("message")); | ||||
| 					} else { | ||||
| 						error("Connection error"); | ||||
| 					} | ||||
| 				} catch (JSONException e) { | ||||
| 					error(e.getMessage()); | ||||
| 				} catch (Exception e) { | ||||
| 					error(e.getMessage()); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 				.execute(); | ||||
| 		mSku = sku; | ||||
| 		new ValidateAsyncTask(this).execute(); | ||||
| 	} | ||||
| 
 | ||||
| 	private void onPreExecute() { | ||||
| 		dialog = ProgressDialog.show(context, null, "Please wait..."); | ||||
| 	} | ||||
| 
 | ||||
| 	private String doInBackground(String... params) { | ||||
| 		PaymentsCache pc = new PaymentsCache(context); | ||||
| 		String url = godotPaymentsV3.getPurchaseValidationUrlPrefix(); | ||||
| 		RequestParams param = new RequestParams(); | ||||
| 		param.setUrl(url); | ||||
| 		param.put("ticket", pc.getConsumableValue("ticket", mSku)); | ||||
| 		param.put("purchaseToken", pc.getConsumableValue("token", mSku)); | ||||
| 		param.put("sku", mSku); | ||||
| 		//Log.d("XXX", "Haciendo request a " + url); | ||||
| 		//Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku)); | ||||
| 		//Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku)); | ||||
| 		//Log.d("XXX", "sku: " + sku); | ||||
| 		param.put("package", context.getApplicationContext().getPackageName()); | ||||
| 		HttpRequester requester = new HttpRequester(); | ||||
| 		String jsonResponse = requester.post(param); | ||||
| 		//Log.d("XXX", "Validation response:\n"+jsonResponse); | ||||
| 		return jsonResponse; | ||||
| 	} | ||||
| 
 | ||||
| 	private void onPostExecute(String response) { | ||||
| 		if (dialog != null) { | ||||
| 			dialog.dismiss(); | ||||
| 			dialog = null; | ||||
| 		} | ||||
| 		JSONObject j; | ||||
| 		try { | ||||
| 			j = new JSONObject(response); | ||||
| 			if (j.getString("status").equals("OK")) { | ||||
| 				success(); | ||||
| 				return; | ||||
| 			} else if (j.getString("status") != null) { | ||||
| 				error(j.getString("message")); | ||||
| 			} else { | ||||
| 				error("Connection error"); | ||||
| 			} | ||||
| 		} catch (JSONException e) { | ||||
| 			error(e.getMessage()); | ||||
| 		} catch (Exception e) { | ||||
| 			error(e.getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	abstract protected void success(); | ||||
| 	abstract protected void error(String message); | ||||
| 	abstract protected void canceled(); | ||||
|  |  | |||
|  | @ -105,7 +105,7 @@ public class HttpRequester { | |||
| 			long timeInit = new Date().getTime(); | ||||
| 			response = request(httpget); | ||||
| 			long delay = new Date().getTime() - timeInit; | ||||
| 			Log.d("com.app11tt.android.utils.HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds"); | ||||
| 			Log.d("HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds"); | ||||
| 			if (response == null || response.length() == 0) { | ||||
| 				response = ""; | ||||
| 			} else { | ||||
|  | @ -200,7 +200,7 @@ public class HttpRequester { | |||
| 		SharedPreferences.Editor editor = sharedPref.edit(); | ||||
| 		editor.putString("request_" + Crypt.md5(request), response); | ||||
| 		editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl()); | ||||
| 		editor.commit(); | ||||
| 		editor.apply(); | ||||
| 	} | ||||
| 
 | ||||
| 	public String getResponseFromCache(String request) { | ||||
|  |  | |||
 volzhs
						volzhs